Showing preview only (303K chars total). Download the full file or copy to clipboard to get everything.
Repository: vv-vim/vv
Branch: main
Commit: f54a3463cd2b
Files: 140
Total size: 268.5 KB
Directory structure:
gitextract_afbqh6f5/
├── .eslintrc.js
├── .github/
│ └── workflows/
│ ├── link_typecheck.yml
│ └── tests.yml
├── .gitignore
├── .husky/
│ ├── .gitignore
│ └── pre-commit
├── .nvmrc
├── .prettierrc.js
├── LICENSE
├── README.md
├── babel.config.json
├── codecov.yml
├── jest.config.js
├── package.json
├── packages/
│ ├── browser-renderer/
│ │ ├── README.md
│ │ ├── babel.config.json
│ │ ├── config/
│ │ │ ├── jest/
│ │ │ │ ├── afterEnv.js
│ │ │ │ ├── globalSetup.js
│ │ │ │ ├── globalTeardown.js
│ │ │ │ └── testServer.js
│ │ │ ├── webpack.config.js
│ │ │ └── webpack.prod.config.js
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── renderer.test.ts
│ │ │ │ └── screen.test.ts
│ │ │ ├── features/
│ │ │ │ └── hideMouseCursor.ts
│ │ │ ├── index.ts
│ │ │ ├── input/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── keyboard.test.ts
│ │ │ │ ├── keyboard.ts
│ │ │ │ └── mouse.ts
│ │ │ ├── lib/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── getColor.test.ts
│ │ │ │ ├── getColor.ts
│ │ │ │ └── isWeb.ts
│ │ │ ├── preloaded/
│ │ │ │ └── electron.ts
│ │ │ ├── renderer.ts
│ │ │ ├── screen.ts
│ │ │ ├── transport/
│ │ │ │ ├── __tests__/
│ │ │ │ │ ├── ipc.test.ts
│ │ │ │ │ └── websocket.test.ts
│ │ │ │ ├── ipc.ts
│ │ │ │ ├── transport.ts
│ │ │ │ └── websocket.ts
│ │ │ └── types.ts
│ │ ├── tsconfig.declaration.json
│ │ └── tsconfig.json
│ ├── electron/
│ │ ├── @types/
│ │ │ └── html2plaintext.d.ts
│ │ ├── README.md
│ │ ├── assets/
│ │ │ ├── generic.icns
│ │ │ └── icon.icns
│ │ ├── babel.config.json
│ │ ├── bin/
│ │ │ ├── openInProject.vim
│ │ │ ├── reloadChanged.vim
│ │ │ ├── vv
│ │ │ ├── vv.vim
│ │ │ └── vvset.vim
│ │ ├── config/
│ │ │ ├── electron-builder/
│ │ │ │ ├── build.js
│ │ │ │ ├── fileAssociations.json
│ │ │ │ └── release.js
│ │ │ ├── webpack.common.config.js
│ │ │ ├── webpack.config.js
│ │ │ ├── webpack.main.config.js
│ │ │ ├── webpack.prod.config.js
│ │ │ └── webpack.renderer.config.js
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── filetypes.js
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── isDev.ts
│ │ │ │ └── log.ts
│ │ │ ├── main/
│ │ │ │ ├── autoUpdate.ts
│ │ │ │ ├── checkNeovim.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── installCli.ts
│ │ │ │ ├── lib/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── args.test.ts
│ │ │ │ │ ├── args.ts
│ │ │ │ │ ├── store.ts
│ │ │ │ │ └── which.ts
│ │ │ │ ├── menu.ts
│ │ │ │ ├── nvim/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── nvim.test.ts
│ │ │ │ │ ├── features/
│ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ ├── backrdoundColor.test.ts
│ │ │ │ │ │ │ └── windowSize.test.ts
│ │ │ │ │ │ ├── backrdoundColor.ts
│ │ │ │ │ │ ├── closeWindow.ts
│ │ │ │ │ │ ├── copyPaste.ts
│ │ │ │ │ │ ├── focusAutocmd.ts
│ │ │ │ │ │ ├── quit.ts
│ │ │ │ │ │ ├── reloadChanged.ts
│ │ │ │ │ │ ├── windowSize.ts
│ │ │ │ │ │ ├── windowTitle.ts
│ │ │ │ │ │ └── zoom.ts
│ │ │ │ │ ├── nvim.ts
│ │ │ │ │ ├── nvimByWindow.ts
│ │ │ │ │ └── settings.ts
│ │ │ │ ├── preload.js
│ │ │ │ └── transport/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── ipc.test.ts
│ │ │ │ └── ipc.ts
│ │ │ └── renderer/
│ │ │ ├── index.html
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── nvim/
│ │ ├── README.md
│ │ ├── babel.config.json
│ │ ├── config/
│ │ │ ├── webpack.config.js
│ │ │ └── webpack.prod.config.js
│ │ ├── jest.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── Nvim.ts
│ │ │ ├── ProcNvimTransport.ts
│ │ │ ├── __generated__/
│ │ │ │ ├── constants.ts
│ │ │ │ └── types.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── Nvim.test.ts
│ │ │ │ ├── ProcNvimTransport.test.ts
│ │ │ │ ├── process.test.ts
│ │ │ │ └── utils.test.ts
│ │ │ ├── browser.ts
│ │ │ ├── index.ts
│ │ │ ├── process.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.declaration.json
│ │ └── tsconfig.json
│ └── server/
│ ├── README.md
│ ├── babel.config.json
│ ├── bin/
│ │ ├── vv.vim
│ │ └── vvset.vim
│ ├── config/
│ │ ├── webpack.common.config.js
│ │ ├── webpack.config.js
│ │ ├── webpack.prod.config.js
│ │ ├── webpack.renderer.config.js
│ │ └── webpack.server.config.js
│ ├── jest.config.js
│ ├── package.json
│ ├── src/
│ │ ├── lib/
│ │ │ └── isDev.ts
│ │ ├── renderer/
│ │ │ ├── index.html
│ │ │ └── index.ts
│ │ └── server/
│ │ ├── index.ts
│ │ ├── nvim/
│ │ │ ├── nvim.ts
│ │ │ └── settings.ts
│ │ └── transport/
│ │ └── websocket.ts
│ └── tsconfig.json
├── scripts/
│ └── codegen.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['jest'],
extends: [
'airbnb-base',
// Temporary disable it until upgrade to the Prettier 3
// 'plugin:prettier/recommended',
'plugin:jest/recommended',
'plugin:jest/style',
'plugin:import/typescript',
],
env: {
browser: true,
'jest/globals': true,
},
ignorePatterns: [
'**/tmp/**',
'**/build/**',
'**/dist/**',
'**/node_modules/**',
'**/@types/**',
'**/__generated__/**',
],
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts'],
},
},
},
overrides: [
{
files: ['*.ts'],
plugins: ['jest', '@typescript-eslint'],
extends: [
'airbnb-base',
// 'plugin:prettier/recommended',
'plugin:jest/recommended',
'plugin:jest/style',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
],
rules: {
'prefer-destructuring': 'off',
'no-console': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
},
],
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'import/prefer-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'off', // TypeScript handles this
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
},
],
// Styling. Remove after prettier upgrade
'object-curly-newline': 'off',
'function-paren-newline': 'off',
'implicit-arrow-linebreak': 'off',
'arrow-body-style': 'off',
'max-len': 'off',
'no-confusing-arrow': 'off',
quotes: 'off',
'operator-linebreak': 'off',
'quote-props': 'off',
indent: 'off',
},
},
],
rules: {
'prefer-destructuring': 'off',
'no-console': 'error',
'no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
},
],
'no-mixed-operators': [
'error',
{
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
allowSamePrecedence: true,
},
],
'import/prefer-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
},
],
// Styling. Remove after prettier upgrade
'object-curly-newline': 'off',
'function-paren-newline': 'off',
'implicit-arrow-linebreak': 'off',
'arrow-body-style': 'off',
'max-len': 'off',
'no-confusing-arrow': 'off',
quotes: 'off',
'operator-linebreak': 'off',
'quote-props': 'off',
indent: 'off',
},
};
================================================
FILE: .github/workflows/link_typecheck.yml
================================================
name: Lint & Typecheck
on:
pull_request:
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Read .nvmrc
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
id: nvm
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: '${{ steps.nvm.outputs.NVMRC }}'
- name: Get Yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Yarn
uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: yarn-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
yarn-${{ runner.os }}-
- name: Install Dependencies
run: yarn
- name: Build Required Packages
run: yarn bootstrap
- name: Run ESLint
run: yarn lint
if: always()
- name: Run TypeScript check
run: yarn typecheck
if: always()
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Read .nvmrc
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
id: nvm
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '${{ steps.nvm.outputs.NVMRC }}'
- name: Install Nvim
run: brew install nvim
- name: Get Yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Yarn
uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: yarn-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
yarn-${{ runner.os }}-
- name: Install Dependencies
run: yarn
- name: Build Required Packages
run: yarn bootstrap
- name: Run Tests
run: yarn test --reporters="default" --reporters="jest-github-actions-reporter" --coverage
- name: Upload snapshot diffs
uses: actions/upload-artifact@v4
with:
name: failed-image-snapshots
path: packages/**/__diff_output__/*
retention-days: 5
if: failure()
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
**/node_modules/**
**/build/**
**/dist/**
**/coverage/**
**/tmp/**
.DS_Store
.env
.nyc_output
yarn-error.log
__diff_output__
================================================
FILE: .husky/.gitignore
================================================
_
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged
================================================
FILE: .nvmrc
================================================
20.9.0
================================================
FILE: .prettierrc.js
================================================
module.exports = {
trailingComma: 'all',
printWidth: 100,
singleQuote: true,
};
================================================
FILE: LICENSE
================================================
Copyright (c) 2018-present Igor Gladkoborodov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# VV
VV is a Neovim client for macOS. A pure, fast, minimalistic Vim experience with good macOS integration. Optimized for speed and nice font rendering.

- Fast text render via WebGL.
- OS integration: copy/paste, mouse, scroll.
- Fullscreen support for native and simple (fast) mode.
- All app settings configurable via Vimscript.
- Command line launcher.
- “Save All” dialog on quit and “Refresh” dialog on external changes.
- Text zoom.
VV is built on Electron. There are no barriers to porting it to Windows or Linux, or making plugins with Javascript, HTML, and CSS.
## Installation
### Install via Homebrew
VV is available via Homebrew Cask:
```
$ brew install vv
```
NOTE: older versions of brew require a special command to install `vv`
```
$ brew cask install vv
```
It will also install Neovim (if it is not installed) and command line launcher `vv`.
### Download
Or you can download the most recent release from the [Releases](https://github.com/vv-vim/vv/releases/latest) page.
You need Neovim to run VV. You can install it via Homebrew: `$ brew install neovim`. Or you can find Neovim installation instructions here: [https://github.com/neovim/neovim/wiki/Installing-Neovim](https://github.com/neovim/neovim/wiki/Installing-Neovim). Neovim version 0.4.0 and higher is required.
### Build manually
You can also build it manually. You will need [Node.js](https://nodejs.org/en/download/) and [Yarn](https://yarnpkg.com/lang/en/) installed.
```
$ git clone git@github.com:vv-vim/vv.git
$ cd vv
$ yarn
$ yarn build:electron
```
This will generate a VV.app binary in the dist directory. Copy VV.app to your /Applications folder and add the CLI launcher `vv` to your `/usr/local/bin`.
## Command Line Launcher
You can use the `vv` command to run VV in a Terminal. Install it via the `VV → Command Line Launcher...` menu item. VV will add the command to your `/usr/local/bin` folder. If you prefer another place, you can link the command manually:
```
ln -s -f /Applications/VV.app/Contents/Resources/bin/vv [dir from $PATH]/vv
```
Usage: `vv [options] [file ...]`
Options are passed to `nvim`. You can check available options in nvim help: `nvim --help`.
## Settings
You can setup VV-specific options via the `:VVset` command. It works the same as the vim built-in command `:set`. For example `:VVset nofullscreen` is the same as `:VVset fullscreen=0`. You can use `:help set` for syntax reference.
- `fullscreen`, `fu`: Switch to fullscreen mode. You can also toggle fullscreen with `Cmd+Ctrl+F`. Default: `0`.
- `simplefullscreen`, `sfu`: Use simple or standard fullscreen mode. Simple mode is faster than standard macOS fullscreen mode. It does not have any transition animation. Default: `1`.
- `bold`: Allow bold font. You can completely disable bold even if the colorscheme uses it. Default: `1`.
- `italic`: Allow italic. Default: `1`.
- `underline`: Allow underline. Default: `1`.
- `undercurl`: Allow undercurl. Default: `1`.
- `strikethrough`: Allow strikethrough. Default: `1`.
- `fontfamily`: Font family. Syntax is the same as CSS [`font-family`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family). You can use comma-separated list of fonts. It will use first installed font in the list and fallback to default monospace font if none of them installed. Spaces should be excaped by `\`. For example: `:VVset fontfamily=Menlo,\ Courier\ New`. Default: `monospace`.
- `fontsize`: Font size in pixels. Default: `12`.
- `lineheight`: Line height related to font size. Pixel value is `fontsize * lineheight`. Default: `1.25`.
- `letterspacing`: Fine-tuning letter spacing in retina pixels. Can be a negative value. For retina screens the value is physical pixels. For non-retina screens it works differently: it divides the value by 2 and rounds it. For example, `:VVset letterspacing=1` will make characters 1 pixel wider on retina displays and will do nothing on non-retina displays. Value 2 is 2 physical pixels on retina and 1 physical pixel on non-retina. Default: `0`.
- `windowwidth`, `width`: Window width. Can be a number in pixels or percentage of display width.
- `windowheight`, `height`: Window height.
- `windowleft`, `left`: Window position from left. Can be a number in pixels or a percentage. Percent values work the same as the `background-position` rule in CSS. For example: `25%` means that the vertical line on the window that is 25% from the left will be placed at the line that is 25% from the display's left. 0% — the very left, 100% — the very right, 50% — center.
- `windowtop`, `top`: Window position top.
- `quitoncloselastwindow`: Quit app on close last window. Default: `0`.
- `autoupdateinterval`: Autoupdate interval in minutes. `0` — disable autoupdate. Default: `1440`, one day.
- `openinproject`: Open file in existing VV instance if this file is located inside current directory of this instance. By default it will obey [`switchbuf`](https://neovim.io/doc/user/options.html#'switchbuf') option, but you can set `switchbuf` override as a value of this option, for example: `:VVset openinproject=newtab`. Possible values are: `1` use switchbuf, `0` open in new instance, any valid `switchbuf` value. Default: `1`.
You can use these settings in your `init.vim` or change them any time. You can check if VV is loaded by checking the `g:vv` variable:
```
if exists('g:vv')
VVset nobold
VVset noitalic
VVset windowheight=100%
VVset windowwidth=60%
VVset windowleft=0
VVset windowtop=0
endif
```
VV also sets `set termguicolors` on startup.
## Development
First, you need start a Webpack watch process in a separate terminal:
```
yarn dev
```
Then you can run the app:
```
yarn start:electron
```
You can run tests with `yarn test` and ESLint with `yarn lint` commands.
It is written on TypeScript, but it uses Babel to build. It does not check types during the build. If you want do do type check manually you can run it with `yarn typecheck`.
## Server
You can run Neovim remotely in browser via VV Server. More info: [packages/server/README.md](packages/server/README.md)
## Browser Renderer
[Browser Renderer](packages/browser-renderer/README.md) is a separate package used in Electron app and Server.
## Name
The VV name comes from the bash shortcut `vv` that I use to start Vim.
## License
VV is released under the [MIT License](https://opensource.org/licenses/MIT).
================================================
FILE: babel.config.json
================================================
{
"presets": [["@babel/preset-env", { "modules": "commonjs" }], "@babel/preset-typescript"],
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime",
[
"module-resolver",
{
"root": ["."],
"alias": {
"src": "./src"
}
}
]
]
}
================================================
FILE: codecov.yml
================================================
comment:
layout: "reach, diff, flags, files"
require_changes: false
ignore:
- "packages/browser-renderer/src/screen.ts" # Tested by Puppeteer
================================================
FILE: jest.config.js
================================================
module.exports = {
clearMocks: true,
testEnvironment: 'node',
collectCoverageFrom: ['src/**/*.{ts,js}'],
projects: ['<rootDir>/packages/*'],
};
================================================
FILE: package.json
================================================
{
"name": "vv",
"description": "Neovim GUI Client",
"author": "Igor Gladkoborodov <igor.gladkoborodov@gmail.com>",
"keywords": [
"vim",
"neovim",
"client",
"gui",
"electron"
],
"license": "MIT",
"main": "./build/main.js",
"sideEffects": false,
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"bootstrap": "yarn build:nvim; yarn build:browser-renderer; yarn build:server",
"build:nvim": "yarn workspace @vvim/nvim build",
"build:browser-renderer": "yarn workspace @vvim/browser-renderer build",
"build:electron": "yarn bootstrap; yarn workspace @vvim/electron build",
"build:server": "yarn workspace @vvim/server build",
"dev:nvim": "yarn workspace @vvim/nvim dev",
"dev:browser-renderer": "yarn workspace @vvim/browser-renderer dev",
"dev:electron": "yarn workspace @vvim/electron dev",
"dev:server": "yarn workspace @vvim/server dev",
"dev": "yarn bootstrap; npm-run-all --parallel dev:*",
"start:electron": "yarn workspace @vvim/electron start",
"start:server": "yarn workspace @vvim/server start",
"lint": "eslint . --ext .js,.ts",
"test": "jest",
"typecheck": "tsc -p packages/browser-renderer; tsc -p packages/electron; tsc -p packages/server",
"prepare": "husky install",
"codegen": "babel-node -x \".ts\" scripts/codegen.ts"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/node": "^7.23.9",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/plugin-transform-runtime": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.23.3",
"@types/jest": "^30.0.0",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"babel-jest": "^30.1.1",
"babel-loader": "^8.2.5",
"babel-plugin-module-resolver": "^4.1.0",
"codecov": "^3.8.1",
"eslint": "^7.21.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.5",
"eslint-plugin-prettier": "^3.3.1",
"husky": "^5.2.0",
"jest": "^30.1.1",
"jest-github-actions-reporter": "^1.0.3",
"lint-staged": "^10.5.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.2.1",
"regenerator": "^0.14.7",
"ts-jest": "^29.4.1",
"typescript": "^4.2.2",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.10.0"
},
"lint-staged": {
"*.{ts,js,css,json,md}": [
"prettier --write"
]
}
}
================================================
FILE: packages/browser-renderer/README.md
================================================
# @vvim/browser-renderer
This package is used to render Neovim in browser for [VV Electron App](../electron) and [VV server](../server).
It is in active development, the API is not stable yet.
## Development
Run in watch mode:
```
yarn dev
```
================================================
FILE: packages/browser-renderer/babel.config.json
================================================
{
"extends": "../../babel.config.json",
"plugins": [
[
"module-resolver",
{
"root": ["."],
"alias": {
"src": "./src",
"config": "./config"
}
}
]
]
}
================================================
FILE: packages/browser-renderer/config/jest/afterEnv.js
================================================
const { toMatchImageSnapshot } = require('jest-image-snapshot');
expect.extend({ toMatchImageSnapshot });
================================================
FILE: packages/browser-renderer/config/jest/globalSetup.js
================================================
/* eslint-disable no-underscore-dangle, no-undef */
import { setupTestServer } from './testServer';
const globalSetup = async () => {
globalThis.__PUPPETEER_SERVER__ = await setupTestServer();
};
export default globalSetup;
================================================
FILE: packages/browser-renderer/config/jest/globalTeardown.js
================================================
/* eslint-disable no-underscore-dangle, no-undef */
import { teardownTestServer } from './testServer';
const globalTeardown = async () => {
await teardownTestServer(globalThis.__PUPPETEER_SERVER__);
};
export default globalTeardown;
================================================
FILE: packages/browser-renderer/config/jest/testServer.js
================================================
import { setup, teardown } from 'jest-dev-server';
// TODO: make it configurable
export const PORT = 3001;
export async function setupTestServer() {
const server = await setup({
command: `PORT=${PORT} yarn start:server -u NONE`,
launchTimeout: 10000,
port: PORT,
usedPortAction: 'kill',
});
return server;
}
export async function teardownTestServer(server) {
if (server) {
await teardown(server);
}
}
================================================
FILE: packages/browser-renderer/config/webpack.config.js
================================================
const path = require('path');
const buildPath = path.resolve(__dirname, './../dist');
const config = {
mode: 'development',
entry: './src/index.ts',
output: {
path: buildPath,
filename: 'index.js',
libraryTarget: 'umd',
},
target: 'web',
devtool: 'eval-cheap-source-map',
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
};
module.exports = config;
================================================
FILE: packages/browser-renderer/config/webpack.prod.config.js
================================================
const { merge } = require('webpack-merge');
const webpackConfig = require('./webpack.config');
const prod = {
mode: 'production',
devtool: 'source-map',
};
const webpackConfigProd = merge(webpackConfig, prod);
module.exports = webpackConfigProd;
================================================
FILE: packages/browser-renderer/jest.config.js
================================================
module.exports = {
testEnvironment: 'jsdom',
clearMocks: true,
moduleNameMapper: {
'\\./src/(.*)': ['<rootDir>/src/$1'],
'config/(.*)': ['<rootDir>/config/$1'],
},
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
setupFilesAfterEnv: ['<rootDir>/config/jest/afterEnv.js'],
globalSetup: '<rootDir>/config/jest/globalSetup.js',
globalTeardown: '<rootDir>/config/jest/globalTeardown.js',
};
================================================
FILE: packages/browser-renderer/package.json
================================================
{
"name": "@vvim/browser-renderer",
"version": "0.0.1",
"description": "VV Browser Renderer",
"author": "Igor Gladkoborodov <igor.gladkoborodov@gmail.com>",
"keywords": [
"vim",
"neovim",
"client",
"gui",
"renderer",
"browser",
"webgl"
],
"homepage": "https://github.com/vv-vim/vv#readme",
"license": "MIT",
"main": "dist/index.js",
"sideEffects": false,
"scripts": {
"test": "jest",
"clean": "rm -rf dist/*",
"build:types": "tsc -p tsconfig.declaration.json",
"build:dev": "webpack --config ./config/webpack.config.js",
"build:prod": "webpack --config ./config/webpack.prod.config.js",
"build": "npm-run-all clean build:types build:prod",
"dev": "npm-run-all --parallel \"build:types --watch\" \"build:dev --watch\""
},
"publishConfig": {
"registry": "https://registry.yarnpkg.com"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vv-vim/vv.git"
},
"bugs": {
"url": "https://github.com/vv-vim/vv/issues"
},
"browserslist": [
"defaults",
"last 2 electron versions"
],
"devDependencies": {
"@types/express": "^4.17.11",
"@types/jest-dev-server": "^5.0.3",
"@types/jest-image-snapshot": "^6.4.0",
"@types/lodash": "^4.14.168",
"@types/node": "^16.0.0",
"@types/ws": "^7.4.0",
"jest-dev-server": "^11.0.0",
"jest-environment-jsdom": "^30.1.1",
"jest-image-snapshot": "^6.5.1",
"jsdom": "^26.1.0",
"puppeteer": "^24.17.1"
},
"dependencies": {
"@vvim/nvim": "0.0.1",
"lodash": "^4.17.21",
"ws": "^7.4.6"
}
}
================================================
FILE: packages/browser-renderer/src/__tests__/renderer.test.ts
================================================
import { EventEmitter } from 'events';
import initRenderer from 'src/renderer';
import Nvim from '@vvim/nvim';
import initScreen from 'src/screen';
import initKeyboard from 'src/input/keyboard';
import initMouse from 'src/input/mouse';
import hideMouseCursor from 'src/features/hideMouseCursor';
const mockTransport = new EventEmitter();
jest.mock('src/transport/transport', () => {
return jest.fn().mockImplementation(() => mockTransport);
});
jest.mock('@vvim/nvim');
jest.mock('src/screen', () => jest.fn(() => 'fakeScreen'));
jest.mock('src/input/keyboard', () => jest.fn());
jest.mock('src/input/mouse', () => jest.fn());
jest.mock('src/features/hideMouseCursor', () => jest.fn());
describe('renderer', () => {
const mockedNvim = (Nvim as unknown) as jest.Mock<Nvim>;
beforeEach(() => {
mockTransport.removeAllListeners();
initRenderer();
});
test('init screen', () => {
mockTransport.emit('initRenderer', 'settings');
expect(initScreen).toHaveBeenCalledWith({
nvim: mockedNvim.mock.instances[0],
settings: 'settings',
transport: mockTransport,
});
});
test('init nvim', () => {
mockTransport.emit('initRenderer', 'settings');
expect(Nvim).toHaveBeenCalledWith(mockTransport, true);
});
test('init keyboard', () => {
mockTransport.emit('initRenderer', 'settings');
expect(initKeyboard).toHaveBeenCalledWith({
nvim: mockedNvim.mock.instances[0],
screen: 'fakeScreen',
});
});
test('init mouse', () => {
mockTransport.emit('initRenderer', 'settings');
expect(initMouse).toHaveBeenCalledWith({
nvim: mockedNvim.mock.instances[0],
screen: 'fakeScreen',
});
});
test('init hideMouseCursor', () => {
mockTransport.emit('initRenderer', 'settings');
expect(hideMouseCursor).toHaveBeenCalledWith();
});
});
================================================
FILE: packages/browser-renderer/src/__tests__/screen.test.ts
================================================
/** @jest-environment node */
import puppeteer from 'puppeteer';
import { PORT } from 'config/jest/testServer';
import type { Browser, Page } from 'puppeteer';
describe('Screen', () => {
jest.setTimeout(30000);
let browser: Browser;
let page: Page;
beforeAll(async () => {
browser = await puppeteer.launch({
headless: true,
slowMo: 10,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
});
afterAll(async () => {
// await browser.close();
});
beforeEach(async () => {
page = await browser.newPage();
await page.setViewport({
width: 300,
height: 200,
deviceScaleFactor: 2,
});
await page.goto(`http://localhost:${PORT}`);
await page.waitForSelector('input');
await page.keyboard.type(':VVset fontfamily=Courier\\ New');
await page.keyboard.press('Enter');
});
afterEach(async () => {
await page.close();
});
it('match snapshot', async () => {
await page.keyboard.type('iHello');
await page.keyboard.press('Escape');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
it('redraw screen on default_colors_set', async () => {
await page.keyboard.type(':colorscheme desert');
await page.keyboard.press('Enter');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
test('show undercurl behind the text', async () => {
await page.keyboard.type(':set filetype=javascript');
await page.keyboard.press('Enter');
await page.keyboard.type(':VVset lineheight=1');
await page.keyboard.press('Enter');
await page.keyboard.type(':syntax on');
await page.keyboard.press('Enter');
await page.keyboard.type(':hi Comment gui=undercurl guifg=white guisp=red');
await page.keyboard.press('Enter');
await page.keyboard.type('i// Hey!');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
test('overlap chars', async () => {
await page.keyboard.type(':VVset letterspacing=-8');
await page.keyboard.press('Enter');
await page.keyboard.type(
'i\n\n\nO O O O O O O O O O O OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO',
);
await page.keyboard.press('Escape');
await page.keyboard.type('hhhi ');
await page.keyboard.press('Escape');
await page.keyboard.type('hhh');
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
await page.keyboard.type(':vs');
await page.keyboard.press('Enter');
await page.keyboard.type(':vs');
await page.keyboard.press('Enter');
await page.keyboard.type('i');
await page.keyboard.press('Escape');
await page.mouse.move(150, 100);
await page.mouse.wheel({ deltaY: 100 });
const image1 = await page.screenshot();
expect(image1).toMatchImageSnapshot();
});
});
================================================
FILE: packages/browser-renderer/src/features/hideMouseCursor.ts
================================================
/**
* Hides mouse cursor when you start typing. Shows it again when you move mouse.
*/
function showCursor() {
document.body.style.cursor = 'auto';
document.addEventListener('keydown', hideCursor); // eslint-disable-line no-use-before-define
document.removeEventListener('mousemove', showCursor);
}
function hideCursor(): void {
document.body.style.cursor = 'none';
document.addEventListener('mousemove', showCursor);
document.removeEventListener('keydown', hideCursor);
}
export default hideCursor;
================================================
FILE: packages/browser-renderer/src/index.ts
================================================
// Only use relative imports here because https://github.com/microsoft/TypeScript/issues/32999#issuecomment-523558695
import renderer from './renderer';
export default renderer;
================================================
FILE: packages/browser-renderer/src/input/__tests__/keyboard.test.ts
================================================
import initKeyboard from 'src/input/keyboard';
import Nvim from '@vvim/nvim';
import { Screen } from 'src/screen';
describe('Keyboard input', () => {
const nvimOn = jest.fn();
const screen = ({
getCursorElement: jest.fn<HTMLDivElement, void[]>(),
} as unknown) as Screen;
const nvim = ({
input: jest.fn(),
on: nvimOn,
} as unknown) as Nvim;
const simulateKeyDown = (options: KeyboardEventInit) => {
const event = new KeyboardEvent('keydown', options);
document.dispatchEvent(event);
};
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
beforeEach(() => {
initKeyboard({ screen, nvim });
});
afterEach(() => {
const rootElm = document.documentElement;
rootElm.innerHTML = '<head></head><body></body>';
addEventListenerSpy.mock.calls.forEach(([event, callback, options]) =>
document.removeEventListener(event, callback, options),
);
});
describe('key input', () => {
test('sends key value to nvim input', () => {
simulateKeyDown({ key: 'i' });
expect(nvim.input).toHaveBeenCalledWith('i');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('special key', () => {
simulateKeyDown({ code: 'Insert' });
expect(nvim.input).toHaveBeenCalledWith('<Insert>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('special key with Shift', () => {
simulateKeyDown({ code: 'Insert', shiftKey: true });
expect(nvim.input).toHaveBeenCalledWith('<S-Insert>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('special key with modifier', () => {
simulateKeyDown({ code: 'Insert', altKey: true });
expect(nvim.input).toHaveBeenCalledWith('<A-Insert>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('< key', () => {
simulateKeyDown({ key: '<', shiftKey: true });
expect(nvim.input).toHaveBeenCalledWith('<lt>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('\\ key', () => {
simulateKeyDown({ key: '\\' });
expect(nvim.input).toHaveBeenCalledWith('<Bslash>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('| key', () => {
simulateKeyDown({ key: '|' });
expect(nvim.input).toHaveBeenCalledWith('<Bar>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test.todo('TODO: test all special keys');
});
describe('motifiers', () => {
test('CTRL key adds <C-*> modifier', () => {
simulateKeyDown({ key: 'i', ctrlKey: true });
expect(nvim.input).toHaveBeenCalledWith('<C-i>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Option key adds <A-*> modifier', () => {
simulateKeyDown({ key: 'i', altKey: true });
expect(nvim.input).toHaveBeenCalledWith('<A-i>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('CMD key adds <D-*> modifier', () => {
simulateKeyDown({ key: 'i', metaKey: true });
expect(nvim.input).toHaveBeenCalledWith('<D-i>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Shift key does not add modifier without other motifiers', () => {
simulateKeyDown({ key: 'I', shiftKey: true });
expect(nvim.input).toHaveBeenCalledWith('I');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Shift adds modifier with other motifiers', () => {
simulateKeyDown({ key: 'i', ctrlKey: true, shiftKey: true });
expect(nvim.input).toHaveBeenCalledWith('<C-S-i>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('multiple motifiers', () => {
simulateKeyDown({ key: 'i', ctrlKey: true, metaKey: true, altKey: true, shiftKey: true });
expect(nvim.input).toHaveBeenCalledWith('<D-A-C-S-i>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
});
describe('Option key modifier', () => {
test("Map Dead key with Option to it's latin value", () => {
simulateKeyDown({ key: 'Dead', code: 'KeyI', altKey: true });
expect(nvim.input).toHaveBeenCalledWith('<A-i>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Skip Dead key if Option is not pressed', () => {
simulateKeyDown({ key: 'Dead', code: 'KeyI' });
expect(nvim.input).toHaveBeenCalledTimes(0);
});
test('Skip Dead key if there are no latin code for it', () => {
simulateKeyDown({ key: 'Dead', code: 'NotKeyI', altKey: true });
expect(nvim.input).toHaveBeenCalledTimes(0);
});
test('Adds A- modifier for non-Dead key', () => {
simulateKeyDown({ key: '∆', altKey: true });
expect(nvim.input).toHaveBeenCalledWith('<A-∆>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
describe('with input mode', () => {
beforeEach(() => {
nvimOn.mock.calls[0][1]([['mode_change', ['insert']]]);
});
test('Does not add A- modifier', () => {
simulateKeyDown({ key: '∆', code: 'KeyJ', altKey: true });
expect(nvim.input).toHaveBeenCalledWith('∆');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Does not add A- modifier with Shift', () => {
simulateKeyDown({ key: 'Ô', code: 'KeyJ', altKey: true, shiftKey: true });
expect(nvim.input).toHaveBeenCalledWith('Ô');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Adds A- modifier with Control', () => {
simulateKeyDown({ key: '∆', code: 'KeyJ', altKey: true, ctrlKey: true });
expect(nvim.input).toHaveBeenCalledWith('<A-C-∆>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
test('Adds A- modifier with Command', () => {
simulateKeyDown({ key: '∆', code: 'KeyJ', altKey: true, metaKey: true });
expect(nvim.input).toHaveBeenCalledWith('<D-A-∆>');
expect(nvim.input).toHaveBeenCalledTimes(1);
});
});
});
describe('focus input', () => {
let input: HTMLInputElement;
let focusSpy: jest.SpyInstance;
let blurSpy: jest.SpyInstance;
beforeEach(() => {
input = document.getElementsByTagName('input')[0];
focusSpy = jest.spyOn(input, 'focus');
blurSpy = jest.spyOn(input, 'blur');
});
test('focus input on insert mode', () => {
nvimOn.mock.calls[0][1]([['mode_change', ['insert']]]);
expect(focusSpy).toHaveBeenCalled();
});
test('focus input on cmdline_normal mode', () => {
nvimOn.mock.calls[0][1]([['mode_change', ['cmdline_normal']]]);
expect(focusSpy).toHaveBeenCalled();
});
test('blurs input on other modes', () => {
nvimOn.mock.calls[0][1]([['mode_change', ['normal']]]);
expect(blurSpy).toHaveBeenCalled();
});
});
});
================================================
FILE: packages/browser-renderer/src/input/keyboard.ts
================================================
import type { Nvim } from '@vvim/nvim';
import { Screen } from 'src/screen';
// :help keyCode
const specialKey = ({ key, code }: KeyboardEvent): string =>
(({
Insert: 'Insert',
Numpad0: 'k0',
Numpad1: 'k1',
Numpad2: 'k2',
Numpad3: 'k3',
Numpad4: 'k4',
Numpad5: 'k5',
Numpad6: 'k6',
Numpad7: 'k7',
Numpad8: 'k8',
Numpad9: 'k9',
NumpadAdd: 'kPlus',
NumpadSubtract: 'kMinus',
NumpadMultiply: 'kMultiply',
NumpadDivide: 'kDivide',
NumpadEnter: 'kEnter',
NumpadDecimal: 'kPoint',
Escape: 'Esc',
Backspace: 'BS',
Delete: 'Del',
Enter: 'CR',
Tab: 'Tab',
ArrowUp: 'Up',
ArrowDown: 'Down',
ArrowLeft: 'Left',
ArrowRight: 'Right',
PageUp: 'PageUp',
PageDown: 'PageDown',
Home: 'Home',
End: 'End',
F1: 'F1',
F2: 'F2',
F3: 'F3',
F4: 'F4',
F5: 'F5',
F6: 'F6',
F7: 'F7',
F8: 'F8',
F9: 'F9',
F10: 'F10',
F11: 'F11',
F12: 'F12',
} as Record<string, string>)[code] ||
({
'<': 'lt',
'\\': 'Bslash',
'|': 'Bar',
} as Record<string, string>)[key]);
const skip = (key: string) =>
(({
Shift: true,
Control: true,
Alt: true,
Meta: true,
CapsLock: true,
} as Record<string, boolean>)[key]);
export const modifierPrefix = (
{ metaKey, altKey, ctrlKey }: KeyboardEvent | MouseEvent,
insertMode?: boolean,
): string => {
if (insertMode && altKey && !ctrlKey && !metaKey) {
return '';
}
return `${metaKey ? 'D-' : ''}${altKey ? 'A-' : ''}${ctrlKey ? 'C-' : ''}`;
};
export const shiftPrefix = ({ shiftKey, key }: KeyboardEvent): string =>
shiftKey && key !== '<' ? 'S-' : '';
/**
* Filter hotkeys from menu.
* TODO: Make it customizable and make it work differently in browser and electron app.
*/
const filterResult = (result: string) =>
!({
'<D-c>': true, // Cmd+C
'<D-v>': true, // Cmd+V
'<D-a>': true, // Cmd+A: "Select all" menu item
'<D-=>': true, // Cmd+Plus: "Zoom In" menu item
'<D-->': true, // Cmd+-: "Zoom Out" menu item
'<D-0>': true, // Cmd+0: "Actual Size" menu item
'<D-C-f>': true, // Cmd+Ctrl+F: "Toggle Full Screen" menu item
'<D-m>': true, // Cmd+M: "Minimize" menu item
'<D-h>': true, // Cmd+H: Hide window
'<D-q>': true, // Cmd+Q: Quit
'<D-o>': true, // Cmd+O: Open file
'<D-n>': true, // Cmd+N: New window
'<D-w>': true, // Cmd+W: Close window
} as Record<string, boolean>)[result] && result;
// https://github.com/rhysd/NyaoVim/issues/87
const replaceResult = (result: string) =>
(({
'<C-6>': '<C-^>',
'<C-->': '<C-_>',
'<C-2>': '<C-@>',
} as Record<string, string>)[result] || result);
const eventKeyCode = (event: KeyboardEvent, insertMode?: boolean): string | null => {
const { key } = event;
if (skip(key)) return null;
// Handle Alt + modifier key input (for example Alt + i)
let deadKey;
if (key === 'Dead') {
if (!insertMode && event.altKey && event.code.match(/^Key[A-Z]$/)) {
deadKey = event.code[3].toLowerCase();
} else {
return null;
}
}
const modifier = modifierPrefix(event, insertMode);
const shift = shiftPrefix(event);
const special = specialKey(event);
const keyCode = deadKey || special || key;
const result = modifier || special ? `<${modifier}${shift}${keyCode}>` : keyCode;
const filteredResult = filterResult(result);
if (!filteredResult) {
return null;
}
return replaceResult(filteredResult);
};
const initKeyboard = ({ nvim, screen }: { nvim: Nvim; screen: Screen }): void => {
const { getCursorElement } = screen;
let disableNextInput = false;
let inputKey: string | null = null;
let isComposing = false;
let compositionValue = null;
let insertMode = false;
const input = document.createElement('input');
input.style.position = 'absolute';
input.style.opacity = '0';
input.style.left = '0';
input.style.top = '0';
input.style.width = '0';
input.style.height = '0';
(getCursorElement() || document.getElementsByTagName('body')[0]).appendChild(input);
const handleKeydown = async (event: KeyboardEvent) => {
disableNextInput = true;
if (!isComposing) {
inputKey = eventKeyCode(event, insertMode);
if (inputKey) {
nvim.input(inputKey);
}
}
};
// Non-keyboard input. For example insert emoji.
const handleInput = (event: InputEvent) => {
if (disableNextInput || isComposing) {
disableNextInput = false;
return;
}
if (event.data) {
nvim.input(event.data);
}
};
// Composition input for logograms or diacritical signs. Also works for speech input.
const handleCompositionStart = () => {
isComposing = true;
compositionValue = inputKey || '';
};
const handleCompositionEnd = () => {
isComposing = false;
};
const handleCompositionUpdate = (event: CompositionEvent) => {
nvim.input(`${'<BS>'.repeat(compositionValue.length)}${event.data}`);
compositionValue = event.data;
};
document.addEventListener('keydown', handleKeydown);
// @ts-expect-error input event type is incorrect
input.addEventListener('input', handleInput);
input.addEventListener('compositionstart', handleCompositionStart);
input.addEventListener('compositionupdate', handleCompositionUpdate);
input.addEventListener('compositionend', handleCompositionEnd);
// Enable composition input only for insert and command-line modes. Enabling if for other modes
// is tricky. `preventDefault` does not work for compositionstart, so we need to blur/focus input
// element for this.
nvim.on('redraw', (args) => {
args.forEach((arg) => {
if (arg[0] === 'mode_change') {
const [mode] = arg[1];
// https://github.com/neovim/neovim/blob/master/src/nvim/cursor_shape.c#L18
if (['insert', 'cmdline_normal'].includes(mode)) {
insertMode = true;
input.focus();
} else {
insertMode = false;
input.blur();
}
}
});
});
};
export default initKeyboard;
================================================
FILE: packages/browser-renderer/src/input/mouse.ts
================================================
import throttle from 'lodash/throttle';
import { modifierPrefix } from 'src/input/keyboard';
import { Screen } from 'src/screen';
import type Nvim from '@vvim/nvim';
const GRID = 0;
const SCROLL_STEP_X = 6;
const SCROLL_STEP_Y = 3;
const MOUSE_BUTTON = {
0: 'left',
1: 'middle',
2: 'right',
WHEEL: 'wheel',
};
const ACTION = {
UP: 'up',
DOWN: 'down',
LEFT: 'left',
RIGHT: 'right',
PRESS: 'press',
DRAG: 'drag',
RELEASE: 'release',
} as const;
type Action = typeof ACTION[keyof typeof ACTION];
// const initMouse = ({ screenCoords }: Screen, nvim: Nvim): void => {
const initMouse = ({ screen, nvim }: { screen: Screen; nvim: Nvim }): void => {
const { screenCoords } = screen;
let scrollDeltaX = 0;
let scrollDeltaY = 0;
let mouseCoords: [number, number] = [0, 0];
let mouseButtonDown: boolean;
const mouseCoordsChanged = (event: MouseEvent) => {
const newCoords = screenCoords(event.clientX, event.clientY);
if (newCoords[0] !== mouseCoords[0] || newCoords[1] !== mouseCoords[1]) {
mouseCoords = newCoords;
return true;
}
return false;
};
const buttonName = (event: MouseEvent) =>
// @ts-expect-error TODO
event.type === 'wheel' ? MOUSE_BUTTON.WHEEL : MOUSE_BUTTON[event.button];
const mouseInput = (event: MouseEvent, action: Action) => {
mouseCoordsChanged(event);
const [col, row] = screenCoords(event.clientX, event.clientY);
const button = buttonName(event);
const modifier = modifierPrefix(event);
nvim.inputMouse(button, action, modifier, GRID, row, col);
};
const calculateScroll = (event: MouseEvent) => {
let [scrollX, scrollY] = screenCoords(Math.abs(scrollDeltaX), Math.abs(scrollDeltaY));
scrollX = Math.floor(scrollX / SCROLL_STEP_X);
scrollY = Math.floor(scrollY / SCROLL_STEP_Y);
if (scrollY === 0 && scrollX === 0) return;
if (scrollY !== 0) {
mouseInput(event, scrollDeltaY > 0 ? ACTION.DOWN : ACTION.UP);
scrollDeltaY = 0;
}
if (scrollX !== 0) {
mouseInput(event, scrollDeltaX > 0 ? ACTION.RIGHT : ACTION.LEFT);
scrollDeltaX = 0;
}
};
const handleMousewheel = (event: WheelEvent) => {
const { deltaX, deltaY } = event;
if (scrollDeltaY * deltaY < 0) scrollDeltaY = 0;
scrollDeltaX += deltaX;
scrollDeltaY += deltaY;
calculateScroll(event);
};
const handleMousedown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
mouseButtonDown = true;
mouseInput(event, ACTION.PRESS);
};
const handleMouseup = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
mouseButtonDown = false;
mouseInput(event, ACTION.RELEASE);
};
const handleMousemove = (event: MouseEvent) => {
if (mouseButtonDown) {
event.preventDefault();
event.stopPropagation();
if (mouseCoordsChanged(event)) mouseInput(event, ACTION.DRAG);
}
};
nvim.command('set mouse=a'); // Enable mouse events
document.addEventListener('mousedown', handleMousedown);
document.addEventListener('mouseup', handleMouseup);
document.addEventListener('mousemove', throttle(handleMousemove, 50));
document.addEventListener('wheel', handleMousewheel);
};
export default initMouse;
================================================
FILE: packages/browser-renderer/src/lib/__tests__/getColor.test.ts
================================================
import { getColor, getColorNum } from 'src/lib/getColor';
describe('getColor', () => {
test('0 is black', () => {
expect(getColor(0)).toBe('rgb(0,0,0)');
});
test('0xffffff is white', () => {
expect(getColor(0xffffff)).toBe('rgb(255,255,255)');
});
test('0x333333 is gray', () => {
expect(getColor(0x333333)).toBe('rgb(51,51,51)');
});
test('0x003300 is rgb(0,51,0)', () => {
expect(getColor(0x003300)).toBe('rgb(0,51,0)');
});
});
describe('getColorNum', () => {
test('rgb(0, 0, 0) is 0', () => {
expect(getColorNum('rgb(0,0,0)')).toBe(0);
});
test('rgb(255,255,255) is 0xffffff', () => {
expect(getColorNum('rgb(255,255,255)')).toBe(0xffffff);
});
test('rgb(51,51,51) is 0x333333', () => {
expect(getColorNum('rgb(51,51,51)')).toBe(0x333333);
});
test('rgb(0,51,0) is 0x00ff00', () => {
expect(getColorNum('rgb(0,51,0)')).toBe(0x003300);
});
test('returns undefined for undefined param', () => {
expect(getColorNum()).toBeUndefined();
});
});
================================================
FILE: packages/browser-renderer/src/lib/getColor.ts
================================================
/* eslint-disable no-bitwise */
import memoize from 'lodash/memoize';
/**
* Get color by number, for example hex number `0xFF0000` becomes `rgb(255,0,0)`
* @param color Color in number
* @param defaultColor Use default color if color is undefined or -1
*/
export const getColor = (color: number | undefined, defaultColor?: string): string | undefined => {
if (typeof color !== 'number' || color === -1) return defaultColor;
return `rgb(${(color >> 16) & 0xff},${(color >> 8) & 0xff},${color & 0xff})`;
};
/**
* Get color number from string, for example `rgb(255,0,0)` becomes `0xFF0000`
* @param color Color in rgb string
*/
export const getColorNum = memoize((color?: string): number | undefined => {
if (color) {
const [r, g, b] = color
.replace(/([^0-9,])/g, '')
.split(',')
.map((s) => parseInt(s, 10));
return (r << 16) + (g << 8) + b;
}
return undefined;
});
================================================
FILE: packages/browser-renderer/src/lib/isWeb.ts
================================================
const isWeb = (): boolean =>
window.location.protocol === 'http:' || window.location.protocol === 'https:';
export default isWeb;
================================================
FILE: packages/browser-renderer/src/preloaded/electron.ts
================================================
export interface PreloadedIpcRenderer {
/**
* Listens to `channel`, when a new message arrives `listener` would be called with
* `listener(args...)`.
*/
on(channel: string, listener: (...args: any[]) => void): this;
/**
* Adds a one time `listener` function for the event. This `listener` is invoked
* only the next time a message is sent to `channel`, after which it is removed.
*/
removeListener(channel: string, listener: (...args: any[]) => void): this;
/**
* Send an asynchronous message to the main process via `channel`, along with
* arguments. Arguments will be serialized with the Structured Clone Algorithm,
* just like `window.postMessage`, so prototype chains will not be included.
* Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an
* exception.
*
* > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special
* Electron objects is deprecated, and will begin throwing an exception starting
* with Electron 9.
*
* The main process handles it by listening for `channel` with the `ipcMain`
* module.
*/
send(channel: string, ...args: any[]): void;
}
declare global {
interface Window {
electron: {
ipcRenderer: PreloadedIpcRenderer;
};
}
}
export const { ipcRenderer } = window.electron || {};
================================================
FILE: packages/browser-renderer/src/renderer.ts
================================================
import Nvim from '@vvim/nvim';
import { Settings } from 'src/types';
import Transport from 'src/transport/transport';
import initScreen from 'src/screen';
import initKeyboard from 'src/input/keyboard';
import initMouse from 'src/input/mouse';
import hideMouseCursor from 'src/features/hideMouseCursor';
/**
* Browser renderer
*/
const renderer = (): void => {
const transport = new Transport();
const initRenderer = (settings: Settings) => {
const nvim = new Nvim(transport, true);
const screen = initScreen({ nvim, settings, transport });
initKeyboard({ nvim, screen });
initMouse({ nvim, screen });
hideMouseCursor();
};
transport.on('initRenderer', initRenderer);
};
export default renderer;
================================================
FILE: packages/browser-renderer/src/screen.ts
================================================
import isEqual from 'lodash/isEqual';
import emojiRegex from 'emoji-regex';
import { getColor } from 'src/lib/getColor';
import type { Settings } from 'src/types';
import type {
Nvim,
Transport,
UiEventsHandlers,
UiEventsArgs,
ModeInfo,
HighlightAttrs,
} from '@vvim/nvim';
export type Screen = {
screenCoords: (width: number, height: number) => [number, number];
getCursorElement: () => HTMLDivElement;
};
type CalculatedProps = {
bgColor: string;
fgColor: string;
spColor?: string;
hiItalic: boolean;
hiBold: boolean;
hiUnderline: boolean;
hiUndercurl: boolean;
hiStrikethrough: boolean;
};
type HighlightProps = {
calculated?: CalculatedProps;
value?: HighlightAttrs;
};
type HighlightTable = Record<number, HighlightProps>;
type Char = {
bitmap?: HTMLCanvasElement;
char?: string | null;
hlId?: number;
};
const DEFAULT_FONT_FAMILY = 'Courier New';
const DEFAULT_FG_COLOR = 'rgb(255,255,255)';
const DEFAULT_BG_COLOR = 'rgb(0,0,0)';
const DEFAULT_SP_COLOR = 'rgb(255,255,255)';
const DEFAULT_FONT_SIZE = 12;
const DEFAULT_LINE_HEIGHT = 1.25;
const DEFAULT_LETTER_SPACING = 0;
let cursorAnimation: Animation;
const isEmoji = (char: string): boolean => {
const regex = emojiRegex();
return !!char.match(regex);
};
const screen = ({
settings,
transport,
nvim,
}: {
settings: Settings;
transport: Transport;
nvim: Nvim;
}): Screen => {
let screenContainer: HTMLDivElement;
let cursorEl: HTMLDivElement;
let canvasEl: HTMLCanvasElement;
let context: CanvasRenderingContext2D;
let cursorCanvasEl: HTMLCanvasElement;
let cursorContext: CanvasRenderingContext2D;
let cursorPosition: [number, number] = [0, 0];
let cursorChar: string;
let scale: number;
let charWidth: number;
let charHeight: number;
let fontFamily = DEFAULT_FONT_FAMILY;
let fontSize = DEFAULT_FONT_SIZE;
let lineHeight = DEFAULT_LINE_HEIGHT;
let letterSpacing = DEFAULT_LETTER_SPACING;
let cols: number;
let rows: number;
let modeInfoSet: Record<string, ModeInfo>;
let mode: string;
let showBold = true;
let showItalic = true;
let showUnderline = true;
let showUndercurl = true;
let showStrikethrough = true;
let chars: Char[][] = [];
const highlightTable: HighlightTable = {
'0': {
calculated: {
bgColor: DEFAULT_BG_COLOR,
fgColor: DEFAULT_FG_COLOR,
spColor: DEFAULT_SP_COLOR,
hiItalic: false,
hiBold: false,
hiUnderline: false,
hiUndercurl: false,
hiStrikethrough: false,
},
},
// Inverted default color for cursor
'-1': {
calculated: {
bgColor: DEFAULT_FG_COLOR,
fgColor: DEFAULT_BG_COLOR,
spColor: DEFAULT_SP_COLOR,
hiItalic: false,
hiBold: false,
hiUnderline: false,
hiUndercurl: false,
hiStrikethrough: false,
},
},
};
let isResizing = false;
let isResizingTimeout: NodeJS.Timeout | undefined;
const setResizing = () => {
isResizing = true;
if (isResizingTimeout) {
clearTimeout(isResizingTimeout);
isResizingTimeout = undefined;
}
isResizingTimeout = setTimeout(() => {
isResizing = false;
}, 300);
};
const getCursorElement = (): HTMLDivElement => cursorEl;
const initCursor = () => {
cursorEl = document.createElement('div');
cursorEl.style.position = 'absolute';
cursorEl.style.zIndex = '100';
cursorEl.style.top = '0';
cursorEl.style.left = '0';
screenContainer.appendChild(cursorEl);
cursorCanvasEl = document.createElement('canvas');
cursorCanvasEl.style.position = 'absolute';
cursorCanvasEl.style.top = '0px';
cursorCanvasEl.style.left = '0px';
cursorContext = cursorCanvasEl.getContext('2d', { alpha: true }) as CanvasRenderingContext2D;
cursorEl.appendChild(cursorCanvasEl);
};
const initScreen = () => {
screenContainer = document.createElement('div');
document.body.appendChild(screenContainer);
screenContainer.style.position = 'absolute';
screenContainer.style.left = '0';
screenContainer.style.top = '0';
screenContainer.style.transformOrigin = '0 0';
canvasEl = document.createElement('canvas');
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0px';
canvasEl.style.left = '0px';
context = canvasEl.getContext('2d', { alpha: false }) as CanvasRenderingContext2D;
screenContainer.appendChild(canvasEl);
};
const RETINA_SCALE = 2;
const charsCache: Map<string, HTMLCanvasElement> = new Map();
const isRetina = () => window.devicePixelRatio === RETINA_SCALE;
const scaledLetterSpacing = () => {
if (isRetina() || letterSpacing === 0) {
return letterSpacing;
}
return letterSpacing > 0
? Math.floor(letterSpacing / RETINA_SCALE)
: Math.ceil(letterSpacing / RETINA_SCALE);
};
const scaledFontSize = () => fontSize * scale;
const measureCharSize = () => {
const char = document.createElement('span');
char.innerHTML = '0';
char.style.fontFamily = fontFamily;
char.style.fontSize = `${scaledFontSize()}px`;
char.style.lineHeight = `${Math.round(scaledFontSize() * lineHeight)}px`;
char.style.position = 'absolute';
char.style.left = '-1000px';
char.style.top = '0';
screenContainer.appendChild(char);
const oldCharWidth = charWidth;
const oldCharHeight = charHeight;
charWidth = Math.max(char.offsetWidth + scaledLetterSpacing(), 1);
charHeight = char.offsetHeight;
if (oldCharWidth !== charWidth || oldCharHeight !== charHeight) {
cursorEl.style.width = `${charWidth}px`;
cursorEl.style.height = `${charHeight}px`;
cursorCanvasEl.width = charWidth;
cursorCanvasEl.height = charHeight;
charsCache.clear();
}
screenContainer.removeChild(char);
};
const font = (p: CalculatedProps, isEmojiFont?: boolean) =>
[
p.hiItalic ? 'italic' : '',
p.hiBold ? 'bold' : '',
`${scaledFontSize()}px`,
isEmojiFont ? 'Apple Color Emoji' : fontFamily,
].join(' ');
const getCharBitmap = (char: string, hlId: number) => {
// eslint-disable-next-line
const p = highlightTable[hlId].calculated!;
const key = `${char}:${p.bgColor}:${p.fgColor}:${
p.hiUndercurl || p.hiUnderline ? p.spColor : '-'
}:${p.hiItalic}:${p.hiBold}:${p.hiUndercurl}:${p.hiStrikethrough}`;
if (!charsCache.has(key)) {
// TODO: worker maybe?
// const charCanvas = new OffscreenCanvas(charWidth * 3, charHeight);
const charCanvas = document.createElement('canvas');
charCanvas.width = charWidth * 3;
charCanvas.height = charHeight;
// eslint-disable-next-line
const charCtx = charCanvas.getContext('2d', {
alpha: true,
}) as CanvasRenderingContext2D;
if (p.hiUndercurl) {
charCtx.strokeStyle = p.spColor as string;
charCtx.lineWidth = scaledFontSize() * 0.08;
const x = charWidth;
const y = charHeight - (scaledFontSize() * 0.08) / 2;
const h = charHeight * 0.2; // Height of the wave
charCtx.beginPath();
charCtx.moveTo(x, y);
charCtx.bezierCurveTo(x + x / 4, y, x + x / 4, y - h / 2, x + x / 2, y - h / 2);
charCtx.bezierCurveTo(x + (x / 4) * 3, y - h / 2, x + (x / 4) * 3, y, x + x, y);
charCtx.stroke();
}
charCtx.fillStyle = p.fgColor;
charCtx.font = font(p, isEmoji(char));
charCtx.textAlign = 'left';
charCtx.textBaseline = 'middle';
if (char) {
charCtx.fillText(
char,
Math.round(scaledLetterSpacing() / 2) + charWidth,
Math.round(charHeight / 2),
);
}
if (p.hiUnderline) {
charCtx.strokeStyle = p.fgColor;
charCtx.lineWidth = scale;
charCtx.beginPath();
charCtx.moveTo(charWidth, charHeight - scale);
charCtx.lineTo(charWidth * 2, charHeight - scale);
charCtx.stroke();
}
if (p.hiStrikethrough) {
charCtx.strokeStyle = p.fgColor;
charCtx.lineWidth = scale;
charCtx.beginPath();
charCtx.moveTo(charWidth, charHeight * 0.5);
charCtx.lineTo(charWidth * 2, charHeight * 0.5);
charCtx.stroke();
}
charsCache.set(key, charCanvas);
}
// eslint-disable-next-line
return charsCache.get(key)!;
};
const initChar = (i: number, j: number) => {
if (!chars[i]) chars[i] = [];
if (!chars[i][j]) chars[i][j] = {};
};
const printBackground = (hlId: number, i: number, j: number, length: number) => {
const fillStyle = highlightTable[hlId]?.calculated?.bgColor;
if (fillStyle) {
context.fillStyle = fillStyle;
// Add an extra BG if this is the edge of the screen to make it look nicer
const isEndOfLine = j + length === cols;
const bgWidth = isEndOfLine ? (length + 1) * charWidth : length * charWidth;
context.fillRect(j * charWidth, i * charHeight, bgWidth, charHeight);
}
};
const printChar = (i: number, j: number, char: string, hlId: number) => {
initChar(i, j);
chars[i][j].char = char;
chars[i][j].hlId = hlId;
chars[i][j].bitmap = getCharBitmap(char, hlId);
context.drawImage(
chars[i][j].bitmap as HTMLCanvasElement,
0,
0,
charWidth * 3,
charHeight,
(j - 1) * charWidth,
i * charHeight,
charWidth * 3,
charHeight,
);
};
// https://github.com/neovim/neovim/blob/5a11e55/runtime/doc/ui.txt#L237
const redrawDefaultColors = () => {
context.fillStyle = highlightTable[0]?.calculated?.bgColor || DEFAULT_BG_COLOR;
context.fillRect(0, 0, canvasEl.width, canvasEl.height);
for (let i = 0; i <= rows; i += 1) {
if (chars[i]) {
for (let j = 0; j <= cols; j += 1) {
const redrawChar = chars[i][j];
if (redrawChar) {
const { hlId } = redrawChar;
if (hlId !== undefined && hlId > 0) {
const { foreground, background, special } = highlightTable[hlId].value || {};
if (!foreground || !background || !special) {
printBackground(hlId, i, j, 1);
}
}
}
}
}
}
for (let i = 0; i <= rows; i += 1) {
if (chars[i]) {
for (let j = 0; j <= cols; j += 1) {
const redrawChar = chars[i][j];
if (redrawChar) {
const { hlId, char } = redrawChar;
if (hlId !== undefined && typeof char === 'string' && char !== ' ') {
const { foreground, background, special } = highlightTable[hlId].value || {};
if (!foreground || !background || !special) {
chars[i][j].bitmap = undefined;
printChar(i, j, char, hlId);
}
}
}
}
}
}
};
const redrawCursor = () => {
const m = modeInfoSet && modeInfoSet[mode];
// TODO: check if cursor changed (char, hlId, etc)
if (!m) return;
const hlId = m.attr_id === 0 ? -1 : m.attr_id;
const { width, height } = cursorCanvasEl;
const fillStyle = highlightTable[hlId]?.calculated?.bgColor;
if (fillStyle) {
cursorContext.fillStyle = fillStyle;
}
if (m.cursor_shape === 'block') {
cursorChar = chars?.[cursorPosition[0]]?.[cursorPosition[1]]?.char || ' ';
cursorContext.fillRect(0, 0, charWidth, charHeight);
const cursorBitmap = getCharBitmap(cursorChar, hlId);
cursorContext.drawImage(cursorBitmap, -charWidth, 0);
} else if (m.cursor_shape === 'vertical') {
const curWidth = m.cell_percentage
? Math.max(scale, Math.round((charWidth / 100) * m.cell_percentage))
: scale;
cursorContext.clearRect(0, 0, width, height);
cursorContext.fillRect(0, 0, curWidth, charHeight);
} else if (m.cursor_shape === 'horizontal') {
const curHeight = m.cell_percentage
? Math.max(scale, Math.round((charHeight / 100) * m.cell_percentage))
: scale;
// TODO: test
cursorContext.clearRect(0, 0, width, height);
cursorContext.fillRect(0, charHeight - curHeight, charWidth, curHeight);
}
// Cursor blink
if (cursorAnimation) {
cursorAnimation.cancel();
}
if (m.blinkoff && m.blinkon) {
const offset = m.blinkon / (m.blinkon + m.blinkoff);
cursorAnimation = cursorEl.animate(
[
{ opacity: 1, offset: 0 },
{ opacity: 1, offset },
{ opacity: 0, offset },
{ opacity: 0, offset: 1 },
{ opacity: 1, offset: 1 },
],
{
duration: m.blinkoff + m.blinkon,
iterations: Infinity,
delay: m.blinkwait || 0,
},
);
}
};
const repositionCursor = (newCursor: [number, number]): void => {
if (newCursor) cursorPosition = newCursor;
const left = cursorPosition[1] * charWidth;
const top = cursorPosition[0] * charHeight;
cursorEl.style.transform = `translate(${left}px, ${top}px)`;
};
const optionSet = {
guifont: (newFont: string) => {
const [newFontFamily, newFontSize] = newFont.trim().split(':h');
if (newFontFamily && newFontFamily !== '') {
nvim.command(`VVset fontfamily=${newFontFamily.replace(/_/g, '\\ ')}`);
if (newFontSize && newFontFamily !== '') {
nvim.command(`VVset fontsize=${newFontSize}`);
}
}
},
};
const recalculateHighlightTable = () => {
(Object.keys(highlightTable) as unknown as number[]).forEach((id) => {
if (id > 0) {
const {
foreground,
background,
special,
reverse,
standout,
italic,
bold,
underline,
undercurl,
strikethrough,
} = highlightTable[id].value || {};
const r = reverse || standout;
const fg = getColor(foreground, highlightTable[0]?.calculated?.fgColor) as string;
const bg = getColor(background, highlightTable[0]?.calculated?.bgColor) as string;
const sp = getColor(special, highlightTable[0]?.calculated?.spColor) as string;
highlightTable[id as unknown as number].calculated = {
fgColor: r ? bg : fg,
bgColor: r ? fg : bg,
spColor: sp,
hiItalic: showItalic && !!italic,
hiBold: showBold && !!bold,
hiUnderline: showUnderline && !!underline,
hiUndercurl: showUndercurl && !!undercurl,
hiStrikethrough: showStrikethrough && !!strikethrough,
};
}
});
};
/**
* If char previous to the current cell is wider that char width, we need to draw that part
* of it that overlaps the current cell when we redraw it.
*/
const overlapPrev = (i: number, j: number) => {
if (chars[i] && chars[i][j - 1] && chars[i][j - 1].bitmap) {
context.drawImage(
chars[i][j - 1].bitmap as HTMLCanvasElement,
charWidth * 2,
0,
charWidth,
charHeight,
j * charWidth,
i * charHeight,
charWidth,
charHeight,
);
}
};
/**
* If char next to the cell is wider that char width, we need to draw that part
* of it that overlaps the current cell when we redraw it.
*/
const overlapNext = (i: number, j: number) => {
if (chars[i] && chars[i][j + 1] && chars[i][j + 1].bitmap) {
context.drawImage(
chars[i][j + 1].bitmap as HTMLCanvasElement,
0,
0,
charWidth,
charHeight,
j * charWidth,
i * charHeight,
charWidth,
charHeight,
);
}
};
/** Clean char from previous overlapping left and right symbols. */
const cleanOverlap = (i: number, j: number) => {
if (chars[i] && chars[i][j]) {
const { hlId } = chars[i][j];
if (hlId !== undefined) {
const fillStyle = highlightTable[hlId]?.calculated?.bgColor;
if (fillStyle) {
context.fillStyle = fillStyle;
context.fillRect(j * charWidth, i * charHeight, charWidth, charHeight);
context.drawImage(
chars[i][j].bitmap as HTMLCanvasElement,
charWidth,
0,
charWidth,
charHeight,
j * charWidth,
i * charHeight,
charWidth,
charHeight,
);
overlapPrev(i, j);
overlapNext(i, j);
}
}
}
};
// https://github.com/neovim/neovim/blob/master/runtime/doc/ui.txt
const redrawCmd: Partial<UiEventsHandlers> = {
set_title: () => {
/* empty */
},
set_icon: () => {
/* empty */
},
win_viewport: () => {
/* empty */
},
mode_info_set: (props) => {
modeInfoSet = props[0][1].reduce((r, modeInfo) => ({ ...r, [modeInfo.name]: modeInfo }), {});
},
option_set: (options) => {
options.forEach(([option, value]) => {
// @ts-expect-error TODO
if (optionSet[option]) {
// @ts-expect-error TODO
optionSet[option](value);
} else {
// console.warn('Unknown option', option, value); // eslint-disable-line no-console
}
});
},
mode_change: (modes) => {
mode = modes[modes.length - 1][0];
},
mouse_on: () => {
/* empty */
},
mouse_off: () => {
/* empty */
},
busy_start: () => {
/* empty */
},
busy_stop: () => {
/* empty */
},
suspend: () => {
/* empty */
},
update_menu: () => {
/* empty */
},
bell: () => {
/* empty */
},
visual_bell: () => {
/* empty */
},
hl_group_set: () => {
/* empty */
},
flush: () => {
redrawCursor();
},
grid_resize: ([[, newCols, newRows]]) => {
const oldCols = cols;
const oldRows = rows;
cols = newCols;
rows = newRows;
// Add extra column on the right to fill it with adjacent color to have a nice right border
if ((cols + 1) * charWidth > canvasEl.width || rows * charHeight > canvasEl.height) {
const width = (cols + 1) * charWidth;
const height = rows * charHeight;
screenContainer.style.width = `${width}px`;
screenContainer.style.height = `${height}px`;
canvasEl.width = (cols + 1) * charWidth;
canvasEl.height = rows * charHeight;
context.fillStyle = highlightTable[0]?.calculated?.bgColor || DEFAULT_BG_COLOR;
context.fillRect(0, 0, canvasEl.width, canvasEl.height);
}
// If we are not resizing the window, then we triggered resize from vim using `:set columns` or `:set lines`.
// We need to send message to the main to resize the window.
if (!isResizing) {
if (oldCols !== cols) {
const width = Math.ceil((cols * charWidth) / scale);
transport.send('set-screen-width', width);
}
if (oldRows !== rows) {
const height = Math.ceil((rows * charHeight) / scale);
transport.send('set-screen-height', height);
}
}
},
default_colors_set: (props) => {
const [foreground, background, special] = props[props.length - 1];
const calculated = {
bgColor: getColor(background, DEFAULT_BG_COLOR) as string,
fgColor: getColor(foreground, DEFAULT_FG_COLOR) as string,
spColor: getColor(special, DEFAULT_SP_COLOR),
hiItalic: false,
hiBold: false,
hiUnderline: false,
hiUndercurl: false,
hiStrikethrough: false,
};
if (!highlightTable[0] || !isEqual(highlightTable[0].calculated, calculated)) {
highlightTable[0] = { calculated };
highlightTable[-1] = {
calculated: {
...calculated,
bgColor: getColor(foreground, DEFAULT_FG_COLOR) as string,
fgColor: getColor(background, DEFAULT_BG_COLOR) as string,
},
};
recalculateHighlightTable();
if (highlightTable[0]?.calculated?.bgColor) {
document.body.style.background = highlightTable[0].calculated.bgColor;
transport.send('set-background-color', highlightTable[0].calculated.bgColor);
}
redrawDefaultColors();
}
},
hl_attr_define: (props) => {
props.forEach(([id, value]) => {
highlightTable[id] = {
value,
};
});
recalculateHighlightTable();
},
grid_line: (props) => {
// eslint-disable-next-line
for (const [, row, col, cells] of props) {
let lineLength = 0;
let currentHlId = 0;
// eslint-disable-next-line
for (const [_char, hlId, length = 1] of cells) {
if (hlId !== undefined) {
currentHlId = hlId;
}
if (length > 0) {
printBackground(currentHlId, row, col + lineLength, length);
lineLength += length;
}
}
currentHlId = 0;
lineLength = 0;
// eslint-disable-next-line
for (const [char, hlId, length = 1] of cells) {
if (hlId !== undefined) {
currentHlId = hlId;
}
for (let j = 0; j < length; j += 1) {
printChar(row, col + lineLength + j, char, currentHlId);
}
lineLength += length;
}
cleanOverlap(row, col - 1);
cleanOverlap(row, col + lineLength);
overlapPrev(row, col);
overlapNext(row, col + lineLength - 1);
}
},
grid_clear: () => {
cursorPosition = [0, 0];
context.fillStyle = highlightTable[0]?.calculated?.bgColor || DEFAULT_BG_COLOR;
context.fillRect(0, 0, canvasEl.width, canvasEl.height);
chars = [];
},
grid_destroy: () => {
/* empty */
},
grid_cursor_goto: ([[_, ...newCursor]]) => {
repositionCursor(newCursor);
// Temporary workaround to fix cursor position in terminal mode. Nvim API does not send the very last cursor
// position in terminal on redraw, but when you send any command to nvim, it redraws it correctly. Need to
// investigate it and find a better permanent fix. Maybe this is a bug in nvim and then
// TODO: file a ticket to nvim.
nvim.getMode();
},
grid_scroll: ([[_grid, top, bottom, left, right, scrollCount]]) => {
const x = left * charWidth; // region left
let y; // region top
let w = (right - left) * charWidth; // clipped part width
const h = (bottom - top - Math.abs(scrollCount)) * charHeight; // clipped part height
const X = x; // destination left
let Y; // destination top
if (right === cols) {
// Add extra char if it is far right rect
w += charWidth;
}
if (scrollCount > 0) {
// scroll down
y = (top + scrollCount) * charHeight;
Y = top * charHeight;
} else {
// scroll up
y = top * charHeight;
Y = (top - scrollCount) * charHeight;
}
// Copy scrolled lines
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
context.drawImage(canvasEl, x, y, w, h, X, Y, w, h);
for (
let i = scrollCount > 0 ? top : bottom - 1;
scrollCount > 0 ? i <= bottom - scrollCount - 1 : i >= top - scrollCount;
i += scrollCount > 0 ? 1 : -1
) {
for (let j = left; j <= right - 1; j += 1) {
const sourceI = i + scrollCount;
initChar(i, j);
initChar(sourceI, j);
// Swap char to scroll to destination
[chars[i][j], chars[sourceI][j]] = [chars[sourceI][j], chars[i][j]];
}
}
for (let i = top; i <= bottom; i += 1) {
cleanOverlap(i, left - 1);
cleanOverlap(i, right);
}
},
};
const handleSet = {
fontfamily: (newFontFamily: string) => {
fontFamily = `${newFontFamily}, ${DEFAULT_FONT_FAMILY}`;
},
fontsize: (newFontSize: string) => {
fontSize = parseInt(newFontSize, 10);
},
letterspacing: (newLetterSpacing: string) => {
letterSpacing = parseInt(newLetterSpacing, 10);
},
lineheight: (newLineHeight: string) => {
lineHeight = parseFloat(newLineHeight);
},
bold: (value: boolean) => {
showBold = value;
},
italic: (value: boolean) => {
showItalic = value;
},
underline: (value: boolean) => {
showUnderline = value;
},
undercurl: (value: boolean) => {
showUndercurl = value;
},
strikethrough: (value: boolean) => {
showStrikethrough = value;
},
};
const redraw = (args: UiEventsArgs) => {
try {
args.forEach(([cmd, ...props]) => {
const command = redrawCmd[cmd];
if (command) {
// console.log('hey', cmd, props);
// @ts-expect-error TODO: find the way to type it without errors
command(props);
} else {
console.warn('Unknown redraw command', cmd, props); // eslint-disable-line no-console
}
});
} catch (e) {
// eslint-disable-next-line
console.error(e);
}
};
const setScale = () => {
scale = isRetina() ? RETINA_SCALE : 1;
screenContainer.style.transform = `scale(${1 / scale})`;
screenContainer.style.width = `${scale * 100}%`;
screenContainer.style.height = `${scale * 100}%`;
// Detect when you drag between retina/non-retina displays
window.matchMedia('screen and (min-resolution: 2dppx)').addListener(() => {
setScale();
measureCharSize();
setResizing();
nvim.uiTryResize(cols, rows);
});
};
/**
* Return grid [col, row] coordinates by pixel coordinates.
*/
const screenCoords = (width: number, height: number): [number, number] => {
return [Math.floor((width * scale) / charWidth), Math.floor((height * scale) / charHeight)];
};
const resize = (forceRedraw = false) => {
const [newCols, newRows] = screenCoords(window.innerWidth, window.innerHeight);
if (newCols !== cols || newRows !== rows || forceRedraw) {
cols = newCols;
rows = newRows;
setResizing();
nvim.uiTryResize(cols, rows);
}
};
const uiAttach = () => {
[cols, rows] = screenCoords(window.innerWidth, window.innerHeight);
nvim.uiAttach(cols, rows, { ext_linegrid: true });
window.addEventListener('resize', () => resize());
};
const updateSettings = (newSettings: Settings, isInitial = false) => {
let requireRedraw = isInitial;
let requireRecalculateHighlight = false;
const requireRedrawProps = [
'fontfamily',
'fontsize',
'letterspacing',
'lineheight',
'bold',
'italic',
'underline',
'undercurl',
'strikethrough',
];
const requireRecalculateHighlightProps = [
'bold',
'italic',
'underline',
'undercurl',
'strikethrough',
];
Object.keys(newSettings).forEach((key) => {
// @ts-expect-error TODO
if (handleSet[key]) {
requireRedraw = requireRedraw || requireRedrawProps.includes(key);
requireRecalculateHighlight =
requireRecalculateHighlight || requireRecalculateHighlightProps.includes(key);
// @ts-expect-error TODO
handleSet[key](newSettings[key]);
}
});
if (requireRecalculateHighlight && !isInitial) {
recalculateHighlightTable();
}
if (requireRedraw) {
measureCharSize();
charsCache.clear();
if (!isInitial) {
resize(true);
}
}
};
initScreen();
initCursor();
setScale();
nvim.on('redraw', redraw);
transport.on('updateSettings', (s) => updateSettings(s));
updateSettings(settings, true);
transport.on('force-resize', () => {
resize();
});
uiAttach();
return {
screenCoords,
getCursorElement,
};
};
export default screen;
================================================
FILE: packages/browser-renderer/src/transport/__tests__/ipc.test.ts
================================================
import { EventEmitter } from 'events';
import { ipcRenderer } from 'src/preloaded/electron';
import type { PreloadedIpcRenderer } from 'src/preloaded/electron';
import IpcRendererTransport from 'src/transport/ipc';
jest.mock('src/preloaded/electron', () => ({
ipcRenderer: {
on: jest.fn(),
send: jest.fn(),
},
}));
describe('main transport', () => {
let transport: IpcRendererTransport;
let ipcRendererMock: NodeJS.EventEmitter;
const send = jest.fn();
beforeEach(() => {
ipcRendererMock = Object.assign(new EventEmitter(), {
send,
});
transport = new IpcRendererTransport((ipcRendererMock as unknown) as PreloadedIpcRenderer);
});
describe('on', () => {
const listener = jest.fn();
test('calls listener', () => {
transport.on('test-event', listener);
ipcRendererMock.emit('test-event', 'arg1', 'arg2');
expect(listener).toHaveBeenCalledWith('arg1', 'arg2');
});
test('does not call listener twice listener', () => {
const anotherListener = jest.fn();
transport.on('test-event', listener);
transport.on('test-event', anotherListener);
ipcRendererMock.emit('test-event', new Event('test-event'), 'arg1', 'arg2');
expect(listener).toHaveBeenCalledTimes(1);
expect(anotherListener).toHaveBeenCalledTimes(1);
});
test('listener with no args', () => {
transport.on('test-event', listener);
ipcRendererMock.emit('test-event');
expect(listener).toHaveBeenCalledWith();
});
test('use preloaded ipcRenderer if it is not passed', () => {
transport = new IpcRendererTransport();
transport.on('test-event', listener);
expect(ipcRenderer.on).toHaveBeenCalledWith('test-event', expect.any(Function));
});
});
describe('once', () => {
const listener = jest.fn();
test('calls listener once', () => {
transport.once('test-event', listener);
ipcRendererMock.emit('test-event', new Event('test-event'), 'arg1', 'arg2');
ipcRendererMock.emit('test-event', new Event('test-event'), 'arg1', 'arg2');
expect(listener).toHaveBeenCalledTimes(1);
});
});
describe('removeListener', () => {
const listener = jest.fn();
test('does not call listener after off', () => {
transport.on('test-event', listener);
transport.removeListener('test-event', listener);
ipcRendererMock.emit('test-event', new Event('test-event'), 'arg1', 'arg2');
expect(listener).not.toHaveBeenCalled();
});
test('other subscribed events work', () => {
const anotherListener = jest.fn();
transport.on('test-event', listener);
transport.on('test-event', anotherListener);
transport.removeListener('test-event', anotherListener);
ipcRendererMock.emit('test-event', new Event('test-event'), 'arg1', 'arg2');
expect(listener).toHaveBeenCalled();
});
test('unsubscribes from ipc event if there are not subscriptions left', () => {
const addListenerSpy = jest.spyOn(ipcRendererMock, 'on');
const removeListenerSpy = jest.spyOn(ipcRendererMock, 'removeListener');
transport.on('test-event', listener);
transport.off('test-event', listener);
expect(removeListenerSpy).toHaveBeenCalledWith('test-event', addListenerSpy.mock.calls[0][1]);
});
});
describe('send', () => {
test('pass args to win.webContents', () => {
transport.send('test-event', 'arg1', 'arg2');
expect(send).toHaveBeenCalledWith('test-event', 'arg1', 'arg2');
});
test('with no args', () => {
transport.send('test-event');
expect(send).toHaveBeenCalledWith('test-event');
});
});
});
================================================
FILE: packages/browser-renderer/src/transport/__tests__/websocket.test.ts
================================================
import WebSocketTransport from 'src/transport/websocket';
describe('websocket transport', () => {
const OriginalWebSocket = WebSocket;
const constructor = jest.fn();
const send = jest.fn();
let onmessage: (x: { data: string }) => void;
const listener = jest.fn();
class MockWebSocket {
constructor(...args: any[]) {
constructor(...args);
}
// eslint-disable-next-line class-methods-use-this
send(...args: any[]) {
send(...args);
}
// eslint-disable-next-line class-methods-use-this
set onmessage(value: (x: { data: string }) => void) {
onmessage = value;
}
}
let transport: WebSocketTransport;
beforeEach(() => {
// @ts-expect-error Mocking WebSocket
global.WebSocket = MockWebSocket;
transport = new WebSocketTransport();
});
afterEach(() => {
global.WebSocket = OriginalWebSocket;
});
test('connects to websocket', () => {
expect(constructor).toHaveBeenCalledWith('ws://localhost');
});
test('send method sends channel and args to websocket', () => {
transport.send('channel', 'arg1', 'arg2');
expect(send).toHaveBeenCalledWith('["channel","arg1","arg2"]');
});
test('sent message is JSON stringified', () => {
transport.send('channel', { complex: { object: true } });
expect(send).toHaveBeenCalledWith('["channel",{"complex":{"object":true}}]');
});
test('receive message if you subscribe to chanel', () => {
transport.on('channel', listener);
onmessage({ data: JSON.stringify(['channel', ['arg1', 'arg2']]) });
expect(listener).toHaveBeenCalledWith(['arg1', 'arg2']);
onmessage({ data: JSON.stringify(['channel', ['arg3']]) });
expect(listener).toHaveBeenCalledWith(['arg3']);
});
test("don't receive messages for channels you did not subscribe", () => {
transport.on('channel', listener);
onmessage({ data: JSON.stringify(['other-channel', ['arg1', 'arg2']]) });
expect(listener).not.toHaveBeenCalled();
});
});
================================================
FILE: packages/browser-renderer/src/transport/ipc.ts
================================================
import { EventEmitter } from 'events';
import memoize from 'lodash/memoize';
import { ipcRenderer } from 'src/preloaded/electron';
import type { PreloadedIpcRenderer } from 'src/preloaded/electron';
import type { Transport, Args } from '@vvim/nvim';
/**
* Init transport between main and renderer via Electron ipcRenderer.
*/
class IpcRendererTransport extends EventEmitter implements Transport {
private ipc: PreloadedIpcRenderer;
constructor(ipc = ipcRenderer) {
super();
this.ipc = ipc;
this.on('newListener', (eventName: string) => {
if (
!this.listenerCount(eventName) &&
!['newListener', 'removeListener'].includes(eventName)
) {
this.ipc.on(eventName, this.handleEvent(eventName));
}
});
this.on('removeListener', (eventName: string) => {
if (
!this.listenerCount(eventName) &&
!['newListener', 'removeListener'].includes(eventName)
) {
this.ipc.removeListener(eventName, this.handleEvent(eventName));
}
});
}
handleEvent = memoize((eventName: string) => (...args: Args): void => {
this.emit(eventName, ...args);
});
send(channel: string, ...params: Args): void {
this.ipc.send(channel, ...params);
}
}
export default IpcRendererTransport;
================================================
FILE: packages/browser-renderer/src/transport/transport.ts
================================================
import IpcRendererTransport from 'src/transport/ipc';
import WebSocketTransport from 'src/transport/websocket';
import isWeb from 'src/lib/isWeb';
const Transport = isWeb() ? WebSocketTransport : IpcRendererTransport;
export default Transport;
================================================
FILE: packages/browser-renderer/src/transport/websocket.ts
================================================
import { EventEmitter } from 'events';
import type { Transport, Args } from '@vvim/nvim';
/**
* Init transport between main and renderer via WebSocket.
*/
class WebSocketTransport extends EventEmitter implements Transport {
socket: WebSocket;
constructor() {
super();
this.socket = new WebSocket(`ws://${window.location.host}`);
this.socket.onmessage = ({ data }) => {
const [channel, args] = JSON.parse(data);
this.emit(channel, args);
};
}
send(channel: string, ...args: Args): void {
this.socket.send(JSON.stringify([channel, ...args]));
}
}
export default WebSocketTransport;
================================================
FILE: packages/browser-renderer/src/types.ts
================================================
type BooleanSetting = 0 | 1;
export type Settings = {
fullscreen: BooleanSetting;
simplefullscreen: BooleanSetting;
bold: BooleanSetting;
italic: BooleanSetting;
underline: BooleanSetting;
undercurl: BooleanSetting;
strikethrough: BooleanSetting;
fontfamily: string;
fontsize: string; // TODO: number
lineheight: string; // TODO: number
letterspacing: string; // TODO: number
reloadchanged: BooleanSetting;
quitoncloselastwindow: BooleanSetting;
autoupdateinterval: string; // TODO: number
openInProject: BooleanSetting;
};
================================================
FILE: packages/browser-renderer/tsconfig.declaration.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declarationMap": true,
"noEmit": false,
"outDir": "dist"
},
"include": ["src/index.ts", "@types"]
}
================================================
FILE: packages/browser-renderer/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"baseUrl": "."
},
"include": ["src", "@types"]
}
================================================
FILE: packages/electron/@types/html2plaintext.d.ts
================================================
declare module 'html2plaintext' {
const html2plaintext: (x: string) => string;
export default html2plaintext;
}
================================================
FILE: packages/electron/README.md
================================================
# VV
VV is a Neovim client for macOS. A pure, fast, minimalistic Vim experience with good macOS integration. Optimized for speed and nice font rendering.
Please check main readme file for details: [README.md](../../README.md).
================================================
FILE: packages/electron/babel.config.json
================================================
{
"extends": "../../babel.config.json"
}
================================================
FILE: packages/electron/bin/openInProject.vim
================================================
" Opens file respecting switchbuf setting.
function! VVopenInProject(filename, ...)
" Take switch override from second parameter or from VV settings.
let l:switchbuf_override = get(a:, 1, VVsettingValue('openInProject'))
silent call VVopenInProjectLoud(a:filename, l:switchbuf_override)
endfunction
function! VVopenInProjectLoud(fileName, switchbuf_override)
" Temporary override switchbuf if we have custom openInProject setting.
if type(a:switchbuf_override) == v:t_string && a:switchbuf_override != '0' && a:switchbuf_override != '1'
let l:original_switchbuf = &switchbuf
let &switchbuf = a:switchbuf_override
endif
" Create temporary quickfix list with file we want to open
if (!exists('g:vvOpenInProjectQfId') || getqflist({ 'id': 0 }).id != g:vvOpenInProjectQfId)
call setqflist([], ' ', { 'title': 'VV Temporary Quickfix' })
let g:vvOpenInProjectQfId = getqflist({ 'id': 0 }).id
end
" Add file to list and open it. It will be opened according to current switchbuf
" setting.
call setqflist([], 'r', { 'id': g:vvOpenInProjectQfId, 'items': [{ 'filename': a:fileName }] })
cc! 1
" Switch to previous quickfix list if there are other lists.
if getqflist({'nr' : 0 }).nr > 1
colder
end
" Rollback to original switchbuf option if needed.
if exists('l:original_switchbuf')
let &switchbuf = l:original_switchbuf
endif
endfunction
function! VVprojectRoot()
return getcwd()
endfunction
================================================
FILE: packages/electron/bin/reloadChanged.vim
================================================
" TODO: Remove on the next major version
" Iterate on buffers and reload them from disk. No questions asked.
" Do it in temporary tab to keep the same windows layout.
function! VVrefresh(...)
-tabnew
for bufnr in a:000
execute "buffer" bufnr
execute "e!"
endfor
tabclose!
endfunction
function! VVenableReloadChanged(enabled)
if a:enabled
augroup ReloadChanged
autocmd!
autocmd FileChangedShell * call rpcnotify(get(g:, "vv_channel", 1), "vv:file_changed", { "name": expand("<afile>"), "bufnr": expand("<abuf>") })
autocmd CursorHold * checktime
augroup END
else
autocmd! ReloadChanged *
endif
endfunction
================================================
FILE: packages/electron/bin/vv
================================================
#!/bin/sh
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
cat << END
VV - NeoVim GUI Client
Usage:
vv [options] [file ...]
Options:
--debug Debug mode. Keep process attached to terminal and
output errors.
All other options will be passed to nvim. You can check available options
by running: nvim --help
END
else
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
BIN="$DIR/../../MacOS/VV"
if [[ "${@#--debug}" = "$@" ]]; then
exec "$BIN" "$@" &>/dev/null & disown
else
exec "$BIN" "${@#--debug}"
fi
fi
================================================
FILE: packages/electron/bin/vv.vim
================================================
let g:vv = 1
let s:dir = expand('<sfile>:p:h')
execute 'source ' . fnameescape(s:dir . '/vvset.vim')
execute 'source ' . fnameescape(s:dir . '/reloadChanged.vim')
execute 'source ' . fnameescape(s:dir . '/openInProject.vim')
set termguicolors
autocmd VimEnter * call rpcnotify(get(g:, 'vv_channel', 1), "vv:vim_enter")
" Send unsaved buffers to client
function! VVunsavedBuffers()
let l:buffers = getbufinfo()
call filter(l:buffers, "v:val['changed'] == 1")
let l:buffers = map(l:buffers , "{ 'name': v:val['name'] }" )
return l:buffers
endfunction
================================================
FILE: packages/electron/bin/vvset.vim
================================================
let g:vv_settings_synonims = {
\ 'fu': 'fullscreen',
\ 'sfu': 'simplefullscreen',
\ 'width': 'windowwidth',
\ 'height': 'windowheight',
\ 'top': 'windowtop',
\ 'left': 'windowleft',
\ 'openinproject': 'openInProject'
\}
let g:vv_default_settings = {
\ 'fullscreen': 0,
\ 'simplefullscreen': 1,
\ 'bold': 1,
\ 'italic': 1,
\ 'underline': 1,
\ 'undercurl': 1,
\ 'strikethrough': 1,
\ 'fontfamily': 'monospace',
\ 'fontsize': 12,
\ 'lineheight': 1.25,
\ 'letterspacing': 0,
\ 'reloadchanged': 0,
\ 'windowwidth': v:null,
\ 'windowheight': v:null,
\ 'windowleft': v:null,
\ 'windowtop': v:null,
\ 'quitoncloselastwindow': 0,
\ 'autoupdateinterval': 1440,
\ 'openInProject': 1
\}
let g:vv_settings = deepcopy(g:vv_default_settings)
" Custom VVset command, mimic default set command (:help set) with
" settings specified in g:vv_default_settings
function! VVset(...)
for arg in a:000
call VVsetItem(arg)
endfor
endfunction
function! VVsettingValue(name)
let l:name = VVsettingName(a:name)
if has_key(g:vv_settings, l:name)
return g:vv_settings[l:name]
else
echoerr "Unknown option: ".a:name
endif
endfunction
function! VVsettingName(name)
if has_key(g:vv_settings_synonims, a:name)
return g:vv_settings_synonims[a:name]
else
return a:name
endif
endfunction
function! VVsetItem(name)
if a:name == 'all'
echo g:vv_settings
return
elseif a:name =~ '?'
let l:name = VVsettingName(split(a:name, '?')[0])
echo VVsettingValue(l:name)
return
elseif a:name =~ '&'
let l:name = VVsettingName(split(a:name, '&')[0])
if l:name == 'all'
let g:vv_settings = deepcopy(g:vv_default_settings)
call VVsettings()
return
elseif has_key(g:vv_default_settings, l:name)
let l:value = g:vv_default_settings[l:name]
else
echoerr "Unknown option: ".l:name
return
endif
elseif a:name =~ '+='
let l:split = split(a:name, '+=')
let l:name = VVsettingName(l:split[0])
let l:value = VVsettingValue(l:name) + l:split[1]
elseif a:name =~ '-='
let l:split = split(a:name, '-=')
let l:name = VVsettingName(l:split[0])
let l:value = VVsettingValue(l:name) - l:split[1]
elseif a:name =~ '\^='
let l:split = split(a:name, '\^=')
let l:name = VVsettingName(l:split[0])
let l:value = VVsettingValue(l:name) * l:split[1]
elseif a:name =~ '='
let l:split = split(a:name, '=')
let l:name = l:split[0]
let l:value = l:split[1]
elseif a:name =~ ':'
let l:split = split(a:name, ':')
let l:name = l:split[0]
let l:value = l:split[1]
elseif a:name =~ '!'
let l:name = VVsettingName(split(a:name, '!')[0])
if VVsettingValue(l:name) == 0
let l:value = 1
else
let l:value = 0
endif
elseif a:name =~ '^inv'
let l:name = VVsettingName(strpart(a:name, 3))
if VVsettingValue(l:name) == 0
let l:value = 1
else
let l:value = 0
endif
elseif a:name =~ '^no'
let l:name = strpart(a:name, 2)
let l:value = 0
else
let l:name = a:name
let l:value = 1
endif
let l:name = VVsettingName(l:name)
if has_key(g:vv_settings, l:name)
let g:vv_settings[l:name] = l:value
call rpcnotify(get(g:, "vv_channel", 1), "vv:set", l:name, l:value)
else
echoerr "Unknown option: ".l:name
endif
endfunction
function! VVsettings()
for key in keys(g:vv_settings)
call rpcnotify(get(g:, "vv_channel", 1), "vv:set", key, g:vv_settings[key])
endfor
endfunction
command! -nargs=* VVset :call VVset(<f-args>)
command! -nargs=* VVse :call VVset(<f-args>)
command! -nargs=0 VVsettings :call VVsettings() " Send all settings to client
================================================
FILE: packages/electron/config/electron-builder/build.js
================================================
require('dotenv').config();
const { join } = require('path');
const { readFileSync } = require('fs');
const fileAssociations = require('./fileAssociations.json');
const path = require.resolve('electron');
const data = readFileSync(join(path, '..', 'package.json'), { encoding: 'utf-8' });
const electronVersion = JSON.parse(data).version;
const build = {
appId: process.env.APPID || 'app.vvim.vv',
productName: 'VV',
extraMetadata: {
name: 'VV',
},
files: ['build/**/*'],
extraResources: ['bin/**/*', 'src/main/preload.js'],
electronVersion,
directories: {
buildResources: 'assets',
},
fileAssociations: [
...fileAssociations,
{
name: 'Document',
role: 'Editor',
ext: '*',
icon: 'generic.icns',
},
],
mac: {
category: 'public.app-category.developer-tools',
target: {
target: 'dir',
arch: 'universal',
},
},
};
module.exports = build;
================================================
FILE: packages/electron/config/electron-builder/fileAssociations.json
================================================
[
{
"name": "1C Enterprise",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bsl", "os"]
},
{
"name": "ABAP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["abap"]
},
{
"name": "ABNF",
"role": "Editor",
"icon": "generic.icns",
"ext": ["abnf"]
},
{
"name": "AGS Script",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asc", "ash"]
},
{
"name": "AMPL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ampl", "mod"]
},
{
"name": "ANTLR",
"role": "Editor",
"icon": "generic.icns",
"ext": ["g4"]
},
{
"name": "API Blueprint",
"role": "Editor",
"icon": "generic.icns",
"ext": ["apib"]
},
{
"name": "APL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["apl", "dyalog"]
},
{
"name": "ASN.1",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asn", "asn1"]
},
{
"name": "ASP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asp", "asax", "ascx", "ashx", "asmx", "aspx", "axd"]
},
{
"name": "ATS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dats", "hats", "sats"]
},
{
"name": "ActionScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["as"]
},
{
"name": "Ada",
"role": "Editor",
"icon": "generic.icns",
"ext": ["adb", "ada", "ads"]
},
{
"name": "Adobe Font Metrics",
"role": "Editor",
"icon": "generic.icns",
"ext": ["afm"]
},
{
"name": "Agda",
"role": "Editor",
"icon": "generic.icns",
"ext": ["agda"]
},
{
"name": "Alloy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["als"]
},
{
"name": "Altium Designer",
"role": "Editor",
"icon": "generic.icns",
"ext": ["OutJob", "PcbDoc", "PrjPCB", "SchDoc"]
},
{
"name": "AngelScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["as", "angelscript"]
},
{
"name": "ApacheConf",
"role": "Editor",
"icon": "generic.icns",
"ext": ["apacheconf", "vhost"]
},
{
"name": "Apex",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cls"]
},
{
"name": "Apollo Guidance Computer",
"role": "Editor",
"icon": "generic.icns",
"ext": ["agc"]
},
{
"name": "AppleScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["applescript", "scpt"]
},
{
"name": "Arc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["arc"]
},
{
"name": "AsciiDoc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asciidoc", "adoc", "asc"]
},
{
"name": "AspectJ",
"role": "Editor",
"icon": "generic.icns",
"ext": ["aj"]
},
{
"name": "Assembly",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asm", "a51", "inc", "nasm"]
},
{
"name": "Asymptote",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asy"]
},
{
"name": "Augeas",
"role": "Editor",
"icon": "generic.icns",
"ext": ["aug"]
},
{
"name": "AutoHotkey",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ahk", "ahkl"]
},
{
"name": "AutoIt",
"role": "Editor",
"icon": "generic.icns",
"ext": ["au3"]
},
{
"name": "Awk",
"role": "Editor",
"icon": "generic.icns",
"ext": ["awk", "auk", "gawk", "mawk", "nawk"]
},
{
"name": "Ballerina",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bal"]
},
{
"name": "Batchfile",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bat", "cmd"]
},
{
"name": "Befunge",
"role": "Editor",
"icon": "generic.icns",
"ext": ["befunge"]
},
{
"name": "BibTeX",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bib"]
},
{
"name": "Bison",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bison"]
},
{
"name": "BitBake",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bb"]
},
{
"name": "Blade",
"role": "Editor",
"icon": "generic.icns",
"ext": ["blade", "blade.php"]
},
{
"name": "BlitzBasic",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bb", "decls"]
},
{
"name": "BlitzMax",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bmx"]
},
{
"name": "Bluespec",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bsv"]
},
{
"name": "Boo",
"role": "Editor",
"icon": "generic.icns",
"ext": ["boo"]
},
{
"name": "Brainfuck",
"role": "Editor",
"icon": "generic.icns",
"ext": ["b", "bf"]
},
{
"name": "Brightscript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["brs"]
},
{
"name": "C",
"role": "Editor",
"icon": "generic.icns",
"ext": ["c", "cats", "h", "idc"]
},
{
"name": "C#",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cs", "cake", "csx"]
},
{
"name": "C++",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"cpp",
"c++",
"cc",
"cp",
"cxx",
"h",
"h++",
"hh",
"hpp",
"hxx",
"inc",
"inl",
"ino",
"ipp",
"re",
"tcc",
"tpp"
]
},
{
"name": "C-ObjDump",
"role": "Editor",
"icon": "generic.icns",
"ext": ["c-objdump"]
},
{
"name": "C2hs Haskell",
"role": "Editor",
"icon": "generic.icns",
"ext": ["chs"]
},
{
"name": "CLIPS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["clp"]
},
{
"name": "CMake",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cmake", "cmake.in"]
},
{
"name": "COBOL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cob", "cbl", "ccp", "cobol", "cpy"]
},
{
"name": "COLLADA",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dae"]
},
{
"name": "CSON",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cson"]
},
{
"name": "CSS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["css"]
},
{
"name": "CSV",
"role": "Editor",
"icon": "generic.icns",
"ext": ["csv"]
},
{
"name": "CWeb",
"role": "Editor",
"icon": "generic.icns",
"ext": ["w"]
},
{
"name": "Cabal Config",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cabal"]
},
{
"name": "Cap'n Proto",
"role": "Editor",
"icon": "generic.icns",
"ext": ["capnp"]
},
{
"name": "CartoCSS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mss"]
},
{
"name": "Ceylon",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ceylon"]
},
{
"name": "Chapel",
"role": "Editor",
"icon": "generic.icns",
"ext": ["chpl"]
},
{
"name": "Charity",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ch"]
},
{
"name": "ChucK",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ck"]
},
{
"name": "Cirru",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cirru"]
},
{
"name": "Clarion",
"role": "Editor",
"icon": "generic.icns",
"ext": ["clw"]
},
{
"name": "Clean",
"role": "Editor",
"icon": "generic.icns",
"ext": ["icl", "dcl"]
},
{
"name": "Click",
"role": "Editor",
"icon": "generic.icns",
"ext": ["click"]
},
{
"name": "Clojure",
"role": "Editor",
"icon": "generic.icns",
"ext": ["clj", "boot", "cl2", "cljc", "cljs", "cljs.hl", "cljscm", "cljx", "hic"]
},
{
"name": "Closure Templates",
"role": "Editor",
"icon": "generic.icns",
"ext": ["soy"]
},
{
"name": "CoNLL-U",
"role": "Editor",
"icon": "generic.icns",
"ext": ["conllu", "conll"]
},
{
"name": "CoffeeScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["coffee", "_coffee", "cake", "cjsx", "iced"]
},
{
"name": "ColdFusion",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cfm", "cfml"]
},
{
"name": "ColdFusion CFC",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cfc"]
},
{
"name": "Common Lisp",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
},
{
"name": "Common Workflow Language",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cwl"]
},
{
"name": "Component Pascal",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cp", "cps"]
},
{
"name": "Cool",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cl"]
},
{
"name": "Coq",
"role": "Editor",
"icon": "generic.icns",
"ext": ["coq", "v"]
},
{
"name": "Cpp-ObjDump",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cppobjdump", "c++-objdump", "c++objdump", "cpp-objdump", "cxx-objdump"]
},
{
"name": "Creole",
"role": "Editor",
"icon": "generic.icns",
"ext": ["creole"]
},
{
"name": "Crystal",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cr"]
},
{
"name": "Csound",
"role": "Editor",
"icon": "generic.icns",
"ext": ["orc", "udo"]
},
{
"name": "Csound Document",
"role": "Editor",
"icon": "generic.icns",
"ext": ["csd"]
},
{
"name": "Csound Score",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sco"]
},
{
"name": "Cuda",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cu", "cuh"]
},
{
"name": "Cycript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cy"]
},
{
"name": "Cython",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pyx", "pxd", "pxi"]
},
{
"name": "D",
"role": "Editor",
"icon": "generic.icns",
"ext": ["d", "di"]
},
{
"name": "D-ObjDump",
"role": "Editor",
"icon": "generic.icns",
"ext": ["d-objdump"]
},
{
"name": "DIGITAL Command Language",
"role": "Editor",
"icon": "generic.icns",
"ext": ["com"]
},
{
"name": "DM",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dm"]
},
{
"name": "DNS Zone",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zone", "arpa"]
},
{
"name": "DTrace",
"role": "Editor",
"icon": "generic.icns",
"ext": ["d"]
},
{
"name": "Darcs Patch",
"role": "Editor",
"icon": "generic.icns",
"ext": ["darcspatch", "dpatch"]
},
{
"name": "Dart",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dart"]
},
{
"name": "DataWeave",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dwl"]
},
{
"name": "Dhall",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dhall"]
},
{
"name": "Diff",
"role": "Editor",
"icon": "generic.icns",
"ext": ["diff", "patch"]
},
{
"name": "Dockerfile",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dockerfile"]
},
{
"name": "Dogescript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["djs"]
},
{
"name": "Dylan",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dylan", "dyl", "intr", "lid"]
},
{
"name": "E",
"role": "Editor",
"icon": "generic.icns",
"ext": ["E"]
},
{
"name": "EBNF",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ebnf"]
},
{
"name": "ECL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ecl", "eclxml"]
},
{
"name": "ECLiPSe",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ecl"]
},
{
"name": "EJS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ejs"]
},
{
"name": "EML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["eml", "mbox"]
},
{
"name": "EQ",
"role": "Editor",
"icon": "generic.icns",
"ext": ["eq"]
},
{
"name": "Eagle",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sch", "brd"]
},
{
"name": "Easybuild",
"role": "Editor",
"icon": "generic.icns",
"ext": ["eb"]
},
{
"name": "Ecere Projects",
"role": "Editor",
"icon": "generic.icns",
"ext": ["epj"]
},
{
"name": "Edje Data Collection",
"role": "Editor",
"icon": "generic.icns",
"ext": ["edc"]
},
{
"name": "Eiffel",
"role": "Editor",
"icon": "generic.icns",
"ext": ["e"]
},
{
"name": "Elixir",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ex", "exs"]
},
{
"name": "Elm",
"role": "Editor",
"icon": "generic.icns",
"ext": ["elm"]
},
{
"name": "Emacs Lisp",
"role": "Editor",
"icon": "generic.icns",
"ext": ["el", "emacs", "emacs.desktop"]
},
{
"name": "EmberScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["em", "emberscript"]
},
{
"name": "Erlang",
"role": "Editor",
"icon": "generic.icns",
"ext": ["erl", "app.src", "es", "escript", "hrl", "xrl", "yrl"]
},
{
"name": "F#",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fs", "fsi", "fsx"]
},
{
"name": "F*",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fst"]
},
{
"name": "FIGlet Font",
"role": "Editor",
"icon": "generic.icns",
"ext": ["flf"]
},
{
"name": "FLUX",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fx", "flux"]
},
{
"name": "Factor",
"role": "Editor",
"icon": "generic.icns",
"ext": ["factor"]
},
{
"name": "Fancy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fy", "fancypack"]
},
{
"name": "Fantom",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fan"]
},
{
"name": "Filebench WML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["f"]
},
{
"name": "Filterscript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fs"]
},
{
"name": "Formatted",
"role": "Editor",
"icon": "generic.icns",
"ext": ["for", "eam.fs"]
},
{
"name": "Forth",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fth", "4th", "f", "for", "forth", "fr", "frt", "fs"]
},
{
"name": "Fortran",
"role": "Editor",
"icon": "generic.icns",
"ext": ["f90", "f", "f03", "f08", "f77", "f95", "for", "fpp"]
},
{
"name": "FreeMarker",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ftl"]
},
{
"name": "Frege",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fr"]
},
{
"name": "G-code",
"role": "Editor",
"icon": "generic.icns",
"ext": ["g", "cnc", "gco", "gcode"]
},
{
"name": "GAML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gaml"]
},
{
"name": "GAMS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gms"]
},
{
"name": "GAP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["g", "gap", "gd", "gi", "tst"]
},
{
"name": "GCC Machine Description",
"role": "Editor",
"icon": "generic.icns",
"ext": ["md"]
},
{
"name": "GDB",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gdb", "gdbinit"]
},
{
"name": "GDScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gd"]
},
{
"name": "GLSL",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"glsl",
"fp",
"frag",
"frg",
"fs",
"fsh",
"fshader",
"geo",
"geom",
"glslv",
"gshader",
"shader",
"tesc",
"tese",
"vert",
"vrx",
"vsh",
"vshader"
]
},
{
"name": "GN",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gn", "gni"]
},
{
"name": "Game Maker Language",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gml"]
},
{
"name": "Genie",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gs"]
},
{
"name": "Genshi",
"role": "Editor",
"icon": "generic.icns",
"ext": ["kid"]
},
{
"name": "Gentoo Ebuild",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ebuild"]
},
{
"name": "Gentoo Eclass",
"role": "Editor",
"icon": "generic.icns",
"ext": ["eclass"]
},
{
"name": "Gerber Image",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"gbr",
"gbl",
"gbo",
"gbp",
"gbs",
"gko",
"gml",
"gpb",
"gpt",
"gtl",
"gto",
"gtp",
"gts"
]
},
{
"name": "Gettext Catalog",
"role": "Editor",
"icon": "generic.icns",
"ext": ["po", "pot"]
},
{
"name": "Gherkin",
"role": "Editor",
"icon": "generic.icns",
"ext": ["feature"]
},
{
"name": "Git Config",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gitconfig"]
},
{
"name": "Glyph",
"role": "Editor",
"icon": "generic.icns",
"ext": ["glf"]
},
{
"name": "Glyph Bitmap Distribution Format",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bdf"]
},
{
"name": "Gnuplot",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gp", "gnu", "gnuplot", "plot", "plt"]
},
{
"name": "Go",
"role": "Editor",
"icon": "generic.icns",
"ext": ["go"]
},
{
"name": "Golo",
"role": "Editor",
"icon": "generic.icns",
"ext": ["golo"]
},
{
"name": "Gosu",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gs", "gst", "gsx", "vark"]
},
{
"name": "Grace",
"role": "Editor",
"icon": "generic.icns",
"ext": ["grace"]
},
{
"name": "Gradle",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gradle"]
},
{
"name": "Grammatical Framework",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gf"]
},
{
"name": "Graph Modeling Language",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gml"]
},
{
"name": "GraphQL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["graphql", "gql", "graphqls"]
},
{
"name": "Graphviz (DOT)",
"role": "Editor",
"icon": "generic.icns",
"ext": ["dot", "gv"]
},
{
"name": "Groovy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["groovy", "grt", "gtpl", "gvy"]
},
{
"name": "Groovy Server Pages",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gsp"]
},
{
"name": "HAProxy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cfg"]
},
{
"name": "HCL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hcl", "tf", "tfvars", "workflow"]
},
{
"name": "HLSL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hlsl", "cginc", "fx", "fxh", "hlsli"]
},
{
"name": "HTML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["html", "htm", "html.hl", "inc", "st", "xht", "xhtml"]
},
{
"name": "HTML+Django",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jinja", "jinja2", "mustache", "njk"]
},
{
"name": "HTML+ECR",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ecr"]
},
{
"name": "HTML+EEX",
"role": "Editor",
"icon": "generic.icns",
"ext": ["eex"]
},
{
"name": "HTML+ERB",
"role": "Editor",
"icon": "generic.icns",
"ext": ["erb", "erb.deface"]
},
{
"name": "HTML+PHP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["phtml"]
},
{
"name": "HTML+Razor",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cshtml", "razor"]
},
{
"name": "HTTP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["http"]
},
{
"name": "HXML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hxml"]
},
{
"name": "Hack",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hack", "hh", "php"]
},
{
"name": "Haml",
"role": "Editor",
"icon": "generic.icns",
"ext": ["haml", "haml.deface"]
},
{
"name": "Handlebars",
"role": "Editor",
"icon": "generic.icns",
"ext": ["handlebars", "hbs"]
},
{
"name": "Harbour",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hb"]
},
{
"name": "Haskell",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hs", "hs-boot", "hsc"]
},
{
"name": "Haxe",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hx", "hxsl"]
},
{
"name": "HiveQL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["q"]
},
{
"name": "HolyC",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hc"]
},
{
"name": "Hy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["hy"]
},
{
"name": "HyPhy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["bf"]
},
{
"name": "IDL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pro", "dlm"]
},
{
"name": "IGOR Pro",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ipf"]
},
{
"name": "INI",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ini", "cfg", "lektorproject", "prefs", "pro", "properties"]
},
{
"name": "IRC log",
"role": "Editor",
"icon": "generic.icns",
"ext": ["irclog", "weechatlog"]
},
{
"name": "Idris",
"role": "Editor",
"icon": "generic.icns",
"ext": ["idr", "lidr"]
},
{
"name": "Ignore List",
"role": "Editor",
"icon": "generic.icns",
"ext": ["gitignore"]
},
{
"name": "Inform 7",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ni", "i7x"]
},
{
"name": "Inno Setup",
"role": "Editor",
"icon": "generic.icns",
"ext": ["iss"]
},
{
"name": "Io",
"role": "Editor",
"icon": "generic.icns",
"ext": ["io"]
},
{
"name": "Ioke",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ik"]
},
{
"name": "Isabelle",
"role": "Editor",
"icon": "generic.icns",
"ext": ["thy"]
},
{
"name": "J",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ijs"]
},
{
"name": "JFlex",
"role": "Editor",
"icon": "generic.icns",
"ext": ["flex", "jflex"]
},
{
"name": "JSON",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"json",
"avsc",
"geojson",
"gltf",
"har",
"ice",
"JSON-tmLanguage",
"jsonl",
"mcmeta",
"tfstate",
"tfstate.backup",
"topojson",
"webapp",
"webmanifest",
"yy",
"yyp"
]
},
{
"name": "JSON with Comments",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"sublime-build",
"sublime-commands",
"sublime-completions",
"sublime-keymap",
"sublime-macro",
"sublime-menu",
"sublime-mousemap",
"sublime-project",
"sublime-settings",
"sublime-theme",
"sublime-workspace",
"sublime_metrics",
"sublime_session"
]
},
{
"name": "JSON5",
"role": "Editor",
"icon": "generic.icns",
"ext": ["json5"]
},
{
"name": "JSONLD",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jsonld"]
},
{
"name": "JSONiq",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jq"]
},
{
"name": "JSX",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jsx"]
},
{
"name": "Jasmin",
"role": "Editor",
"icon": "generic.icns",
"ext": ["j"]
},
{
"name": "Java",
"role": "Editor",
"icon": "generic.icns",
"ext": ["java"]
},
{
"name": "Java Properties",
"role": "Editor",
"icon": "generic.icns",
"ext": ["properties"]
},
{
"name": "Java Server Pages",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jsp"]
},
{
"name": "JavaScript",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"js",
"_js",
"bones",
"es",
"es6",
"frag",
"gs",
"jake",
"jsb",
"jscad",
"jsfl",
"jsm",
"jss",
"mjs",
"njs",
"pac",
"sjs",
"ssjs",
"xsjs",
"xsjslib"
]
},
{
"name": "JavaScript+ERB",
"role": "Editor",
"icon": "generic.icns",
"ext": ["js.erb"]
},
{
"name": "Jison",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jison"]
},
{
"name": "Jison Lex",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jisonlex"]
},
{
"name": "Jolie",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ol", "iol"]
},
{
"name": "Jsonnet",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jsonnet", "libsonnet"]
},
{
"name": "Julia",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jl"]
},
{
"name": "Jupyter Notebook",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ipynb"]
},
{
"name": "KRL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["krl"]
},
{
"name": "KiCad Layout",
"role": "Editor",
"icon": "generic.icns",
"ext": ["kicad_pcb", "kicad_mod", "kicad_wks"]
},
{
"name": "KiCad Legacy Layout",
"role": "Editor",
"icon": "generic.icns",
"ext": ["brd"]
},
{
"name": "KiCad Schematic",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sch"]
},
{
"name": "Kit",
"role": "Editor",
"icon": "generic.icns",
"ext": ["kit"]
},
{
"name": "Kotlin",
"role": "Editor",
"icon": "generic.icns",
"ext": ["kt", "ktm", "kts"]
},
{
"name": "LFE",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lfe"]
},
{
"name": "LLVM",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ll"]
},
{
"name": "LOLCODE",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lol"]
},
{
"name": "LSL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lsl", "lslp"]
},
{
"name": "LTspice Symbol",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asy"]
},
{
"name": "LabVIEW",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lvproj"]
},
{
"name": "Lasso",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lasso", "las", "lasso8", "lasso9"]
},
{
"name": "Latte",
"role": "Editor",
"icon": "generic.icns",
"ext": ["latte"]
},
{
"name": "Lean",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lean", "hlean"]
},
{
"name": "Less",
"role": "Editor",
"icon": "generic.icns",
"ext": ["less"]
},
{
"name": "Lex",
"role": "Editor",
"icon": "generic.icns",
"ext": ["l", "lex"]
},
{
"name": "LilyPond",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ly", "ily"]
},
{
"name": "Limbo",
"role": "Editor",
"icon": "generic.icns",
"ext": ["b", "m"]
},
{
"name": "Linker Script",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ld", "lds", "x"]
},
{
"name": "Linux Kernel Module",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mod"]
},
{
"name": "Liquid",
"role": "Editor",
"icon": "generic.icns",
"ext": ["liquid"]
},
{
"name": "Literate Agda",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lagda"]
},
{
"name": "Literate CoffeeScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["litcoffee"]
},
{
"name": "Literate Haskell",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lhs"]
},
{
"name": "LiveScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ls", "_ls"]
},
{
"name": "Logos",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xm", "x", "xi"]
},
{
"name": "Logtalk",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lgt", "logtalk"]
},
{
"name": "LookML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lookml", "model.lkml", "view.lkml"]
},
{
"name": "LoomScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ls"]
},
{
"name": "Lua",
"role": "Editor",
"icon": "generic.icns",
"ext": ["lua", "fcgi", "nse", "p8", "pd_lua", "rbxs", "wlua"]
},
{
"name": "M",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mumps", "m"]
},
{
"name": "M4",
"role": "Editor",
"icon": "generic.icns",
"ext": ["m4"]
},
{
"name": "M4Sugar",
"role": "Editor",
"icon": "generic.icns",
"ext": ["m4"]
},
{
"name": "MATLAB",
"role": "Editor",
"icon": "generic.icns",
"ext": ["matlab", "m"]
},
{
"name": "MAXScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ms", "mcr"]
},
{
"name": "MLIR",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mlir"]
},
{
"name": "MQL4",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mq4", "mqh"]
},
{
"name": "MQL5",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mq5", "mqh"]
},
{
"name": "MTML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mtml"]
},
{
"name": "MUF",
"role": "Editor",
"icon": "generic.icns",
"ext": ["muf", "m"]
},
{
"name": "Makefile",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mak", "d", "make", "mk", "mkfile"]
},
{
"name": "Mako",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mako", "mao"]
},
{
"name": "Markdown",
"role": "Editor",
"icon": "generic.icns",
"ext": ["md", "markdown", "mdown", "mdwn", "mdx", "mkd", "mkdn", "mkdown", "ronn", "workbook"]
},
{
"name": "Marko",
"role": "Editor",
"icon": "generic.icns",
"ext": ["marko"]
},
{
"name": "Mask",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mask"]
},
{
"name": "Mathematica",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mathematica", "cdf", "m", "ma", "mt", "nb", "nbp", "wl", "wlt"]
},
{
"name": "Max",
"role": "Editor",
"icon": "generic.icns",
"ext": ["maxpat", "maxhelp", "maxproj", "mxt", "pat"]
},
{
"name": "MediaWiki",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mediawiki", "wiki"]
},
{
"name": "Mercury",
"role": "Editor",
"icon": "generic.icns",
"ext": ["m", "moo"]
},
{
"name": "Metal",
"role": "Editor",
"icon": "generic.icns",
"ext": ["metal"]
},
{
"name": "MiniD",
"role": "Editor",
"icon": "generic.icns",
"ext": ["minid"]
},
{
"name": "Mirah",
"role": "Editor",
"icon": "generic.icns",
"ext": ["druby", "duby", "mirah"]
},
{
"name": "Modelica",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mo"]
},
{
"name": "Modula-2",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mod"]
},
{
"name": "Modula-3",
"role": "Editor",
"icon": "generic.icns",
"ext": ["i3", "ig", "m3", "mg"]
},
{
"name": "Module Management System",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mms", "mmk"]
},
{
"name": "Monkey",
"role": "Editor",
"icon": "generic.icns",
"ext": ["monkey", "monkey2"]
},
{
"name": "Moocode",
"role": "Editor",
"icon": "generic.icns",
"ext": ["moo"]
},
{
"name": "MoonScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["moon"]
},
{
"name": "Motorola 68K Assembly",
"role": "Editor",
"icon": "generic.icns",
"ext": ["X68"]
},
{
"name": "Muse",
"role": "Editor",
"icon": "generic.icns",
"ext": ["muse"]
},
{
"name": "Myghty",
"role": "Editor",
"icon": "generic.icns",
"ext": ["myt"]
},
{
"name": "NCL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ncl"]
},
{
"name": "NL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nl"]
},
{
"name": "NSIS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nsi", "nsh"]
},
{
"name": "Nearley",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ne", "nearley"]
},
{
"name": "Nemerle",
"role": "Editor",
"icon": "generic.icns",
"ext": ["n"]
},
{
"name": "NetLinx",
"role": "Editor",
"icon": "generic.icns",
"ext": ["axs", "axi"]
},
{
"name": "NetLinx+ERB",
"role": "Editor",
"icon": "generic.icns",
"ext": ["axs.erb", "axi.erb"]
},
{
"name": "NetLogo",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nlogo"]
},
{
"name": "NewLisp",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nl", "lisp", "lsp"]
},
{
"name": "Nextflow",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nf"]
},
{
"name": "Nginx",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nginxconf", "vhost"]
},
{
"name": "Nim",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nim", "nim.cfg", "nimble", "nimrod", "nims"]
},
{
"name": "Ninja",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ninja"]
},
{
"name": "Nit",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nit"]
},
{
"name": "Nix",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nix"]
},
{
"name": "Nu",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nu"]
},
{
"name": "NumPy",
"role": "Editor",
"icon": "generic.icns",
"ext": ["numpy", "numpyw", "numsc"]
},
{
"name": "OCaml",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ml", "eliom", "eliomi", "ml4", "mli", "mll", "mly"]
},
{
"name": "ObjDump",
"role": "Editor",
"icon": "generic.icns",
"ext": ["objdump"]
},
{
"name": "ObjectScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cls"]
},
{
"name": "Objective-C",
"role": "Editor",
"icon": "generic.icns",
"ext": ["m", "h"]
},
{
"name": "Objective-C++",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mm"]
},
{
"name": "Objective-J",
"role": "Editor",
"icon": "generic.icns",
"ext": ["j", "sj"]
},
{
"name": "Omgrofl",
"role": "Editor",
"icon": "generic.icns",
"ext": ["omgrofl"]
},
{
"name": "Opa",
"role": "Editor",
"icon": "generic.icns",
"ext": ["opa"]
},
{
"name": "Opal",
"role": "Editor",
"icon": "generic.icns",
"ext": ["opal"]
},
{
"name": "Open Policy Agent",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rego"]
},
{
"name": "OpenCL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cl", "opencl"]
},
{
"name": "OpenEdge ABL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["p", "cls", "w"]
},
{
"name": "OpenSCAD",
"role": "Editor",
"icon": "generic.icns",
"ext": ["scad"]
},
{
"name": "OpenStep Property List",
"role": "Editor",
"icon": "generic.icns",
"ext": ["plist"]
},
{
"name": "OpenType Feature File",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fea"]
},
{
"name": "Org",
"role": "Editor",
"icon": "generic.icns",
"ext": ["org"]
},
{
"name": "Ox",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ox", "oxh", "oxo"]
},
{
"name": "Oxygene",
"role": "Editor",
"icon": "generic.icns",
"ext": ["oxygene"]
},
{
"name": "Oz",
"role": "Editor",
"icon": "generic.icns",
"ext": ["oz"]
},
{
"name": "P4",
"role": "Editor",
"icon": "generic.icns",
"ext": ["p4"]
},
{
"name": "PHP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["php", "aw", "ctp", "fcgi", "inc", "php3", "php4", "php5", "phps", "phpt"]
},
{
"name": "PLSQL",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"pls",
"bdy",
"ddl",
"fnc",
"pck",
"pkb",
"pks",
"plb",
"plsql",
"prc",
"spc",
"sql",
"tpb",
"tps",
"trg",
"vw"
]
},
{
"name": "PLpgSQL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pgsql", "sql"]
},
{
"name": "POV-Ray SDL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pov", "inc"]
},
{
"name": "Pan",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pan"]
},
{
"name": "Papyrus",
"role": "Editor",
"icon": "generic.icns",
"ext": ["psc"]
},
{
"name": "Parrot",
"role": "Editor",
"icon": "generic.icns",
"ext": ["parrot"]
},
{
"name": "Parrot Assembly",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pasm"]
},
{
"name": "Parrot Internal Representation",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pir"]
},
{
"name": "Pascal",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pas", "dfm", "dpr", "inc", "lpr", "pascal", "pp"]
},
{
"name": "Pawn",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pwn", "inc", "sma"]
},
{
"name": "Pep8",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pep"]
},
{
"name": "Perl",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pl", "al", "cgi", "fcgi", "perl", "ph", "plx", "pm", "psgi", "t"]
},
{
"name": "Perl 6",
"role": "Editor",
"icon": "generic.icns",
"ext": ["6pl", "6pm", "nqp", "p6", "p6l", "p6m", "pl", "pl6", "pm", "pm6", "t"]
},
{
"name": "Pic",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pic", "chem"]
},
{
"name": "Pickle",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pkl"]
},
{
"name": "PicoLisp",
"role": "Editor",
"icon": "generic.icns",
"ext": ["l"]
},
{
"name": "PigLatin",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pig"]
},
{
"name": "Pike",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pike", "pmod"]
},
{
"name": "Pod",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pod"]
},
{
"name": "Pod 6",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pod", "pod6"]
},
{
"name": "PogoScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pogo"]
},
{
"name": "Pony",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pony"]
},
{
"name": "PostCSS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pcss"]
},
{
"name": "PostScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ps", "eps", "pfa"]
},
{
"name": "PowerBuilder",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pbt", "sra", "sru", "srw"]
},
{
"name": "PowerShell",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ps1", "psd1", "psm1"]
},
{
"name": "Prisma",
"role": "Editor",
"icon": "generic.icns",
"ext": ["prisma"]
},
{
"name": "Processing",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pde"]
},
{
"name": "Prolog",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pl", "pro", "prolog", "yap"]
},
{
"name": "Propeller Spin",
"role": "Editor",
"icon": "generic.icns",
"ext": ["spin"]
},
{
"name": "Protocol Buffer",
"role": "Editor",
"icon": "generic.icns",
"ext": ["proto"]
},
{
"name": "Public Key",
"role": "Editor",
"icon": "generic.icns",
"ext": ["asc", "pub"]
},
{
"name": "Pug",
"role": "Editor",
"icon": "generic.icns",
"ext": ["jade", "pug"]
},
{
"name": "Puppet",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pp"]
},
{
"name": "Pure Data",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pd"]
},
{
"name": "PureBasic",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pb", "pbi"]
},
{
"name": "PureScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["purs"]
},
{
"name": "Python",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"py",
"bzl",
"cgi",
"fcgi",
"gyp",
"gypi",
"lmi",
"py3",
"pyde",
"pyi",
"pyp",
"pyt",
"pyw",
"rpy",
"spec",
"tac",
"wsgi",
"xpy"
]
},
{
"name": "Python traceback",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pytb"]
},
{
"name": "QML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["qml", "qbs"]
},
{
"name": "QMake",
"role": "Editor",
"icon": "generic.icns",
"ext": ["pro", "pri"]
},
{
"name": "R",
"role": "Editor",
"icon": "generic.icns",
"ext": ["r", "rd", "rsx"]
},
{
"name": "RAML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["raml"]
},
{
"name": "RDoc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rdoc"]
},
{
"name": "REALbasic",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rbbas", "rbfrm", "rbmnu", "rbres", "rbtbar", "rbuistate"]
},
{
"name": "REXX",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rexx", "pprx", "rex"]
},
{
"name": "RHTML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rhtml"]
},
{
"name": "RMarkdown",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rmd"]
},
{
"name": "RPC",
"role": "Editor",
"icon": "generic.icns",
"ext": ["x"]
},
{
"name": "RPM Spec",
"role": "Editor",
"icon": "generic.icns",
"ext": ["spec"]
},
{
"name": "RUNOFF",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rnh", "rno"]
},
{
"name": "Racket",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rkt", "rktd", "rktl", "scrbl"]
},
{
"name": "Ragel",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rl"]
},
{
"name": "Rascal",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rsc"]
},
{
"name": "Raw token data",
"role": "Editor",
"icon": "generic.icns",
"ext": ["raw"]
},
{
"name": "Reason",
"role": "Editor",
"icon": "generic.icns",
"ext": ["re", "rei"]
},
{
"name": "Rebol",
"role": "Editor",
"icon": "generic.icns",
"ext": ["reb", "r", "r2", "r3", "rebol"]
},
{
"name": "Red",
"role": "Editor",
"icon": "generic.icns",
"ext": ["red", "reds"]
},
{
"name": "Redcode",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cw"]
},
{
"name": "Regular Expression",
"role": "Editor",
"icon": "generic.icns",
"ext": ["regexp", "regex"]
},
{
"name": "Ren'Py",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rpy"]
},
{
"name": "RenderScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rs", "rsh"]
},
{
"name": "Rich Text Format",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rtf"]
},
{
"name": "Ring",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ring"]
},
{
"name": "Riot",
"role": "Editor",
"icon": "generic.icns",
"ext": ["riot"]
},
{
"name": "RobotFramework",
"role": "Editor",
"icon": "generic.icns",
"ext": ["robot"]
},
{
"name": "Roff",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"roff",
"1",
"1in",
"1m",
"1x",
"2",
"3",
"3in",
"3m",
"3p",
"3pm",
"3qt",
"3x",
"4",
"5",
"6",
"7",
"8",
"9",
"l",
"man",
"mdoc",
"me",
"ms",
"n",
"nr",
"rno",
"tmac"
]
},
{
"name": "Roff Manpage",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"1",
"1in",
"1m",
"1x",
"2",
"3",
"3in",
"3m",
"3p",
"3pm",
"3qt",
"3x",
"4",
"5",
"6",
"7",
"8",
"9",
"man",
"mdoc"
]
},
{
"name": "Rouge",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rg"]
},
{
"name": "Ruby",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"rb",
"builder",
"eye",
"fcgi",
"gemspec",
"god",
"jbuilder",
"mspec",
"pluginspec",
"podspec",
"rabl",
"rake",
"rbuild",
"rbw",
"rbx",
"ru",
"ruby",
"spec",
"thor",
"watchr"
]
},
{
"name": "Rust",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rs", "rs.in"]
},
{
"name": "SAS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sas"]
},
{
"name": "SCSS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["scss"]
},
{
"name": "SMT",
"role": "Editor",
"icon": "generic.icns",
"ext": ["smt2", "smt"]
},
{
"name": "SPARQL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sparql", "rq"]
},
{
"name": "SQF",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sqf", "hqf"]
},
{
"name": "SQL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sql", "cql", "ddl", "inc", "mysql", "prc", "tab", "udf", "viw"]
},
{
"name": "SQLPL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sql", "db2"]
},
{
"name": "SRecode Template",
"role": "Editor",
"icon": "generic.icns",
"ext": ["srt"]
},
{
"name": "STON",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ston"]
},
{
"name": "SVG",
"role": "Editor",
"icon": "generic.icns",
"ext": ["svg"]
},
{
"name": "Sage",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sage", "sagews"]
},
{
"name": "SaltStack",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sls"]
},
{
"name": "Sass",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sass"]
},
{
"name": "Scala",
"role": "Editor",
"icon": "generic.icns",
"ext": ["scala", "kojo", "sbt", "sc"]
},
{
"name": "Scaml",
"role": "Editor",
"icon": "generic.icns",
"ext": ["scaml"]
},
{
"name": "Scheme",
"role": "Editor",
"icon": "generic.icns",
"ext": ["scm", "sch", "sld", "sls", "sps", "ss"]
},
{
"name": "Scilab",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sci", "sce", "tst"]
},
{
"name": "Self",
"role": "Editor",
"icon": "generic.icns",
"ext": ["self"]
},
{
"name": "ShaderLab",
"role": "Editor",
"icon": "generic.icns",
"ext": ["shader"]
},
{
"name": "Shell",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sh", "bash", "bats", "cgi", "command", "fcgi", "ksh", "sh.in", "tmux", "tool", "zsh"]
},
{
"name": "ShellSession",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sh-session"]
},
{
"name": "Shen",
"role": "Editor",
"icon": "generic.icns",
"ext": ["shen"]
},
{
"name": "Slash",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sl"]
},
{
"name": "Slice",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ice"]
},
{
"name": "Slim",
"role": "Editor",
"icon": "generic.icns",
"ext": ["slim"]
},
{
"name": "SmPL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["cocci"]
},
{
"name": "Smali",
"role": "Editor",
"icon": "generic.icns",
"ext": ["smali"]
},
{
"name": "Smalltalk",
"role": "Editor",
"icon": "generic.icns",
"ext": ["st", "cs"]
},
{
"name": "Smarty",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tpl"]
},
{
"name": "SourcePawn",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sp", "inc"]
},
{
"name": "Spline Font Database",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sfd"]
},
{
"name": "Squirrel",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nut"]
},
{
"name": "Stan",
"role": "Editor",
"icon": "generic.icns",
"ext": ["stan"]
},
{
"name": "Standard ML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ML", "fun", "sig", "sml"]
},
{
"name": "Stata",
"role": "Editor",
"icon": "generic.icns",
"ext": ["do", "ado", "doh", "ihlp", "mata", "matah", "sthlp"]
},
{
"name": "Stylus",
"role": "Editor",
"icon": "generic.icns",
"ext": ["styl"]
},
{
"name": "SubRip Text",
"role": "Editor",
"icon": "generic.icns",
"ext": ["srt"]
},
{
"name": "SugarSS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sss"]
},
{
"name": "SuperCollider",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sc", "scd"]
},
{
"name": "Svelte",
"role": "Editor",
"icon": "generic.icns",
"ext": ["svelte"]
},
{
"name": "Swift",
"role": "Editor",
"icon": "generic.icns",
"ext": ["swift"]
},
{
"name": "SystemVerilog",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sv", "svh", "vh"]
},
{
"name": "TI Program",
"role": "Editor",
"icon": "generic.icns",
"ext": ["8xp", "8xk", "8xk.txt", "8xp.txt"]
},
{
"name": "TLA",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tla"]
},
{
"name": "TOML",
"role": "Editor",
"icon": "generic.icns",
"ext": ["toml"]
},
{
"name": "TSQL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sql"]
},
{
"name": "TSX",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tsx"]
},
{
"name": "TXL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["txl"]
},
{
"name": "Tcl",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tcl", "adp", "tm"]
},
{
"name": "Tcsh",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tcsh", "csh"]
},
{
"name": "TeX",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"tex",
"aux",
"bbx",
"cbx",
"cls",
"dtx",
"ins",
"lbx",
"ltx",
"mkii",
"mkiv",
"mkvi",
"sty",
"toc"
]
},
{
"name": "Tea",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tea"]
},
{
"name": "Terra",
"role": "Editor",
"icon": "generic.icns",
"ext": ["t"]
},
{
"name": "Texinfo",
"role": "Editor",
"icon": "generic.icns",
"ext": ["texinfo", "texi", "txi"]
},
{
"name": "Text",
"role": "Editor",
"icon": "generic.icns",
"ext": ["txt", "fr", "nb", "ncl", "no"]
},
{
"name": "Textile",
"role": "Editor",
"icon": "generic.icns",
"ext": ["textile"]
},
{
"name": "Thrift",
"role": "Editor",
"icon": "generic.icns",
"ext": ["thrift"]
},
{
"name": "Turing",
"role": "Editor",
"icon": "generic.icns",
"ext": ["t", "tu"]
},
{
"name": "Turtle",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ttl"]
},
{
"name": "Twig",
"role": "Editor",
"icon": "generic.icns",
"ext": ["twig"]
},
{
"name": "Type Language",
"role": "Editor",
"icon": "generic.icns",
"ext": ["tl"]
},
{
"name": "TypeScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ts"]
},
{
"name": "Unified Parallel C",
"role": "Editor",
"icon": "generic.icns",
"ext": ["upc"]
},
{
"name": "Unity3D Asset",
"role": "Editor",
"icon": "generic.icns",
"ext": ["anim", "asset", "mat", "meta", "prefab", "unity"]
},
{
"name": "Unix Assembly",
"role": "Editor",
"icon": "generic.icns",
"ext": ["s", "ms"]
},
{
"name": "Uno",
"role": "Editor",
"icon": "generic.icns",
"ext": ["uno"]
},
{
"name": "UnrealScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["uc"]
},
{
"name": "UrWeb",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ur", "urs"]
},
{
"name": "V",
"role": "Editor",
"icon": "generic.icns",
"ext": ["v"]
},
{
"name": "VCL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vcl"]
},
{
"name": "VHDL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vhdl", "vhd", "vhf", "vhi", "vho", "vhs", "vht", "vhw"]
},
{
"name": "Vala",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vala", "vapi"]
},
{
"name": "Verilog",
"role": "Editor",
"icon": "generic.icns",
"ext": ["v", "veo"]
},
{
"name": "Vim Snippet",
"role": "Editor",
"icon": "generic.icns",
"ext": ["snip", "snippet", "snippets"]
},
{
"name": "Vim script",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vim", "vba", "vmb"]
},
{
"name": "Visual Basic",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vb", "bas", "cls", "frm", "frx", "vba", "vbhtml", "vbs"]
},
{
"name": "Volt",
"role": "Editor",
"icon": "generic.icns",
"ext": ["volt"]
},
{
"name": "Vue",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vue"]
},
{
"name": "Wavefront Material",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mtl"]
},
{
"name": "Wavefront Object",
"role": "Editor",
"icon": "generic.icns",
"ext": ["obj"]
},
{
"name": "Web Ontology Language",
"role": "Editor",
"icon": "generic.icns",
"ext": ["owl"]
},
{
"name": "WebAssembly",
"role": "Editor",
"icon": "generic.icns",
"ext": ["wast", "wat"]
},
{
"name": "WebIDL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["webidl"]
},
{
"name": "WebVTT",
"role": "Editor",
"icon": "generic.icns",
"ext": ["vtt"]
},
{
"name": "Windows Registry Entries",
"role": "Editor",
"icon": "generic.icns",
"ext": ["reg"]
},
{
"name": "Wollok",
"role": "Editor",
"icon": "generic.icns",
"ext": ["wlk"]
},
{
"name": "World of Warcraft Addon Data",
"role": "Editor",
"icon": "generic.icns",
"ext": ["toc"]
},
{
"name": "X BitMap",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xbm"]
},
{
"name": "X PixMap",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xpm", "pm"]
},
{
"name": "X10",
"role": "Editor",
"icon": "generic.icns",
"ext": ["x10"]
},
{
"name": "XC",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xc"]
},
{
"name": "XML",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"xml",
"adml",
"admx",
"ant",
"axml",
"builds",
"ccproj",
"ccxml",
"clixml",
"cproject",
"cscfg",
"csdef",
"csl",
"csproj",
"ct",
"depproj",
"dita",
"ditamap",
"ditaval",
"dll.config",
"dotsettings",
"filters",
"fsproj",
"fxml",
"glade",
"gml",
"gmx",
"grxml",
"iml",
"ivy",
"jelly",
"jsproj",
"kml",
"launch",
"mdpolicy",
"mjml",
"mm",
"mod",
"mxml",
"natvis",
"ncl",
"ndproj",
"nproj",
"nuspec",
"odd",
"osm",
"pkgproj",
"pluginspec",
"proj",
"props",
"ps1xml",
"psc1",
"pt",
"rdf",
"resx",
"rss",
"sch",
"scxml",
"sfproj",
"shproj",
"srdf",
"storyboard",
"sublime-snippet",
"targets",
"tml",
"ts",
"tsx",
"ui",
"urdf",
"ux",
"vbproj",
"vcxproj",
"vsixmanifest",
"vssettings",
"vstemplate",
"vxml",
"wixproj",
"workflow",
"wsdl",
"wsf",
"wxi",
"wxl",
"wxs",
"x3d",
"xacro",
"xaml",
"xib",
"xlf",
"xliff",
"xmi",
"xml.dist",
"xproj",
"xsd",
"xspec",
"xul",
"zcml"
]
},
{
"name": "XML Property List",
"role": "Editor",
"icon": "generic.icns",
"ext": ["plist", "stTheme", "tmCommand", "tmLanguage", "tmPreferences", "tmSnippet", "tmTheme"]
},
{
"name": "XPages",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xsp-config", "xsp.metadata"]
},
{
"name": "XProc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xpl", "xproc"]
},
{
"name": "XQuery",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xquery", "xq", "xql", "xqm", "xqy"]
},
{
"name": "XS",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xs"]
},
{
"name": "XSLT",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xslt", "xsl"]
},
{
"name": "Xojo",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xojo_code", "xojo_menu", "xojo_report", "xojo_script", "xojo_toolbar", "xojo_window"]
},
{
"name": "Xtend",
"role": "Editor",
"icon": "generic.icns",
"ext": ["xtend"]
},
{
"name": "YAML",
"role": "Editor",
"icon": "generic.icns",
"ext": [
"yml",
"mir",
"reek",
"rviz",
"sublime-syntax",
"syntax",
"yaml",
"yaml-tmlanguage",
"yml.mysql"
]
},
{
"name": "YANG",
"role": "Editor",
"icon": "generic.icns",
"ext": ["yang"]
},
{
"name": "YARA",
"role": "Editor",
"icon": "generic.icns",
"ext": ["yar", "yara"]
},
{
"name": "YASnippet",
"role": "Editor",
"icon": "generic.icns",
"ext": ["yasnippet"]
},
{
"name": "Yacc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["y", "yacc", "yy"]
},
{
"name": "ZAP",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zap", "xzap"]
},
{
"name": "ZIL",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zil", "mud"]
},
{
"name": "Zeek",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zeek", "bro"]
},
{
"name": "ZenScript",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zs"]
},
{
"name": "Zephir",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zep"]
},
{
"name": "Zig",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zig"]
},
{
"name": "Zimpl",
"role": "Editor",
"icon": "generic.icns",
"ext": ["zimpl", "zmpl", "zpl"]
},
{
"name": "desktop",
"role": "Editor",
"icon": "generic.icns",
"ext": ["desktop", "desktop.in"]
},
{
"name": "eC",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ec", "eh"]
},
{
"name": "edn",
"role": "Editor",
"icon": "generic.icns",
"ext": ["edn"]
},
{
"name": "fish",
"role": "Editor",
"icon": "generic.icns",
"ext": ["fish"]
},
{
"name": "mIRC Script",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mrc"]
},
{
"name": "mcfunction",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mcfunction"]
},
{
"name": "mupad",
"role": "Editor",
"icon": "generic.icns",
"ext": ["mu"]
},
{
"name": "nanorc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nanorc"]
},
{
"name": "nesC",
"role": "Editor",
"icon": "generic.icns",
"ext": ["nc"]
},
{
"name": "ooc",
"role": "Editor",
"icon": "generic.icns",
"ext": ["ooc"]
},
{
"name": "q",
"role": "Editor",
"icon": "generic.icns",
"ext": ["q"]
},
{
"name": "reStructuredText",
"role": "Editor",
"icon": "generic.icns",
"ext": ["rst", "rest", "rest.txt", "rst.txt"]
},
{
"name": "sed",
"role": "Editor",
"icon": "generic.icns",
"ext": ["sed"]
},
{
"name": "wdl",
"role": "Editor",
"icon": "generic.icns",
"ext": ["wdl"]
},
{
"name": "wisp",
"role": "Editor",
"icon": "generic.icns",
"ext": ["wisp"]
},
{
"name": "xBase",
"role": "Editor",
"icon": "generic.icns",
"ext": ["prg", "ch", "prw"]
}
]
================================================
FILE: packages/electron/config/electron-builder/release.js
================================================
// Notarize needs APP_ID, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, TEAM_ID env variables.
// Github repo to release is automatically detected from package.json.
// GH_TOKEN env variable is required to upload release.
// eslint-disable-next-line import/extensions
const build = require('./build.js');
const publish = {
...build,
mac: {
category: 'public.app-category.developer-tools',
target: {
target: 'default',
arch: 'universal',
},
notarize: {
teamId: process.env.TEAM_ID,
},
},
};
module.exports = publish;
================================================
FILE: packages/electron/config/webpack.common.config.js
================================================
const path = require('path');
const buildPath = path.resolve(__dirname, './../build');
module.exports = {
mode: 'development',
output: {
path: buildPath,
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
};
================================================
FILE: packages/electron/config/webpack.config.js
================================================
const rendererConfig = require('./webpack.renderer.config');
const mainConfig = require('./webpack.main.config');
module.exports = [rendererConfig, mainConfig];
================================================
FILE: packages/electron/config/webpack.main.config.js
================================================
const { merge } = require('webpack-merge');
const common = require('./webpack.common.config');
const config = merge(common, {
entry: './src/main/index.ts',
output: {
filename: 'main.js',
},
target: 'electron-main',
node: {
__dirname: false,
__filename: false,
},
});
module.exports = config;
================================================
FILE: packages/electron/config/webpack.prod.config.js
================================================
const { merge } = require('webpack-merge');
const rendererConfig = require('./webpack.renderer.config');
const mainConfig = require('./webpack.main.config');
const prod = {
mode: 'production',
};
const rendererConfigProd = merge(rendererConfig, prod);
const mainConfigProd = merge(mainConfig, prod);
module.exports = [rendererConfigProd, mainConfigProd];
================================================
FILE: packages/electron/config/webpack.renderer.config.js
================================================
const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common.config');
const config = merge(common, {
entry: './src/renderer/index.ts',
output: {
filename: 'renderer.js',
},
plugins: [
new HtmlWebpackPlugin({
template: './src/renderer/index.html',
}),
],
target: 'web',
devtool: 'eval-cheap-source-map',
});
module.exports = config;
================================================
FILE: packages/electron/jest.config.js
================================================
module.exports = {
clearMocks: true,
moduleNameMapper: {
'src/(.*)': ['<rootDir>/src/$1'],
},
};
================================================
FILE: packages/electron/package.json
================================================
{
"name": "@vvim/electron",
"description": "Neovim GUI Client",
"author": "Igor Gladkoborodov <igor.gladkoborodov@gmail.com>",
"version": "2.6.2",
"private": true,
"keywords": [
"vim",
"neovim",
"client",
"gui",
"electron"
],
"repository": {
"type": "git",
"url": "https://github.com:vv-vim/vv.git"
},
"license": "MIT",
"main": "./build/main.js",
"sideEffects": false,
"scripts": {
"test": "jest",
"clean": "rm -rf dist/*",
"webpack:dev": "webpack --watch --config ./config/webpack.config.js",
"webpack:prod": "webpack --config ./config/webpack.prod.config.js",
"build:local": "yarn webpack:prod; electron-builder -c.mac.identity=null -c.extraMetadata.main=build/main.js --config config/electron-builder/build.js",
"build:release": "electron-builder -c.extraMetadata.main=build/main.js --config config/electron-builder/release.js --publish always",
"build:link": "rm -rf /Applications/VV.app; cp -R dist/mac-universal/VV.app /Applications; ln -s -f /Applications/VV.app/Contents/Resources/bin/vv /usr/local/bin/vv",
"build": "npm-run-all clean webpack:prod build:local build:link",
"release:open-github": "open https://github.com/vv-vim/vv/releases",
"release": "npm-run-all clean webpack:prod build:release release:open-github",
"filetypes": "node scripts/filetypes.js",
"dev": "yarn webpack:dev",
"start": "electron ."
},
"browserslist": [
"chrome 122",
"node 20"
],
"devDependencies": {
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168",
"@types/node": "^16.0.0",
"chalk": "^4.1.0",
"dotenv": "^8.2.0",
"electron": "^29",
"electron-builder": "^24.13.3",
"html-webpack-plugin": "^5.6.0",
"js-yaml": "^3.14.0",
"node-fetch": "^2.6.7"
},
"dependencies": {
"@vvim/browser-renderer": "0.0.1",
"@vvim/nvim": "0.0.1",
"electron-store": "^7.0.2",
"electron-updater": "^4.3.5",
"emoji-regex": "^10.3.0",
"html2plaintext": "^2.1.2",
"lodash": "^4.17.21",
"semver": "^7.5.2"
}
}
================================================
FILE: packages/electron/scripts/filetypes.js
================================================
// Generate fileAssociations for electron-builder.
// File types are generated from [Github Linguist](https://github.com/github/linguist)
// languates list.
const fetch = require('node-fetch');
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');
const SOURCE_YAML =
'https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml';
const SAVE_TO = path.join(__dirname, '../config/electron-builder/fileAssociations.json');
const filetypes = async () => {
const yamlDoc = await fetch(SOURCE_YAML).then((res) => res.text());
const parsed = yaml.safeLoad(yamlDoc);
const fileAssociations = Object.keys(parsed)
.filter((key) => parsed[key].extensions)
.map((key) => ({
name: key,
role: 'Editor',
icon: 'generic.icns',
ext: parsed[key].extensions.map((e) => e.replace('.', '')),
}));
fs.writeFileSync(SAVE_TO, JSON.stringify(fileAssociations, null, 2), { encoding: 'utf-8' });
};
filetypes();
================================================
FILE: packages/electron/src/lib/isDev.ts
================================================
type IsDevFunction = {
<T, F>(dev: T, notDev: F): T | F;
(): boolean;
};
const isDev: IsDevFunction = (dev = true, notDev = false) =>
process.env.NODE_ENV === 'development' ? dev : notDev;
export default isDev;
================================================
FILE: packages/electron/src/lib/log.ts
================================================
const initNow = Date.now();
let lastNow = initNow;
const log = (...text: string[]): void => {
// eslint-disable-next-line no-console
console.log(...text, Date.now() - lastNow, Date.now() - initNow, initNow, Date.now());
lastNow = Date.now();
};
log('Init log');
export default log;
================================================
FILE: packages/electron/src/main/autoUpdate.ts
================================================
import { dialog, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
import html2plaintext from 'html2plaintext';
import { getSettings, onChangeSettings, SettingsCallback } from 'src/main/nvim/settings';
import store from 'src/main/lib/store';
let interval = 0;
let updaterIntervalId: NodeJS.Timeout;
const LAST_CHECKED = 'autoUpdate.lastCheckedForUpdate';
const MINUTE = 60 * 1000;
const needToCheck = () => {
if (interval === 0) {
return false;
}
const lastChecked = store.get(LAST_CHECKED);
if (!lastChecked) {
return true;
}
return Date.now() - lastChecked > interval * MINUTE;
};
const updater = () => {
if (needToCheck()) {
store.set(LAST_CHECKED, Date.now());
autoUpdater.checkForUpdates();
}
};
const startUpdater = () => {
if (!updaterIntervalId) {
updaterIntervalId = setInterval(updater, MINUTE);
}
};
const updateInterval = (newInterval: string) => {
if (interval !== parseInt(newInterval, 10)) {
interval = parseInt(newInterval, 10);
startUpdater();
}
};
const handleChangeSettings: SettingsCallback = (_newSettings, allSettings) => {
const { autoupdateinterval } = allSettings;
if (autoupdateinterval !== undefined) {
updateInterval(autoupdateinterval);
}
};
const handleUpdateAvailable = ({
version,
releaseNotes,
}: {
version: string;
releaseNotes: string;
}) => {
const response = dialog.showMessageBoxSync({
type: 'question',
buttons: ['Update', 'Ignore'],
defaultId: 0,
message: `Version ${version} is available, do you want to install it now?`,
detail: html2plaintext(releaseNotes),
title: 'Update available',
});
if (response === 0) {
autoUpdater.downloadUpdate();
}
};
const handleUpdateDownloaded = () => {
dialog.showMessageBox({
type: 'question',
buttons: ['OK'],
defaultId: 0,
message: `Update Downloaded`,
detail: 'Please restart app to install update.',
title: 'Update Downloaded',
});
};
const initAutoUpdate = ({ win }: { win: BrowserWindow }): void => {
updateInterval(getSettings().autoupdateinterval);
onChangeSettings(win, handleChangeSettings);
autoUpdater.autoDownload = false;
autoUpdater.on('update-available', handleUpdateAvailable);
autoUpdater.on('update-downloaded', handleUpdateDownloaded);
};
export default initAutoUpdate;
================================================
FILE: packages/electron/src/main/checkNeovim.ts
================================================
import { app, dialog, shell } from 'electron';
import semver from 'semver';
import { nvimVersion } from '@vvim/nvim';
const REQUIRED_VERSION = '0.4.0';
const checkNeovim = (): void => {
const version = nvimVersion();
if (!version) {
const result = dialog.showMessageBoxSync({
message: 'Neovim is not installed',
detail: `VV requires Neovim. You can install it via Homebrew:
brew install neovim
Or you can find Neovim installation instructions here:
https://github.com/neovim/neovim/wiki/Installing-Neovim
`,
defaultId: 0,
buttons: ['Open Installation Instructions', 'Close'],
});
if (result === 0) {
shell.openExternal('https://github.com/neovim/neovim/wiki/Installing-Neovim');
}
app.exit();
} else if (semver.lt(version, REQUIRED_VERSION)) {
const result = dialog.showMessageBoxSync({
message: 'Neovim is outdated',
detail: `VV requires Neovim version ${REQUIRED_VERSION} and later.
You have ${version}.
If you installed Neovim via Homebrew, please run:
brew upgrade neovim
Otherwise please check installation instructions here:
https://github.com/neovim/neovim/wiki/Installing-Neovim
`,
defaultId: 0,
buttons: ['Open Installation Instructions', 'Close'],
});
if (result === 0) {
shell.openExternal('https://github.com/neovim/neovim/wiki/Installing-Neovim');
}
app.exit();
}
};
export default checkNeovim;
================================================
FILE: packages/electron/src/main/index.ts
================================================
import { app, BrowserWindow, dialog } from 'electron';
import { statSync, existsSync } from 'fs';
import { join, resolve } from 'path';
import isDev from 'src/lib/isDev';
import menu from 'src/main/menu';
import installCli from 'src/main/installCli';
import checkNeovim from 'src/main/checkNeovim';
import { setShouldQuit } from 'src/main/nvim/features/quit';
import { getSettings } from 'src/main/nvim/settings';
import { getNvimByWindow } from 'src/main/nvim/nvimByWindow';
import initAutoUpdate from 'src/main/autoUpdate';
import initNvim from 'src/main/nvim/nvim';
import { parseArgs, joinArgs, filterArgs, cliArgs, argValue } from 'src/main/lib/args';
import IpcTransport from 'src/main/transport/ipc';
let currentWindow: BrowserWindow | undefined | null;
const windows: BrowserWindow[] = [];
/** Empty windows created in advance to make windows creation faster */
const emptyWindows: BrowserWindow[] = [];
app.commandLine.appendSwitch('force_high_performance_gpu');
const openDeveloperTools = (win: BrowserWindow) => {
win.webContents.openDevTools({ mode: 'detach' });
win.webContents.on('devtools-opened', () => {
win.webContents.focus();
});
};
const handleAllClosed = () => {
const { quitoncloselastwindow } = getSettings();
if (quitoncloselastwindow || process.platform !== 'darwin') {
app.quit();
}
};
const createEmptyWindow = (isDebug = false) => {
const options = {
width: 800,
height: 600,
show: isDebug,
fullscreenable: false,
// frame: false,
// roundedCorners: false,
webPreferences: {
preload: join(app.getAppPath(), isDev('./', '../'), 'src/main/preload.js'),
},
};
let win = new BrowserWindow(options);
// @ts-expect-error TODO
win.zoomLevel = 0;
win.on('closed', async () => {
if (currentWindow === win) currentWindow = null;
const i = windows.indexOf(win);
if (i !== -1) windows.splice(i, 1);
// @ts-expect-error TODO
win = null;
if (windows.length === 0) handleAllClosed();
});
win.on('focus', () => {
currentWindow = win;
});
win.loadURL(
process.env.DEV_SERVER ? 'http://localhost:3000' : `file://${join(__dirname, './index.html')}`,
);
return win;
};
const getEmptyWindow = (isDebug = false): BrowserWindow => {
if (emptyWindows.length > 0) {
return emptyWindows.pop() as BrowserWindow;
}
return createEmptyWindow(isDebug);
};
const createWindow = async (originalArgs: string[] = [], newCwd?: string) => {
const settings = getSettings();
const cwd = newCwd || process.cwd();
const isDebug = originalArgs.includes('--debug') || originalArgs.includes('--inspect');
// TODO: Use yargs maybe.
const { args, files } = parseArgs(filterArgs(originalArgs));
let unopenedFiles = files;
let { openInProject } = settings;
let openInProjectArg = argValue(originalArgs, '--open-in-project');
if (openInProjectArg === '0' || openInProjectArg === 'false') {
openInProjectArg = undefined;
openInProject = 0;
}
if (openInProjectArg === 'true') {
openInProjectArg = '1';
}
// TODO: Rafactor this somewhere to a separate file or function.
if (openInProject || openInProjectArg) {
await Promise.all(
windows.map(async (win) => {
const nvim = getNvimByWindow(win);
if (nvim) {
// @ts-expect-error TODO: don't add custom props to win
win.cwd = await nvim.callFunction<string>('VVprojectRoot', []); // eslint-disable-line
}
return Promise.resolve();
}),
);
unopenedFiles = files.reduce<string[]>((result, fileName) => {
const resolvedFileName = resolve(cwd, fileName);
const openInWindow = windows.find(
// @ts-expect-error TODO: don't add custom props to win
(w) => resolvedFileName.startsWith(w.cwd) && !w.isMinimized(),
);
if (openInWindow) {
const nvim = getNvimByWindow(openInWindow);
if (nvim) {
// @ts-expect-error TODO: don't add custom props to win
const relativeFileName = resolvedFileName.substring(openInWindow.cwd.length + 1);
nvim.callFunction(
'VVopenInProject',
openInProjectArg ? [relativeFileName, openInProjectArg] : [relativeFileName],
);
openInWindow.focus();
app.focus({ steal: true });
return result;
}
}
return [...result, fileName];
}, []);
}
if (files.length === 0 || unopenedFiles.length > 0) {
const win = getEmptyWindow(isDebug);
// @ts-expect-error TODO: don't add custom props to win
win.cwd = cwd;
if (currentWindow && !currentWindow.isFullScreen() && !currentWindow.isSimpleFullScreen()) {
const [x, y] = currentWindow.getPosition();
const [width, height] = currentWindow.getSize();
win.setBounds({ x: x + 20, y: y + 20, width, height }, false);
}
const transport = new IpcTransport(win);
initNvim({
args: joinArgs({ args, files: unopenedFiles }),
cwd,
win,
transport,
});
const initRenderer = () => transport.send('initRenderer', settings);
if (win.webContents.isLoading()) {
win.webContents.on('did-finish-load', initRenderer);
} else {
initRenderer();
}
win.focus();
windows.push(win);
if (isDebug) {
openDeveloperTools(win);
} else {
setTimeout(() => emptyWindows.push(createEmptyWindow()), 1000);
}
initAutoUpdate({ win });
}
};
const openFileOrDir = (fileName: string) => {
app.addRecentDocument(fileName);
if (existsSync(fileName) && statSync(fileName).isDirectory()) {
createWindow([fileName], fileName);
} else {
createWindow([fileName]);
}
};
const openFile = () => {
const fileNames = dialog.showOpenDialogSync({
properties: ['openFile', 'openDirectory', 'createDirectory', 'multiSelections'],
});
if (fileNames) {
fileNames.forEach(openFileOrDir);
}
};
const gotTheLock = isDev() || app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
let fileToOpen: string | undefined | null;
app.on('will-finish-launching', () => {
app.on('open-file', (_e, file) => {
fileToOpen = file;
});
});
app.on('ready', () => {
checkNeovim();
if (fileToOpen) {
openFileOrDir(fileToOpen);
fileToOpen = null;
} else {
createWindow(cliArgs());
}
menu({
createWindow,
openFile,
installCli: installCli(join(app.getAppPath(), '../bin/vv')),
});
app.on('open-file', (_e, file) => openFileOrDir(file));
app.focus();
});
app.on('second-instance', (_e, args, cwd) => {
createWindow(cliArgs(args), cwd);
});
app.on('before-quit', (e) => {
setShouldQuit(true);
const visibleWindows = windows.filter((w) => w.isVisible());
if (visibleWindows.length > 0) {
e.preventDefault();
(currentWindow || visibleWindows[0]).close();
}
});
app.on('window-all-closed', handleAllClosed);
app.on('activate', (_e, hasVisibleWindows) => {
if (!hasVisibleWindows) {
createWindow();
}
});
}
================================================
FILE: packages/electron/src/main/installCli.ts
================================================
import { dialog } from 'electron';
import { execSync } from 'child_process';
import which from 'src/main/lib/which';
const showInstallCliDialog = () =>
dialog.showMessageBoxSync({
message: 'Command line launcher',
detail: `With command line launcher you can run VV from terminal:
$ vv [filename]
Do you wish to install it? It will be placed
to /usr/local/bin.
`,
cancelId: 1,
defaultId: 0,
buttons: ['Install', 'Cancel'],
});
const showCliInstalledDialog = (message: string, path: string) =>
dialog.showMessageBox({
message,
detail: `Command line launcher installed at ${path}. You can run VV from terminal by typing:
$ vv [filename]
`,
defaultId: 0,
buttons: ['Ok'],
});
const showErrorDialog = (error: Error) => {
dialog.showMessageBox({
message: 'Error',
detail: error.message,
defaultId: 0,
buttons: ['Ok'],
});
};
const installCli = (binPath: string) => (): void => {
let path = which('vv');
if (path && path.indexOf('VV.app/Contents/MacOS/vv') === -1) {
path = path.replace('\n', '');
showCliInstalledDialog('Command Line Launcher', path);
} else {
const response = showInstallCliDialog();
if (response === 0) {
try {
execSync(`ln -sf ${binPath} /usr/local/bin/`);
} catch (error) {
showErrorDialog(error);
return;
}
showCliInstalledDialog('Done', '/usr/local/bin/vv');
}
}
};
export default installCli;
================================================
FILE: packages/electron/src/main/lib/__tests__/args.test.ts
================================================
import { parseArgs, joinArgs, filterArgs, argValue } from 'src/main/lib/args';
describe('parseArgs', () => {
test('return empty array if input is empty', () => {
expect(parseArgs([])).toEqual({ args: [], files: [] });
expect(parseArgs()).toEqual({ args: [], files: [] });
});
test('returns everything if there ar no params', () => {
expect(parseArgs(['file1', 'file2'])).toEqual({
args: [],
files: ['file1', 'file2'],
});
});
test('returns everything after --', () => {
expect(parseArgs(['before1', 'before2', '--', 'after1', 'after2'])).toEqual({
args: ['before1', 'before2'],
files: ['after1', 'after2'],
});
});
test('skip params started with - or +', () => {
['-param1', '--param2', '+cmd1'].forEach((param) => {
expect(parseArgs([param, 'file1', 'file2'])).toEqual({
args: [param],
files: ['file1', 'file2'],
});
});
});
test('skip params with argument', () => {
['--cmd', '-c', '-i', '-r', '-s', '-S', '-u', '--listen', '--startuptime'].forEach((param) => {
expect(parseArgs([param, 'arg', 'file1', 'file2'])).toEqual({
args: [param, 'arg'],
files: ['file1', 'file2'],
});
});
});
test('does not mutate arguments', () => {
const args = ['arg1', 'arg2'];
parseArgs(args);
expect(args).toEqual(['arg1', 'arg2']);
});
});
describe('joinArgs', () => {
test('joins args and files arrays and put -- between them', () => {
expect(joinArgs({ args: ['arg1', 'arg2'], files: ['file1', 'file2'] })).toEqual([
'arg1',
'arg2',
'--',
'file1',
'file2',
]);
});
test("don't add -- if args is empty", () => {
expect(joinArgs({ args: [], files: ['file1', 'file2'] })).toEqual(['file1', 'file2']);
});
test("don't add -- if files is empty", () => {
expect(joinArgs({ args: ['arg1'], files: [] })).toEqual(['arg1']);
});
});
describe('filterArgs', () => {
test('returns all args if none of them are VV-specific', () => {
expect(filterArgs(['arg1', 'arg2'])).toEqual(['arg1', 'arg2']);
});
test('filters out --inspect', () => {
expect(filterArgs(['arg1', '--inspect', 'arg2'])).toEqual(['arg1', 'arg2']);
expect(filterArgs(['--inspect', 'arg1', 'arg2'])).toEqual(['arg1', 'arg2']);
expect(filterArgs(['arg1', 'arg2', '--inspect'])).toEqual(['arg1', 'arg2']);
expect(filterArgs(['--inspect'])).toEqual([]);
});
test('filters out --open-in-project with value', () => {
expect(filterArgs(['arg1', '--open-in-project', 'value', 'arg2'])).toEqual(['arg1', 'arg2']);
expect(filterArgs(['--open-in-project', 'value', 'arg1', 'arg2'])).toEqual(['arg1', 'arg2']);
expect(filterArgs(['arg1', 'arg2', '--open-in-project'])).toEqual(['arg1', 'arg2']);
expect(filterArgs(['--open-in-project'])).toEqual([]);
expect(filterArgs(['--open-in-project', 'value'])).toEqual([]);
});
test('filters out chromium flags', () => {
expect(
filterArgs(['arg1', '--allow-file-access-from-files', '--enable-avfoundation', 'arg2']),
).toEqual(['arg1', 'arg2']);
});
});
describe('argValue', () => {
test('returns true if argument is present', () => {
expect(argValue(['--arg1', '--arg2'], '--arg1')).toBe(true);
expect(argValue(['--arg1', '--arg2', 'file1'], '--arg1')).toBe(true);
expect(argValue(['--arg1', '--arg2', '--', 'file1'], '--arg1')).toBe(true);
});
test('returns undefined if argument is not present', () => {
expect(argValue(['--arg1', '--arg2'], '--arg3')).toBeUndefined();
expect(argValue(['--arg1', '--', '--arg2'], '--arg2')).toBeUndefined();
});
test('returns value for argument with param', () => {
expect(argValue(['--arg1', '--cmd', 'cmdValue', '--arg2'], '--cmd')).toBe('cmdValue');
expect(argValue(['--cmd', 'cmdValue'], '--cmd')).toBe('cmdValue');
expect(argValue(['--cmd', 'cmdValue', 'file1'], '--cmd')).toBe('cmdValue');
expect(argValue(['--cmd', 'cmdValue', '--', '--cmd', 'invalid'], '--cmd')).toBe('cmdValue');
});
test('returns undefined invalid argument with param', () => {
expect(argValue(['--arg1', '--cmd'], '--cmd')).toBeUndefined();
expect(argValue(['--', '--cmd', 'cmdValue'], '--cmd')).toBeUndefined();
});
});
================================================
FILE: packages/electron/src/main/lib/args.ts
================================================
// TODO: Use commander or yargs
import isDev from 'src/lib/isDev';
const ARGS_WITH_PARAM = [
'--cmd',
'-c',
'-i',
'-r',
'-s',
'-S',
'-u',
'--listen',
'--startuptime',
'--open-in-project',
];
/**
* Args specific to VV.
*/
const VV_ARGS = ['--debug', '--inspect', '--open-in-project'];
/**
* Chromium args added by electron.
* TODO: find more reliable way to filter them.
*/
const CHROMIUM_ARGS = [
'--allow-file-access-from-files',
'--enable-avfoundation',
'--force_high_performance_gpu',
];
/**
* Parse CLI args and return the list of files and arguments.
*/
export const parseArgs = (
originalArgs: string[] = [],
): {
args: string[];
files: string[];
} => {
const args = [...originalArgs];
const filesSeparator = args.indexOf('--');
if (filesSeparator !== -1) {
return {
args: args.slice(0, filesSeparator),
files: args.slice(filesSeparator + 1),
};
}
const files: string[] = [];
for (let i = args.length - 1; i >= 0; i -= 1) {
if (['-', '+'].includes(args[i][0]) || (args[i - 1] && ARGS_WITH_PARAM.includes(args[i - 1]))) {
break;
}
files.unshift(args.pop() as string);
}
return { args, files };
};
/**
* Join previously parsed args.
*/
export const joinArgs = ({ args, files }: { args: string[]; files: string[] }): string[] => {
if (args.length === 0) {
return files;
}
if (files.length === 0) {
return args;
}
return [...args, '--', ...files];
};
/**
* Argument value.
* Returns true for argument that does not require argument if it is present.
* Returns argument param for argumenst with params (for example --cmd).
* Undefined if param is not present.
*/
export const argValue = (originalArgs: string[], argName: string): string | true | undefined => {
const { args } = parseArgs(originalArgs);
const index = args.indexOf(argName);
if (index === -1) {
return undefined;
}
if (ARGS_WITH_PARAM.includes(argName)) {
return args[index + 1];
}
return true;
};
/**
* Remove VV specific arguments not supported by nvim
*/
export const filterArgs = (args: string[]): string[] =>
args.reduce<string[]>((result, a, i) => {
if (VV_ARGS.includes(a) || CHROMIUM_ARGS.includes(a)) {
return result;
}
if (args[i - 1] && VV_ARGS.includes(args[i - 1]) && ARGS_WITH_PARAM.includes(args[i - 1])) {
return result;
}
return [...result, a];
}, []);
/**
* Get CLI arguments
*/
export const cliArgs = (args?: string[]): string[] | undefined =>
(args || process.argv).slice(isDev(2, 1));
================================================
FILE: packages/electron/src/main/lib/store.ts
================================================
import Store from 'electron-store';
type BooleanSetting = 0 | 1;
export type Settings = {
fullscreen: BooleanSetting;
simplefullscreen: BooleanSetting;
bold: BooleanSetting;
italic: BooleanSetting;
underline: BooleanSetting;
undercurl: BooleanSetting;
strikethrough: BooleanSetting;
fontfamily: string;
fontsize: string; // TODO: number
lineheight: string; // TODO: number
letterspacing: string; // TODO: number
reloadchanged: BooleanSetting;
quitoncloselastwindow: BooleanSetting;
autoupdateinterval: string; // TODO: number
openInProject: BooleanSetting;
};
type StoreData = {
lastSettings: Settings;
autoUpdate: {
lastCheckedForUpdate: number;
};
'autoUpdate.lastCheckedForUpdate': number;
};
const store = new Store<StoreData>();
export default store;
================================================
FILE: packages/electron/src/main/lib/which.ts
================================================
import { execSync } from 'child_process';
import { shellEnv } from '@vvim/nvim';
/**
* Checks if command exists in shell.
*/
const which = (command: string): string | null => {
let result: string | null | undefined;
try {
result = execSync(`which ${command}`, {
encoding: 'utf-8',
env: shellEnv(),
});
} catch (e) {
result = null;
}
return result;
};
export default which;
================================================
FILE: packages/electron/src/main/menu.ts
================================================
import { Menu, MenuItemConstructorOptions } from 'electron';
// import { handleCloseWindow } from 'src/main/nvim/features/closeWindow';
import { copyMenuItem, pasteMenuItem, selectAllMenuItem } from 'src/main/nvim/features/copyPaste';
import { zoomInMenuItem, zoomOutMenuItem, actualSizeMenuItem } from 'src/main/nvim/features/zoom';
import { closeWindowMenuItem } from 'src/main/nvim/features/closeWindow';
import { toggleFullScreenMenuItem } from 'src/main/nvim/features/windowSize';
let menu: Menu;
const createMenu = ({
createWindow,
openFile,
installCli,
}: {
createWindow: () => void;
openFile: MenuItemConstructorOptions['click'];
installCli: MenuItemConstructorOptions['click'];
}): void => {
const menuTemplate: MenuItemConstructorOptions[] = [
{
label: 'VV',
submenu: [
{ role: 'about' },
{
label: 'Command Line Launcher...',
click: installCli,
},
{ type: 'separator' },
{ role: 'services', submenu: [] },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
},
{
label: 'File',
submenu: [
{
label: 'New Window',
accelerator: 'CmdOrCtrl+N',
click: () => createWindow(),
},
{
label: 'Open...',
accelerator: 'CmdOrCtrl+O',
click: openFile,
},
{
role: 'recentDocuments',
submenu: [
{
role: 'clearRecentDocuments',
},
],
},
{ type: 'separator' },
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
click: closeWindowMenuItem,
},
],
},
{
label: 'Edit',
submenu: [
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
click: copyMenuItem,
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
click: pasteMenuItem,
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
click: selectAllMenuItem,
},
],
},
{
label: 'View',
submenu: [
{
label: 'Toggle Full Screen',
accelerator: 'Cmd+Ctrl+F',
click: toggleFullScreenMenuItem,
},
{
label: 'Actual Size',
id: 'actualSize',
accelerator: 'CmdOrCtrl+0',
click: actualSizeMenuItem,
enabled: false,
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
click: zoomInMenuItem,
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click: zoomOutMenuItem,
},
{ type: 'separator' },
{
label: 'Developer',
submenu: [{ role: 'toggleDevTools' }],
},
],
},
{
role: 'window',
submenu: [{ role: 'minimize' }, { role: 'zoom' }, { type: 'separator' }, { role: 'front' }],
},
];
menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
};
export default createMenu;
================================================
FILE: packages/electron/src/main/nvim/__tests__/nvim.test.ts
================================================
// eslint-disable-next-line
import initNvim from 'src/main/nvim/nvim';
describe('initNvim', () => {
test.todo('TODO');
});
================================================
FILE: packages/electron/src/main/nvim/features/__tests__/backrdoundColor.test.ts
================================================
import backgroundColor from 'src/main/nvim/features/backrdoundColor';
import type { Transport } from '@vvim/nvim';
import type { BrowserWindow } from 'electron';
describe('backrdoundColor', () => {
const setBackgroundColor = jest.fn();
let emitSetBackgroundColor: (color: string) => void;
const transport = ({
on: (event: string, callback: (...args: any[]) => void) => {
if (event === 'set-background-color') {
emitSetBackgroundColor = callback;
}
},
} as unknown) as Transport;
const win = ({
setBackgroundColor,
} as unknown) as BrowserWindow;
test('set window background color on `set-backround-color` event', () => {
backgroundColor({ transport, win });
emitSetBackgroundColor('red');
expect(setBackgroundColor).toHaveBeenCalledWith('red');
});
});
================================================
FILE: packages/electron/src/main/nvim/features/__tests__/windowSize.test.ts
================================================
import initWindowSize from 'src/main/nvim/features/windowSize';
import { EventEmitter } from 'events';
import type { Transport } from '@vvim/nvim';
import type { BrowserWindow } from 'electron';
describe('initWindowSize', () => {
describe('set-screen-width', () => {
const setContentSize = jest.fn();
const getContentSize = jest.fn();
const send = jest.fn();
// TODO: Come up with the better way to mock BrowserWindow
const win = ({
setContentSize,
getContentSize,
getBounds: () => {
/* empty */
},
setBounds: () => {
/* empty */
},
isFullScreen: () => false,
setSimpleFullScreen: () => {
/* empty */
},
webContents: {
focus: () => {
/* empty */
},
},
} as unknown) as BrowserWindow;
let transport: Transport;
beforeEach(() => {
jest.clearAllMocks();
getContentSize.mockReturnValue([100, 200]);
transport = Object.assign(new EventEmitter(), {
send,
});
});
test('set window size on set-screen-width', () => {
initWindowSize({ transport, win });
transport.emit('set-screen-width', 150);
expect(setContentSize).toHaveBeenCalledWith(150, 200);
});
test('set window size on set-screen-height', () => {
initWindowSize({ transport, win });
getContentSize.mockReturnValueOnce([100, 200]).mockReturnValueOnce([100, 250]);
transport.emit('set-screen-height', 250);
expect(setContentSize).toHaveBeenCalledWith(100, 250);
expect(send).not.toHaveBeenCalledWith('force-resize');
});
test('send force-resize if window height is the same after resize', () => {
initWindowSize({ transport, win });
getContentSize.mockReturnValueOnce([100, 200]).mockReturnValueOnce([100, 200]);
transport.emit('set-screen-height', 250);
expect(send).toHaveBeenCalledWith('force-resize');
});
});
});
================================================
FILE: packages/electron/src/main/nvim/features/backrdoundColor.ts
================================================
import type { BrowserWindow } from 'electron';
import type { Transport } from '@vvim/nvim';
/**
* Change Electron window background color depending when renderer ask for it.
*/
const backroundColor = ({ transport, win }: { transport: Transport; win: BrowserWindow }): void => {
transport.on('set-background-color', (bgColor: string) => {
win.setBackgroundColor(bgColor);
});
};
export default backroundColor;
================================================
FILE: packages/electron/src/main/nvim/features/closeWindow.ts
================================================
import { MenuItemConstructorOptions } from 'electron';
import { getNvimByWindow } from 'src/main/nvim/nvimByWindow';
export const closeWindowMenuItem: MenuItemConstructorOptions['click'] = async (_item, win) => {
if (win) {
const nvim = getNvimByWindow(win);
if (nvim) {
const isNotLastWindow = await nvim.eval<boolean>('tabpagenr("$") > 1 || winnr("$") > 1');
if (isNotLastWindow) {
nvim.command(`q`);
} else {
win.close();
}
}
}
};
================================================
FILE: packages/electron/src/main/nvim/features/copyPaste.ts
================================================
import { clipboard, MenuItemConstructorOptions } from 'electron';
import { getNvimByWindow } from 'src/main/nvim/nvimByWindow';
export const pasteMenuItem: MenuItemConstructorOptions['click'] = async (_item, win) => {
const nvim = getNvimByWindow(win);
if (nvim) {
const clipboardText = clipboard.readText();
nvim.paste(clipboardText, true, -1);
}
};
export const copyMenuItem: MenuItemConstructorOptions['click'] = async (_item, win) => {
const nvim = getNvimByWindow(win);
if (nvim) {
const mode = await nvim.getShortMode();
if (mode === 'v' || mode === 'V') {
nvim.input('"*y');
}
}
};
export const selectAllMenuItem: MenuItemConstructorOptions['click'] = (_item, win) => {
const nvim = getNvimByWindow(win);
if (nvim) {
nvim.input('ggVG');
}
};
================================================
FILE: packages/electron/src/main/nvim/features/focusAutocmd.ts
================================================
import { BrowserWindow } from 'electron';
import type Nvim from '@vvim/nvim';
/**
* Emit FocusGained or FocusLost autocmd when app window get or loose focus.
* https://neovim.io/doc/user/autocmd.html#FocusGained
*/
const focusAutocmd = ({ win, nvim }: { win: BrowserWindow; nvim: Nvim }): void => {
win.on('focus', () => {
nvim.command('doautocmd FocusGained');
});
win.on('blur', () => {
nvim.command('doautocmd
gitextract_afbqh6f5/ ├── .eslintrc.js ├── .github/ │ └── workflows/ │ ├── link_typecheck.yml │ └── tests.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .nvmrc ├── .prettierrc.js ├── LICENSE ├── README.md ├── babel.config.json ├── codecov.yml ├── jest.config.js ├── package.json ├── packages/ │ ├── browser-renderer/ │ │ ├── README.md │ │ ├── babel.config.json │ │ ├── config/ │ │ │ ├── jest/ │ │ │ │ ├── afterEnv.js │ │ │ │ ├── globalSetup.js │ │ │ │ ├── globalTeardown.js │ │ │ │ └── testServer.js │ │ │ ├── webpack.config.js │ │ │ └── webpack.prod.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── renderer.test.ts │ │ │ │ └── screen.test.ts │ │ │ ├── features/ │ │ │ │ └── hideMouseCursor.ts │ │ │ ├── index.ts │ │ │ ├── input/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── keyboard.test.ts │ │ │ │ ├── keyboard.ts │ │ │ │ └── mouse.ts │ │ │ ├── lib/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── getColor.test.ts │ │ │ │ ├── getColor.ts │ │ │ │ └── isWeb.ts │ │ │ ├── preloaded/ │ │ │ │ └── electron.ts │ │ │ ├── renderer.ts │ │ │ ├── screen.ts │ │ │ ├── transport/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── ipc.test.ts │ │ │ │ │ └── websocket.test.ts │ │ │ │ ├── ipc.ts │ │ │ │ ├── transport.ts │ │ │ │ └── websocket.ts │ │ │ └── types.ts │ │ ├── tsconfig.declaration.json │ │ └── tsconfig.json │ ├── electron/ │ │ ├── @types/ │ │ │ └── html2plaintext.d.ts │ │ ├── README.md │ │ ├── assets/ │ │ │ ├── generic.icns │ │ │ └── icon.icns │ │ ├── babel.config.json │ │ ├── bin/ │ │ │ ├── openInProject.vim │ │ │ ├── reloadChanged.vim │ │ │ ├── vv │ │ │ ├── vv.vim │ │ │ └── vvset.vim │ │ ├── config/ │ │ │ ├── electron-builder/ │ │ │ │ ├── build.js │ │ │ │ ├── fileAssociations.json │ │ │ │ └── release.js │ │ │ ├── webpack.common.config.js │ │ │ ├── webpack.config.js │ │ │ ├── webpack.main.config.js │ │ │ ├── webpack.prod.config.js │ │ │ └── webpack.renderer.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── scripts/ │ │ │ └── filetypes.js │ │ ├── src/ │ │ │ ├── lib/ │ │ │ │ ├── isDev.ts │ │ │ │ └── log.ts │ │ │ ├── main/ │ │ │ │ ├── autoUpdate.ts │ │ │ │ ├── checkNeovim.ts │ │ │ │ ├── index.ts │ │ │ │ ├── installCli.ts │ │ │ │ ├── lib/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── args.test.ts │ │ │ │ │ ├── args.ts │ │ │ │ │ ├── store.ts │ │ │ │ │ └── which.ts │ │ │ │ ├── menu.ts │ │ │ │ ├── nvim/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── nvim.test.ts │ │ │ │ │ ├── features/ │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ ├── backrdoundColor.test.ts │ │ │ │ │ │ │ └── windowSize.test.ts │ │ │ │ │ │ ├── backrdoundColor.ts │ │ │ │ │ │ ├── closeWindow.ts │ │ │ │ │ │ ├── copyPaste.ts │ │ │ │ │ │ ├── focusAutocmd.ts │ │ │ │ │ │ ├── quit.ts │ │ │ │ │ │ ├── reloadChanged.ts │ │ │ │ │ │ ├── windowSize.ts │ │ │ │ │ │ ├── windowTitle.ts │ │ │ │ │ │ └── zoom.ts │ │ │ │ │ ├── nvim.ts │ │ │ │ │ ├── nvimByWindow.ts │ │ │ │ │ └── settings.ts │ │ │ │ ├── preload.js │ │ │ │ └── transport/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── ipc.test.ts │ │ │ │ └── ipc.ts │ │ │ └── renderer/ │ │ │ ├── index.html │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── nvim/ │ │ ├── README.md │ │ ├── babel.config.json │ │ ├── config/ │ │ │ ├── webpack.config.js │ │ │ └── webpack.prod.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── Nvim.ts │ │ │ ├── ProcNvimTransport.ts │ │ │ ├── __generated__/ │ │ │ │ ├── constants.ts │ │ │ │ └── types.ts │ │ │ ├── __tests__/ │ │ │ │ ├── Nvim.test.ts │ │ │ │ ├── ProcNvimTransport.test.ts │ │ │ │ ├── process.test.ts │ │ │ │ └── utils.test.ts │ │ │ ├── browser.ts │ │ │ ├── index.ts │ │ │ ├── process.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.declaration.json │ │ └── tsconfig.json │ └── server/ │ ├── README.md │ ├── babel.config.json │ ├── bin/ │ │ ├── vv.vim │ │ └── vvset.vim │ ├── config/ │ │ ├── webpack.common.config.js │ │ ├── webpack.config.js │ │ ├── webpack.prod.config.js │ │ ├── webpack.renderer.config.js │ │ └── webpack.server.config.js │ ├── jest.config.js │ ├── package.json │ ├── src/ │ │ ├── lib/ │ │ │ └── isDev.ts │ │ ├── renderer/ │ │ │ ├── index.html │ │ │ └── index.ts │ │ └── server/ │ │ ├── index.ts │ │ ├── nvim/ │ │ │ ├── nvim.ts │ │ │ └── settings.ts │ │ └── transport/ │ │ └── websocket.ts │ └── tsconfig.json ├── scripts/ │ └── codegen.ts └── tsconfig.json
SYMBOL INDEX (98 symbols across 27 files)
FILE: packages/browser-renderer/config/jest/testServer.js
constant PORT (line 4) | const PORT = 3001;
function setupTestServer (line 6) | async function setupTestServer() {
function teardownTestServer (line 16) | async function teardownTestServer(server) {
FILE: packages/browser-renderer/src/features/hideMouseCursor.ts
function showCursor (line 4) | function showCursor() {
function hideCursor (line 10) | function hideCursor(): void {
FILE: packages/browser-renderer/src/input/mouse.ts
constant GRID (line 7) | const GRID = 0;
constant SCROLL_STEP_X (line 9) | const SCROLL_STEP_X = 6;
constant SCROLL_STEP_Y (line 10) | const SCROLL_STEP_Y = 3;
constant MOUSE_BUTTON (line 11) | const MOUSE_BUTTON = {
constant ACTION (line 18) | const ACTION = {
type Action (line 28) | type Action = typeof ACTION[keyof typeof ACTION];
FILE: packages/browser-renderer/src/preloaded/electron.ts
type PreloadedIpcRenderer (line 1) | interface PreloadedIpcRenderer {
type Window (line 30) | interface Window {
FILE: packages/browser-renderer/src/screen.ts
type Screen (line 17) | type Screen = {
type CalculatedProps (line 22) | type CalculatedProps = {
type HighlightProps (line 33) | type HighlightProps = {
type HighlightTable (line 38) | type HighlightTable = Record<number, HighlightProps>;
type Char (line 40) | type Char = {
constant DEFAULT_FONT_FAMILY (line 46) | const DEFAULT_FONT_FAMILY = 'Courier New';
constant DEFAULT_FG_COLOR (line 48) | const DEFAULT_FG_COLOR = 'rgb(255,255,255)';
constant DEFAULT_BG_COLOR (line 49) | const DEFAULT_BG_COLOR = 'rgb(0,0,0)';
constant DEFAULT_SP_COLOR (line 50) | const DEFAULT_SP_COLOR = 'rgb(255,255,255)';
constant DEFAULT_FONT_SIZE (line 51) | const DEFAULT_FONT_SIZE = 12;
constant DEFAULT_LINE_HEIGHT (line 52) | const DEFAULT_LINE_HEIGHT = 1.25;
constant DEFAULT_LETTER_SPACING (line 53) | const DEFAULT_LETTER_SPACING = 0;
FILE: packages/browser-renderer/src/transport/__tests__/websocket.test.ts
class MockWebSocket (line 11) | class MockWebSocket {
method constructor (line 12) | constructor(...args: any[]) {
method send (line 17) | send(...args: any[]) {
method onmessage (line 22) | set onmessage(value: (x: { data: string }) => void) {
FILE: packages/browser-renderer/src/transport/ipc.ts
class IpcRendererTransport (line 12) | class IpcRendererTransport extends EventEmitter implements Transport {
method constructor (line 15) | constructor(ipc = ipcRenderer) {
method send (line 43) | send(channel: string, ...params: Args): void {
FILE: packages/browser-renderer/src/transport/websocket.ts
class WebSocketTransport (line 7) | class WebSocketTransport extends EventEmitter implements Transport {
method constructor (line 10) | constructor() {
method send (line 21) | send(channel: string, ...args: Args): void {
FILE: packages/browser-renderer/src/types.ts
type BooleanSetting (line 1) | type BooleanSetting = 0 | 1;
type Settings (line 3) | type Settings = {
FILE: packages/electron/scripts/filetypes.js
constant SOURCE_YAML (line 10) | const SOURCE_YAML =
constant SAVE_TO (line 13) | const SAVE_TO = path.join(__dirname, '../config/electron-builder/fileAss...
FILE: packages/electron/src/lib/isDev.ts
type IsDevFunction (line 1) | type IsDevFunction = {
FILE: packages/electron/src/main/autoUpdate.ts
constant LAST_CHECKED (line 12) | const LAST_CHECKED = 'autoUpdate.lastCheckedForUpdate';
constant MINUTE (line 14) | const MINUTE = 60 * 1000;
FILE: packages/electron/src/main/checkNeovim.ts
constant REQUIRED_VERSION (line 6) | const REQUIRED_VERSION = '0.4.0';
FILE: packages/electron/src/main/lib/args.ts
constant ARGS_WITH_PARAM (line 5) | const ARGS_WITH_PARAM = [
constant VV_ARGS (line 21) | const VV_ARGS = ['--debug', '--inspect', '--open-in-project'];
constant CHROMIUM_ARGS (line 27) | const CHROMIUM_ARGS = [
FILE: packages/electron/src/main/lib/store.ts
type BooleanSetting (line 3) | type BooleanSetting = 0 | 1;
type Settings (line 5) | type Settings = {
type StoreData (line 23) | type StoreData = {
FILE: packages/electron/src/main/nvim/features/reloadChanged.ts
type Buffer (line 19) | type Buffer = {
FILE: packages/electron/src/main/nvim/settings.ts
type SettingsCallback (line 8) | type SettingsCallback = (newSettings: Partial<Settings>, allSettings: Se...
FILE: packages/electron/src/main/transport/ipc.ts
class IpcTransport (line 10) | class IpcTransport extends EventEmitter implements Transport {
method constructor (line 17) | constructor(win: Electron.BrowserWindow, ipc = ipcMain) {
method send (line 61) | send(channel: string, ...args: any[]): void {
FILE: packages/nvim/src/Nvim.ts
class Nvim (line 12) | class Nvim extends NvimEventEmitter {
method constructor (line 24) | constructor(transport: Transport, isRenderer = false) {
method request (line 73) | request<R = void>(command: string, params: any[] = []): Promise<R> {
method handleResponse (line 87) | private handleResponse(id: number, error: Error, result?: any): void {
FILE: packages/nvim/src/ProcNvimTransport.ts
class ProcNvimTransport (line 12) | class ProcNvimTransport extends EventEmitter implements Transport {
method constructor (line 17) | constructor(proc: ChildProcessWithoutNullStreams, remoteTransport?: Tr...
method attachRemoteTransport (line 33) | attachRemoteTransport(remoteTransport: Transport): void {
method write (line 39) | private write(id: number, command: string, params: string[]): void {
method send (line 45) | send(channel: string, id: number, command: string, params: string[]): ...
FILE: packages/nvim/src/__generated__/types.ts
type UiEvents (line 15) | type UiEvents = {
type NvimCommands (line 194) | type NvimCommands = {
FILE: packages/nvim/src/types.ts
type RequestMessage (line 14) | type RequestMessage = [0, number, string, any[]];
type ResponseMessage (line 15) | type ResponseMessage = [1, number, any, any];
type NotificationMessage (line 16) | type NotificationMessage = [2, string, any[]];
type MessageType (line 18) | type MessageType = RequestMessage | ResponseMessage | NotificationMessage;
type ReadCallback (line 19) | type ReadCallback = (message: MessageType) => void;
type OnCloseCallback (line 20) | type OnCloseCallback = () => void;
type Args (line 23) | type Args = any[];
type Listener (line 25) | type Listener = (...args: Args) => void;
type Transport (line 31) | type Transport = EventEmitter & {
type ModeInfo (line 41) | type ModeInfo = {
type OptionSet (line 55) | type OptionSet = [
type HighlightAttrs (line 80) | type HighlightAttrs = {
type Cell (line 94) | type Cell = [text: string, hl_id?: number, repeat?: number];
type UiEventsPatch (line 96) | type UiEventsPatch = {
type UiEvents (line 103) | type UiEvents = Omit<UiEventsOriginal, keyof UiEventsPatch> & UiEventsPa...
type UiEventsHandlers (line 105) | type UiEventsHandlers = {
type UiEventsArgsByKey (line 109) | type UiEventsArgsByKey = {
type UiEventsArgs (line 113) | type UiEventsArgs = Array<UiEventsArgsByKey[keyof UiEventsArgsByKey]>;
type NvimEvents (line 115) | interface NvimEvents {
type NvimCommandsPatch (line 123) | type NvimCommandsPatch = {
type NvimCommands (line 127) | type NvimCommands = Omit<NvimCommandsOriginal, keyof NvimCommandsPatch> ...
type NvimCommandsMethods (line 129) | type NvimCommandsMethods = {
type NvimInterface (line 136) | type NvimInterface = TypedEventEmitter<EventEmitter, NvimEvents> & NvimC...
FILE: packages/nvim/src/utils.ts
type IsDevFunction (line 3) | type IsDevFunction = {
FILE: packages/server/src/lib/isDev.ts
type IsDevFunction (line 1) | type IsDevFunction = {
FILE: packages/server/src/server/nvim/settings.ts
type BooleanSetting (line 5) | type BooleanSetting = 0 | 1;
type Settings (line 7) | type Settings = {
type SettingsCallback (line 25) | type SettingsCallback = (newSettings: Partial<Settings>, allSettings: Se...
FILE: packages/server/src/server/transport/websocket.ts
class WsTransport (line 8) | class WsTransport extends EventEmitter implements Transport {
method constructor (line 11) | constructor(ws: WebSocket) {
method send (line 26) | send(channel: string, ...args: Args) {
FILE: scripts/codegen.ts
constant TYPES_FILE_NAME (line 9) | const TYPES_FILE_NAME = 'packages/nvim/src/__generated__/types.ts';
constant CONST_FILE_NAME (line 10) | const CONST_FILE_NAME = 'packages/nvim/src/__generated__/constants.ts';
Condensed preview — 140 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (307K chars).
[
{
"path": ".eslintrc.js",
"chars": 3207,
"preview": "module.exports = {\n parser: '@typescript-eslint/parser',\n plugins: ['jest'],\n extends: [\n 'airbnb-base',\n // Te"
},
{
"path": ".github/workflows/link_typecheck.yml",
"chars": 1190,
"preview": "name: Lint & Typecheck\n\non:\n pull_request:\n\njobs:\n build:\n runs-on: macos-latest\n\n steps:\n - name: Checkout"
},
{
"path": ".github/workflows/tests.yml",
"chars": 1551,
"preview": "name: Tests\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n build:\n runs-on: macos-latest\n\n steps"
},
{
"path": ".gitignore",
"chars": 203,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n**/node_modules/**\n\n**/build/**\n**/dist/**\n*"
},
{
"path": ".husky/.gitignore",
"chars": 2,
"preview": "_\n"
},
{
"path": ".husky/pre-commit",
"chars": 59,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nyarn lint-staged\n"
},
{
"path": ".nvmrc",
"chars": 7,
"preview": "20.9.0\n"
},
{
"path": ".prettierrc.js",
"chars": 86,
"preview": "module.exports = {\n trailingComma: 'all',\n printWidth: 100,\n singleQuote: true,\n};\n"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "Copyright (c) 2018-present Igor Gladkoborodov\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 6442,
"preview": "# VV\n\nVV is a Neovim client for macOS. A pure, fast, minimalistic Vim experience with good macOS integration. Optimized "
},
{
"path": "babel.config.json",
"chars": 380,
"preview": "{\n \"presets\": [[\"@babel/preset-env\", { \"modules\": \"commonjs\" }], \"@babel/preset-typescript\"],\n \"plugins\": [\n \"@babe"
},
{
"path": "codecov.yml",
"chars": 151,
"preview": "comment:\n layout: \"reach, diff, flags, files\"\n require_changes: false\n\nignore:\n - \"packages/browser-renderer/src/scre"
},
{
"path": "jest.config.js",
"chars": 152,
"preview": "module.exports = {\n clearMocks: true,\n testEnvironment: 'node',\n collectCoverageFrom: ['src/**/*.{ts,js}'],\n project"
},
{
"path": "package.json",
"chars": 2615,
"preview": "{\n \"name\": \"vv\",\n \"description\": \"Neovim GUI Client\",\n \"author\": \"Igor Gladkoborodov <igor.gladkoborodov@gmail.com>\","
},
{
"path": "packages/browser-renderer/README.md",
"chars": 249,
"preview": "# @vvim/browser-renderer\n\nThis package is used to render Neovim in browser for [VV Electron App](../electron) and [VV se"
},
{
"path": "packages/browser-renderer/babel.config.json",
"chars": 225,
"preview": "{\n \"extends\": \"../../babel.config.json\",\n \"plugins\": [\n [\n \"module-resolver\",\n {\n \"root\": [\".\"],\n "
},
{
"path": "packages/browser-renderer/config/jest/afterEnv.js",
"chars": 107,
"preview": "const { toMatchImageSnapshot } = require('jest-image-snapshot');\n\nexpect.extend({ toMatchImageSnapshot });\n"
},
{
"path": "packages/browser-renderer/config/jest/globalSetup.js",
"chars": 229,
"preview": "/* eslint-disable no-underscore-dangle, no-undef */\n\nimport { setupTestServer } from './testServer';\n\nconst globalSetup "
},
{
"path": "packages/browser-renderer/config/jest/globalTeardown.js",
"chars": 238,
"preview": "/* eslint-disable no-underscore-dangle, no-undef */\n\nimport { teardownTestServer } from './testServer';\n\nconst globalTea"
},
{
"path": "packages/browser-renderer/config/jest/testServer.js",
"chars": 434,
"preview": "import { setup, teardown } from 'jest-dev-server';\n\n// TODO: make it configurable\nexport const PORT = 3001;\n\nexport asyn"
},
{
"path": "packages/browser-renderer/config/webpack.config.js",
"chars": 524,
"preview": "const path = require('path');\n\nconst buildPath = path.resolve(__dirname, './../dist');\n\nconst config = {\n mode: 'develo"
},
{
"path": "packages/browser-renderer/config/webpack.prod.config.js",
"chars": 254,
"preview": "const { merge } = require('webpack-merge');\n\nconst webpackConfig = require('./webpack.config');\n\nconst prod = {\n mode: "
},
{
"path": "packages/browser-renderer/jest.config.js",
"chars": 416,
"preview": "module.exports = {\n testEnvironment: 'jsdom',\n clearMocks: true,\n moduleNameMapper: {\n '\\\\./src/(.*)': ['<rootDir>"
},
{
"path": "packages/browser-renderer/package.json",
"chars": 1603,
"preview": "{\n \"name\": \"@vvim/browser-renderer\",\n \"version\": \"0.0.1\",\n \"description\": \"VV Browser Renderer\",\n \"author\": \"Igor Gl"
},
{
"path": "packages/browser-renderer/src/__tests__/renderer.test.ts",
"chars": 1843,
"preview": "import { EventEmitter } from 'events';\nimport initRenderer from 'src/renderer';\n\nimport Nvim from '@vvim/nvim';\nimport i"
},
{
"path": "packages/browser-renderer/src/__tests__/screen.test.ts",
"chars": 2934,
"preview": "/** @jest-environment node */\n\nimport puppeteer from 'puppeteer';\nimport { PORT } from 'config/jest/testServer';\n\nimport"
},
{
"path": "packages/browser-renderer/src/features/hideMouseCursor.ts",
"chars": 516,
"preview": "/**\n * Hides mouse cursor when you start typing. Shows it again when you move mouse.\n */\nfunction showCursor() {\n docum"
},
{
"path": "packages/browser-renderer/src/index.ts",
"chars": 179,
"preview": "// Only use relative imports here because https://github.com/microsoft/TypeScript/issues/32999#issuecomment-523558695\nim"
},
{
"path": "packages/browser-renderer/src/input/__tests__/keyboard.test.ts",
"chars": 6719,
"preview": "import initKeyboard from 'src/input/keyboard';\n\nimport Nvim from '@vvim/nvim';\nimport { Screen } from 'src/screen';\n\ndes"
},
{
"path": "packages/browser-renderer/src/input/keyboard.ts",
"chars": 6067,
"preview": "import type { Nvim } from '@vvim/nvim';\nimport { Screen } from 'src/screen';\n\n// :help keyCode\nconst specialKey = ({ key"
},
{
"path": "packages/browser-renderer/src/input/mouse.ts",
"chars": 3267,
"preview": "import throttle from 'lodash/throttle';\n\nimport { modifierPrefix } from 'src/input/keyboard';\nimport { Screen } from 'sr"
},
{
"path": "packages/browser-renderer/src/lib/__tests__/getColor.test.ts",
"chars": 1026,
"preview": "import { getColor, getColorNum } from 'src/lib/getColor';\n\ndescribe('getColor', () => {\n test('0 is black', () => {\n "
},
{
"path": "packages/browser-renderer/src/lib/getColor.ts",
"chars": 911,
"preview": "/* eslint-disable no-bitwise */\n\nimport memoize from 'lodash/memoize';\n\n/**\n * Get color by number, for example hex numb"
},
{
"path": "packages/browser-renderer/src/lib/isWeb.ts",
"chars": 133,
"preview": "const isWeb = (): boolean =>\n window.location.protocol === 'http:' || window.location.protocol === 'https:';\n\nexport de"
},
{
"path": "packages/browser-renderer/src/preloaded/electron.ts",
"chars": 1338,
"preview": "export interface PreloadedIpcRenderer {\n /**\n * Listens to `channel`, when a new message arrives `listener` would be "
},
{
"path": "packages/browser-renderer/src/renderer.ts",
"chars": 731,
"preview": "import Nvim from '@vvim/nvim';\n\nimport { Settings } from 'src/types';\n\nimport Transport from 'src/transport/transport';\n"
},
{
"path": "packages/browser-renderer/src/screen.ts",
"chars": 27951,
"preview": "import isEqual from 'lodash/isEqual';\n\nimport emojiRegex from 'emoji-regex';\n\nimport { getColor } from 'src/lib/getColor"
},
{
"path": "packages/browser-renderer/src/transport/__tests__/ipc.test.ts",
"chars": 3679,
"preview": "import { EventEmitter } from 'events';\nimport { ipcRenderer } from 'src/preloaded/electron';\nimport type { PreloadedIpcR"
},
{
"path": "packages/browser-renderer/src/transport/__tests__/websocket.test.ts",
"chars": 1995,
"preview": "import WebSocketTransport from 'src/transport/websocket';\n\ndescribe('websocket transport', () => {\n const OriginalWebSo"
},
{
"path": "packages/browser-renderer/src/transport/ipc.ts",
"chars": 1289,
"preview": "import { EventEmitter } from 'events';\nimport memoize from 'lodash/memoize';\n\nimport { ipcRenderer } from 'src/preloaded"
},
{
"path": "packages/browser-renderer/src/transport/transport.ts",
"chars": 246,
"preview": "import IpcRendererTransport from 'src/transport/ipc';\nimport WebSocketTransport from 'src/transport/websocket';\nimport i"
},
{
"path": "packages/browser-renderer/src/transport/websocket.ts",
"chars": 630,
"preview": "import { EventEmitter } from 'events';\nimport type { Transport, Args } from '@vvim/nvim';\n\n/**\n * Init transport between"
},
{
"path": "packages/browser-renderer/src/types.ts",
"chars": 555,
"preview": "type BooleanSetting = 0 | 1;\n\nexport type Settings = {\n fullscreen: BooleanSetting;\n simplefullscreen: BooleanSetting;"
},
{
"path": "packages/browser-renderer/tsconfig.declaration.json",
"chars": 207,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"emitDeclarationOnly\": true,\n \"declarationMap\": true,\n "
},
{
"path": "packages/browser-renderer/tsconfig.json",
"chars": 113,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"baseUrl\": \".\"\n },\n \"include\": [\"src\", \"@types\"]\n}\n"
},
{
"path": "packages/electron/@types/html2plaintext.d.ts",
"chars": 116,
"preview": "declare module 'html2plaintext' {\n const html2plaintext: (x: string) => string;\n export default html2plaintext;\n}\n"
},
{
"path": "packages/electron/README.md",
"chars": 229,
"preview": "# VV\n\nVV is a Neovim client for macOS. A pure, fast, minimalistic Vim experience with good macOS integration. Optimized "
},
{
"path": "packages/electron/babel.config.json",
"chars": 43,
"preview": "{\n \"extends\": \"../../babel.config.json\"\n}\n"
},
{
"path": "packages/electron/bin/openInProject.vim",
"chars": 1455,
"preview": "\" Opens file respecting switchbuf setting.\nfunction! VVopenInProject(filename, ...)\n \" Take switch override from second"
},
{
"path": "packages/electron/bin/reloadChanged.vim",
"chars": 658,
"preview": "\" TODO: Remove on the next major version\n\" Iterate on buffers and reload them from disk. No questions asked.\n\" Do it in "
},
{
"path": "packages/electron/bin/vv",
"chars": 773,
"preview": "#!/bin/sh\n\nif [ \"$1\" == \"--help\" ] || [ \"$1\" == \"-h\" ]; then\n cat << END \nVV - NeoVim GUI Client\n\nUsage:\n vv [options]"
},
{
"path": "packages/electron/bin/vv.vim",
"chars": 562,
"preview": "let g:vv = 1\n\nlet s:dir = expand('<sfile>:p:h')\n\nexecute 'source ' . fnameescape(s:dir . '/vvset.vim')\nexecute 'source '"
},
{
"path": "packages/electron/bin/vvset.vim",
"chars": 3672,
"preview": "let g:vv_settings_synonims = {\n\\ 'fu': 'fullscreen',\n\\ 'sfu': 'simplefullscreen',\n\\ 'width': 'windowwidth',\n\\ 'heigh"
},
{
"path": "packages/electron/config/electron-builder/build.js",
"chars": 933,
"preview": "require('dotenv').config();\n\nconst { join } = require('path');\nconst { readFileSync } = require('fs');\n\nconst fileAssoci"
},
{
"path": "packages/electron/config/electron-builder/fileAssociations.json",
"chars": 61149,
"preview": "[\n {\n \"name\": \"1C Enterprise\",\n \"role\": \"Editor\",\n \"icon\": \"generic.icns\",\n \"ext\": [\"bsl\", \"os\"]\n },\n {\n "
},
{
"path": "packages/electron/config/electron-builder/release.js",
"chars": 556,
"preview": "// Notarize needs APP_ID, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, TEAM_ID env variables.\n// Github repo to release is aut"
},
{
"path": "packages/electron/config/webpack.common.config.js",
"chars": 369,
"preview": "const path = require('path');\n\nconst buildPath = path.resolve(__dirname, './../build');\n\nmodule.exports = {\n mode: 'dev"
},
{
"path": "packages/electron/config/webpack.config.js",
"chars": 162,
"preview": "const rendererConfig = require('./webpack.renderer.config');\nconst mainConfig = require('./webpack.main.config');\n\nmodul"
},
{
"path": "packages/electron/config/webpack.main.config.js",
"chars": 318,
"preview": "const { merge } = require('webpack-merge');\nconst common = require('./webpack.common.config');\n\nconst config = merge(com"
},
{
"path": "packages/electron/config/webpack.prod.config.js",
"chars": 361,
"preview": "const { merge } = require('webpack-merge');\n\nconst rendererConfig = require('./webpack.renderer.config');\nconst mainConf"
},
{
"path": "packages/electron/config/webpack.renderer.config.js",
"chars": 449,
"preview": "const { merge } = require('webpack-merge');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst common = req"
},
{
"path": "packages/electron/jest.config.js",
"chars": 107,
"preview": "module.exports = {\n clearMocks: true,\n moduleNameMapper: {\n 'src/(.*)': ['<rootDir>/src/$1'],\n },\n};\n"
},
{
"path": "packages/electron/package.json",
"chars": 2077,
"preview": "{\n \"name\": \"@vvim/electron\",\n \"description\": \"Neovim GUI Client\",\n \"author\": \"Igor Gladkoborodov <igor.gladkoborodov@"
},
{
"path": "packages/electron/scripts/filetypes.js",
"chars": 999,
"preview": "// Generate fileAssociations for electron-builder.\n// File types are generated from [Github Linguist](https://github.com"
},
{
"path": "packages/electron/src/lib/isDev.ts",
"chars": 219,
"preview": "type IsDevFunction = {\n <T, F>(dev: T, notDev: F): T | F;\n (): boolean;\n};\n\nconst isDev: IsDevFunction = (dev = true, "
},
{
"path": "packages/electron/src/lib/log.ts",
"chars": 290,
"preview": "const initNow = Date.now();\nlet lastNow = initNow;\nconst log = (...text: string[]): void => {\n // eslint-disable-next-l"
},
{
"path": "packages/electron/src/main/autoUpdate.ts",
"chars": 2357,
"preview": "import { dialog, BrowserWindow } from 'electron';\nimport { autoUpdater } from 'electron-updater';\nimport html2plaintext "
},
{
"path": "packages/electron/src/main/checkNeovim.ts",
"chars": 1427,
"preview": "import { app, dialog, shell } from 'electron';\nimport semver from 'semver';\n\nimport { nvimVersion } from '@vvim/nvim';\n\n"
},
{
"path": "packages/electron/src/main/index.ts",
"chars": 7094,
"preview": "import { app, BrowserWindow, dialog } from 'electron';\nimport { statSync, existsSync } from 'fs';\nimport { join, resolve"
},
{
"path": "packages/electron/src/main/installCli.ts",
"chars": 1457,
"preview": "import { dialog } from 'electron';\nimport { execSync } from 'child_process';\n\nimport which from 'src/main/lib/which';\n\nc"
},
{
"path": "packages/electron/src/main/lib/__tests__/args.test.ts",
"chars": 4268,
"preview": "import { parseArgs, joinArgs, filterArgs, argValue } from 'src/main/lib/args';\n\ndescribe('parseArgs', () => {\n test('re"
},
{
"path": "packages/electron/src/main/lib/args.ts",
"chars": 2565,
"preview": "// TODO: Use commander or yargs\n\nimport isDev from 'src/lib/isDev';\n\nconst ARGS_WITH_PARAM = [\n '--cmd',\n '-c',\n '-i'"
},
{
"path": "packages/electron/src/main/lib/store.ts",
"chars": 803,
"preview": "import Store from 'electron-store';\n\ntype BooleanSetting = 0 | 1;\n\nexport type Settings = {\n fullscreen: BooleanSetting"
},
{
"path": "packages/electron/src/main/lib/which.ts",
"chars": 412,
"preview": "import { execSync } from 'child_process';\n\nimport { shellEnv } from '@vvim/nvim';\n\n/**\n * Checks if command exists in sh"
},
{
"path": "packages/electron/src/main/menu.ts",
"chars": 3243,
"preview": "import { Menu, MenuItemConstructorOptions } from 'electron';\n\n// import { handleCloseWindow } from 'src/main/nvim/featur"
},
{
"path": "packages/electron/src/main/nvim/__tests__/nvim.test.ts",
"chars": 126,
"preview": "// eslint-disable-next-line\nimport initNvim from 'src/main/nvim/nvim';\n\ndescribe('initNvim', () => {\n test.todo('TODO')"
},
{
"path": "packages/electron/src/main/nvim/features/__tests__/backrdoundColor.test.ts",
"chars": 818,
"preview": "import backgroundColor from 'src/main/nvim/features/backrdoundColor';\n\nimport type { Transport } from '@vvim/nvim';\nimpo"
},
{
"path": "packages/electron/src/main/nvim/features/__tests__/windowSize.test.ts",
"chars": 1959,
"preview": "import initWindowSize from 'src/main/nvim/features/windowSize';\n\nimport { EventEmitter } from 'events';\nimport type { Tr"
},
{
"path": "packages/electron/src/main/nvim/features/backrdoundColor.ts",
"chars": 421,
"preview": "import type { BrowserWindow } from 'electron';\nimport type { Transport } from '@vvim/nvim';\n\n/**\n * Change Electron wind"
},
{
"path": "packages/electron/src/main/nvim/features/closeWindow.ts",
"chars": 492,
"preview": "import { MenuItemConstructorOptions } from 'electron';\n\nimport { getNvimByWindow } from 'src/main/nvim/nvimByWindow';\n\ne"
},
{
"path": "packages/electron/src/main/nvim/features/copyPaste.ts",
"chars": 801,
"preview": "import { clipboard, MenuItemConstructorOptions } from 'electron';\n\nimport { getNvimByWindow } from 'src/main/nvim/nvimBy"
},
{
"path": "packages/electron/src/main/nvim/features/focusAutocmd.ts",
"chars": 485,
"preview": "import { BrowserWindow } from 'electron';\n\nimport type Nvim from '@vvim/nvim';\n\n/**\n * Emit FocusGained or FocusLost aut"
},
{
"path": "packages/electron/src/main/nvim/features/quit.ts",
"chars": 2173,
"preview": "/**\n * Handle close window routine\n */\n\nimport { dialog, app, BrowserWindow } from 'electron';\nimport { deleteNvimByWind"
},
{
"path": "packages/electron/src/main/nvim/features/reloadChanged.ts",
"chars": 2784,
"preview": "import { dialog, BrowserWindow } from 'electron';\n\nimport type Nvim from '@vvim/nvim';\n\n/**\n * Show dialog when opened f"
},
{
"path": "packages/electron/src/main/nvim/features/windowSize.ts",
"chars": 4288,
"preview": "import { screen, MenuItemConstructorOptions, BrowserWindow } from 'electron';\nimport type { Transport } from '@vvim/nvim"
},
{
"path": "packages/electron/src/main/nvim/features/windowTitle.ts",
"chars": 952,
"preview": "import fs from 'fs';\nimport { BrowserWindow } from 'electron';\n\nimport type { Nvim } from '@vvim/nvim';\n\nconst initWindo"
},
{
"path": "packages/electron/src/main/nvim/features/zoom.ts",
"chars": 1664,
"preview": "import { app, MenuItemConstructorOptions, BrowserWindow } from 'electron';\nimport { getNvimByWindow } from 'src/main/nvi"
},
{
"path": "packages/electron/src/main/nvim/nvim.ts",
"chars": 1521,
"preview": "import { app } from 'electron';\n\nimport Nvim, { startNvimProcess, ProcNvimTransport, Transport } from '@vvim/nvim';\n\nimp"
},
{
"path": "packages/electron/src/main/nvim/nvimByWindow.ts",
"chars": 701,
"preview": "import type { BrowserWindow } from 'electron';\nimport type Nvim from '@vvim/nvim';\n\nconst nvimByWindowId: Record<number,"
},
{
"path": "packages/electron/src/main/nvim/settings.ts",
"chars": 3071,
"preview": "import debounce from 'lodash/debounce';\n\nimport type { BrowserWindow } from 'electron';\nimport type { Nvim, Transport } "
},
{
"path": "packages/electron/src/main/preload.js",
"chars": 379,
"preview": "const { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('electron', {\n ipcRenderer"
},
{
"path": "packages/electron/src/main/transport/__tests__/ipc.test.ts",
"chars": 2722,
"preview": "import { BrowserWindow, IpcMain } from 'electron';\nimport { EventEmitter } from 'events';\n\nimport IpcTransport from 'src"
},
{
"path": "packages/electron/src/main/transport/ipc.ts",
"chars": 1636,
"preview": "import { ipcMain } from 'electron';\nimport { EventEmitter } from 'events';\nimport memoize from 'lodash/memoize';\n\nimport"
},
{
"path": "packages/electron/src/renderer/index.html",
"chars": 389,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "packages/electron/src/renderer/index.ts",
"chars": 60,
"preview": "import renderer from '@vvim/browser-renderer';\n\nrenderer();\n"
},
{
"path": "packages/electron/tsconfig.json",
"chars": 113,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"baseUrl\": \".\"\n },\n \"include\": [\"src\", \"@types\"]\n}\n"
},
{
"path": "packages/nvim/README.md",
"chars": 99,
"preview": "# @vvim/nvim\n\nLightweight transport agnostic Neovim API client to be used in other @vvim packages.\n"
},
{
"path": "packages/nvim/babel.config.json",
"chars": 193,
"preview": "{\n \"extends\": \"../../babel.config.json\",\n \"plugins\": [\n [\n \"module-resolver\",\n {\n \"root\": [\".\"],\n "
},
{
"path": "packages/nvim/config/webpack.config.js",
"chars": 802,
"preview": "const path = require('path');\nconst { merge } = require('webpack-merge');\n\nconst buildPath = path.resolve(__dirname, './"
},
{
"path": "packages/nvim/config/webpack.prod.config.js",
"chars": 278,
"preview": "const { merge } = require('webpack-merge');\n\nconst webpackConfig = require('./webpack.config');\n\nconst prod = {\n mode: "
},
{
"path": "packages/nvim/jest.config.js",
"chars": 163,
"preview": "module.exports = {\n clearMocks: true,\n moduleNameMapper: {\n 'src/(.*)': ['<rootDir>/src/$1'],\n },\n testPathIgnore"
},
{
"path": "packages/nvim/package.json",
"chars": 1513,
"preview": "{\n \"name\": \"@vvim/nvim\",\n \"version\": \"0.0.1\",\n \"description\": \"Lightweight transport agnostic Neovim API client to be"
},
{
"path": "packages/nvim/src/Nvim.ts",
"chars": 3170,
"preview": "import { EventEmitter } from 'events';\n\nimport { nvimCommandNames } from 'src/__generated__/constants';\n\nimport type { T"
},
{
"path": "packages/nvim/src/ProcNvimTransport.ts",
"chars": 1732,
"preview": "import { createDecodeStream, encode } from 'msgpack-lite';\nimport { EventEmitter } from 'events';\n\nimport type { ChildPr"
},
{
"path": "packages/nvim/src/__generated__/constants.ts",
"chars": 5384,
"preview": "/* eslint-disable camelcase */\n/**\n * Constants generated by `yarn generate-types`. Do not edit manually.\n *\n * Version:"
},
{
"path": "packages/nvim/src/__generated__/types.ts",
"chars": 13249,
"preview": "/* eslint-disable camelcase */\n/**\n * Types generated by `yarn generate-types`. Do not edit manually.\n *\n * Version: 0.5"
},
{
"path": "packages/nvim/src/__tests__/Nvim.test.ts",
"chars": 5752,
"preview": "import { EventEmitter } from 'events';\nimport Nvim from 'src/nvim';\n\nimport type { Transport } from 'src/types';\n\ndescri"
},
{
"path": "packages/nvim/src/__tests__/ProcNvimTransport.test.ts",
"chars": 2694,
"preview": "import { EventEmitter } from 'events';\nimport { PassThrough } from 'stream';\nimport { encode } from 'msgpack-lite';\n\nimp"
},
{
"path": "packages/nvim/src/__tests__/process.test.ts",
"chars": 870,
"preview": "import { PassThrough } from 'stream';\nimport { spawn } from 'child_process';\nimport type { ChildProcessWithoutNullStream"
},
{
"path": "packages/nvim/src/__tests__/utils.test.ts",
"chars": 1591,
"preview": "import { execSync } from 'child_process';\n\nimport { shellEnv, nvimVersion, resetCache } from 'src/utils';\n\njest.mock('ch"
},
{
"path": "packages/nvim/src/browser.ts",
"chars": 194,
"preview": "// Only use relative imports here because https://github.com/microsoft/TypeScript/issues/32999#issuecomment-523558695\n\ni"
},
{
"path": "packages/nvim/src/index.ts",
"chars": 437,
"preview": "// Only use relative imports here because https://github.com/microsoft/TypeScript/issues/32999#issuecomment-523558695\n//"
},
{
"path": "packages/nvim/src/process.ts",
"chars": 1239,
"preview": "import { spawn } from 'child_process';\nimport path from 'path';\n\nimport debounce from 'lodash/debounce';\n\nimport { shell"
},
{
"path": "packages/nvim/src/types.ts",
"chars": 3856,
"preview": "/* eslint-disable camelcase */\n\nimport type { EventEmitter } from 'events';\nimport type TypedEventEmitter from 'strict-e"
},
{
"path": "packages/nvim/src/utils.ts",
"chars": 2431,
"preview": "import { execSync } from 'child_process';\n\ntype IsDevFunction = {\n <T, F>(dev: T, notDev: F): T | F;\n (): boolean;\n};\n"
},
{
"path": "packages/nvim/tsconfig.declaration.json",
"chars": 225,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"emitDeclarationOnly\": true,\n \"declarationMap\": true,\n "
},
{
"path": "packages/nvim/tsconfig.json",
"chars": 113,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"baseUrl\": \".\"\n },\n \"include\": [\"src\", \"@types\"]\n}\n"
},
{
"path": "packages/server/README.md",
"chars": 385,
"preview": "# VV Server\n\nRun Neovim remotely in browser via VV Server:\n\n```\nyarn start\n```\n\nThen open [http://localhost:3000](http:/"
},
{
"path": "packages/server/babel.config.json",
"chars": 43,
"preview": "{\n \"extends\": \"../../babel.config.json\"\n}\n"
},
{
"path": "packages/server/bin/vv.vim",
"chars": 376,
"preview": "let g:vv = 1\n\nsource <sfile>:h/vvset.vim\n\nset termguicolors\n\nautocmd VimEnter * call rpcnotify(get(g:, \"vv_channel\", 1),"
},
{
"path": "packages/server/bin/vvset.vim",
"chars": 3672,
"preview": "let g:vv_settings_synonims = {\n\\ 'fu': 'fullscreen',\n\\ 'sfu': 'simplefullscreen',\n\\ 'width': 'windowwidth',\n\\ 'heigh"
},
{
"path": "packages/server/config/webpack.common.config.js",
"chars": 369,
"preview": "const path = require('path');\n\nconst buildPath = path.resolve(__dirname, './../build');\n\nmodule.exports = {\n mode: 'dev"
},
{
"path": "packages/server/config/webpack.config.js",
"chars": 168,
"preview": "const rendererConfig = require('./webpack.renderer.config');\nconst serverConfig = require('./webpack.server.config');\n\nm"
},
{
"path": "packages/server/config/webpack.prod.config.js",
"chars": 392,
"preview": "const { merge } = require('webpack-merge');\n\nconst rendererConfig = require('./webpack.renderer.config');\nconst serverCo"
},
{
"path": "packages/server/config/webpack.renderer.config.js",
"chars": 449,
"preview": "const { merge } = require('webpack-merge');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst common = req"
},
{
"path": "packages/server/config/webpack.server.config.js",
"chars": 303,
"preview": "const { merge } = require('webpack-merge');\nconst common = require('./webpack.common.config');\n\nconst config = merge(com"
},
{
"path": "packages/server/jest.config.js",
"chars": 107,
"preview": "module.exports = {\n clearMocks: true,\n moduleNameMapper: {\n 'src/(.*)': ['<rootDir>/src/$1'],\n },\n};\n"
},
{
"path": "packages/server/package.json",
"chars": 1256,
"preview": "{\n \"name\": \"@vvim/server\",\n \"version\": \"0.0.1\",\n \"description\": \"VV Server: Run Neovim remotely in browser\",\n \"autho"
},
{
"path": "packages/server/src/lib/isDev.ts",
"chars": 219,
"preview": "type IsDevFunction = {\n <T, F>(dev: T, notDev: F): T | F;\n (): boolean;\n};\n\nconst isDev: IsDevFunction = (dev = true, "
},
{
"path": "packages/server/src/renderer/index.html",
"chars": 389,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "packages/server/src/renderer/index.ts",
"chars": 60,
"preview": "import renderer from '@vvim/browser-renderer';\n\nrenderer();\n"
},
{
"path": "packages/server/src/server/index.ts",
"chars": 775,
"preview": "import express from 'express';\nimport http from 'http';\n\nimport initNvim from 'src/server/nvim/nvim';\nimport { getDefaul"
},
{
"path": "packages/server/src/server/nvim/nvim.ts",
"chars": 829,
"preview": "// TODO\n// import quit from '@main/nvim/features/quit';\n// import windowTitle from '@main/nvim/features/windowTitle';\n//"
},
{
"path": "packages/server/src/server/nvim/settings.ts",
"chars": 2631,
"preview": "import debounce from 'lodash/debounce';\n\nimport type { Nvim, Transport } from '@vvim/nvim';\n\ntype BooleanSetting = 0 | 1"
},
{
"path": "packages/server/src/server/transport/websocket.ts",
"chars": 985,
"preview": "import WebSocket from 'ws';\nimport { Server } from 'http';\n\nimport { Transport, Args } from '@vvim/nvim';\n\nimport { Even"
},
{
"path": "packages/server/tsconfig.json",
"chars": 113,
"preview": "{\n \"extends\": \"../../tsconfig\",\n \"compilerOptions\": {\n \"baseUrl\": \".\"\n },\n \"include\": [\"src\", \"@types\"]\n}\n"
},
{
"path": "scripts/codegen.ts",
"chars": 4788,
"preview": "/* eslint-disable camelcase */\n\nimport { spawn } from 'child_process';\nimport { createDecodeStream, encode } from 'msgpa"
},
{
"path": "tsconfig.json",
"chars": 292,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"allowJs\": true,\n \"noEmit\": true,\n \"strict\": true,\n \"modul"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the vv-vim/vv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 140 files (268.5 KB), approximately 84.2k tokens, and a symbol index with 98 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.