Do not add intermediate lines to jumplist with :<linenum> command. - #5751
Conversation
ec43932 to
70ade5f
Compare
pascalkuthe
left a comment
There was a problem hiding this comment.
One small nit so this becomes easier to review.
I am not quite sure if the second fix is correct. Looking trough the code it seems intentional that current may point one past the end (the backward function explicitly checks for that) but I am not sure
|
I removed the jumplist changes but now it's in an interesting state where the jumplist works as expected, unless you rewind and then play back the jumplist to the end. If you do that, then doing another |
You are onto something and that triggered me to take a hard look/think about the jumplist code (something I haven't looked at in too much detail so far). It works as follows:
That means you previous fix was indeed wrong and the code is working as intended. The rope as on for the odd behaviour you are seeing is that We only ever save locations to junp back to and So for this PR you just need to move |
|
Thanks @pascalkuthe for the clarification, that does make sense. I think the code clarity could be improved and may try to rationalize about that in another PR, but will fix this one for now. Thanks again! |
|
Alright I had an interesting thought about this whole thing this morning. Since we want to set a jumplist entry before moving the cursor to allow the user to navigate back to where they started and we need support for aborting the command, can we not just use the jumplist to accomplish both tasks? The updated commit does the following:
This has the added benefit of retaining not only the starting line number, but the starting selection(s) as well. As far as I can tell, it works ideally when invoked with
I have some ideas about how to resolve these, but am pretty new to the project so greatly interested in others' thoughts. My ideas are:
Architecturally I have an idea that might improve this, too. What if typed commands were trait objects that has their own state and tracked in the command mode handler as an |
|
Pushing to the jumplist is potentially destructive: if you jump backwards with C-o a few times you can hit C-i to move forward again in the jumplist. But jump backwards with C-o and then push to the jumplist, the old history going forward is lost. Ideally we should only ever use the jumplist for real jumps rather than re-using it for implementation details like this. I think the |
I agree I preferred the previous simpler fix just with the push_jump moved to the right place as I sugessted. If you want to restore the selection when aborting why not just store the previous selection on the editor instead of the previous line number (like we do now). |
Ah, good point. Oh well, it was a fun exercise to learn more of the code :-).
Fair point that there aren't many implementations that require this, I suppose I'm imagining it being generally useful for future commands. More or less complicated might be a matter of taste, I found it surprising that state was stashed in the editor struct disjoint from context of the typed command and the typed commands source file.
I don't remember the details but it ended up being a bit more complicated. I'll add some context when I work on the next commit if I can remember it. I think your suggestion below might address it.
I like this idea, I'll pursue that next. |
| let text = doc.text().slice(..); | ||
| let line = doc.selection(view.id).primary().cursor_line(text); | ||
| cx.editor.last_line_number.get_or_insert(line + 1); | ||
| view.ensure_cursor_in_view(doc, scrolloff); |
There was a problem hiding this comment.
Stumbled on a neat "bug" in this last pass where we were passing line_number as the scrolloff value of ensure_cursor_in_view. Fixed in the last iteration.
There was a problem hiding this comment.
that could cause very weird issues. Nice catch
|
New iteration is ready for review, appreciate all the pointers on this @the-mikedavis @pascalkuthe @trink! I still think there's some edge cases around the flow of aborting the implicit |
pascalkuthe
left a comment
There was a problem hiding this comment.
Going in the right direction 👍 The last_selection turned out to be an even better idea then I thaught :D There are still some edgecases left tough, I left comments about those.
| PromptEvent::Validate => { | ||
| ensure!(!args.is_empty(), "Line number required"); | ||
| cx.editor.last_line_number = None; |
There was a problem hiding this comment.
I am pretty sure that PromptEvent::Validate can happen immidietly without reciving a PromptEvent::Update. In that case the command does nothing now wheras it worked correctly before. I think you can restructure the funciton slightly to account for that. First check if we are aborting => call abort_goto_line_number_preview and return.
Then run the code that is currently in the update branch.
Then check if we are validating and run the code here. last_selection can then be obtained with cx.editor.last_selection.clone().unwrap()cx.editor.last_selection.take().unwrap() (see below about the take, unwrap is fine as it's always initalized a few lines prior)
There was a problem hiding this comment.
I am pretty sure that PromptEvent::Validate can happen immidietly without reciving a PromptEvent::Update.
How would that happen? Keybinding?
There was a problem hiding this comment.
I think keybindings may do that. Haven't tested it but all other typed commands work when imminently called with PromptEvent::Validate so I wouldn't want to diverge from that here.
There was a problem hiding this comment.
Re-reading your comment:
First check if we are aborting => call abort_goto_line_number_preview and return.
What do you mean by "first check"? The function only handles one event at a time, and it does call the abort function and return then an abort event is fired.
There was a problem hiding this comment.
Right now you do a match on the event:
match event{
PromptEvent::Abort => /* abort */,
...
}Instead use a simple if:
if event == PromptEvent::Abort{
// abort ...
return
}
// update selection
if event == PromptEvent::Validate{
// jumplist entry and clear last_selection
}There was a problem hiding this comment.
Sorry, but I don't understand the difference, both constructs result in the function returning after calling abort, no?
There was a problem hiding this comment.
The difference is that you don't need to copy all the update code into the validate branch.
With a match the full thing would be:
match event{
PromptEvent::Abort => /* abort */,
PromptEvent::Update => /* update selection */,
PromptEvent::Validate => /* update selection, add jumplist entry and clear last_selection */,
...
}I suggested the structure above so you can share the update code between the Update and the Validate branch
There was a problem hiding this comment.
Okay, I see what you're saying. I'm trying to figure out the case where validate is called with arguments without update having been called. If I can find that, I'll restructure the code accordingly.
There was a problem hiding this comment.
@pascalkuthe You were, of course, exactly right -- binding a key in insert mode to :goto 123 sends only the Validate event, which currently doesn't work. New commit addresses that. Thanks for catching that and the detailed explanation!
pascalkuthe
left a comment
There was a problem hiding this comment.
Just one last nit, Apart from this, this LGTM now. Nice improvement both fixing the jumplist bug and making sure that we rust the full selection instead of just placing the primary cursor back on the original line.
| // is moved to the appropriate location. | ||
| update_goto_line_number_preview(cx, args)?; | ||
|
|
||
| if let Some(last_selection) = cx.editor.last_selection.take() { |
There was a problem hiding this comment.
Now that last_selection is always populated in update_goto_line_number_preview you should call unwrap here instead of if let. If last_selection somehow became None in the meantime that is a bug and therefore correct to panic rather than silently ignoring the problem. Fail hard and fail fast...
There was a problem hiding this comment.
Fail hard and fail fast...
Interesting. I did consider this code path and had the opposite instinct. In the event of a code change that results in a bug I didn't want to crash and potentially lose a user's unsaved editor changes just because a jumplist entry couldn't be set. Is this approach to error handling standard for the project, and if so, documented for contributors?
Alternatively, we could use either a debug_assert! or else conditional with logging if you're concerned about detecting such a bug.
There was a problem hiding this comment.
you can bail too in the else case if you are super concerned about the else case causing data loss but If you look throughout the codebase, unwrap and expect are used liberally for impossible code-paths. There was some discussion about adding a panic handler that backups unsaved work. Rust autoamtically panics on many common programmar bugs (out of bounds indexing, etc.) so that problem is not specific to this one case.
Right now the code looks like it's perfectly valid for there to be no last_selection when it should really be impossible. That is an anitpattern to me.. Using unwrap for code-paths that should never happens is a pretty common idiom in rust and something that makes code easier to read (which we care about) and makes it easier to find bugs.
There was a problem hiding this comment.
No problem, happy to be consistent with the rest of the project. Pushed a fix for that and the clippy warnings.
…elix-editor#5751) * Do not add intermediate lines to jumplist with :<linenum> command. * Revert jumplist index changes. * Reduce calculations during update cycle. * Use jumplist for undo, set jumplist before preview. * remove some debug logging * Revert "remove some debug logging" This reverts commit 5772c43. * Revert "Use jumplist for undo, set jumplist before preview." This reverts commit f73a1b2. * Add last_selection, update implementation. * @pascalkuthe initial feedback * Ensure ":goto 123" keybinding works as expected. * fix clippies, prefer expect() for expect last_selection state
…elix-editor#5751) * Do not add intermediate lines to jumplist with :<linenum> command. * Revert jumplist index changes. * Reduce calculations during update cycle. * Use jumplist for undo, set jumplist before preview. * remove some debug logging * Revert "remove some debug logging" This reverts commit 5772c43. * Revert "Use jumplist for undo, set jumplist before preview." This reverts commit f73a1b2. * Add last_selection, update implementation. * @pascalkuthe initial feedback * Ensure ":goto 123" keybinding works as expected. * fix clippies, prefer expect() for expect last_selection state
Based on this discussion. (@the-mikedavis)
This patch:
:<linenum>invocation is accepted with Enter (PromptEvent::Validate).currentpointer points past the end of the list when it's appended to, resulting in a "dead" invocation ofC-o. This caused issues with the previous implementation especially, as it would append to the list twice implicitly during validate (since the movement logic previous lived outside the handler for thePromptEvent::Updateevent.