Allowing users to override package behavior


#1

I would like to provide an easy way for users to change the default behavior of a function in a package I’m writing. The main idea is to provide users with a way to preprocess a block of text before my package processes it. There are packages out there that do it but I don’t really understand how. I know a few things but I’m not well versed in [Java|Coffee]Script or in all things async.

I read about services. Those seems to require that a user write another package to consume them. It wasn’t clear to me from the docs and looking at packages that provide services whether

  1. services allow users to change the behavior of a package, or
  2. users can consume services without writing a package (e.g., from the init script).

As an alternative, I tried emitting events in my package that users can subscribe to (in init) to implement some functionality and return results via a callback. That looks viable but I ran into errors here too.

I’m happy to show code excerpts to illustrate what I’ve tried but I was wondering whether there’s an obvious way to accomplish what I’m after. A simple example would greatly help. Google and the flight manual haven’t provided much help.

Thanks!


#3

Suppose I have a standard package layout with a simple command that calls a function to process the text in the editor (in whatever way):

module.exports =
  subscriptions: null

  activate: ->
    @subscriptions = new CompositeDisposable
    # add a command
    @subscriptions.add atom.commands.add 'atom-workspace',
      "myPkg:do-something": -> @doSomething()

  doSomething: ->
    return unless editor = atom.workspace.getActiveTextEditor()
    text = @preProcess(editor.getText())
    text = @process(text)
    ...

Let’s say I provide default functionality for preProcess in my package:

  preProcess: (text) -> text # do nothing

What’s a convenient way (from a user’s perspective) to implement different behavior for preProcess? The user could want to, e.g., reverse the text, append some text, etc. Ideally, they wouldn’t have to write a new package to do it, or create new files, etc., but do it from their init script if possible.

I couldn’t understand if/how services can be useful here.


#4

I think part of the difficulty is that I need to retrieve the value returned by the user’s preprocessor. I came up with the following, based on events. In doSomething, instead of calling preProcess and process, I emit an event that the user can subscribe to and I supply a callback. Here’s a trimmed-down example. No arguments are involved, but it shows the idea.

{CompositeDisposable} = require "atom"

module.exports =
  subscriptions: null

  activate: ->
    @subscriptions = new CompositeDisposable
    @subscriptions.add atom.commands.add 'atom-workspace',
      "test-pkg:do-something": => @doSomething()

  deactivate: ->
    @subscriptions.dispose()

  onWillPreprocess: (callback) ->
    atom.emitter.on "will-preprocess", callback

  preProcess: ->
    console.log "standard preprocessing"

  process: ->
    console.log "processing"

  doSomething: ->
    atom.emitter.emit "will-preprocess", @process

In the user’s init.coffee, they could write

atom.packages.onDidActivatePackage (pkg) ->
  if pkg.name is "test-pkg"
    pkg.mainModule.onWillPreprocess (callback) ->
      console.log "user-defined preprocessor in action!"
      callback()  # when finished, call the callback

So when will-preprocess is emitted, the user-defined preprocessor is called, and when they’re done, they call the supplied callback, which in this case, is our process function. That works. However, here’s my first question:

Question 1: if the user doesn’t subscribe to the event, nothing will happen. How can I define default behavior in that case (say, call preProcess followed by process)?

Here’s another difficulty with the strategy above. If process calls another package function, I get an error. Suppose

  pkgFunction: ->
    console.log "in package function"

  process: ->
    console.log "processing"
    @pkgFunction()  # <- trouble!

Question 2: I get Uncaught TypeError: this.pkgFunction is not a function. It may have to do with the scope of the callback supplied when emitting the event, but I’m not sure how to fix it?!

If you read this far, thank you!


#6

The scope issue is solved by changing doSomething to

doSomething: ->
    do (@pkgFunction, @process) ->
      atom.emitter.emit "will-preprocess", @process

but I now realize that this approach doesn’t scale. I found a simpler and better approach and will post when I have things working satisfactorily.