diff --git a/editors/code/src/commands/apply_source_change.ts b/editors/code/src/commands/apply_source_change.ts
new file mode 100644
index 00000000000..67765e5a3ca
--- /dev/null
+++ b/editors/code/src/commands/apply_source_change.ts
@@ -0,0 +1,58 @@
+import * as vscode from 'vscode';
+import * as lc from 'vscode-languageclient';
+
+import { Server } from '../server';
+
+interface FileSystemEdit {
+    type: string;
+    uri?: string;
+    src?: string;
+    dst?: string;
+}
+
+export interface SourceChange {
+    label: string;
+    sourceFileEdits: lc.TextDocumentEdit[];
+    fileSystemEdits: FileSystemEdit[];
+    cursorPosition?: lc.TextDocumentPositionParams;
+}
+
+export async function handle(change: SourceChange) {
+    const wsEdit = new vscode.WorkspaceEdit();
+    for (const sourceEdit of change.sourceFileEdits) {
+        const uri = Server.client.protocol2CodeConverter.asUri(sourceEdit.textDocument.uri);
+        const edits = Server.client.protocol2CodeConverter.asTextEdits(sourceEdit.edits);
+        wsEdit.set(uri, edits);
+    }
+    let created;
+    let moved;
+    for (const fsEdit of change.fileSystemEdits) {
+        switch (fsEdit.type) {
+            case 'createFile':
+                const uri = vscode.Uri.parse(fsEdit.uri!);
+                wsEdit.createFile(uri);
+                created = uri;
+                break;
+            case 'moveFile':
+                const src = vscode.Uri.parse(fsEdit.src!);
+                const dst = vscode.Uri.parse(fsEdit.dst!);
+                wsEdit.renameFile(src, dst);
+                moved = dst;
+                break;
+        }
+    }
+    const toOpen = created || moved;
+    const toReveal = change.cursorPosition;
+    await vscode.workspace.applyEdit(wsEdit);
+    if (toOpen) {
+        const doc = await vscode.workspace.openTextDocument(toOpen);
+        await vscode.window.showTextDocument(doc);
+    } else if (toReveal) {
+        const uri = Server.client.protocol2CodeConverter.asUri(toReveal.textDocument.uri);
+        const position = Server.client.protocol2CodeConverter.asPosition(toReveal.position);
+        const editor = vscode.window.activeTextEditor;
+        if (!editor || editor.document.uri.toString() !== uri.toString()) { return; }
+        if (!editor.selection.isEmpty) { return; }
+        editor!.selection = new vscode.Selection(position, position);
+    }
+}
diff --git a/editors/code/src/commands/extend_selection.ts b/editors/code/src/commands/extend_selection.ts
new file mode 100644
index 00000000000..cdc3d10fb3f
--- /dev/null
+++ b/editors/code/src/commands/extend_selection.ts
@@ -0,0 +1,29 @@
+import * as vscode from 'vscode';
+
+import { Range, TextDocumentIdentifier } from 'vscode-languageclient';
+import { Server } from '../server';
+
+interface ExtendSelectionParams {
+    textDocument: TextDocumentIdentifier;
+    selections: Range[];
+}
+
+interface ExtendSelectionResult {
+    selections: Range[];
+}
+
+export async function handle() {
+    const editor = vscode.window.activeTextEditor;
+    if (editor == null || editor.document.languageId !== 'rust') { return; }
+    const request: ExtendSelectionParams = {
+        selections: editor.selections.map((s) => {
+            return Server.client.code2ProtocolConverter.asRange(s);
+        }),
+        textDocument: { uri: editor.document.uri.toString() },
+    };
+    const response = await Server.client.sendRequest<ExtendSelectionResult>('m/extendSelection', request);
+    editor.selections = response.selections.map((range: Range) => {
+        const r = Server.client.protocol2CodeConverter.asRange(range);
+        return new vscode.Selection(r.start, r.end);
+    });
+}
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
new file mode 100644
index 00000000000..dfdcd64545b
--- /dev/null
+++ b/editors/code/src/commands/index.ts
@@ -0,0 +1,17 @@
+import * as applySourceChange from './apply_source_change';
+import * as extendSelection from './extend_selection';
+import * as joinLines from './join_lines';
+import * as matchingBrace from './matching_brace';
+import * as parentModule from './parent_module';
+import * as runnables from './runnables';
+import * as syntaxTree from './syntaxTree';
+
+export {
+    applySourceChange,
+    extendSelection,
+    joinLines,
+    matchingBrace,
+    parentModule,
+    runnables,
+    syntaxTree,
+};
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts
new file mode 100644
index 00000000000..526b698ccd2
--- /dev/null
+++ b/editors/code/src/commands/join_lines.ts
@@ -0,0 +1,21 @@
+import * as vscode from 'vscode';
+
+import { Range, TextDocumentIdentifier } from 'vscode-languageclient';
+import { Server } from '../server';
+import { handle as applySourceChange, SourceChange } from './apply_source_change';
+
+interface JoinLinesParams {
+    textDocument: TextDocumentIdentifier;
+    range: Range;
+}
+
+export async function handle() {
+    const editor = vscode.window.activeTextEditor;
+    if (editor == null || editor.document.languageId !== 'rust') { return; }
+    const request: JoinLinesParams = {
+        range: Server.client.code2ProtocolConverter.asRange(editor.selection),
+        textDocument: { uri: editor.document.uri.toString() },
+    };
+    const change = await Server.client.sendRequest<SourceChange>('m/joinLines', request);
+    await applySourceChange(change);
+}
diff --git a/editors/code/src/commands/matching_brace.ts b/editors/code/src/commands/matching_brace.ts
new file mode 100644
index 00000000000..a80446a8f58
--- /dev/null
+++ b/editors/code/src/commands/matching_brace.ts
@@ -0,0 +1,27 @@
+import * as vscode from 'vscode';
+
+import { Position, TextDocumentIdentifier } from 'vscode-languageclient';
+import { Server } from '../server';
+
+interface FindMatchingBraceParams {
+    textDocument: TextDocumentIdentifier;
+    offsets: Position[];
+}
+
+export async function handle() {
+    const editor = vscode.window.activeTextEditor;
+    if (editor == null || editor.document.languageId !== 'rust') { return; }
+    const request: FindMatchingBraceParams = {
+        textDocument: { uri: editor.document.uri.toString() },
+        offsets: editor.selections.map((s) => {
+            return Server.client.code2ProtocolConverter.asPosition(s.active);
+        }),
+    };
+    const response = await Server.client.sendRequest<Position[]>('m/findMatchingBrace', request);
+    editor.selections = editor.selections.map((sel, idx) => {
+        const active = Server.client.protocol2CodeConverter.asPosition(response[idx]);
+        const anchor = sel.isEmpty ? active : sel.anchor;
+        return new vscode.Selection(anchor, active);
+    });
+    editor.revealRange(editor.selection);
+}
diff --git a/editors/code/src/commands/parent_module.ts b/editors/code/src/commands/parent_module.ts
new file mode 100644
index 00000000000..d66fb30264d
--- /dev/null
+++ b/editors/code/src/commands/parent_module.ts
@@ -0,0 +1,22 @@
+import * as vscode from 'vscode';
+
+import { Location, TextDocumentIdentifier } from 'vscode-languageclient';
+import { Server } from '../server';
+
+export async function handle() {
+    const editor = vscode.window.activeTextEditor;
+    if (editor == null || editor.document.languageId !== 'rust') { return; }
+    const request: TextDocumentIdentifier = {
+        uri: editor.document.uri.toString(),
+    };
+    const response = await Server.client.sendRequest<Location[]>('m/parentModule', request);
+    const loc = response[0];
+    if (loc == null) { return; }
+    const uri = Server.client.protocol2CodeConverter.asUri(loc.uri);
+    const range = Server.client.protocol2CodeConverter.asRange(loc.range);
+
+    const doc = await vscode.workspace.openTextDocument(uri);
+    const e = await vscode.window.showTextDocument(doc);
+    e.selection = new vscode.Selection(range.start, range.start);
+    e.revealRange(range, vscode.TextEditorRevealType.InCenter);
+}
diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts
new file mode 100644
index 00000000000..40f590dceb8
--- /dev/null
+++ b/editors/code/src/commands/runnables.ts
@@ -0,0 +1,88 @@
+import * as vscode from 'vscode';
+import * as lc from 'vscode-languageclient';
+import { Server } from '../server';
+
+interface RunnablesParams {
+    textDocument: lc.TextDocumentIdentifier;
+    position?: lc.Position;
+}
+
+interface Runnable {
+    range: lc.Range;
+    label: string;
+    bin: string;
+    args: string[];
+    env: { [index: string]: string };
+}
+
+class RunnableQuickPick implements vscode.QuickPickItem {
+    public label: string;
+    public description?: string | undefined;
+    public detail?: string | undefined;
+    public picked?: boolean | undefined;
+
+    constructor(public runnable: Runnable) {
+        this.label = runnable.label;
+    }
+}
+
+interface CargoTaskDefinition extends vscode.TaskDefinition {
+    type: 'cargo';
+    label: string;
+    command: string;
+    args: string[];
+    env?: { [key: string]: string };
+}
+
+function createTask(spec: Runnable): vscode.Task {
+    const TASK_SOURCE = 'Rust';
+    const definition: CargoTaskDefinition = {
+        type: 'cargo',
+        label: 'cargo',
+        command: spec.bin,
+        args: spec.args,
+        env: spec.env,
+    };
+
+    const execCmd = `${definition.command} ${definition.args.join(' ')}`;
+    const execOption: vscode.ShellExecutionOptions = {
+        cwd: '.',
+        env: definition.env,
+    };
+    const exec = new vscode.ShellExecution(`clear; ${execCmd}`, execOption);
+
+    const f = vscode.workspace.workspaceFolders![0];
+    const t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']);
+    return t;
+}
+
+let prevRunnable: RunnableQuickPick | undefined;
+export async function handle() {
+    const editor = vscode.window.activeTextEditor;
+    if (editor == null || editor.document.languageId !== 'rust') { return; }
+    const textDocument: lc.TextDocumentIdentifier = {
+        uri: editor.document.uri.toString(),
+    };
+    const params: RunnablesParams = {
+        textDocument,
+        position: Server.client.code2ProtocolConverter.asPosition(editor.selection.active),
+    };
+    const runnables = await Server.client.sendRequest<Runnable[]>('m/runnables', params);
+    const items: RunnableQuickPick[] = [];
+    if (prevRunnable) {
+        items.push(prevRunnable);
+    }
+    for (const r of runnables) {
+        if (prevRunnable && JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)) {
+            continue;
+        }
+        items.push(new RunnableQuickPick(r));
+    }
+    const item = await vscode.window.showQuickPick(items);
+    if (item) {
+        item.detail = 'rerun';
+        prevRunnable = item;
+        const task = createTask(item.runnable);
+        return await vscode.tasks.executeTask(task);
+    }
+}
diff --git a/editors/code/src/commands/syntaxTree.ts b/editors/code/src/commands/syntaxTree.ts
new file mode 100644
index 00000000000..dcb721eee59
--- /dev/null
+++ b/editors/code/src/commands/syntaxTree.ts
@@ -0,0 +1,38 @@
+import * as vscode from 'vscode';
+import { TextDocumentIdentifier } from 'vscode-languageclient';
+
+import { Server } from '../server';
+
+export const syntaxTreeUri = vscode.Uri.parse('ra-lsp://syntaxtree');
+
+export class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
+    public eventEmitter = new vscode.EventEmitter<vscode.Uri>();
+    public syntaxTree: string = 'Not available';
+
+    public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
+        const editor = vscode.window.activeTextEditor;
+        if (editor == null) { return ''; }
+        const request: SyntaxTreeParams = {
+            textDocument: { uri: editor.document.uri.toString() },
+        };
+        return Server.client.sendRequest<SyntaxTreeResult>('m/syntaxTree', request);
+    }
+
+    get onDidChange(): vscode.Event<vscode.Uri> {
+        return this.eventEmitter.event;
+    }
+}
+
+interface SyntaxTreeParams {
+    textDocument: TextDocumentIdentifier;
+}
+
+type SyntaxTreeResult = string;
+
+// Opens the virtual file that will show the syntax tree
+//
+// The contents of the file come from the `TextDocumentContentProvider`
+export async function handle() {
+    const document = await vscode.workspace.openTextDocument(syntaxTreeUri);
+    return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
+}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
new file mode 100644
index 00000000000..740b5be206a
--- /dev/null
+++ b/editors/code/src/config.ts
@@ -0,0 +1,23 @@
+import * as vscode from 'vscode';
+
+import { Server } from './server';
+
+export class Config {
+    public highlightingOn = true;
+
+    constructor() {
+        vscode.workspace.onDidChangeConfiguration((_) => this.userConfigChanged());
+        this.userConfigChanged();
+    }
+
+    public userConfigChanged() {
+        const config = vscode.workspace.getConfiguration('ra-lsp');
+        if (config.has('highlightingOn')) {
+            this.highlightingOn = config.get('highlightingOn') as boolean;
+        }
+
+        if (!this.highlightingOn && Server) {
+            Server.highlighter.removeHighlights();
+        }
+    }
+}
diff --git a/editors/code/src/events/change_active_text_editor.ts b/editors/code/src/events/change_active_text_editor.ts
new file mode 100644
index 00000000000..3440aa0c375
--- /dev/null
+++ b/editors/code/src/events/change_active_text_editor.ts
@@ -0,0 +1,14 @@
+import { TextEditor } from 'vscode';
+import { TextDocumentIdentifier } from 'vscode-languageclient';
+
+import { Decoration } from '../highlighting';
+import { Server } from '../server';
+
+export async function handle(editor: TextEditor | undefined) {
+    if (!Server.config.highlightingOn || !editor || editor.document.languageId !== 'rust') { return; }
+    const params: TextDocumentIdentifier = {
+        uri: editor.document.uri.toString(),
+    };
+    const decorations = await Server.client.sendRequest<Decoration[]>('m/decorationsRequest', params);
+    Server.highlighter.setHighlights(editor, decorations);
+}
diff --git a/editors/code/src/events/change_text_document.ts b/editors/code/src/events/change_text_document.ts
new file mode 100644
index 00000000000..b3000e02618
--- /dev/null
+++ b/editors/code/src/events/change_text_document.ts
@@ -0,0 +1,19 @@
+import * as vscode from 'vscode';
+
+import { syntaxTreeUri, TextDocumentContentProvider } from '../commands/syntaxTree';
+
+export function createHandler(textDocumentContentProvider: TextDocumentContentProvider) {
+    return (event: vscode.TextDocumentChangeEvent) => {
+        const doc = event.document;
+        if (doc.languageId !== 'rust') { return; }
+        afterLs(() => {
+            textDocumentContentProvider.eventEmitter.fire(syntaxTreeUri);
+        });
+    };
+}
+
+// We need to order this after LS updates, but there's no API for that.
+// Hence, good old setTimeout.
+function afterLs(f: () => any) {
+    setTimeout(f, 10);
+}
diff --git a/editors/code/src/events/index.ts b/editors/code/src/events/index.ts
new file mode 100644
index 00000000000..b570a7a926a
--- /dev/null
+++ b/editors/code/src/events/index.ts
@@ -0,0 +1,7 @@
+import * as changeActiveTextEditor from './change_active_text_editor';
+import * as changeTextDocument from './change_text_document';
+
+export {
+    changeActiveTextEditor,
+    changeTextDocument,
+};
diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts
index fde6a480d85..44e74f4cc9d 100644
--- a/editors/code/src/extension.ts
+++ b/editors/code/src/extension.ts
@@ -1,434 +1,56 @@
-'use strict';
 import * as vscode from 'vscode';
-import * as lc from 'vscode-languageclient'
+import * as lc from 'vscode-languageclient';
 
-let client: lc.LanguageClient;
-
-let uris = {
-    syntaxTree: vscode.Uri.parse('ra-lsp://syntaxtree')
-}
-
-let highlightingOn = true;
+import * as commands from './commands';
+import { TextDocumentContentProvider } from './commands/syntaxTree';
+import * as events from './events';
+import * as notifications from './notifications';
+import { Server } from './server';
 
 export function activate(context: vscode.ExtensionContext) {
-    let applyHighlightingOn = () => {
-        let config = vscode.workspace.getConfiguration('ra-lsp');
-        if (config.has('highlightingOn')) {
-            highlightingOn = config.get('highlightingOn') as boolean;
-        };
-
-        if (!highlightingOn) {
-            removeHighlights();
-        }
-    };
-
-    // Apply the highlightingOn config now and whenever the config changes
-    applyHighlightingOn();
-    vscode.workspace.onDidChangeConfiguration(_ => {
-        applyHighlightingOn();
-    });
-
-    let textDocumentContentProvider = new TextDocumentContentProvider()
-    let dispose = (disposable: vscode.Disposable) => {
+    function disposeOnDeactivation(disposable: vscode.Disposable) {
         context.subscriptions.push(disposable);
     }
-    let registerCommand = (name: string, f: any) => {
-        dispose(vscode.commands.registerCommand(name, f))
+
+    function registerCommand(name: string, f: any) {
+        disposeOnDeactivation(vscode.commands.registerCommand(name, f));
     }
 
-    registerCommand('ra-lsp.syntaxTree', () => openDoc(uris.syntaxTree))
-    registerCommand('ra-lsp.extendSelection', async () => {
-        let editor = vscode.window.activeTextEditor
-        if (editor == null || editor.document.languageId != "rust") return
-        let request: ExtendSelectionParams = {
-            textDocument: { uri: editor.document.uri.toString() },
-            selections: editor.selections.map((s) => {
-                return client.code2ProtocolConverter.asRange(s)
-            })
-        }
-        let response = await client.sendRequest<ExtendSelectionResult>("m/extendSelection", request)
-        editor.selections = response.selections.map((range) => {
-            let r = client.protocol2CodeConverter.asRange(range)
-            return new vscode.Selection(r.start, r.end)
-        })
-    })
-    registerCommand('ra-lsp.matchingBrace', async () => {
-        let editor = vscode.window.activeTextEditor
-        if (editor == null || editor.document.languageId != "rust") return
-        let request: FindMatchingBraceParams = {
-            textDocument: { uri: editor.document.uri.toString() },
-            offsets: editor.selections.map((s) => {
-                return client.code2ProtocolConverter.asPosition(s.active)
-            })
-        }
-        let response = await client.sendRequest<lc.Position[]>("m/findMatchingBrace", request)
-        editor.selections = editor.selections.map((sel, idx) => {
-            let active = client.protocol2CodeConverter.asPosition(response[idx])
-            let anchor = sel.isEmpty ? active : sel.anchor
-            return new vscode.Selection(anchor, active)
-        })
-        editor.revealRange(editor.selection)
-    })
-    registerCommand('ra-lsp.joinLines', async () => {
-        let editor = vscode.window.activeTextEditor
-        if (editor == null || editor.document.languageId != "rust") return
-        let request: JoinLinesParams = {
-            textDocument: { uri: editor.document.uri.toString() },
-            range: client.code2ProtocolConverter.asRange(editor.selection),
-        }
-        let change = await client.sendRequest<SourceChange>("m/joinLines", request)
-        await applySourceChange(change)
-    })
-    registerCommand('ra-lsp.parentModule', async () => {
-        let editor = vscode.window.activeTextEditor
-        if (editor == null || editor.document.languageId != "rust") return
-        let request: lc.TextDocumentIdentifier = {
-            uri: editor.document.uri.toString()
-        }
-        let response = await client.sendRequest<lc.Location[]>("m/parentModule", request)
-        let loc = response[0]
-        if (loc == null) return
-        let uri = client.protocol2CodeConverter.asUri(loc.uri)
-        let range = client.protocol2CodeConverter.asRange(loc.range)
+    // Commands are requests from vscode to the language server
+    registerCommand('ra-lsp.syntaxTree', commands.syntaxTree.handle);
+    registerCommand('ra-lsp.extendSelection', commands.extendSelection.handle);
+    registerCommand('ra-lsp.matchingBrace', commands.matchingBrace.handle);
+    registerCommand('ra-lsp.joinLines', commands.joinLines.handle);
+    registerCommand('ra-lsp.parentModule', commands.parentModule.handle);
+    registerCommand('ra-lsp.run', commands.runnables.handle);
+    registerCommand('ra-lsp.applySourceChange', commands.applySourceChange.handle);
 
-        let doc = await vscode.workspace.openTextDocument(uri)
-        let e = await vscode.window.showTextDocument(doc)
-        e.selection = new vscode.Selection(range.start, range.start)
-        e.revealRange(range, vscode.TextEditorRevealType.InCenter)
-    })
+    // Notifications are events triggered by the language server
+    const allNotifications: Iterable<[string, lc.GenericNotificationHandler]> = [
+        ['m/publishDecorations', notifications.publishDecorations.handle],
+    ];
 
-    let prevRunnable: RunnableQuickPick | undefined = undefined
-    registerCommand('ra-lsp.run', async () => {
-        let editor = vscode.window.activeTextEditor
-        if (editor == null || editor.document.languageId != "rust") return
-        let textDocument: lc.TextDocumentIdentifier = {
-            uri: editor.document.uri.toString()
-        }
-        let params: RunnablesParams = {
-            textDocument,
-            position: client.code2ProtocolConverter.asPosition(editor.selection.active)
-        }
-        let runnables = await client.sendRequest<Runnable[]>('m/runnables', params)
-        let items: RunnableQuickPick[] = []
-        if (prevRunnable) {
-            items.push(prevRunnable)
-        }
-        for (let r of runnables) {
-            if (prevRunnable && JSON.stringify(prevRunnable.runnable) == JSON.stringify(r)) {
-                continue
-            }
-            items.push(new RunnableQuickPick(r))
-        }
-        let item = await vscode.window.showQuickPick(items)
-        if (item) {
-            item.detail = "rerun"
-            prevRunnable = item
-            let task = createTask(item.runnable)
-            return await vscode.tasks.executeTask(task)
-        }
-    })
-    registerCommand('ra-lsp.applySourceChange', applySourceChange)
+    // The events below are plain old javascript events, triggered and handled by vscode
+    vscode.window.onDidChangeActiveTextEditor(events.changeActiveTextEditor.handle);
 
-    dispose(vscode.workspace.registerTextDocumentContentProvider(
+    const textDocumentContentProvider = new TextDocumentContentProvider();
+    disposeOnDeactivation(vscode.workspace.registerTextDocumentContentProvider(
         'ra-lsp',
-        textDocumentContentProvider
-    ))
-    startServer()
-    vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => {
-        let doc = event.document
-        if (doc.languageId != "rust") return
-        afterLs(() => {
-            textDocumentContentProvider.eventEmitter.fire(uris.syntaxTree)
-        })
-    }, null, context.subscriptions)
-    vscode.window.onDidChangeActiveTextEditor(async (editor) => {
-        if (!highlightingOn || !editor || editor.document.languageId != 'rust') return
-        let params: lc.TextDocumentIdentifier = {
-            uri: editor.document.uri.toString()
-        }
-        let decorations = await client.sendRequest<Decoration[]>("m/decorationsRequest", params)
-        setHighlights(editor, decorations)
-    })
-}
+        textDocumentContentProvider,
+    ));
 
-// We need to order this after LS updates, but there's no API for that.
-// Hence, good old setTimeout.
-function afterLs(f: () => any) {
-    setTimeout(f, 10)
+    vscode.workspace.onDidChangeTextDocument(
+        events.changeTextDocument.createHandler(textDocumentContentProvider),
+        null,
+        context.subscriptions);
+
+    // Start the language server, finally!
+    Server.start(allNotifications);
 }
 
 export function deactivate(): Thenable<void> {
-    if (!client) {
+    if (!Server.client) {
         return Promise.resolve();
     }
-    return client.stop();
-}
-
-function startServer() {
-    let run: lc.Executable = {
-        command: "ra_lsp_server",
-        options: { cwd: "." }
-    }
-    let serverOptions: lc.ServerOptions = {
-        run,
-        debug: run
-    };
-
-    let clientOptions: lc.LanguageClientOptions = {
-        documentSelector: [{ scheme: 'file', language: 'rust' }],
-    };
-
-    client = new lc.LanguageClient(
-        'ra-lsp',
-        'rust-analyzer languge server',
-        serverOptions,
-        clientOptions,
-    );
-    client.onReady().then(() => {
-        client.onNotification(
-            "m/publishDecorations",
-            (params: PublishDecorationsParams) => {
-                let editor = vscode.window.visibleTextEditors.find(
-                    (editor) => editor.document.uri.toString() == params.uri
-                )
-                if (!highlightingOn || !editor) return;
-                setHighlights(
-                    editor,
-                    params.decorations,
-                )
-            }
-        )
-    })
-    client.start();
-}
-
-async function openDoc(uri: vscode.Uri) {
-    let document = await vscode.workspace.openTextDocument(uri)
-    return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true)
-}
-
-class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
-    public eventEmitter = new vscode.EventEmitter<vscode.Uri>()
-    public syntaxTree: string = "Not available"
-
-    public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult<string> {
-        let editor = vscode.window.activeTextEditor;
-        if (editor == null) return ""
-        let request: SyntaxTreeParams = {
-            textDocument: { uri: editor.document.uri.toString() }
-        };
-        return client.sendRequest<SyntaxTreeResult>("m/syntaxTree", request);
-    }
-
-    get onDidChange(): vscode.Event<vscode.Uri> {
-        return this.eventEmitter.event
-    }
-}
-
-let decorations: { [index: string]: vscode.TextEditorDecorationType } = {};
-
-function initDecorations() {
-    const decor = (obj: any) => vscode.window.createTextEditorDecorationType({ color: obj })
-    decorations = {
-        background: decor("#3F3F3F"),
-        error: vscode.window.createTextEditorDecorationType({
-            borderColor: "red",
-            borderStyle: "none none dashed none",
-        }),
-        comment: decor("#7F9F7F"),
-        string: decor("#CC9393"),
-        keyword: decor("#F0DFAF"),
-        function: decor("#93E0E3"),
-        parameter: decor("#94BFF3"),
-        builtin: decor("#DD6718"),
-        text: decor("#DCDCCC"),
-        attribute: decor("#BFEBBF"),
-        literal: decor("#DFAF8F"),
-    }
-}
-
-function removeHighlights() {
-    for (let tag in decorations) {
-        decorations[tag].dispose();
-    }
-
-    decorations = {};
-}
-
-function setHighlights(
-    editor: vscode.TextEditor,
-    highlights: Array<Decoration>
-) {
-    // Initialize decorations if necessary
-    //
-    // Note: decoration objects need to be kept around so we can dispose them
-    // if the user disables syntax highlighting
-    if (Object.keys(decorations).length === 0) {
-        initDecorations();
-    }
-
-    let byTag: Map<string, vscode.Range[]> = new Map()
-    for (let tag in decorations) {
-        byTag.set(tag, [])
-    }
-
-    for (let d of highlights) {
-        if (!byTag.get(d.tag)) {
-            console.log(`unknown tag ${d.tag}`)
-            continue
-        }
-        byTag.get(d.tag)!.push(
-            client.protocol2CodeConverter.asRange(d.range)
-        )
-    }
-
-    for (let tag of byTag.keys()) {
-        let dec: vscode.TextEditorDecorationType = decorations[tag]
-        let ranges = byTag.get(tag)!
-        editor.setDecorations(dec, ranges)
-    }
-}
-
-interface SyntaxTreeParams {
-    textDocument: lc.TextDocumentIdentifier;
-}
-
-type SyntaxTreeResult = string
-
-interface ExtendSelectionParams {
-    textDocument: lc.TextDocumentIdentifier;
-    selections: lc.Range[];
-}
-
-interface ExtendSelectionResult {
-    selections: lc.Range[];
-}
-
-interface FindMatchingBraceParams {
-    textDocument: lc.TextDocumentIdentifier;
-    offsets: lc.Position[];
-}
-
-interface JoinLinesParams {
-    textDocument: lc.TextDocumentIdentifier;
-    range: lc.Range;
-}
-
-interface PublishDecorationsParams {
-    uri: string,
-    decorations: Decoration[],
-}
-
-interface RunnablesParams {
-    textDocument: lc.TextDocumentIdentifier,
-    position?: lc.Position,
-}
-
-interface Runnable {
-    range: lc.Range;
-    label: string;
-    bin: string;
-    args: string[];
-    env: { [index: string]: string },
-}
-
-class RunnableQuickPick implements vscode.QuickPickItem {
-    label: string;
-    description?: string | undefined;
-    detail?: string | undefined;
-    picked?: boolean | undefined;
-
-    constructor(public runnable: Runnable) {
-        this.label = runnable.label
-    }
-}
-
-interface Decoration {
-    range: lc.Range,
-    tag: string,
-}
-
-
-interface CargoTaskDefinition extends vscode.TaskDefinition {
-    type: 'cargo';
-    label: string;
-    command: string;
-    args: Array<string>;
-    env?: { [key: string]: string };
-}
-
-function createTask(spec: Runnable): vscode.Task {
-    const TASK_SOURCE = 'Rust';
-    let definition: CargoTaskDefinition = {
-        type: 'cargo',
-        label: 'cargo',
-        command: spec.bin,
-        args: spec.args,
-        env: spec.env
-    }
-
-    let execCmd = `${definition.command} ${definition.args.join(' ')}`;
-    let execOption: vscode.ShellExecutionOptions = {
-        cwd: '.',
-        env: definition.env,
-    };
-    let exec = new vscode.ShellExecution(`clear; ${execCmd}`, execOption);
-
-    let f = vscode.workspace.workspaceFolders![0]
-    let t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']);
-    return t;
-}
-
-interface FileSystemEdit {
-    type: string;
-    uri?: string;
-    src?: string;
-    dst?: string;
-}
-
-interface SourceChange {
-    label: string,
-    sourceFileEdits: lc.TextDocumentEdit[],
-    fileSystemEdits: FileSystemEdit[],
-    cursorPosition?: lc.TextDocumentPositionParams,
-}
-
-async function applySourceChange(change: SourceChange) {
-    console.log(`applySOurceChange ${JSON.stringify(change)}`)
-    let wsEdit = new vscode.WorkspaceEdit()
-    for (let sourceEdit of change.sourceFileEdits) {
-        let uri = client.protocol2CodeConverter.asUri(sourceEdit.textDocument.uri)
-        let edits = client.protocol2CodeConverter.asTextEdits(sourceEdit.edits)
-        wsEdit.set(uri, edits)
-    }
-    let created;
-    let moved;
-    for (let fsEdit of change.fileSystemEdits) {
-        if (fsEdit.type == "createFile") {
-            let uri = vscode.Uri.parse(fsEdit.uri!)
-            wsEdit.createFile(uri)
-            created = uri
-        } else if (fsEdit.type == "moveFile") {
-            let src = vscode.Uri.parse(fsEdit.src!)
-            let dst = vscode.Uri.parse(fsEdit.dst!)
-            wsEdit.renameFile(src, dst)
-            moved = dst
-        } else {
-            console.error(`unknown op: ${JSON.stringify(fsEdit)}`)
-        }
-    }
-    let toOpen = created || moved
-    let toReveal = change.cursorPosition
-    await vscode.workspace.applyEdit(wsEdit)
-    if (toOpen) {
-        let doc = await vscode.workspace.openTextDocument(toOpen)
-        await vscode.window.showTextDocument(doc)
-    } else if (toReveal) {
-        let uri = client.protocol2CodeConverter.asUri(toReveal.textDocument.uri)
-        let position = client.protocol2CodeConverter.asPosition(toReveal.position)
-        let editor = vscode.window.activeTextEditor;
-        if (!editor || editor.document.uri.toString() != uri.toString()) return
-        if (!editor.selection.isEmpty) return
-        editor!.selection = new vscode.Selection(position, position)
-    }
+    return Server.client.stop();
 }
diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts
new file mode 100644
index 00000000000..e2ac4d62907
--- /dev/null
+++ b/editors/code/src/highlighting.ts
@@ -0,0 +1,82 @@
+import * as vscode from 'vscode';
+import * as lc from 'vscode-languageclient';
+
+import { Server } from './server';
+
+export interface Decoration {
+    range: lc.Range;
+    tag: string;
+}
+
+export class Highlighter {
+    private static initDecorations(): Map<string, vscode.TextEditorDecorationType> {
+        const decor = (color: string) => vscode.window.createTextEditorDecorationType({ color });
+
+        const decorations: Iterable<[string, vscode.TextEditorDecorationType]> = [
+            ['background', decor('#3F3F3F')],
+            ['error', vscode.window.createTextEditorDecorationType({
+                borderColor: 'red',
+                borderStyle: 'none none dashed none',
+            })],
+            ['comment', decor('#7F9F7F')],
+            ['string', decor('#CC9393')],
+            ['keyword', decor('#F0DFAF')],
+            ['function', decor('#93E0E3')],
+            ['parameter', decor('#94BFF3')],
+            ['builtin', decor('#DD6718')],
+            ['text', decor('#DCDCCC')],
+            ['attribute', decor('#BFEBBF')],
+            ['literal', decor('#DFAF8F')],
+        ];
+
+        return new Map<string, vscode.TextEditorDecorationType>(decorations);
+    }
+
+    private decorations: (Map<string, vscode.TextEditorDecorationType> | null) = null;
+
+    public removeHighlights() {
+        if (this.decorations == null) {
+            return;
+        }
+
+        // Decorations are removed when the object is disposed
+        for (const decoration of this.decorations.values()) {
+            decoration.dispose();
+        }
+
+        this.decorations = null;
+    }
+
+    public setHighlights(
+        editor: vscode.TextEditor,
+        highlights: Decoration[],
+    ) {
+        // Initialize decorations if necessary
+        //
+        // Note: decoration objects need to be kept around so we can dispose them
+        // if the user disables syntax highlighting
+        if (this.decorations == null) {
+            this.decorations = Highlighter.initDecorations();
+        }
+
+        const byTag: Map<string, vscode.Range[]> = new Map();
+        for (const tag of this.decorations.keys()) {
+            byTag.set(tag, []);
+        }
+
+        for (const d of highlights) {
+            if (!byTag.get(d.tag)) {
+                continue;
+            }
+            byTag.get(d.tag)!.push(
+                Server.client.protocol2CodeConverter.asRange(d.range),
+            );
+        }
+
+        for (const tag of byTag.keys()) {
+            const dec = this.decorations.get(tag) as vscode.TextEditorDecorationType;
+            const ranges = byTag.get(tag)!;
+            editor.setDecorations(dec, ranges);
+        }
+    }
+}
diff --git a/editors/code/src/notifications/index.ts b/editors/code/src/notifications/index.ts
new file mode 100644
index 00000000000..c5657686588
--- /dev/null
+++ b/editors/code/src/notifications/index.ts
@@ -0,0 +1,5 @@
+import * as publishDecorations from './publish_decorations';
+
+export {
+    publishDecorations,
+};
diff --git a/editors/code/src/notifications/publish_decorations.ts b/editors/code/src/notifications/publish_decorations.ts
new file mode 100644
index 00000000000..d8790386b1a
--- /dev/null
+++ b/editors/code/src/notifications/publish_decorations.ts
@@ -0,0 +1,20 @@
+import * as vscode from 'vscode';
+
+import { Decoration } from '../highlighting';
+import { Server } from '../server';
+
+export interface PublishDecorationsParams {
+    uri: string;
+    decorations: Decoration[];
+}
+
+export function handle(params: PublishDecorationsParams) {
+    const targetEditor = vscode.window.visibleTextEditors.find(
+        (editor) => editor.document.uri.toString() === params.uri,
+    );
+    if (!Server.config.highlightingOn || !targetEditor) { return; }
+    Server.highlighter.setHighlights(
+        targetEditor,
+        params.decorations,
+    );
+}
diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts
new file mode 100644
index 00000000000..01fd80756fd
--- /dev/null
+++ b/editors/code/src/server.ts
@@ -0,0 +1,37 @@
+import * as lc from 'vscode-languageclient';
+
+import { Config } from './config';
+import { Highlighter } from './highlighting';
+
+export class Server {
+    public static highlighter = new Highlighter();
+    public static config = new Config();
+    public static client: lc.LanguageClient;
+
+    public static start(notificationHandlers: Iterable<[string, lc.GenericNotificationHandler]>) {
+        const run: lc.Executable = {
+            command: 'ra_lsp_server',
+            options: { cwd: '.' },
+        };
+        const serverOptions: lc.ServerOptions = {
+            run,
+            debug: run,
+        };
+        const clientOptions: lc.LanguageClientOptions = {
+            documentSelector: [{ scheme: 'file', language: 'rust' }],
+        };
+
+        Server.client = new lc.LanguageClient(
+            'ra-lsp',
+            'rust-analyzer languge server',
+            serverOptions,
+            clientOptions,
+        );
+        Server.client.onReady().then(() => {
+            for (const [type, handler] of notificationHandlers) {
+                Server.client.onNotification(type, handler);
+            }
+        });
+        Server.client.start();
+    }
+}
diff --git a/editors/code/tslint.json b/editors/code/tslint.json
new file mode 100644
index 00000000000..ce48dfc95e1
--- /dev/null
+++ b/editors/code/tslint.json
@@ -0,0 +1,13 @@
+{
+    "defaultSeverity": "error",
+    "extends": [
+        "tslint:recommended"
+    ],
+    "jsRules": {},
+    "rules": {
+        "quotemark": [true, "single"],
+        "interface-name": false,
+        "object-literal-sort-keys": false
+    },
+    "rulesDirectory": []
+}