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(workspace): add drag and drop support to EditorView #1759

Merged
merged 9 commits into from
May 26, 2020

Conversation

zbaylin
Copy link
Member

@zbaylin zbaylin commented May 13, 2020

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!

oni2-drag-drop1

Todo:

  • Investigate multi-file drop (@bryphe )

Test Cases:

  • Single file drop on Windows (dev build)
  • Single file drop on Windows (packaged build)
  • Single file drop on OSX (dev build)
  • Single file drop on OSX (packaged build)
  • Single drop on Linux

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!
@glennsl glennsl changed the title feat(drag-and-drop): add drag and drop support to EditorView feat(workspace): add drag and drop support to EditorView May 13, 2020
Copy link
Member

@glennsl glennsl left a 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.

Comment on lines 28 to 34
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),
);
};
Copy link
Member

@glennsl glennsl May 13, 2020

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.

Copy link
Member Author

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!

@zbaylin zbaylin added A-workspace Area: Overall UX, editor layout, tabs/groups/splits etc. enhancement New feature or request WIP labels May 13, 2020
@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

@glennsl I re-architected this to what (I think) is more in line with the Isolinear/Oni architecture as a whole. Let me know if I messed anything up.

And thanks again to @bryphe for walking me through the architecture last night!

@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

As a side note, I'm getting weird behavior with dropping multiple files that I obviously would like to fix before this gets merged. If I drop two files and then try to close one of them, it just swaps positions with the other in the tabs.

Probably easier to just watch what I mean:
oni2-multi-drag-drop-error

You can't really tell from the framerate of the GIF but the log in the back is also going crazy -- also not sure why.

All I'm doing is dispatching, in this case, 2 OpenFileByPath Actions, so I'm not exactly sure why that would cause this behavior.

src/Feature/FileDrop/dune Outdated Show resolved Hide resolved
@@ -0,0 +1,3 @@
[@deriving show({with_path: false})]
Copy link
Member

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 👍

Copy link
Member

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.

Copy link
Member

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).

src/Store/Features.re Outdated Show resolved Hide resolved
@bryphe
Copy link
Member

bryphe commented May 14, 2020

Looking good to me, @zbaylin ! Awesome work.

All I'm doing is dispatching, in this case, 2 OpenFileByPath Actions, so I'm not exactly sure why that would cause this behavior.

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:

  • Single file drop on Windows
  • Single file drop on OSX
  • Single drop on Linux

And then a couple other thoughts:

  • How does this work with Zen mode? Does it switch to the last file, or should it leave zen mode? 🤔
  • For now, we just ignore directories, but maybe as a next step we could look at changing directories / workspaces, if just a single folder is dragged.

@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

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.

Sounds good. Thanks for that.

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:

I can test on Linux. Unfortunately I can't get Oni to build on my Windows VM for various reasons.

For now, we just ignore directories, but maybe as a next step we could look at changing directories / workspaces, if just a single folder is dragged.

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.

@bryphe
Copy link
Member

bryphe commented May 14, 2020

I can test on Linux. Unfortunately I can't get Oni to build on my Windows VM for various reasons.

Ok - I added a list of test cases at the top so we get a nice progress bar on the PR to track whats left:
image

I'll test Windows for you - and seems like OSX is already covered. Are there any other test cases you can think of that we need to check?

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.

Awesome, that'd be great! We can implement this in a separate PR, so it's not blocking this work. I'm thinking we may want another msg to model this, like FolderDropped

@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

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.

@bryphe
Copy link
Member

bryphe commented May 14, 2020

Sounds great @zbaylin !

@bryphe
Copy link
Member

bryphe commented May 14, 2020

One other test case I think of is verifying on the packaged application (just in case there's something busted with the Info.plist metadata - I added it to the list). Let me know if you need any help creating a packaged build via esy create-release

@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

@bryphe Linux works!

GIF:
oni2-single-drag-drop-linux

@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

Same with macOS packaged!

GIF:
oni2-single-drag-drop-mac-packaged

@glennsl
Copy link
Member

glennsl commented May 14, 2020

You can't really tell from the framerate of the GIF but the log in the back is also going crazy -- also not sure why.

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.

@bryphe
Copy link
Member

bryphe commented May 14, 2020

That sounds a lot like the issue I had with opening both the update screen and welcome screen initially

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:

@bryphe
Copy link
Member

bryphe commented May 14, 2020

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:

  • We could restrict this PR to just a single file, and merge as-is, and add in the multiple-file behavior once I've worked those open file issues
  • You could switch gears to another piece of work (like the changelog links?), and we could merge this to master once I've finished that set of work and the multiple-file-open is unblocked

@zbaylin
Copy link
Member Author

zbaylin commented May 14, 2020

@bryphe I'm fine leaving it until the multiple files bug is fixed. I can help @glennsl with the changelog in the meantime!

@@ -0,0 +1,3 @@
[@deriving show({with_path: false})]
Copy link
Member

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.

Comment on lines 193 to 204
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,
)
})
Copy link
Member

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.

Copy link
Member

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:

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:

Effects.load(
lastNode.path,
state.languageInfo,
state.iconTheme,
state.configuration,
~onComplete=node =>
Actions.FileExplorer(FocusNodeLoaded(node))
),

And another:

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:

Feature_SCM.Effects.getOriginalUri(
extHostClient, state.scm, path, uri =>
Actions.GotOriginalUri({bufferId: metadata.id, uri})
),

Copy link
Member Author

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!

@bryphe
Copy link
Member

bryphe commented May 15, 2020

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.

@glennsl - do you think we should move this to a feature project like Feature_Workspace? In discussing we figured the state that we managed would potentially be a drag state for the gesture (ie, the drag events Zach implemented here: revery-ui/esy-sdl2#13

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

So something like Feature_Workspace.Effects.stat: (~path: string, ~mapResult: StatResult => 'msg) => Isolinear.Effect.t('msg)?

@glennsl
Copy link
Member

glennsl commented May 16, 2020

do you think we should move this to a feature project like Feature_Workspace?

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.

So something like Feature_Workspace.Effects.stat: (~path: string, ~mapResult: StatResult => 'msg) => Isolinear.Effect.t('msg)?

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 ~onResult instead of ~mapResult as that suggests more strongly that the 'msg will be dispatched.

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);
Copy link
Member

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-workspace Area: Overall UX, editor layout, tabs/groups/splits etc. enhancement New feature or request WIP
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants