DESOSA 2021

VSCode - From Vision to Architecture

Main Architectural Style

Let’s start by taking a look at the project as a whole. As stated in the Source Code Organization document1, the project takes both a layered as well as a modular approach to software architecture for the core package. This core package is built to be extended using the Extension API to provide nearly all features that exist in VSCode. The layer model of the core starts with a base package which provides “general utilities and user interface building blocks.” Next comes the platform which handles all services through dependency injection. The next layer is the monaco2 editor, where the users can write their code. The final layer is the workbench, which hosts the Monaco editor, but also all other “viewlets”, i.e. the file explorer and status bar. These layers all have their own package/folder in the project, which is where the modular pattern comes from. Some modules such as base and editor could be used standalone, while all of them could be swapped with different implementations of the same interfaces and still be fully functional. This style of architecture design helps separate different responsibilities of the code editor both for implementing and testing3. It makes it clear for developers where they should implement certain parts of their features and changes in one layer will not affect another layer, which increases maintainability.

Containers & Components

For a good understanding of the system’s architecture, we present a high-level overview of the VS Code project. When talking about main logical execution environments, we refer only to the desktop application environment.

Figure: Container View

Visual Studio Code and all of its components are encapsulated inside a single container: The desktop application.

Figure: Component View

Being a code editor with capabilities similar to most integrated development environments, the container view of VS Code comes to a single application with many components. Everything that the user sees and can interact with is rendered to the screen by Electron. As a user you can write your code using the monaco text editor2; debug that code in the desired environment and you can handle the version control with git. If desired, all of these functionalities can also be managed through the integrated terminal. Inside the text editor, code completion is taken care of by Intellisense4. Support for all default languages and those that the user chooses to add are provided by the extension service. As explained by Eric Gamma5, the extensions run in a separate process in order to protect the main program. This design choice strives to maintain the lightweight user experience. VS Code also needs a component that reads from and writes to the local file system. Access to the file system occurs when the user saves or loads a script, updates his preferences, checks out code from a repository, uses the terminal or when a script performs IO tasks at runtime.

Connector View

To expand a bit more on the component view, we can consider the underlying logic that connects the components together. We present this visually with the connector view below.

Figure: Connector View

Most of the components require user input in order to start doing something meaningful. This can be done directly through the GUI rendered by electron, or through the integrated terminal. The latter will activate the other components by means of system and method calls. The component that manages version control will pass file changes back and forth with a repository over a SSH connection. Finally, all the extensions can communicate with the main program API calls. The API enables many different options such as: altering the theming of VS Code, adding custom UI components, support for additional programming language comprehension and much more6.

Development View

Visual Studio Code has a very well defined process when it comes to contributing & building process. As of right now VS Code has 1.3k contributors. There are clear guidelines when it comes to contributing - asking questions, reporting issues, and contributing to fixes. For the last one, there are additional guidelines - “How to Contribute”. In order to successfully create a PR and get it merged, one needs to build the source code, by following the recommended steps. When talking about dependencies, the project is mainly structured using Node.JS, Yarn, Python, and a C/C++ compiler tool, depending on the build platform (Windows, macOS, Linux).

Runtime View

Now that we know the building blocks of the program, let’s see what happens when we start up Visual Studio Code. At startup, the program is bootstrapped from the code package. During initialization this calls the platform module, where necessary services are instantiated, such as the Instantiation Service, which can be used by all future services to be started. After this the “electron app” module is started, which, among others, will open the window in which the editor will live. Before the window opens the lifecycle phase is “ready”, which signals to the main service to register more listeners. This will open the workspace after the window is ready. Following up, a new lifecycle phase is entered which other services have been waiting for, like the menu bar and keyboard service. We will also take a look at how extensions function7 within the editor. After an extension has been added, the activate command within the extension is called. From this point the extension calls upon the vscode.commands where commands for the extension are registered. The commands registry then stores the action and sends out the message that the command has been registered. At the same time, in the workbench package, the defined so-called contributions are read from the extension manifest. These make sure the services associated with the extension get started. Now, when a command is called, e.g. through a combination of key-presses, it gets relayed to the commands service, which in turn calls the registered function which lives in the service that was just instantiated.

Realization of Key Quality Attributes

The main key quality attributes as described in the previous essay, the product vision, were all about the fact that Visual Studio Code should be lightweight. This lightweightness was approached from two different angles, giving us the key quality attributes.

The first angle viewed lightweightness from the perspective of the user experience. The program should feel lightweight when being used. This can be achieved by having a nicely designed UI / UX enforced through clear design guidelines. This part of design doesn’t directly influence the way the architecture was designed, but the fact that Visual Studio Code was built on top of a web based electron base has led to an easier work process for a lot of designers, as the space of designers working with html and css is a lot bigger than that of most GUI frameworks. It also helps to make the program look and feel the same on a lot of different devices using the same code.

The second angle approached a more direct way in which the application can feel lightweight, not through its UX design but through raw performance. Because things happen faster based on user input the program will feel lighter. Now it is true that Visual Studio Code has an electron base, but this doesn’t mean it’s too slow. Vim and sublime will always be faster, but Visual Studio Code is fast enough. One thing architecturally to keep performance up to par is their use of the modular pattern, allowing them to swap out parts of the code and adapt to new, more performant, solutions easily. Extensions are also run in a separate process, reducing their impact on the performance of the main editor. Combined these choices all come together to create a lightweight experience for the user.

API Design principles

The API principles which the vscode team and contributors must adhere to are stated in their list on the wiki8. A lot of these are about javascript specific concepts and performance based guidelines, but some of these also indirectly state more broad guidelines. Such as how using certain ways of implementing something in javascript will reduce the amount of times the users are surprised by the behaviour of the API. Some concepts are more assumed. Such as that an API is precisely as much as it needs to be is. In the end all of these rules are enforced through the API design process.

This API design process the team uses is very rigorous9. During the proposal of a new addition to the API the professor will have to explain their thoughts and support their decisions at every step of the design process. The different steps from draft to finalization can be seen in the flowchart they use provided below:

Figure: The API proposal process

They also have a weekly API call, open to everyone, where they discuss the different API proposals.

References


  1. Source Code Organization. https://github.com/microsoft/vscode/wiki/Source-Code-Organization ↩︎

  2. Monaco text editor. https://microsoft.github.io/monaco-editor/ ↩︎

  3. Layered Architecture. https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html ↩︎

  4. Intellisense. https://vscode.readthedocs.io/en/latest/editor/intellisense/ ↩︎

  5. Interview with Erich Gamma. https://youtube.com/watch?v=Tw8l0WzQxmY ↩︎

  6. Extension API. https://code.visualstudio.com/api ↩︎

  7. Extension Anatomy. https://code.visualstudio.com/api/get-started/extension-anatomy ↩︎

  8. API principles. https://github.com/microsoft/vscode/wiki/Extension-API-guidelines ↩︎

  9. API Process. https://github.com/Microsoft/vscode/wiki/Extension-API-process ↩︎

Visual Studio Code
Authors
Rens Hijdra
Hunter van Geffen
Stefan Petrescu
Tim Yarally