Notes on constants highlighting. Part 2/2

This is the second post on the constants highlighting in Sanny Builder. Read the first one there.



After assessing the first naive implementation it became obvious that the scanning of a source file in the GUI (main) thread impacts the editor performance severely. So I made a decision to move this process into a separate thread.


I created a new service, called the language service, that lives in the program memory for the entire lifetime and provides an API to register source files and monitor their changes. Initially I just wanted to rescan files whenever they are changed on disk, i.e. when a user saves the file. In this implementation when the user opened a file in Sanny Builder or saved the opened file, the editor invoked the service API with the file path. Then the service read the file from the disk and started scanning it. If there were imported files (e.g. via the $INCLUDE directive), the service added them into the document tree and watched their changes too. Once the scanning phase was completed the service sent a message to the main thread to notify the editor that it can re-highlight the source code. The highlighter used another service API method to check if a symbol was in fact a constant name and then colorized it accordingly.


It worked very well and didn't slowed down the editor performance. The user could work with many opened documents and save them occasionally to see their changes reflected in highlighting. Or, if they opened one of the files constituting the document tree in another text editor and changed the content, it also triggered the highlighting update.


But there was an issue. It didn't work if the source code lived only in the Sanny Builder memory and had never been saved to the disk. Imagine that you have created a new document and write some code. No constants get highlighted until you save it to the disk.


To fix this issue the service-client interaction model was changed. Instead of registering a file name with the service, the editor sent the current source code (as a null-terminated string) to the service to let the latter read it and build the document tree with all imported files. The rest of the flow remained the same. But there was a question when to send the source code to the service? Files opened in Sanny Builder can be really huge, comprising of hundreds of thousands of lines. Copying the text buffer between the main and service threads on each button press is CPU and memory hogging and causes the editor slowness.


After a few iterations I came up with the following model to solve this problem. First of all, the amount of lines that the editor sends to the service has been limited. There is a value in the Sanny's options that controls the range in which we are looking for declared constants. By default it's equal to 250. The user may increase the value if their workstation allows. In most cases it's not necessary to scan each of preceding thousands of lines as the constants usually get declared closer to their usage. 


The second, the throttling method has been implemented. To avoid unnecessary re-scans while the user is quickly typing, a simple state machine was added. Both the editor and the service could be in several states: "idle", "pending scan", and "scanning". Initially both are in the "idle" state. If the source code changes, the editor sets itself in the "pending scan" state. All subsequent changes does not influence the state and just get ignored. Once the service is "idle", the editor that is in the "pending scan" state sends the source code to the service and changes its state to "scanning". When the service is done with scanning the code and collecting the symbols it becomes "idle" again. The editor then updates highlighting and sets itself to the "idle" state. And it all repeats.


Finally, a few refactorings and improvements then were made such as caching intermediate results and use them if the imported files didn't change ever since.


With this implemented, the editor could reflect code changes in real-time and highlight the constants as soon as you added them in the code. It also scans the files that are implicitly available to the user, such as constants.txt in the edit mode config and make those constants highlighted too.


I've been very satisfied with the result. I learned a lot about concurrent programming and different ways to synchronize threads and pass data between them, including message buses and mutexes. The Rust language and crates like hotwatch helped to implement this functionality with confidence. 


All in all, the language service opens new ways to improve the user experience as I now have an ability to seamlessly collect any type of information from the source code.


Code for the service and all attempts can be found in the pull request on GitHub.