Introduction
My name is Alex Dima, and I’m a senior software engineer on the Visual Studio Code team at Microsoft. We work out of Zurich, and today I would like to cover about some of the lessons we’ve learned while developing and shipping Visual Studio Code.
Visual Studio Code
Visual Studio Code comes in two parts. Firstly it’s a web app built using JavaScript, specifically using the Node.js API on top of a browser API. The browser comes in the shape of Electron, which is a merger between Chromium and Node.js.
Secondly, we ship a library which called the Monaco Editor. We take it out of our product and we ship it separately as a JavaScript library that can run in any modern web browser.
Growing the Code
We have been working on the code since the autumn of 2011. At the time, we were given the mission to redefine and create a better developer experience; development tools that work in the browser.
We started in JavaScript - our patterns consisted of creating classes using prototypical inheritance, and wrote idiomatic JavaScript to try to create classes, patterns, and types in our source code.
One thing we got correct from the start was to use promises as a way to abstract away asynchronicity. As an example, in the UI when a computation had to be completed behind the scenes, it makes a call, and that call returns a promise with the actual thing to show.
This was a big win for us because whenever we changed architectures, we could just substitute the implementation of the hover providers. Instead of executing them in a web worker, we could spawn a process and do it in a separate process, as a result, there were no changes to the UI code.
TypeScript to the Rescue
TypeScript provides an extra layer of security. More specifically, it protects you against your own mistakes.
TypeScript compiles down to JavaScript, but it is a superset of JavaScript. What this means is that any JavaScript you write is actually TypeScript as well. TypeScript adds optional syntactical constructs, such as type annotations and interfaces, and generally makes authoring JavaScript more pleasant.
TypeScript Demo
Code Organization
One of the things which we did not get correct from the beginning were dependencies. We failed to keep track of what each JavaScript file needs and what it provides.
One of the reasons for that is because we were using Namespaces, which is a fancy word for globals. For example, in a certain file, we would reach on to its Namespace.Util.Strings, and we define a new method on this Namespace. The problem here is that there’s no correlation between the file that does this and the Namespace that’s being defined.
var Namespace = {};
Namespace.Util = {};
Namespace.Util.Strings = {};
Namespace.Util.Strings.trim = function() {
/* etc */
};
I could name the file Strings.js or Utils.js, but nobody in my team would know where to find the implementation of the trim
function. They might be using it everywhere, but they wouldn’t necessarily know which file on disk they should go open and change.
Renaming files was difficulty. Renaming Namespaces was something we never did because we wouldn’t know where they are all used.
Module Systems
CommonJS is great because you know what’s in a file, and what will be provided in the form of the export. The problem with it is that it’s very suitable for running on a server where there is local disk access, but you cannot do this in a browser. You cannot expect to ship this code, then do an asynchronous XHR request to try to fetch the dependency.
my_module.js
var dependency = require('dependency_id');
// code
exports.myExports = {
// exports
}
AMD stands for Asynchronous Module Definition, and AMD was release right around the time we were exploring this issue. The real difference in the code is that all the dependencies of the code are extracted outside in an array.
define('module_id', ['dependency_id'], function(dependency) {
// code
return {
// exports
};
}
As a result, I will know the dependencies I need to fulfill before executing that code.
TypeScript: First Class Module Support
Typescript supports both AMD and CommonJS with on syntax, making code sharing between AMD and CommonJS easy.
import { Dependency } from "./dependency_id";
// code
export function helper() {
// export
}
Lazy Code Loading
One of the advantages of AMD, is the lazy code loading. We ship the app with about 30 languages. If you open a .txt file, and there are 30 different languages on it, it would all load.
The current approach we use is to have a contribution system that describes the type of language that exists.
When a text file is opened, the only thing that is loaded is this piece. Later, when you open a PHP file, PHP get loaded.
// php.contribution.ts
ModesRegistry.registerMode({
id: 'php',
extensions: ['.php', '.php4', '.php5'],
aliases: ['PHP', php'],
minetypes: ['application/x-php'],
moduleId: 'vs/languages/php/common/php',
ctorName: 'PHPMode'
});
// php.ts
export class PHPMode extends AbstractMode {
constructor() {
super('php');
}
}
CSS Dependencies
We also take advantage of AMD loader plugins. When we write our code, we write a dependency to the CSS that this file needs.
// currentLineHeight.ts
import 'vs/css!./currentLineHightlight';
import {ViewOverlay} from '...';
export class CurrentLineHightlight extends ViewOverlay {
}
How this highlight works is that it creates a DOM node, then sets a certain class name on it. If you move your cursor up and down, it moves up and down.
Performance: Bundle and Minify your Code
You should look into bundling and minifying your code, even if it runs on the server side. In Visual Code Studio, the startup process is split into three: Electron Startup, Modules, and Shell/Viewlet and Editor.
What we found by bundling all the files into one, you get a performance benefit.
Components
It’s possible to compile with TypeScript. Imagine there are two teams, or two different projects. For example, from the TypeScript guys, we consume this typescriptServices.js that they produce.
This is how the editor work: you take the buffer that you’re typing, and it returns to offer suggestions. To accomplish that, they ship us a bundled minified JavaScript file, accompanied by a DTS, which describes what is inside that JavaScript.
Conceptually, DTS is the API that we have between our projects. If they make up a new version, we get both new files, and we compile against the new DTS.
Dependency Injection
TypeScript supports constructor decorators, and we use them for service injection. This is an action in the editor that is triggered by pressing F12. After the promise returns, it jumps to the definition of a certain symbol.
You would do this on a function, and it would take you to where the function is implemented. We use these decorators to signal that we should inject some services to this action.
If there is an error, this action calls inside this message service and says ‘show error’. We have an implementation of the message service that shows a little drop-down at the top.
Electron Demo
Electron is a combination of Chromium and Node.js. How they work together is through a script, in this case, main.js, which provides the entry point.
A new browser window that shows the width and height is created and it loads the file index.html. Inside the index.html is where there is a browser and Node API.
The main.js is a driver of these windows. That process is always there and communicates to the OS. It owns things like the buttons and the menus.
Main.js is a very important process. Inside, you should not do what I’m about to do now, which is a set interval. In this example, we’re computing Fibonacci.
If you run this, it will be non-responsive because Electron inherits the process architecture of Chromium. If the main process becomes busy, it will bring down everything with it.
Execution Environments
New processes are created when doing things like checking for updates or managing extensions, as it will impact the responsiveness.
For each renderer process, we create an extension host, and also launch on-demand processes. For instance, if you do a search, we will launch a process and it will search on the folder open and return the results.
Each one of these processes has a different execution environment.
About the content
This talk was delivered live in June 2016 at goto; Amsterdam. The video was transcribed by Realm and is published here with the permission of the conference organizers.