~glacambre/firenvim

2fd6401d047b5d4f9dae0dfafe658c1b8ba77476 — glacambre 3 years ago 2c560e6
Move to webpack, use node's Stream for stdin/out, make msg JSON
11 files changed, 100 insertions(+), 210 deletions(-)

M Cargo.toml
M native_src/main.rs
M package.json
D src/EventEmitter.ts
M src/NeovimProcess.ts
M src/Stdin.ts
M src/Stdout.ts
M src/background.ts
M tsconfig.json
M tslint.json
A webpack.config.js
M Cargo.toml => Cargo.toml +3 -0
@@ 9,3 9,6 @@ name = "firenvim"

[dependencies]
byteorder = "*"
serde = "*"
serde_derive = "*"
serde_json = "*"

M native_src/main.rs => native_src/main.rs +27 -8
@@ 1,34 1,50 @@
extern crate byteorder;
use byteorder::{NativeEndian, WriteBytesExt, ReadBytesExt};

extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;

use std::process::{Command, Stdio};
use std::thread;
use std::io::{self, Read, Write};
use byteorder::{NativeEndian, WriteBytesExt, ReadBytesExt};

#[derive(Serialize, Deserialize)]
struct Message {
    #[serde(rename="type")]
    json_type: String,
    data: Vec<u8>,
}

fn forward_inputs (ff: &mut Read, nvim: &mut Write) {
    let mut buf = vec![];
    let mut msg: Message;
    while let Ok(message_size) = ff.read_u32::<NativeEndian>() {
        if message_size > 0 {
            buf.clear();
            ff.take((message_size) as u64).read_to_end(&mut buf).unwrap();
            nvim.write(&buf[1..((message_size - 1) as usize)]).unwrap();
            msg = serde_json::from_slice(&mut buf).unwrap();
            nvim.write(&msg.data).unwrap();
            nvim.flush().unwrap();
        }
    }
}

fn forward_outputs (ff: &mut Write, nvim: &mut Read) {
    let mut buf = [0; 4096];
    let mut buf = [0; 1000000]; // Max size for native messaging is 1MB
    while let Ok(message_size) = nvim.read(&mut buf) {
        if message_size <= 0 {
            // Process died
            return
        }
        ff.write_u32::<NativeEndian>((message_size + 2) as u32).unwrap();
        // Inefficient, use .format() instead
        ff.write("\"".as_bytes()).unwrap();
        ff.write(&buf[0 .. message_size]).unwrap();
        ff.write("\"".as_bytes()).unwrap();
        let msg = serde_json::to_string(&Message {
            json_type: "Buffer".to_owned(),
            data: buf[0..message_size].to_owned(),
        }).unwrap();
        ff.write_u32::<NativeEndian>((msg.len() + 0) as u32).unwrap();
        ff.write(msg.as_bytes()).unwrap();
        ff.flush().unwrap();
    }
}



@@ 36,6 52,9 @@ fn main() {
    let mut ff_in = io::stdin();
    let mut ff_out = io::stdout();

    // Easy debug :)
    // let nvim = Command::new("tee")
    //     .args(&["/tmp/firenvim_log"])
    let nvim = Command::new("nvim")
        .args(&["-u", "NORC", "--embed"])
        .stdout(Stdio::piped())

M package.json => package.json +6 -3
@@ 4,13 4,16 @@
  "description": "A webextension to turn Firefox into a Neovim client.",
  "devDependencies": {
    "@types/node": "^8.0.46",
    "typescript": "^2.5.3",
    "awesome-typescript-loader": "^3.2.3",
    "copy-webpack-plugin": "^4.2.0",
    "promised-neovim-client": "^2.0.2",
    "tslint": "^5.9.1",
    "typescript": "^2.5.3",
    "web-ext": "^1.8.1",
    "promised-neovim-client": "^2.0.2"
    "webpack": "^3.8.1"
  },
  "scripts": {
    "build": "((cargo build --release && (unlink ~/bin/firenvim ; cp target/release/firenvim ~/bin)) & tsc --out target/background.js && cp src/manifest.json target/manifest.json && cp src/native_manifest.json $HOME/.mozilla/native-messaging-hosts/firenvim.json) || true",
    "build": "((cargo build --release && (unlink ~/bin/firenvim ; cp target/release/firenvim ~/bin)) & webpack && cp src/native_manifest.json $HOME/.mozilla/native-messaging-hosts/firenvim.json && tslint --fix --project .) || true",
    "run": "web-ext run --source-dir=./target --keep-profile-changes --firefox-profile=$HOME/.mozilla/firefox/firenvim.profile",
    "package": "web-ext target",
    "clean": "rm -rf target"

D src/EventEmitter.ts => src/EventEmitter.ts +0 -104
@@ 1,104 0,0 @@
// This is an incomplete implementation of
// https://nodejs.org/api/events.html
class EventEmitter {
    private allListeners: {[key: string]: Array<() => any>};

    constructor() {
        this.allListeners = {};
    }

    public addListener(evname: string | symbol, listener: (...args: any[]) => any) {
        this.on(evname, listener);
        return this as any;
    }

    public emit(evname: string | symbol, ...args: any[]) {
        if (!this.allListeners[evname]) {
            return;
        }
        this.allListeners[evname].map((l) => l.apply(this, args));
        return this as any;
    }

    public eventNames() {
        return Object.keys(this.allListeners);
    }

    public getMaxListeners() {
        // Infinity
        return 1 / 0;
    }

    public listenerCount(evname: string) {
        if (!this.allListeners[evname].length) {
            return 0;
        }
        return this.allListeners[evname].length;
    }

    public listeners(evname: string) {
        if (!this.allListeners[evname]) {
            return;
        }
        return this.allListeners[evname].slice();
    }

    public on(evname: string | symbol, listener: (...args: any[]) => any) {
        if (!this.allListeners[evname]) {
            this.allListeners[evname] = [];
        }
        this.allListeners[evname].push(listener);
        return this as any;
    }

    // Creates a lambda that will try to remove itself from the `listeners`
    // array when called
    public selfRemovingListener(evname: string | symbol, listener: (...args: any[]) => any) {
        const fn = (...args: any[]) => {
            this.removeListener(evname, fn);
            listener.apply(this, args);
        };
        return fn;
    }

    public once(evname: string | symbol, listener: (...args: any[]) => any) {
        return this.on(evname, this.selfRemovingListener(evname, listener));
    }

    public prependListener(evname: string | symbol, listener: (...args: any[]) => any) {
        if (!this.allListeners[evname]) {
            this.allListeners[evname] = [];
        }
        this.allListeners[evname].unshift(listener);
        return this as any;
    }

    public prependOnceListener(evname: string | symbol, listener: (...args: any[]) => any) {
        return this.prependListener(evname, this.selfRemovingListener(evname, listener));
    }

    public removeAllListeners(evname: string | symbol) {
        this.allListeners[evname] = [];
        return this as any;
    }

    public removeListener(evname: string | symbol, listener: (...args: any[]) => any) {
        if (!this.allListeners[evname]) {
            return;
        }
        const i = this.allListeners[evname].indexOf(listener);
        if (i >= 0) {
            this.allListeners[evname] = this.allListeners[evname].splice(i, 1);
        }
        return this as any;
    }

    public setMaxListeners(n: number) {
        console.warn("setMaxListeners not implemented");
        return this as any;
    }

    public rawListeners(evname: string) {
        return this.listeners(evname);
    }
}

M src/NeovimProcess.ts => src/NeovimProcess.ts +0 -17
@@ 7,23 7,6 @@ export class NeovimProcess {

    constructor() {
        const port = browser.runtime.connectNative("firenvim");
        const proxy = {
            get: (obj: any, prop: any): any => {
                if (obj[prop] !== undefined) {
                    return obj[prop];
                }
                console.log(obj);
                throw new Error(`Property "${prop}" doesn't exist in "${obj}"`);
            },
            set: (obj: any, prop: any, value: any): boolean => {
                const retval = obj[prop] === undefined;
                if (retval) {
                    console.warn(`Setting new property "${prop}" to `, value, " in ", obj);
                }
                obj[prop] = value;
                return retval;
            },
        };
        this.stdin = new Stdin(port);
        this.stdout = new Stdout(port);
    }

M src/Stdin.ts => src/Stdin.ts +12 -25
@@ 1,37 1,24 @@
export class Stdin extends EventEmitter {
import * as stream from "stream";

export class Stdin extends stream.Writable {
    public port: Port;
    public writable: boolean;
    public writableBuffer: boolean;

    constructor(port: Port) {
        super();
        this.port = port;
        this.writable = true;
        this.writableBuffer = true;
        this.port.onDisconnect.addListener(this.onDisconnect.bind(this));
    }

    public write(str: string) {
        this.port.postMessage(str);
    public _write(chunk: any, encoding: any, cb: any) {
        console.warn("Stdin._write called: ", chunk);
        this.port.postMessage(chunk);
        return false;
    }

    public pipe() {
        throw new Error("Trying to pipe Stdin");
    }

    public cork() {
        throw new Error("Trying to cork Stdin");
    }

    public uncork() {
        throw new Error("Trying to uncork Stdin");
    }

    public setDefaultEncoding() {
        throw new Error("Trying to setDefaultEncoding Stdin");
    }

    public end() {
        throw new Error("Trying to end Stdin");
    private onDisconnect(port: Port) {
        if (port.error) {
            console.log("Disconnected due to an error:", port);
        }
        this.emit("close");
    }
}

M src/Stdout.ts => src/Stdout.ts +14 -48
@@ 1,63 1,29 @@
export class Stdout extends EventEmitter {
import * as stream from "stream";

export class Stdout extends stream.Readable {
    public port: Port;
    public readable: boolean;

    constructor(port: Port) {
        super();
        this.port = port;
        this.port.onMessage.addListener(this.onMessage.bind(this));
        this.readable = true;
    }

    public isPaused() {
        console.warn("Calling isPaused on Stdout");
        return false;
    }

    public pause() {
        console.warn("Calling pause on Stdout");
        return this as any;
    }

    public pipe(destination: any, options?: {end?: boolean; }) {
        console.warn("Calling pipe on Stdout");
        return destination;
    }

    public read(size: number) {
        console.warn("Calling read on Stdout");
        return "";
    }

    public resume() {
        console.warn("Calling resume on Stdout");
        return this as any;
    }

    public setEncoding(encoding: string) {
        console.warn("Calling setEncoding on Stdout");
        return this as any;
    }

    public unpipe(destination: any) {
        console.warn("Calling unpipe on Stdout");
        return this as any;
    }

    public unshift() {
        console.warn("Calling unshift on Stdout");
        this.port.onDisconnect.addListener(this.onDisconnect.bind(this));
    }

    public wrap(oldStream: any) {
        console.warn("Calling wrap on Stdout");
        return this as any;
    public _read(n: any) {
        console.log("Stdout._read called:", n);
    }

    public destroy() {
        console.warn("Calling destroy on Stdout");
    private onDisconnect(port: Port) {
        if (port.error) {
            console.log("Disconnected due to an error:", port);
        }
        console.log("Stdout.onDisconnect");
    }

    private onMessage(msg: any) {
        console.log(msg);
        console.log("Stdout.onMessage: ", msg);
        this.push(new Uint8Array(msg.data));
        // this.emit("data", msg.data);
    }
}

M src/background.ts => src/background.ts +2 -1
@@ 1,10 1,11 @@
import NeovimClient from "promised-neovim-client";
import * as NeovimClient from "promised-neovim-client";
import {NeovimProcess} from "./NeovimProcess";

console.log("Firenvim content script loaded.");
const nvimProc = new NeovimProcess();

NeovimClient.attach(nvimProc.stdin, nvimProc.stdout).then((nvim: any) => {
    console.log("Neovim attached");
    nvim.on("request", (method: any, args: any, resp: any) => {
        console.log("request", method, args, resp);
    });

M tsconfig.json => tsconfig.json +0 -3
@@ 1,10 1,7 @@
{
  "compilerOptions": {
    "module": "amd",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "outDir": "./target",
    "sourceMap": true,
    "target": "ES2017",
    "typeRoots": ["node_modules/@types", "node_modules/web-ext-types/"],

M tslint.json => tslint.json +3 -1
@@ 1,7 1,9 @@
{
  "extends": "tslint:recommended",
  "rules": {
    "arrow-parens": false,
    "no-console": false,
    "no-namespace": false
    "no-namespace": false,
    "semicolon": [true, "always"]
  }
}

A webpack.config.js => webpack.config.js +33 -0
@@ 0,0 1,33 @@
const CopyWebPackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: {
    background: "./src/background.ts",
    content: "./src/content.ts",
  },
  output: {
    filename: "[name].js",
    path: __dirname + "/target/extension",
  },

  // Enable sourcemaps for debugging webpack's output.
  devtool: "inline-source-map",

  resolve: {
    // Add '.ts' and '.tsx' as resolvable extensions.
    extensions: [".ts", ".tsx", ".js", ".json"],
  },

  module: {
    rules: [
      // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
      { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
    ],
  },

  plugins: [
    new CopyWebPackPlugin([
      { from: "src/manifest.json" },
    ]),
  ],
};