Skip to content
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

Feat: Rename imports #119

Closed
wants to merge 7 commits into from
Closed

Feat: Rename imports #119

wants to merge 7 commits into from

Conversation

predragnikolic
Copy link
Member

When we rename or move a file,
the user is prompted to rename all imports.

This PR also adds a new setting updateImportsOnFileMove with the following options "prompt", "always", "never".

output

@predragnikolic
Copy link
Member Author

Notes:

  • I haven't tested this on Mac/Windows.
  • This feature will work only when we rename one file. There are some File Managers that allows renaming multiple files at once,
    but this feature wont work in cases like that.

rename_imports.py Outdated Show resolved Hide resolved
Copy link
Member

@rchl rchl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a bunch of pyright and mypy issues in those changes.

rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
if len(queued_events) == 2:
first_event, new_name = queued_events[0]
second_event, old_name = queued_events[1]
if (first_event == 'create' and second_event == 'delete'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid that there is not really a reliable way to detect renames. It could be just two unrelated files being added and removed (for example when changing branches).

Copy link
Member Author

@predragnikolic predragnikolic Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that chokidar has a way to detect a rename, but that is not the case.
there is a different lib, that supports rename detection => paulmillr/chokidar#303 (comment)

For the time being, I would rely on this logic:

        if len(self.events) == 2:
            create_event = next((event for event in self.events if event[0] == 'create'), None)
            delete_event = next((event for event in self.events if event[0] == 'delete'), None)
            if (create_event and delete_event):
                # Rename detected
                new_name = create_event[1]
                old_name = delete_event[1]
                self.prompt_rename(old_name, new_name)

If we have 2 events,
and when we have a create and delete event,
only then prompt the rename....

It could be just two unrelated files being added and removed (for example when changing branches).

While than can happen,
I do not know how to better handle that at the moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that chokidar has a way to detect a rename, but that is not the case.
there is a different lib, that supports rename detection => paulmillr/chokidar#303 (comment)

Interesting watcher. At least if we are to trust the author's claims.

I wouldn't want to switch to using it as that's too much work and the claims would have to be tested in practice also but we could take inspiration from it and implement support for rename events in our watcher.

I do not know how to better to handle that than at the moment.

Me neither that's why I'm suggesting to remove the potentially dangerous option to rename without confirmation.

Copy link
Member

@rchl rchl Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a different lib, that supports rename detection => paulmillr/chokidar#303 (comment)

BTW. The code style of that project is... interesting.

  • Blank lines between (almost) every line of code
  • excessive use of spaces (leading/trailing spaces within parenthesis for example)

https://github.com/fabiospampinato/watcher/blob/master/src/watcher.ts

It would not be fun to contribute to such project :)

rename_imports.py Outdated Show resolved Hide resolved
plugin.py Outdated
Comment on lines 68 to 69
if workspace_folders:
register_rename_import_file_watcher(workspace_folders[0].path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be done for every folder?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to add support form multiple folders,
I just need more time to think about how to handle the cleanup logic (when one folder closes, and so on)

I was thinking of having a Map, where the key is the root_path and the value is a RenameImportHandler instance.

Copy link
Member

@rchl rchl Jan 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you know if/when the folders have changed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple folders are semi supported.

The current code already supports sublime project.
When having a ST project with multiple folders:

// example.sublime-project
{
	"folders":
	[
		{
			"path": "/home/predragnikolic/Documents/sandbox/my-react-ts-app"
		},
		{
			"path": "/home/predragnikolic/Documents/sandbox/coc-tsserver"
		},
		{
			"path": "/home/predragnikolic/Documents/sandbox/volar"
		},
		{
			"path": "/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript"
		}
	]
}

It works until the moment you try to add/remove folders to a project while ST is open. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code only takes the path of the first folder.

plugin.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
rename_imports.py Outdated Show resolved Hide resolved
Comment on lines +29 to +31
if len(self.events) == 2:
create_event = next((event for event in self.events if event[0] == 'create'), None)
delete_event = next((event for event in self.events if event[0] == 'delete'), None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems very fragile and implementation dependent. The logic should just be: if there are two sibling events where one is 'create' and one is 'delete' then trigger the rename.

Same for the timeout that calls this method. There is no guarantee that both events will come within 10ms and if they don't, we'll clear the list and miss the whole rename.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to clarify, I meant without checking if there are exactly 2 events as that is not guaranteed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic should just be: if there are two sibling events where one is 'create' and one is 'delete' then trigger the rename.

We cannot rely on sibling events. :)

Here are the logs when renaming multiple files at once on Linux, .
the folder structure is the following:

├── test
│   ├── file1.ts
│   ├── file2.ts
│   └── file3.ts

if we rename multiple files at once,
we will have the following events logged:

events [('delete', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file1.ts')]
events [('delete', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file2.ts')]
events [('delete', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file3.ts')]
events [('create', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file11.ts')]
events [('create', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file21.ts')]
events [('create', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file31.ts')]
see the gif

output

So to jump to my point is,
we cannot rely on two sibilling events,
as that would result in incorrect behavior.

In the above example, it would mean to rename file3.ts to file11.ts, which is not correct.

events [('delete', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file3.ts')]
events [('create', '/home/predragnikolic/.config/sublime-text/Packages/LSP-typescript/test/file11.ts')]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case it seems like we need more sophisticated solution. Something similar to what is mentioned in the link you've posted before (paulmillr/chokidar#303 (comment))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also can imagine that 3 renames in a row would not be handled well by the quick panel that we are triggering.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally didn't want to support multiple file renames at once. only because I do not want to spam the user will prompts.

I only wanted to cover the case when a user renames a single file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case it seems like we need more sophisticated solution. Something similar to what is mentioned in the link you've posted before ...

Will improve the logic by checkin the iNode number of a file. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well,
I tried to use os.stat(file_name).st_ino to get the ino number for a file.

But I quickly run into problems.

Here was my soloution:

image

events_with_ino = [(event[0], event[1], os.stat(event[1]).st_ino) for event in events]
#					^^^^^^^^ event_type
#							  ^^^^^^^^ file_name
#										^^^^^^^^^^^^^^^^^^^^^^^^ ino number for that file_name

I tried to attach an ino number for a file_name,
but this works only for files that exist.

That said os.stat(event[1]).st_ino will work only for create or change events,
but not for when the event type is delete.
Because by the time we receive the delete event in python,
the file no longer exists,
so calling os.stat(file_name_that_no_longer_exist).st_ino will throw.


Ideally I would shift this logic to be the responsibility of the file_watcher,
so we have an interface like this:
https://github.com/neoclide/coc-tsserver/blob/master/src/server/features/updatePathOnRename.ts#L33

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supporting this in the watcher is the way to go but not sure if we would be able to do it in LSP-file-watcher-chokidar. Might need to be implemented upstream in chokidar instead.

rename_imports.py Outdated Show resolved Hide resolved
plugin.py Outdated Show resolved Hide resolved
super().__init__(*args, **kwargs)

def register_rename_import_file_watcher(self, session: Session) -> None:
allow_rename_of_import_path = session.config.init_options.get('preferences.allowRenameOfImportPath')
workspace_folders = session.get_workspace_folders()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is only needed with the if file_watcher condition.

@rchl
Copy link
Member

rchl commented Feb 21, 2023

Quite a lot of work was put into it but ultimately I don't think that this is the way to go. It would not be fine to trigger disrupting rename prompts whenever something would be renamed. Renames should only be handled by the server when user explicitly asks to rename some file so we need LSP-specific rename actions instead.

Maybe if there would be a way to offer the user suggestion to update imports in a non-distracting way then it would be more acceptable but it might be better to just only do that on explicit request to rename.

@predragnikolic
Copy link
Member Author

Agreed, I will close this PR.

I saw the willRename PR on the ts server repo.
I believe that is the way to go.

@rchl rchl deleted the feat/rename-imports branch February 21, 2023 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants