These are important questions. I think there is a trade-off between supporting many cases and having a simple desugaring. We should find a sweet-spot where the desugaring is reasonably simple and covers most idiomatic cases.
So I guess it's possible to detect the pattern:do x1 <- foo1; ...; xN <- fooN; [res <-] return (f {x1..xN})where {x1..xN} means "x1..xN" in some order"
and turn it into:do [res <-] (\x1..xN -> f {x1..xN}) <$> foo1 <*> ... <*> fooN
Open issues would be detection of the correct "return"-like thing.
The current desugaring of do-notation is very simple because it doesn't even need to know about the monad laws.
They are used implicitly by the optimiser (e.g., "foo >>= \x -> return x" is optimised to just "foo" after inlining), but the desugarer doesn't need to know about them.