Skip to content

Syntax validation using workers

Samuel Pastva edited this page Jun 10, 2018 · 5 revisions

In order to perform syntax checking in Ace, you typically need a web worker which will do the heavy lifting asynchronously in the background. Ace provides web workers for several programming languages, but building a custom worker is not exactly simple, because you have to integrate it into Ace and then build it from source.

To make this process a little more accessible, the worker module provides the necessary Ace classes in a separate javascript file, which you simply load before your main worker script. This will give you access to the standard Ace classes like Mirror and Document which you can use to communicate with the editor.

Project structure

Remember that the worker always runs in a separate context and therefore you should not mix worker and web code in one module (you can include it, but only as a runtime dependency). What you need to do is to create one module with your worker code and then another for your web related code. If you have some code you want to share between those two (such as the parser or the tokenizer), put it into a separate module, ending up with a structure similar to this:

common:
   depends on whatever you need...
my-worker:
   implementation common
   implementation ace-worker
my-web:
   implementation common
   implementation ace-web
   runtime my-worker

The full example of how to set-up your worker related classes is in the demo and demo-worker modules.

WARNING: Unfortunately, due to the way Ace handles workers, this approach does not work in pure static html files, because the worker is started as a blob and cannot access the dependencies using relative paths or file paths. Therefore, in order to use workers created using this method, you will have to serve them using a web server. Of course, for testing, you can use a local web server, the only restriction is that each dependency needs an absolute url.

Creating a web worker

Once the modules are created, you can define your first web worker. You will want to extend the Mirror class, as it gives you access to a copy of the Document managed by the live editor:

class DemoWorker(sender: Sender) : Mirror(sender) {
   init { setTimeout(2000) } // set the refresh interval
   
   override fun onUpdate() {
      // Perform syntax validation - using document to access the current text in the editor
      // and sender.emit to send the results back to the main context.
   }
}

fun main(args: Array<String>) {
   // You also have to register the worker class in your main method, otherwise
   // Ace won't know how to find it.
   DemoWorker::class.js.register()
}

The full example is again included in the demo module. Note that here, we perform a very simple task (checking the parenthesis match), so we don't have any common module.

WARNING: The data you send using sender.emit will be serialised and deserialised in order to move it between the contexts. Kotlin's meta-information about classes will be lost during this process. Therefore, you can only send primitive types (including Arrays) if you want to preserve type safety. You can also send structured data, but you have to treat the data as dynamic objects in your web module because the type info is missing.

Starting a web worker

To start the worker, you have to override the createWorker method in your custom TextMode and create a WorkerClient which will run and communicate with our worker. In order to safely load all the dependencies, the worker module provides a worker-init.js file, which will load all dependencies sent using the importScripts parameter of WorkerClient. To make this process more intuitive, there is a convenience method named startWorkerClient which you can use:

override fun createWorker(session: EditSession): WorkerClient? {
   val client = startWorkerClient(
      workerClassName = "DemoWorker", // the name of your worker class
      workerInitUrl = "http://.../worker-init.js" // path to the worker-init.js script
      dependencies = arrayOf(
         "http://.../kotlin.js",       // first, load Kotlin   
         "http://.../ace-classes.js",  // then load Ace worker bundle
         "http://.../ace-common.js",   // wrapper for common classes
         "http://.../ace-worker.js",   // wrapper for worker-specific classes
         "http://.../my-worker.js",    // your custom worker
      )
   )
   client.attachToDocument(session.getDocument())
   return client
}

This will load the given JS files and then start a worker with class name "DemoWorker" (assuming it is properly registered).

Communication

In order to listen to events sent from the worker context, you simply need to listen to events emitted on the corresponding WorkerClient. Ace will also wrap your data into a custom event object, which is described by the WorkerClient.Event interface:

// In worker context:
sender.emit("isValid", true)

// In main context:
client.on<WorkerClient.Event<Boolean>>("isValid") { event -> 
   if (event.data) ... // do something
}
Clone this wiki locally