~glacambre/firenvim

ref: f48fc5ae81389dc4a9d24c80c55957d0b2d99470 firenvim/src/NeovimFrame.ts -rw-r--r-- 7.0 KiB
f48fc5aeglacambre Remove "sessions" permission 2 years ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import * as browser from "webextension-polyfill";
import { neovim } from "./nvimproc/Neovim";
import { page } from "./page/proxy";
import { getCharSize, getGridSize, toFileName } from "./utils/utils";

const nonLiteralKeys: {[key: string]: string} = {
    " ": "<Space>",
    "<": "<lt>",
    "ArrowDown": "<Down>",
    "ArrowLeft": "<Left>",
    "ArrowRight": "<Right>",
    "ArrowUp": "<Up>",
    "Backspace": "<BS>",
    "Delete": "<Del>",
    "End": "<End>",
    "Enter": "<CR>",
    "Escape": "<Esc>",
    "Home": "<Home>",
    "PageDown": "<PageDown>",
    "PageUp": "<PageUp>",
    "Tab": "<Tab>",
    "\\": "<Bslash>",
    "|": "<Bar>",
};

function translateKey(key: string) {
    if (nonLiteralKeys[key] !== undefined) {
        return nonLiteralKeys[key];
    }
    return key;
}

function addModifier(mod: string, text: string) {
    let match;
    let modifiers = "";
    let key = "";
    if ((match = text.match(/^<([A-Z]{1,5})-(.+)>$/))) {
        modifiers = match[1];
        key = match[2];
    } else if ((match = text.match(/^<(.+)>$/))) {
        key = match[1];
    } else {
        key = text;
    }
    return "<" + mod + modifiers + "-" + key + ">";
}

const locationPromise = page.getEditorLocation();
const connectionPromise = browser.runtime.sendMessage({ funcName: ["getNewNeovimInstance"] });

window.addEventListener("load", async () => {
    const host = document.getElementById("host") as HTMLPreElement;
    const keyHandler = document.getElementById("keyhandler");
    const [[url, selector, cursor], connectionData] = await Promise.all([locationPromise, connectionPromise]);
    const nvimPromise = neovim(host, selector, connectionData);
    const contentPromise = page.getElementContent(selector);

    const [cols, rows] = getGridSize(host);

    const nvim = await nvimPromise;

    nvim.ui_attach(cols, rows, {
        ext_linegrid: true,
        rgb: true,
    });
    let resizeReqId = 0;
    browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse: any) => {
        if (request.selector === selector
            && request.funcName[0] === "resize"
            && request.args[0] > resizeReqId) {
            const [id, width, height] = request.args;
            resizeReqId = id;
            // We need to put the keyHandler at the origin in order to avoid
            // issues when it slips out of the viewport
            keyHandler.style.left = `0px`;
            keyHandler.style.top = `0px`;
            // It's tempting to try to optimize this by only calling
            // ui_try_resize when nCols is different from cols and nRows is
            // different from rows but we can't because redraw notifications
            // might happen without us actually calling ui_try_resize and then
            // the sizes wouldn't be in sync anymore
            const [cellWidth, cellHeight] = getCharSize(host);
            const nCols = Math.floor(width / cellWidth);
            const nRows = Math.floor(height / cellHeight);
            nvim.ui_try_resize(nCols, nRows);
        }
    });

    // Create file, set its content to the textarea's, write it
    const filename = toFileName(url, selector);
    Promise.all([nvim.command(`noswapfile edit ${filename}`), contentPromise])
        .then(([_, content]: [any, string]) => {
            const promise = nvim.buf_set_lines(0, 0, -1, 0, content.split("\n"))
                .then((__: any) => nvim.command(":w!"));

            const beforeCursor = content.slice(0, cursor);
            const newlines = beforeCursor.match(/\n.*/g);
            let line = 1;
            let col = beforeCursor.length;
            if (newlines) {
                line = newlines.length + 1;
                col = newlines[newlines.length - 1].length - 1;
            }
            return promise.then((__: any) => nvim.win_set_cursor(0, [line, col]));
        });

    // Set client info and ask for notifications when the file is written/nvim is closed
    const extInfo = browser.runtime.getManifest();
    const [major, minor, patch] = extInfo.version.split(".");
    nvim.set_client_info(extInfo.name,
        { major, minor, patch },
        "ui",
        {},
        {},
    )
        .then(() => nvim.list_chans())
        .then((channels: any) => {
            const self: any = Object.values(channels)
                .find((channel: any) => channel.client && channel.client.name.match(new RegExp(extInfo.name, "i")));
            if (!self) {
                throw new Error("Couldn't find own channel.");
            }
            nvim.call_atomic((`augroup FirenvimAugroup
                            au!
                            autocmd BufWrite ${filename} `
                                + `call rpcnotify(${self.id}, `
                                    + `'firenvim_bufwrite', `
                                    + `{`
                                        + `'text': nvim_buf_get_lines(0, 0, -1, 0),`
                                        + `'cursor': nvim_win_get_cursor(0),`
                                    + `})
                            autocmd VimLeave * call delete('${filename}')
                            autocmd VimLeave * call rpcnotify(${self.id}, 'firenvim_vimleave')
                        augroup END`).split("\n").map(command => ["nvim_command", [command]]));
        });

    keyHandler.addEventListener("keydown", (evt) => {
        keyHandler.style.left = `0px`;
        keyHandler.style.top = `0px`;

        const specialKeys = [["altKey", "A"], ["ctrlKey", "C"], ["metaKey", "M"]];
        // The event has to be trusted and either have a modifier or a non-literal representation
        if (evt.isTrusted
            && (nonLiteralKeys[evt.key] !== undefined
                || specialKeys.find(([attr, _]: [string, string]) => (evt as any)[attr]))) {
            const text = specialKeys.concat(["shiftKey", "S"])
                .reduce((key: string, [attr, mod]: [string, string]) => {
                    if ((evt as any)[attr]) {
                        return addModifier(mod, key);
                    }
                    return key;
                }, translateKey(evt.key));
            nvim.input(text);
            evt.preventDefault();
            evt.stopImmediatePropagation();
        }
    });
    keyHandler.addEventListener("input", (evt: any) => {
        nvim.input(evt.target.value);
        evt.preventDefault();
        evt.stopImmediatePropagation();
        evt.target.innerText = "";
        evt.target.value = "";
    });
    window.addEventListener("mousemove", (evt: MouseEvent) => {
        keyHandler.style.left = `${evt.clientX}px`;
        keyHandler.style.top = `${evt.clientY}px`;
    });
    window.addEventListener("click", _ => keyHandler.focus());
    keyHandler.focus();
    // Let users know when they focus/unfocus the frame
    function setFocusedStyle() {
        document.documentElement.style.opacity = "1";
    }
    function setBluredStyle() {
        document.documentElement.style.opacity = "0.5";
    }
    window.addEventListener("focus", setFocusedStyle);
    window.addEventListener("blur", setBluredStyle);
});