Sublime Forum

Maintain view object for async operations

#1

Hello,

In my plugin I have a command which invokes an asynchronous operation. Then, based on the result of that call, I want to make operations on the view object of that command (self.view). The problem is that I can’t pass view objects as parameters. So currently, I just wait for the asynchronous call to finish, which practically, makes it synchronous.

Here is a link to the actual code from my plugin:

Just to clarify - rc_thread.execute_rc(rc_params) is the invocation for the asynchronous operation, and rc_thread.received_output is the data which I would like to base my future logic upon.

Is there a way to extract the code that comes after the asynchronous invocation to a separate method, to be called automatically by rc_thread, with access to the self.view instance (or maybe with a way to retrieve it)?

P.S. I thought about creating a dedicated command for each asynchronous operation, but that has two disadvantages:

  1. In case the asynchronous operation takes a while, the user might change the active view and the command will operate on a different view then the original, AKA undefined behaviour…
  2. It will spam the “plugins environment” with a bunch of commands that shouldn’t be called directly. And I feel that it is bad practice.

Thank you for your time,
Have a lovely day :smile:

0 Likes

#2

A sublime.View has an id(), which is just an integer. You can create a sublime.View from this integer. So you can pass around the integer in your callback. After reconstructing the view with v = sublime.View(the_integer_id), you can check wether it’s still valid with v.is_valid().

3 Likes

#3

Thanks @rwols , that’s an interesting idea, which seems like a good starting point.

So let’s say that I’m passing along the id of the view, and creating a new view instance using this undocumented constructor.

Since everything is asynchronous, the view can become invalid at any moment, right? Which means that even if I call view.is_valid(), a moment later it may become invalid if the user, for example, closes that view. What is the result of operations on invalid views? Can I somehow make sure that the view stays valid?

Also, how strong is the uniqueness of the view’s id? Can a new view get an id of a past view which no longer exists?

Moreover, when creating the view with sublime.View() I see that no window is given. Does it know automatically to associate the view instance with the correct window, or does it assume that it should always belong to the active window? (this is relevant for cases when the user switched windows in the middle of the command).

0 Likes

#4

According to the code below, taken from an active plugin, it seems that it is possible to pass view object as parameter.

So I guess that the limitation on passing views as parameter is regarding sublime command, and not all functions. That makes me a little confused. Nonetheless, it doesn’t solve all the problems I raised in the previous post.

0 Likes

#5

As I understand it, Sublime’s asynchronicity is much like JavaScript’s: each individual event is processed synchronously. So a view shouldn’t go away in the middle of a command. Because you are explicitly executing code asynchronously, you should check the view when you resume, and that should suffice.

I believe that you are correct regarding passing the view. The command interface won’t accept complex objects because command arguments are serialized, but this does not apply to most API methods. Passing the view directly should avoid all of the other problems you mentioned.

2 Likes

#6

Thanks @ThomSmith for clearing things up.

So if I understand you correctly:
For every text command which involves an asynchronous operation, I should create a supplementary text command to be invoked by the asynchronous operation. This will be done using view.run_command() with the view object that it got as a parameter from the original text command. Right?
This way I can resume back to a synchronous operation to assure a safe usage of valid view object.

When applying this on my code I provided in my first post:
rc_thread.execute_rc(...) should receive 3 additional parameters:

  1. A callback function (which will be executed asynchronouslly)
  2. self.view (which will be used in view.run_command())
  3. The name of the supplementary text command (to execute when the callback finishes)

Does this seems like a good approach for handling my problem? I love everything about it besides that it makes me have twice the commands the user should actually use.

Edit:
After re-reading your response, I think it’s worth clarifying that I want my command to be non-blocking. Hence, my goal is to remove the call for rc_thread.join(), yet maintaining an access to the view instance.

0 Likes

#7

I would imagine that sublime.set_timeout_async should work without having to manually run threads. In addition, I would expect that the callback passed to set_timeout_async would be handled similarly to a command so that you shouldn’t need to create a second command yourself. I haven’t tried it out myself.

1 Like

#8

That indeed sounds much simpler. However, when passing the view object to the callback, the view is not guaranteed to stay valid throughout the entire operation, right?

I mean, if I perform view.is_valid() at the first line of the callback, the view might become invalid as soon as the second line is executed (in case the user closed the view, for example) - since the callback is asynchronous.

Is there a way to avoid that without creating a second text command and running it from the callback?

0 Likes

#9

Just wrap anything you do with the view in a try block (in the callback) and catch the error if the view becomes invalid.

0 Likes

#10

After some experimentation, it looks like operations on an invalid view are just ignored. There wouldn’t even be an exception unless something else fell apart as a result (e.g. relying on myView.window() not being None).

2 Likes

#11

I personally use the following - _view.is_valid( ) would result in an error if _view is None… so I have some helper functions tell me about it ( I actually use file_name( ) != None as an is valid check although that could be changed - I use it because in my system everything needs a name )

##
## Ternary Operation in Python is just weird and has problems with stacking.. This fixes that...
##
def TernaryFunc( _statement = True, _true = None, _false = None ):
	return ( _false, _true )[ _statement ]


##
## Helper - Returns whether or not the view is not none and is a sublime text view...
##
def IsSublimeView( _view = None ):
	return _view != None and isinstance( _view, sublime.View )


##
## Helper - Returns whether or not the view is valid by ensuring the file name is not none...
##
def IsValidSublimeView( _view = None ):
	return IsSublimeView( _view ) and _view.file_name( ) != None and _view.file_name( ) != ''


##
## Helper - Returns the Sublime Text View File-Name ( either in full with the path if _extended is True, or the BaseName.ext by default )
##
def GetSublimeViewFileName( _view = None, _extended = False ):
	return TernaryFunc( IsValidSublimeView( _view ), TernaryFunc( not _extended, _view.file_name( ).split( '\\' )[ - 1 ], _view.file_name( ) ) , '' )

## or

# def GetSublimeViewFileName( _view = None, _extended = False ):
# 	if ( IsValidSublimeView( _view ) ):
# 		_filename = _view.file_name( )
# 		if ( not _extended ):
# 			return _filename.split( '\\' )[ - 1 ]
# 		else:
# 			return _filename
# 
# 	return ''

Just because something is a SublimeView doesn’t mean it is valid in my case… I’ll try my system with is_valid to see if anything breaks - but helper functions like this save a ton of time, and they’re easily identifiable in their function…

0 Likes

#12

Thank you all for your help!

I made some experiments with the following code:

import sublime
import sublime_plugin
import functools
import time


class AsyncOperationCommand(sublime_plugin.TextCommand):
    """Calls an asynchronous operation, with a callback to be executed
       synchronously afterwards.
    """

    def run(self, edit):
        print("The command '{}' has been triggered.".format(
            self.__class__.__name__))

        sublime.set_timeout_async(
            functools.partial(
                self.async_method, self.view,
                functools.partial(self.view.run_command, "sleep")),
            2000)
        print("timeout is set")

    def async_method(self, view, callback):
        """ A method to be run asynchronously, and execute callback when finish
        """
        print("inside async_method")
        if view.is_valid():
            print("View is valid!")
        else:
            print("View is invalid!")
        time.sleep(5)
        print("done sleeping")
        if view.is_valid():
            print("View is valid!")
        else:
            print("View is invalid!")
        callback()


class SleepCommand(sublime_plugin.TextCommand):
    """Performs a sleep of 2 seconds. Power nap!"""

    def run(self, edit):
        print("The command '{}' has been triggered.".format(
            self.__class__.__name__))

        time.sleep(2)

        print("SLEEPING TIME IS OVER")

I played with it by executing sublime.active_window().active_view().run_command("async_operation") in the console.

I found out that:

  1. The view object can become invalid in the middle of async_method (by closing the active view)
  2. Invoking view.run_command() on an invalid view does nothing (no exception, no return value)
  3. It is possible to invoke a synchronous operation (on the main thread) from an asynchronous operation (on a separate thread)

Conclusion

A text command always starts in the main thread, running synchronously. To make it run asynchronously, put the logic in a separate function, and invoke it from the text command - either via sublime.set_timeout_async() or using a thread instance.

However, manipulating a view (for example, with view.insert()) inside the asynchronous function is dangerous, since the view might become invalid (closed by the user) at any point in time! In order to solve this, the logic which involves view manipulation must execute on the main thread, synchronously.

This can be acheived in two ways:
The first way is to create a supplementary text command which contains the view manipulation logic. The asynchronous function will invoke that supplementary command via view.run_command(), and as I stated at the begginning, a text command always executes on the main thread.

A code example for the first way can be found right above this section.

The second way is to create a variable and a function. The variable stores a boolean telling whether the asynchronous operation has finished, and the function checks that variable. If the variable’s value is true, the operation continues. If the value is false, sublime.set_timeout(function) is called, and the function returns. The text command should contain a call for that function at its bottom.

A code example for the second way can be found in this awesome guide.

Each way has its pros and cons, and I wish there was a better/easier way. But at least I got it all figured out now, thanks to you guys :wink:

So thank you very much, and I hope that my findings will help others as well.

0 Likes