sublimelsp / LSP-typescript

TypeScript, JavaScript support for Sublime LSP plugin
MIT License
132 stars 11 forks source link

organizeImports in lsp_code_actions_on_save is making Undo unusable #202

Closed alexkuz closed 1 year ago

alexkuz commented 1 year ago

Describe the bug When source.organizeImports is enabled in lsp_code_actions_on_save setting, I am unable to use Undo action after the TypeScript file is saved. (this might be a bug in LSP itself)

To Reproduce Steps to reproduce the behavior:

  1. Enable organizeImports:
    "lsp_code_actions_on_save": {
        "source.organizeImports": true
    },
  2. Create a file file.ts:

    import './a';
    import './b';
    
    console.log(1);
  3. Press Ctrl+Z - nothing seemingly happens (unless I press it fast enough multiple times)

Environment (please complete the following information):

Additional context Here's how undo stack looks like when I save the file:

>>> [view.command_history(i) for i in range(0,20)]
[('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1380], [[1, 0], [2, 0], '', 1380]]}, 1), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0)]

and here's how it looks after pressing Ctrl+Z 5 times:

>>> [view.command_history(i) for i in range(0,20)]
[('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1370], [[1, 0], [2, 0], '', 1370]]}, 1), ('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1372], [[1, 0], [2, 0], '', 1372]]}, 1), ('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1374], [[1, 0], [2, 0], '', 1374]]}, 1), ('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1376], [[1, 0], [2, 0], '', 1376]]}, 1), ('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1378], [[1, 0], [2, 0], '', 1378]]}, 1), ('lsp_apply_document_edit', {'changes': [[[0, 0], [1, 0], "import './a';\nimport './b';\n", 1380], [[1, 0], [2, 0], '', 1380]]}, 1), ('', None, 1), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0), ('', None, 0)]

so it looks like organizeImports reapplies every time I press Ctrl+Z (maybe twice?) - even though the sorting hasn't changed.

rchl commented 1 year ago

It's a know issue with Typescript where it provides "organize imports" when imports are already organized. LSP keeps applying this command until it times out. That's why you and up with your undo stack containing many of the same edits repeated.

There are probably some ways to work around this issue but it would probably have to be done in the base LSP package.

One way (but not a cheap and easy one) would be to try to apply the change without actually changing the file and see if anything has actually changed. If not then we could stop processing that code action. But it wouldn't be trivial to implement.

rchl commented 1 year ago

Or maybe we shouldn't be repeatedly asking servers for code actions and instead only do that once.

I feel like this might miss some fixes though. Especially with eslint where auto-fixing one issue might introduce another. I suppose it would be good to check what VSCode really does.

alexkuz commented 1 year ago

@rchl thank you for explanation. As a workaround I'll comment that extra call on code actions completion. Seems to be good enough for me.

One way (but not a cheap and easy one) would be to try to apply the change without actually changing the file and see if anything has actually changed.

I dug in the code a little bit and maybe it's easier to store view version somewhere and to roll it back if file's content didn't change, instead of always calling changes_count() directly. Still need to compare the whole file before-after though, so yeah, it's not cheap.

rchl commented 1 year ago

It's not possible to drop the last item from the undo stack, if that's what you'r thinking when saying "roll back".

Also the change_count is controlled by ST and incremented automatically on each change done to the view. We are just reading its value with change_count(), not changing it.

alexkuz commented 1 year ago

No, sorry for not been very clear, what I meant is now it works like this:

[code_actions]
version = change_count()
...
[edit]
(apply edits)
...
[code_actions]
if (version != change_count()):
  run again

instead it could be something like:

[code_actions]
version = change_count()
...
[edit]
edit_version = change_count()
(apply edits)
if (new_file != old_file):
  edit_version = change_count()
...
[code_actions]
if (version != edit_version):
  run again

so still applying all the edits, just not always relying on edit counter (I'm just not sure what's the proper way to handle edit_version here)

rchl commented 1 year ago

If we would be manually comparing the contents to see if the file has changed then we would not have to rely on change count at all. The problem is that this is a lot more expensive to do.