-
Notifications
You must be signed in to change notification settings - Fork 283
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(workspace): add drag and drop support to EditorView #1759
Conversation
If you drag multiple files onto the editor view, it will check all the paths to make sure they're files (not dir, socket, link, etc.) If they are files, it will dispatch an OpenFileByPath action, which will/should open the file in the editor!
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.
Cool! I think the general approach is good as well, but the implementation needs some architectural work.
I've also changed the title, as the idea of the scope/area part of the title is to provide a general category for the changelog entry so it can be filtered on and such. drag-and-drop
is waaay too specific to be useful for that purpose. The contribution guide suggests using the area issue labels or the name of a feature project.
src/UI/EditorView.re
Outdated
let stats = Unix.stat(path); | ||
// This tests if the path dropped is a file. We can't handle opening directories, etc. yet | ||
if (stats.st_kind == S_REG) { | ||
GlobalContext.current().dispatch( | ||
Model.Actions.OpenFileByPath(path, None, 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.
side-effects should not be triggered in the UI, as that makes it difficult to track and control what's happening, and to evolve the logic when we also need to update the state, for example. In short, it leads to spaghetti code. The rule of thumb is that the UI should only dispatch messages to be handled by stores/update functions, which then update the model and emit side-effects through Isolinear.Effect
.
But even more generally, even in imperative-land, doing blocking I/O on the UI thread is considered a big no-no as it blocks the UI from rendering and in some cases can really hurt responsiveness.
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 that's my bad -- I kinda just quickly worked on this to get it to somewhat work. I should have added a WIP tag on the PR generally.
Thanks for the info!
@@ -0,0 +1,3 @@ | |||
[@deriving show({with_path: false})] |
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 might seem small for now, but this is the perfect place for us to add a model once we incorporate dragging 👍
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 have to admit I'm a bit more concerned. Not so much because of the granularity, although that is a minor concern too, but more because this seems to be a cross-cutting concern. It's not clear to me how this will integrate with other features that need to support drag and drop. Why would dropping a file on the file explorer, for example, even need to involve a separate file drop feature project? It seems like very little code would live here, but would instead have to be implemented in the consuming feature projects (file explorer/workspace) or in the glue code, as is largely the case here.
I can see that there's a missing feature project here for this specific feature to live in however, but I think that missing project is for the workspace as a whole.
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.
Not so much because of the granularity, although that is a minor concern too, but more because this seems to be a cross-cutting concern. It's not clear to me how this will integrate with other features that need to support drag and drop. Why would dropping a file on the file explorer, for example, even need to involve a separate file drop feature project?
Right, I think this is a fair concern! It seems like - at least the way this is implemented right now - FilesDroppedOnEditor
- makes sense to be part of the Feature_Editor
(since it's handling the use-case where a file is dropped on the editors, and not on the explorer or other parts of the shell, at the moment).
The main issue is that Editor
is one of our least-factored features at the moment - we don't have a central msg
or updater
yet.
We could start a Feature_Editor.msg
with FilesDropped
, and stub out an updater - and then incrementally move the remaining functionality towards that (ie, there are lots of Editor*
actions in Actions.t
that could be migrated to this msg
later).
Looking good to me, @zbaylin ! Awesome work.
I'm doing some work in this area today, so I'll investigate this in a bit. Your code seems fine, so I think it's exposing / hitting a bug. One thing we should think about are the set of test cases we should try to make sure this is ready-to-go - some tests I can think of:
And then a couple other thoughts:
|
Sounds good. Thanks for that.
I can test on Linux. Unfortunately I can't get Oni to build on my Windows VM for various reasons.
VSCode switches workspaces on a single folder being dragged. I'm not sure what the behavior is for multiple folders, or a folder mixed in with files. I can look into that today. |
Not for this PR's scope -- but eventually we'll want to expand this to the file tree. I can't think of any more specific tests for this PR. If and when you figure out that bug with multiple files we can test that, but other than that I can't think of anything. |
Sounds great @zbaylin ! |
One other test case I think of is verifying on the packaged application (just in case there's something busted with the |
@bryphe Linux works! |
That sounds a lot like the issue I had with opening both the update screen and welcome screen initially. It would go into some kind of infinite BufferEnter loop. Solved it by just replacing the welcome screen instead, which is less than ideal, so it'd be cool if fixing this bug solved that as well. |
Definitely @glennsl - I'm working through cleaning up that codepath today, to reduce the awkward timing dependencies:
and then:
Hoping to fix the multiple-buffer-open issue, along with these issues today: |
It might take a bit of time to work through it - sorry that you're both blocked on this! In the meantime, @zbaylin - a couple options:
|
@@ -0,0 +1,3 @@ | |||
[@deriving show({with_path: false})] |
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 have to admit I'm a bit more concerned. Not so much because of the granularity, although that is a minor concern too, but more because this seems to be a cross-cutting concern. It's not clear to me how this will integrate with other features that need to support drag and drop. Why would dropping a file on the file explorer, for example, even need to involve a separate file drop feature project? It seems like very little code would live here, but would instead have to be implemented in the consuming feature projects (file explorer/workspace) or in the glue code, as is largely the case here.
I can see that there's a missing feature project here for this specific feature to live in however, but I think that missing project is for the workspace as a whole.
src/Store/Features.re
Outdated
Isolinear.Effect.createWithDispatch( | ||
~name="feature.filedrop.openFiles", dispatch => { | ||
List.iter( | ||
path => { | ||
let stats = Unix.stat(path); | ||
if (stats.st_kind == S_REG) { | ||
dispatch(OpenFileByPath(path, None, None)); | ||
}; | ||
}, | ||
paths, | ||
) | ||
}) |
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.
Ir would be nice to have this effect moved out of the update function, to avoid it turning into a blob. I also think ti does too much. Ideally this would be two separate effects, one for the stat
, which ought to be non-blocking and dispatch a new user-supplied message when done, and then another to open the file. That would also enable the stat effect to be moved into the feature project, while the open file effect would need to be an outmsg
because it's hard-coded to Actions.t
for historic reasons.
This area is still a bit murky with a lot of historical baggage and few good examples of how to do these things properly, so don't worry too much about it, but the general lesson here is that supplying the message to be dispatched when it is invoked allows effects to be generalized.
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.
Here's one example of such an effect:
oni2/src/Store/FileExplorerStore.re
Lines 11 to 25 in d971dae
let load = (directory, languageInfo, iconTheme, configuration, ~onComplete) => { | |
Isolinear.Effect.createWithDispatch(~name="explorer.load", dispatch => { | |
let ignored = | |
Configuration.getValue(c => c.filesExclude, configuration); | |
let tree = | |
FileExplorer.getDirectoryTree( | |
directory, | |
languageInfo, | |
iconTheme, | |
ignored, | |
); | |
dispatch(onComplete(tree)); | |
}); | |
}; |
Used here:
oni2/src/Store/FileExplorerStore.re
Lines 52 to 59 in d971dae
Effects.load( | |
lastNode.path, | |
state.languageInfo, | |
state.iconTheme, | |
state.configuration, | |
~onComplete=node => | |
Actions.FileExplorer(FocusNodeLoaded(node)) | |
), |
And another:
oni2/src/Feature/SCM/Feature_SCM.re
Lines 65 to 74 in d971dae
let getOriginalUri = (extHostClient, model, path, toMsg) => { | |
let handles = | |
model.providers |> List.map((provider: Provider.t) => provider.handle); | |
ExtHostClient.SCM.Effects.provideOriginalResource( | |
~handles, | |
extHostClient, | |
path, | |
toMsg, | |
); | |
}; |
Used here:
oni2/src/Store/ExtensionClientStoreConnector.re
Lines 246 to 249 in d971dae
Feature_SCM.Effects.getOriginalUri( | |
extHostClient, state.scm, path, uri => | |
Actions.GotOriginalUri({bufferId: metadata.id, uri}) | |
), |
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.
@bryphe and I worked this out last night and I think the consensus we came to was that eventually the drag and drop feature will have it's own state once the drag part is implemented. I.e. if you held a dragged file over a certain area for x amount of time, something happens, etc. I'm obviously not as familiar with the underlying architecture as the two of you are, so it's possible @bryphe is able to explain better than I am.
I understand what you mean with regards to separating the actions -- I can do that. Thanks for the examples!
@glennsl - do you think we should move this to a feature project like
So something like |
Yeah, I think that would be more natural. You'd have a hierarchy of features, akin to a hierarchy of components, with the File Explorer as a sub-feature. With ownership of a piece of state at the highest level necessary in the hierarchy. Having state "on-the-side" leads to unclear ownership I think.
I would say more like: Service_Filesystem.stat: (~path: string, ~onResult: StatResult => 'msg) => Isolinear.Effect.t('msg) That is, moved to a service project, or even an external library, and using |
…g-drop/open-file
module Effect: {let openURL: string => Isolinear.Effect.t(_);}; | ||
module Effect: { | ||
let openURL: string => Isolinear.Effect.t(_); | ||
let stat: (string, Unix.stats => 'msg) => Isolinear.Effect.t('msg); |
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.
Thanks for the refactoring here, Zach!
If you drag multiple files onto the editor view, it will check
all the paths to make sure they're files (not dir, socket, link, etc.)
If they are files, it will dispatch an OpenFileByPath action, which
will/should open the file in the editor!
Todo:
Test Cases: