├── .gitignore ├── .ignore ├── .travis.yml ├── LICENSE ├── README.md ├── TODO.md ├── appveyor.yml ├── art ├── header.png ├── header.psd ├── icon.icns ├── icon.ico ├── icon.png └── icon.psd ├── debug-notes.md ├── docs ├── cmdline.png ├── colors.png ├── completion.png ├── echo.png ├── explorer.png ├── files.png ├── grep.png ├── header.png ├── hint.png ├── hover.png ├── layout.png ├── logo.png ├── messages.png ├── nyan.png ├── preview.png ├── problems.png ├── readme.md ├── references.png ├── smart.png ├── symbols.png ├── symbols2.png ├── tasks.png └── user-menu.png ├── package-lock.json ├── package.json ├── runtime ├── colors │ └── veonim.vim ├── darwin │ └── vvim ├── linux │ └── vvim └── win32 │ └── vvim.exe ├── src ├── ai │ ├── breakpoints.ts │ ├── completion-transforms.ts │ ├── completions.ts │ ├── debug.ts │ ├── definition.ts │ ├── diagnostics.ts │ ├── get-debug-config.ts │ ├── highlights.ts │ ├── hover.ts │ ├── implementation.ts │ ├── references.ts │ ├── remote-problems.ts │ ├── rename.ts │ ├── signature-hint.ts │ ├── symbols.ts │ └── type-definition.ts ├── assets │ ├── header.png │ ├── nc.gif │ ├── roboto-mono.ttf │ └── roboto-sizes.json ├── bootstrap │ ├── galaxy.ts │ ├── index.html │ ├── main.ts │ ├── process-explorer.html │ └── process-explorer.ts ├── components │ ├── autocomplete.ts │ ├── buffer-search.ts │ ├── buffers.ts │ ├── change-project.ts │ ├── code-actions.ts │ ├── color-picker.ts │ ├── command-line.ts │ ├── debug.ts │ ├── divination.ts │ ├── explorer-embed.ts │ ├── explorer.ts │ ├── files.ts │ ├── filetype-icon.ts │ ├── generic-menu.ts │ ├── generic-prompt.ts │ ├── grep.ts │ ├── hint.ts │ ├── hover.ts │ ├── inventory.ts │ ├── loading.ts │ ├── lolglobal.ts │ ├── messages.ts │ ├── nc.ts │ ├── overlay.ts │ ├── plugin-container.ts │ ├── problem-info.ts │ ├── problems.ts │ ├── references.ts │ ├── row-container.ts │ ├── spell-check.ts │ ├── statusline.ts │ ├── symbols.ts │ ├── text-input.ts │ ├── user-menu.ts │ ├── user-overlay-menu.ts │ ├── viewport-search.ts │ ├── vim-create.ts │ ├── vim-rename.ts │ ├── vim-search.ts │ └── vim-switch.ts ├── config │ ├── config-reader.ts │ ├── config-service.ts │ └── default-configs.ts ├── core │ ├── ai.ts │ ├── canvas-container.ts │ ├── canvas-window.ts │ ├── cursor.ts │ ├── extensions.ts │ ├── font-atlas.ts │ ├── grid.ts │ ├── input.ts │ ├── inventory-layers.ts │ ├── master-control.ts │ ├── neovim.ts │ ├── render.ts │ ├── sessions.ts │ ├── shadow-buffers.ts │ ├── title.ts │ ├── vim-functions.ts │ ├── vim-startup.ts │ └── windows.ts ├── extensions │ ├── debuggers.ts │ └── extensions.ts ├── langserv │ ├── adapter.ts │ ├── capabilities.ts │ ├── director.ts │ ├── patch.ts │ ├── server-features.ts │ ├── update-server.ts │ └── vsc-languages.ts ├── messaging │ ├── debug-protocol.ts │ ├── dispatch.ts │ ├── rpc.ts │ ├── session-transport.ts │ ├── transport.ts │ ├── worker-client.ts │ └── worker.ts ├── neovim │ ├── api.ts │ ├── full-document-manager.ts │ ├── protocol.ts │ ├── state.ts │ ├── text-document-manager.ts │ └── types.ts ├── services │ ├── colorizer.ts │ ├── dev-recorder.ts │ ├── electron.ts │ ├── job-reader.ts │ ├── mru-buffers.ts │ ├── remote.ts │ └── watch-reload.ts ├── support │ ├── binaries.ts │ ├── colorize-with-highlight.ts │ ├── constants.ts │ ├── dependency-manager.ts │ ├── download.ts │ ├── fake-module.ts │ ├── fetch.ts │ ├── fs-watch.ts │ ├── get-file-contents.ts │ ├── git.ts │ ├── http-server.ts │ ├── language-ids.ts │ ├── local-storage.ts │ ├── localize.ts │ ├── manage-extensions.ts │ ├── manage-plugins.ts │ ├── markdown.ts │ ├── neovim-utils.ts │ ├── please-get.ts │ ├── relative-finder.ts │ ├── shell-env.ts │ ├── trace.ts │ ├── utils.ts │ └── vscode-shim.ts ├── ui │ ├── checkboard.ts │ ├── color-picker.ts │ ├── css.ts │ ├── hyperscript.ts │ ├── lose-focus.ts │ ├── notifications.ts │ ├── styles.ts │ ├── uikit.ts │ └── vanilla.ts ├── vscode │ ├── api.ts │ ├── commands.ts │ ├── debug.ts │ ├── env.ts │ ├── extensions.ts │ ├── languages.ts │ ├── neovim.ts │ ├── scm.ts │ ├── tasks.ts │ ├── window.ts │ └── workspace.ts └── workers │ ├── buffer-search.ts │ ├── download.ts │ ├── extension-host.ts │ ├── get-file-lines.ts │ ├── harvester.ts │ ├── neovim-client.ts │ ├── neovim-colorizer.ts │ ├── neovim-error-reader.ts │ ├── project-file-finder.ts │ ├── search-files.ts │ └── tsconfig.json ├── test ├── data │ ├── blarg.js │ ├── src │ │ ├── blarg.ts │ │ ├── blarg2.ts │ │ └── blarg3.ts │ └── tsconfig.json ├── e2e │ ├── features.test.js │ └── launcher.js ├── integration │ ├── neovim-colorizer.test.js │ ├── vscode-api-commands.test.js │ ├── vscode-api-debug.test.js │ ├── vscode-api-env.test.js │ ├── vscode-api-extensions.test.js │ ├── vscode-api-languages.test.js │ ├── vscode-api-scm.test.js │ ├── vscode-api-tasks.test.js │ ├── vscode-api-window.test.js │ └── vscode-api-workspace.test.js ├── nvim-for-test.js ├── snapshots │ ├── change-dir.png │ ├── explorer.png │ └── files.png ├── unit │ ├── colorize-with-highlight.test.js │ ├── config-service.test.js │ ├── manage-extensions.test.js │ ├── manage-plugins.test.js │ ├── relative-finder.test.js │ └── utils.test.js └── util.js ├── tools ├── build.js ├── devdocs │ └── range.md ├── dummy-exports.js ├── font-sizer │ ├── calc-sizer.js │ ├── index.js │ └── sizer.html ├── gen-api.js ├── package.js ├── postinstall.js ├── remove-debug.js ├── runner.js ├── setup-mirrors.js └── start.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | bindeps 2 | xdg_config 3 | coverage 4 | node_modules 5 | build 6 | logs 7 | bin 8 | dist 9 | results 10 | .vscode 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | src/icons 3 | src/memes 4 | docs 5 | art 6 | dist 7 | bindeps 8 | LICENSE 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | os: 6 | - osx 7 | - linux 8 | install: 9 | - npm install 10 | script: 11 | - npm run build 12 | - npm run test 13 | before_deploy: 14 | - npm run package 15 | deploy: 16 | provider: releases 17 | skip_cleanup: true 18 | api_key: 19 | secure: wcMBXxItD8wuR8jpiIHnMeyCToEgYWQQf5/MslJk2yuG259AQPJLWOkeHo/AuECv+HSRarOBc75j0MLki+cQXoo4lvTP0flqfm+KjnMDySOmFpHkstJz1qZrSTVQkLty3jWROunZizJmHK4W0Zdv6DzzXtkfaC2+QdaBtxNoe8KlAKkVwkMK3SKBRw1jIV0/nGVFSbKBwE0LEoeEoy7YEaENw/Iwh9L66n9Cprrp3MnDkMdHdKJDnU1O0w1k/rjrnUjdJUSWebabc6CtIo8effnt3fSJ7wKhmpIjt47F+PbMuq7O+kLngUkjWZGQRWuCPIGWcv1zT01GPE/adiNzYOoHt9EelU2iOwgIZPL+IxsThyzI/MzJWLzGlCKQYmeGo5lNfCrxUQtVkvWv/xjSxD1cCtyneR4Ds3REoeXp+Gb6uAdmdnylP77ffWhe7wwfoMdHaVra0uP8p3VVXQaFgWDKgBpi1yxCRllSVo0kMkOpREiaytV17cX6orEpEyec+6ouETbkymi+PqqxxK2kqX+HlBu+B+qLUoJuBay8M1WdLkgdsJ0KHkx/CO+t9LUGG6xT2Fjf9qYHovEk8EUupa2yvcGWe18SgvXLOXekfwJeR0sTQzogbw3cxTqNM0DCWnX2n/mJhBIA/SZgz4T0k3kQ5kcniTW6OdktJmIZVNI= 20 | file_glob: true 21 | file: 22 | - dist/*.dmg 23 | - dist/*.zip 24 | - dist/*.AppImage 25 | on: 26 | tags: true 27 | repo: veonim/veonim 28 | cache: 29 | directories: 30 | - node_modules 31 | - $HOME/.electron 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Deomitrus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "10" 3 | 4 | os: unstable 5 | 6 | platform: 7 | - x64 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version x64 11 | - npm install 12 | 13 | test_script: 14 | - node --version 15 | - npm --version 16 | - npm run build 17 | - npm run test 18 | - npm run package 19 | 20 | artifacts: 21 | - path: dist/*.exe 22 | name: SetupExe 23 | 24 | - path: dist/*.zip 25 | name: ProductZip 26 | 27 | deploy: 28 | provider: GitHub 29 | auth_token: 30 | secure: v/U4AfMwTB+O0COfYK45SVK6Nu3/JlRe/jRIES0x4sCc+CAwBuZ8k9i6suDq8B+w 31 | draft: false 32 | prerelease: false 33 | force_update: true 34 | on: 35 | appveyor_repo_tag: true 36 | 37 | cache: 38 | - node_modules 39 | 40 | build: off 41 | -------------------------------------------------------------------------------- /art/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/art/header.png -------------------------------------------------------------------------------- /art/header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/art/header.psd -------------------------------------------------------------------------------- /art/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/art/icon.icns -------------------------------------------------------------------------------- /art/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/art/icon.ico -------------------------------------------------------------------------------- /art/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/art/icon.png -------------------------------------------------------------------------------- /art/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/art/icon.psd -------------------------------------------------------------------------------- /docs/cmdline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/cmdline.png -------------------------------------------------------------------------------- /docs/colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/colors.png -------------------------------------------------------------------------------- /docs/completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/completion.png -------------------------------------------------------------------------------- /docs/echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/echo.png -------------------------------------------------------------------------------- /docs/explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/explorer.png -------------------------------------------------------------------------------- /docs/files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/files.png -------------------------------------------------------------------------------- /docs/grep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/grep.png -------------------------------------------------------------------------------- /docs/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/header.png -------------------------------------------------------------------------------- /docs/hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/hint.png -------------------------------------------------------------------------------- /docs/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/hover.png -------------------------------------------------------------------------------- /docs/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/layout.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/logo.png -------------------------------------------------------------------------------- /docs/messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/messages.png -------------------------------------------------------------------------------- /docs/nyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/nyan.png -------------------------------------------------------------------------------- /docs/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/preview.png -------------------------------------------------------------------------------- /docs/problems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/problems.png -------------------------------------------------------------------------------- /docs/references.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/references.png -------------------------------------------------------------------------------- /docs/smart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/smart.png -------------------------------------------------------------------------------- /docs/symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/symbols.png -------------------------------------------------------------------------------- /docs/symbols2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/symbols2.png -------------------------------------------------------------------------------- /docs/tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/tasks.png -------------------------------------------------------------------------------- /docs/user-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/docs/user-menu.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "veonim", 3 | "version": "0.13.1", 4 | "description": "simple modal IDE built on neovim", 5 | "main": "build/bootstrap/main.js", 6 | "scripts": { 7 | "postinstall": "node tools/postinstall.js", 8 | "start": "node tools/start.js", 9 | "start:release": "electron build/bootstrap/main.js", 10 | "build": "node tools/build.js", 11 | "package": "node tools/package.js", 12 | "test": "mocha test/unit", 13 | "test:e2e": "mocha test/e2e -t 0", 14 | "test:e2e:snapshot": "npm run test:e2e -- --snapshot", 15 | "test:integration": "mocha test/integration -t 10000", 16 | "test:watch": "npm run test -- -w", 17 | "test:integration:watch": "npm run test:integration -- -w", 18 | "gen:api": "node tools/gen-api.js", 19 | "gen:font-sizes": "electron tools/font-sizer/index.js", 20 | "unused-exports": "ts-unused-exports tsconfig.json $(find src -name *.ts)", 21 | "setup-mirrors": "node tools/setup-mirrors.js" 22 | }, 23 | "jest": { 24 | "collectCoverage": true 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/veonim/veonim.git" 29 | }, 30 | "vscode-api-version": "1.27.2", 31 | "repository-mirrors": [ 32 | "git@github.com:veonim/veonim.git", 33 | "https://veonim@gitlab.com/veonim/veonim.git", 34 | "https://veonim@bitbucket.org/veonim/veonim.git" 35 | ], 36 | "author": "VeonimDev", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/veonim/veonim/issues" 40 | }, 41 | "homepage": "https://github.com/veonim/veonim#readme", 42 | "bindeps-darwin": { 43 | "@veonim/neovim-mac": "0.3.1-2", 44 | "@veonim/ripgrep-mac": "0.0.1", 45 | "all-other-unzip-libs-suck-mac": "0.0.2" 46 | }, 47 | "bindeps-win32": { 48 | "@veonim/neovim-win": "0.0.3", 49 | "@veonim/ripgrep-win": "0.0.1", 50 | "all-other-unzip-libs-suck-win": "0.0.2" 51 | }, 52 | "bindeps-linux": { 53 | "@veonim/neovim-linux": "0.0.4", 54 | "@veonim/ripgrep-linux": "0.0.1", 55 | "all-other-unzip-libs-suck-linux": "0.0.2" 56 | }, 57 | "dependencies": { 58 | "fuzzaldrin-plus": "0.4.1", 59 | "hyperapp": "1.2.9", 60 | "hyperapp-feather": "0.4.0", 61 | "hyperapp-seti": "0.2.0", 62 | "marked": "0.5.0", 63 | "msgpack-lite": "0.1.26", 64 | "vscode-debugprotocol": "1.29.0", 65 | "vscode-languageserver-protocol": "3.9.0" 66 | }, 67 | "devDependencies": { 68 | "@deomitrus/hyperapp-redux-devtools": "1.2.0", 69 | "@medv/finder": "1.1.0", 70 | "@types/fuzzaldrin-plus": "0.0.1", 71 | "@types/msgpack-lite": "0.1.4", 72 | "@types/node": "9.4.0", 73 | "electron": "2.0.8", 74 | "electron-builder": "20.13.3", 75 | "electron-devtools-installer": "2.2.3", 76 | "fs-extra": "5.0.0", 77 | "jscodeshift": "0.3.32", 78 | "mocha": "5.2.0", 79 | "proxyquire": "2.0.1", 80 | "spectron": "4.0.0", 81 | "ts-unused-exports": "2.0.5", 82 | "typescript": "2.8.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /runtime/darwin/vvim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/runtime/darwin/vvim -------------------------------------------------------------------------------- /runtime/linux/vvim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/runtime/linux/vvim -------------------------------------------------------------------------------- /runtime/win32/vvim.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/runtime/win32/vvim.exe -------------------------------------------------------------------------------- /src/ai/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { DebugProtocol as DP } from 'vscode-debugprotocol' 2 | import { basename } from 'path' 3 | 4 | export enum BreakpointKind { Source, Function, Exception } 5 | 6 | export interface Breakpoint { 7 | kind: BreakpointKind 8 | path: string 9 | line: number 10 | column: number 11 | condition?: string 12 | functionName?: string 13 | hitCondition?: string 14 | logMessage?: string 15 | } 16 | 17 | const files = new Map() 18 | 19 | // TODO: can we have multiple breakpoints on the path/line/column? like 20 | // conditional breakpoints or logMessage breakpoints? 21 | // if so, then we need to check additional properties for equality comparison 22 | const findBreakpoint = (breakpoint: Breakpoint) => { 23 | const breakpoints = files.get(breakpoint.path) || [] 24 | 25 | const index = breakpoints.findIndex(b => b.kind === breakpoint.kind 26 | && b.path === breakpoint.path 27 | && b.line === breakpoint.line 28 | && b.column === breakpoint.column) 29 | 30 | return { 31 | exists: index !== -1, 32 | remove: () => breakpoints.splice(index, 1), 33 | } 34 | } 35 | 36 | export const add = (breakpoint: Breakpoint) => files.has(breakpoint.path) 37 | ? files.get(breakpoint.path)!.push(breakpoint) 38 | : files.set(breakpoint.path, [ breakpoint ]) 39 | 40 | export const remove = (breakpoint: Breakpoint) => { 41 | if (!files.has(breakpoint.path)) return 42 | const { exists, remove } = findBreakpoint(breakpoint) 43 | if (exists) remove() 44 | } 45 | 46 | export const has = (breakpoint: Breakpoint) => findBreakpoint(breakpoint).exists 47 | 48 | const asSourceBreakpoint = (breakpoint: Breakpoint): DP.SourceBreakpoint => { 49 | const { kind, path, functionName, ...rest } = breakpoint 50 | return rest 51 | } 52 | 53 | const asFunctionBreakpoint = (breakpoint: Breakpoint): DP.FunctionBreakpoint => { 54 | const { functionName: name = '', condition, hitCondition } = breakpoint 55 | return { name, condition, hitCondition } 56 | } 57 | 58 | export const listSourceBreakpoints = () => [...files.entries()].map(([ path, allBreakpoints ]) => ({ 59 | source: { path, name: basename(path) }, 60 | breakpoints: allBreakpoints 61 | .filter(b => b.kind === BreakpointKind.Source) 62 | .map(asSourceBreakpoint), 63 | })) 64 | 65 | export const listFunctionBreakpoints = () => [...files.entries()].map(([ path, allBreakpoints ]) => ({ 66 | source: { path, name: basename(path) }, 67 | breakpoints: allBreakpoints 68 | .filter(b => b.kind === BreakpointKind.Function) 69 | .map(asFunctionBreakpoint) 70 | .filter(b => b.name) 71 | })) 72 | 73 | export const list = () => [...files.values()].reduce((res, breakpoints) => { 74 | return [ ...res, ...breakpoints ] 75 | }, []) 76 | -------------------------------------------------------------------------------- /src/ai/completion-transforms.ts: -------------------------------------------------------------------------------- 1 | import { CompletionOption, CompletionKind } from '../ai/completions' 2 | import { is } from '../support/utils' 3 | import { parse } from 'path' 4 | 5 | interface CompletionTransformRequest { 6 | completionKind: CompletionKind, 7 | lineContent: string, 8 | column: number, 9 | completionOptions: CompletionOption[], 10 | } 11 | 12 | type Transformer = (request: CompletionTransformRequest) => CompletionOption[] 13 | const transforms = new Map() 14 | 15 | export default (filetype: string, request: CompletionTransformRequest) => { 16 | const transformer = transforms.get(filetype) 17 | const callable = is.function(transformer) || is.asyncfunction(transformer) 18 | if (transformer && callable) return transformer(request) 19 | return request.completionOptions 20 | } 21 | 22 | const isModuleImport = (lineContent: string, column: number) => { 23 | const fragment = lineContent.slice(0, column - 1) 24 | return /\b(from|import)\s*["'][^'"]*$/.test(fragment) 25 | || /\b(import|require)\(['"][^'"]*$/.test(fragment) 26 | } 27 | 28 | const removeFileExtensionsInImportPaths = ({ 29 | completionKind, 30 | completionOptions, 31 | lineContent, 32 | column, 33 | }: CompletionTransformRequest) => { 34 | // in the future these can be separated into different modules and organized 35 | // better. for MVP this will be good enough 36 | if (completionKind !== CompletionKind.Path) return completionOptions 37 | 38 | const tryingToCompleteInsideImportPath = isModuleImport(lineContent, column) 39 | if (!tryingToCompleteInsideImportPath) return completionOptions 40 | 41 | return completionOptions.map(o => ({ 42 | ...o, 43 | insertText: parse(o.text).name, 44 | })) 45 | } 46 | 47 | transforms.set('typescript', removeFileExtensionsInImportPaths) 48 | transforms.set('javascript', removeFileExtensionsInImportPaths) 49 | -------------------------------------------------------------------------------- /src/ai/definition.ts: -------------------------------------------------------------------------------- 1 | import { supports } from '../langserv/server-features' 2 | import { definition } from '../langserv/adapter' 3 | import nvim from '../core/neovim' 4 | 5 | nvim.onAction('definition', async () => { 6 | if (!supports.definition(nvim.state.cwd, nvim.state.filetype)) return 7 | 8 | const { path, line, column } = await definition(nvim.state) 9 | if (!line || !column) return 10 | nvim.jumpTo({ path, line, column }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/ai/get-debug-config.ts: -------------------------------------------------------------------------------- 1 | // TODO: this module is a fake until we figure out how to get 2 | // debug configs from the debug extension itself 3 | export default (debugType: string) => { 4 | if (debugType === 'node2') return { 5 | type: 'node2', 6 | request: 'launch', 7 | name: 'Launch Program', 8 | } 9 | } 10 | // TODO: SEE DIS WAT DO? "Instead VS Code passes all arguments from the user's launch configuration to the launch or attach requests" 11 | -------------------------------------------------------------------------------- /src/ai/highlights.ts: -------------------------------------------------------------------------------- 1 | import { highlights, references as getReferences } from '../langserv/adapter' 2 | import { Highlight, HighlightGroupId } from '../neovim/types' 3 | import { supports } from '../langserv/server-features' 4 | import { brighten } from '../ui/css' 5 | import nvim from '../core/neovim' 6 | 7 | const setHighlightColor = () => { 8 | const highlightColor = brighten(nvim.state.background, 25) 9 | nvim.cmd(`highlight ${Highlight.DocumentHighlight} guibg=${highlightColor}`) 10 | } 11 | 12 | nvim.watchState.colorscheme(setHighlightColor) 13 | setHighlightColor() 14 | 15 | nvim.onAction('highlight', async () => { 16 | const referencesSupported = supports.references(nvim.state.cwd, nvim.state.filetype) 17 | const highlightsSupported = supports.highlights(nvim.state.cwd, nvim.state.filetype) 18 | const anySupport = highlightsSupported || referencesSupported 19 | 20 | if (!anySupport) return 21 | 22 | const { references } = highlightsSupported 23 | ? await highlights(nvim.state) 24 | : await getReferences(nvim.state) 25 | 26 | const buffer = nvim.current.buffer 27 | buffer.clearHighlight(HighlightGroupId.DocumentHighlight, 0, -1) 28 | 29 | if (!references.length) return 30 | 31 | references.forEach(hi => buffer.addHighlight( 32 | HighlightGroupId.DocumentHighlight, 33 | Highlight.DocumentHighlight, 34 | hi.line, 35 | hi.column, 36 | hi.endColumn, 37 | )) 38 | }) 39 | 40 | nvim.onAction('highlight-clear', async () => { 41 | nvim.current.buffer.clearHighlight(HighlightGroupId.DocumentHighlight, 0, -1) 42 | }) 43 | -------------------------------------------------------------------------------- /src/ai/hover.ts: -------------------------------------------------------------------------------- 1 | import colorizer, { ColorData } from '../services/colorizer' 2 | import { supports } from '../langserv/server-features' 3 | import * as markdown from '../support/markdown' 4 | import { hover } from '../langserv/adapter' 5 | import { ui } from '../components/hover' 6 | import nvim from '../core/neovim' 7 | 8 | const textByWord = (data: ColorData[]): ColorData[] => data.reduce((res, item) => { 9 | const words = item.text.split(/(\s+)/) 10 | const items = words.map(m => ({ color: item.color, text: m })) 11 | return [...res, ...items] 12 | }, [] as ColorData[]) 13 | 14 | nvim.onAction('hover', async () => { 15 | if (!supports.hover(nvim.state.cwd, nvim.state.filetype)) return 16 | 17 | const { value, doc } = await hover(nvim.state) 18 | if (!value) return 19 | 20 | const cleanData = markdown.remove(value) 21 | const coloredLines: ColorData[][] = await colorizer.request.colorize(cleanData, nvim.state.filetype) 22 | const data = coloredLines 23 | .map(m => textByWord(m)) 24 | .map(m => m.filter(m => m.text.length)) 25 | 26 | ui.show({ data, doc }) 27 | }) 28 | 29 | nvim.on.cursorMove(ui.hide) 30 | nvim.on.insertEnter(ui.hide) 31 | nvim.on.insertLeave(ui.hide) 32 | -------------------------------------------------------------------------------- /src/ai/implementation.ts: -------------------------------------------------------------------------------- 1 | import { EditorLocation, implementation, definition } from '../langserv/adapter' 2 | import { supports } from '../langserv/server-features' 3 | import nvim from '../core/neovim' 4 | 5 | interface StateLocation { 6 | absoluteFilepath: string 7 | line: number 8 | column: number 9 | } 10 | 11 | const recursiveDefinition = async (stateLocation: StateLocation): Promise => { 12 | const { path, line, column } = await definition(Object.assign(nvim.state, stateLocation)) 13 | 14 | if (!path || !line || !column) return { 15 | path: stateLocation.absoluteFilepath, 16 | line: stateLocation.line, 17 | column: stateLocation.column, 18 | } 19 | 20 | return recursiveDefinition({ line, column, absoluteFilepath: path }) 21 | } 22 | 23 | nvim.onAction('implementation', async () => { 24 | const implementationSupported = supports.implementation(nvim.state.cwd, nvim.state.filetype) 25 | const definitionSupported = supports.definition(nvim.state.cwd, nvim.state.filetype) 26 | const anySupport = implementationSupported || definitionSupported 27 | 28 | if (!anySupport) return 29 | 30 | const { path, line, column } = implementationSupported 31 | ? await implementation(nvim.state) 32 | : await recursiveDefinition(nvim.state) 33 | 34 | if (!line || !column) return 35 | nvim.jumpTo({ path, line, column }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/ai/references.ts: -------------------------------------------------------------------------------- 1 | import { references as getReferences, Reference } from '../langserv/adapter' 2 | import { findNext, findPrevious } from '../support/relative-finder' 3 | import { supports } from '../langserv/server-features' 4 | import { show } from '../components/references' 5 | import nvim from '../core/neovim' 6 | 7 | const groupResults = (m: Reference[]) => [...m.reduce((map, ref: Reference) => { 8 | map.has(ref.path) 9 | ? map.get(ref.path)!.push(ref) 10 | : map.set(ref.path, [ ref ]) 11 | 12 | return map 13 | }, new Map())] 14 | 15 | nvim.onAction('references', async () => { 16 | if (!supports.references(nvim.state.cwd, nvim.state.filetype)) return 17 | 18 | const { keyword, references } = await getReferences(nvim.state) 19 | if (!references.length) return 20 | 21 | const referencesForUI = groupResults(references) 22 | show(referencesForUI, keyword) 23 | }) 24 | 25 | nvim.onAction('next-usage', async () => { 26 | if (!supports.references(nvim.state.cwd, nvim.state.filetype)) return 27 | 28 | const { references } = await getReferences(nvim.state) 29 | if (!references.length) return 30 | 31 | const { line, column, absoluteFilepath } = nvim.state 32 | const reference = findNext(references, absoluteFilepath, line, column) 33 | if (reference) nvim.jumpTo(reference) 34 | }) 35 | 36 | nvim.onAction('prev-usage', async () => { 37 | if (!supports.references(nvim.state.cwd, nvim.state.filetype)) return 38 | 39 | const { references } = await getReferences(nvim.state) 40 | if (!references.length) return 41 | 42 | const { line, column, absoluteFilepath } = nvim.state 43 | const reference = findPrevious(references, absoluteFilepath, line, column) 44 | if (reference) nvim.jumpTo(reference) 45 | }) 46 | -------------------------------------------------------------------------------- /src/ai/remote-problems.ts: -------------------------------------------------------------------------------- 1 | import { Problem, setProblems } from '../ai/diagnostics' 2 | import HttpServer from '../support/http-server' 3 | import nvim from '../core/neovim' 4 | 5 | HttpServer(42325).then(({ port, onJsonRequest }) => { 6 | process.env.VEONIM_REMOTE_PROBLEMS_PORT = port + '' 7 | nvim.cmd(`let $VEONIM_REMOTE_PROBLEMS_PORT='${port}'`) 8 | onJsonRequest((data, reply) => (setProblems(data), reply(201))) 9 | }) 10 | -------------------------------------------------------------------------------- /src/ai/rename.ts: -------------------------------------------------------------------------------- 1 | import { rename, textSync } from '../langserv/adapter' 2 | import { supports } from '../langserv/server-features' 3 | import nvim from '../core/neovim' 4 | 5 | // TODO: anyway to improve the glitchiness of undo/apply edit? any way to also pause render in undo 6 | // or maybe figure out how to diff based on the partial modification 7 | // call atomic? tricky with getting target lines for replacements 8 | // even if done before atomic operations, line numbers could be off 9 | nvim.onAction('rename', async () => { 10 | if (!supports.rename(nvim.state.cwd, nvim.state.filetype)) return 11 | 12 | textSync.pause() 13 | const editPosition = { line: nvim.state.line, column: nvim.state.column } 14 | nvim.feedkeys('ciw') 15 | await nvim.untilEvent.insertLeave 16 | const newName = await nvim.expr('@.') 17 | nvim.feedkeys('u') 18 | textSync.resume() 19 | 20 | nvim.applyPatches(await rename({ ...nvim.state, ...editPosition, newName })) 21 | }) 22 | -------------------------------------------------------------------------------- /src/ai/symbols.ts: -------------------------------------------------------------------------------- 1 | import { show, SymbolMode } from '../components/symbols' 2 | import { supports } from '../langserv/server-features' 3 | import { symbols } from '../langserv/adapter' 4 | import nvim from '../core/neovim' 5 | 6 | nvim.onAction('symbols', async () => { 7 | if (!supports.symbols(nvim.state.cwd, nvim.state.filetype)) return 8 | 9 | const listOfSymbols = await symbols(nvim.state) 10 | listOfSymbols && show(listOfSymbols, SymbolMode.Buffer) 11 | }) 12 | 13 | nvim.onAction('workspace-symbols', () => { 14 | if (supports.workspaceSymbols(nvim.state.cwd, nvim.state.filetype)) show([], SymbolMode.Workspace) 15 | }) 16 | -------------------------------------------------------------------------------- /src/ai/type-definition.ts: -------------------------------------------------------------------------------- 1 | import { supports } from '../langserv/server-features' 2 | import { typeDefinition } from '../langserv/adapter' 3 | import nvim from '../core/neovim' 4 | 5 | nvim.onAction('type-definition', async () => { 6 | if (!supports.typeDefinition(nvim.state.cwd, nvim.state.filetype)) return 7 | 8 | const { path, line, column } = await typeDefinition(nvim.state) 9 | if (!line || !column) return 10 | nvim.jumpTo({ path, line, column }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/src/assets/header.png -------------------------------------------------------------------------------- /src/assets/nc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/src/assets/nc.gif -------------------------------------------------------------------------------- /src/assets/roboto-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/src/assets/roboto-mono.ttf -------------------------------------------------------------------------------- /src/assets/roboto-sizes.json: -------------------------------------------------------------------------------- 1 | { 2 | "4": 2, 3 | "5": 3, 4 | "6": 3, 5 | "7": 4, 6 | "8": 4, 7 | "9": 5, 8 | "10": 6, 9 | "11": 6, 10 | "12": 7, 11 | "13": 7, 12 | "14": 8, 13 | "15": 9, 14 | "16": 9, 15 | "17": 10, 16 | "18": 10, 17 | "19": 11, 18 | "20": 12, 19 | "21": 12, 20 | "22": 13, 21 | "23": 13, 22 | "24": 14, 23 | "25": 15, 24 | "26": 15, 25 | "27": 16, 26 | "28": 16, 27 | "29": 17, 28 | "30": 18, 29 | "31": 18, 30 | "32": 19, 31 | "33": 19, 32 | "34": 20, 33 | "35": 21, 34 | "36": 21, 35 | "37": 22, 36 | "38": 22, 37 | "39": 23, 38 | "40": 24, 39 | "41": 24, 40 | "42": 25, 41 | "43": 25, 42 | "44": 26, 43 | "45": 27, 44 | "46": 27, 45 | "47": 28, 46 | "48": 28, 47 | "49": 29, 48 | "50": 30, 49 | "51": 30, 50 | "52": 31, 51 | "53": 31 52 | } 53 | -------------------------------------------------------------------------------- /src/bootstrap/galaxy.ts: -------------------------------------------------------------------------------- 1 | // setup trace 2 | ;(localStorage.getItem('veonim-trace-flags') || '') 3 | .split(',') 4 | .filter(m => m) 5 | .forEach(m => Reflect.set(process.env, `VEONIM_TRACE_${m.toUpperCase()}`, 1)) 6 | // end setup trace 7 | 8 | import { CreateTask, log, delay as timeout, requireDir } from '../support/utils' 9 | import { resize, attachTo, create } from '../core/master-control' 10 | import * as canvasContainer from '../core/canvas-container' 11 | import configReader from '../config/config-reader' 12 | import setDefaultSession from '../core/sessions' 13 | import * as windows from '../core/windows' 14 | import * as uiInput from '../core/input' 15 | import nvim from '../core/neovim' 16 | import '../ui/notifications' 17 | import '../core/render' 18 | import '../core/title' 19 | 20 | const loadingConfig = CreateTask() 21 | 22 | configReader('nvim/init.vim', c => { 23 | canvasContainer.setFont({ 24 | face: c.get('font'), 25 | size: c.get('font_size')-0, 26 | lineHeight: c.get('line_height')-0 27 | }) 28 | 29 | loadingConfig.done('') 30 | }) 31 | 32 | nvim.watchState.background(color => { 33 | if (document.body.style.background !== color) document.body.style.background = color 34 | }) 35 | 36 | canvasContainer.on('resize', ({ rows, cols }) => { 37 | resize(cols, rows) 38 | setImmediate(() => windows.render()) 39 | }) 40 | 41 | const main = async () => { 42 | const { id, path } = await create() 43 | await Promise.race([ loadingConfig.promise, timeout(500) ]) 44 | resize(canvasContainer.size.cols, canvasContainer.size.rows) 45 | uiInput.focus() 46 | attachTo(id) 47 | setDefaultSession(id, path) 48 | 49 | setTimeout(() => { 50 | // TODO: can we load copmonents on demand? 51 | // aka, either load when user requests, or after 10 sec of app startup shit 52 | // in the inventory PR, layer actions are now setup to require the componet. 53 | // this could be a way to lazy load components (or maybe some of the 54 | // non-important ones - color-picker, etc.) 55 | requireDir(`${__dirname}/../services`) 56 | requireDir(`${__dirname}/../components`) 57 | setTimeout(() => require('../core/ai')) 58 | }, 1) 59 | 60 | // TODO: THIS SHOULD BE LOADED IN A WEB WORKER. WTF IS THIS SHIT DOING IN THE MAIN THREAD LOL 61 | // TODO: clearly we are not ready for this greatness 62 | setTimeout(() => require('../support/dependency-manager').default(), 100) 63 | } 64 | 65 | main().catch(log) 66 | 67 | export const touched = () => { 68 | const start = document.getElementById('start') 69 | if (start) start.remove() 70 | } 71 | -------------------------------------------------------------------------------- /src/bootstrap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | veonim 5 | 101 | 102 | 103 |
104 | 105 |
now with more sodium!
106 |
107 | 108 |
109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 |
118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/bootstrap/process-explorer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Veonim - Process Explorer 5 | 64 | 65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/components/code-actions.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'vscode-languageserver-protocol' 2 | import { RowNormal } from '../components/row-container' 3 | import { h, app, vimBlur, vimFocus } from '../ui/uikit' 4 | import { runCodeAction } from '../ai/diagnostics' 5 | import { activeWindow } from '../core/windows' 6 | import Input from '../components/text-input' 7 | import Overlay from '../components/overlay' 8 | import { filter } from 'fuzzaldrin-plus' 9 | import * as Icon from 'hyperapp-feather' 10 | 11 | const state = { 12 | x: 0, 13 | y: 0, 14 | value: '', 15 | visible: false, 16 | actions: [] as Command[], 17 | cache: [] as Command[], 18 | index: 0, 19 | } 20 | 21 | type S = typeof state 22 | 23 | const resetState = { value: '', visible: false } 24 | 25 | const actions = { 26 | show: ({ x, y, actions }: any) => (vimBlur(), { x, y, actions, cache: actions, visible: true }), 27 | hide: () => (vimFocus(), resetState), 28 | 29 | change: (value: string) => (s: S) => ({ value, index: 0, actions: value 30 | ? filter(s.actions, value, { key: 'title' }) 31 | : s.cache 32 | }), 33 | 34 | select: () => (s: S) => { 35 | vimFocus() 36 | if (!s.actions.length) return resetState 37 | const action = s.actions[s.index] 38 | if (action) runCodeAction(action) 39 | return resetState 40 | }, 41 | 42 | next: () => (s: S) => ({ index: s.index + 1 > s.actions.length - 1 ? 0 : s.index + 1 }), 43 | prev: () => (s: S) => ({ index: s.index - 1 < 0 ? s.actions.length - 1 : s.index - 1 }), 44 | } 45 | 46 | const view = ($: S, a: typeof actions) => Overlay({ 47 | x: $.x, 48 | y: $.y, 49 | zIndex: 100, 50 | maxWidth: 600, 51 | visible: $.visible, 52 | anchorAbove: false, 53 | }, [ 54 | 55 | ,h('div', { 56 | style: { 57 | background: 'var(--background-40)', 58 | } 59 | }, [ 60 | 61 | ,Input({ 62 | hide: a.hide, 63 | next: a.next, 64 | prev: a.prev, 65 | change: a.change, 66 | select: a.select, 67 | value: $.value, 68 | focus: true, 69 | small: true, 70 | icon: Icon.Code, 71 | desc: 'run code action', 72 | }) 73 | 74 | ,h('div', $.actions.map((s, ix) => h(RowNormal, { 75 | key: `${s.title}-${s.command}`, 76 | active: ix === $.index, 77 | }, [ 78 | ,h('span', s.title) 79 | ]))) 80 | 81 | ]) 82 | 83 | ]) 84 | 85 | const ui = app({ name: 'code-actions', state, actions, view }) 86 | 87 | export const show = (row: number, col: number, actions: Command[]) => { 88 | if (!actions.length) return 89 | const x = activeWindow() ? activeWindow()!.colToX(col) : 0 90 | const y = activeWindow() ? activeWindow()!.rowToTransformY(row + 1) : 0 91 | ui.show({ x, y, actions }) 92 | } 93 | -------------------------------------------------------------------------------- /src/components/command-line.ts: -------------------------------------------------------------------------------- 1 | import { enableCursor, disableCursor, hideCursor, showCursor } from '../core/cursor' 2 | import { CommandType, CommandUpdate } from '../core/render' 3 | import { Plugin } from '../components/plugin-container' 4 | import { RowNormal } from '../components/row-container' 5 | import Input from '../components/text-input' 6 | import { sub } from '../messaging/dispatch' 7 | import * as Icon from 'hyperapp-feather' 8 | import { is } from '../support/utils' 9 | import { h, app } from '../ui/uikit' 10 | 11 | const modeSwitch = new Map([ 12 | [ CommandType.Ex, Icon.Command], 13 | [ CommandType.Prompt, Icon.ChevronsRight ], 14 | ]) 15 | 16 | const state = { 17 | options: [] as string[], 18 | visible: false, 19 | value: '', 20 | ix: 0, 21 | position: 0, 22 | kind: CommandType.Ex, 23 | } 24 | 25 | type S = typeof state 26 | 27 | const actions = { 28 | // there is logic in text-input to show/hide cursor based on text input 29 | // foucs/blur events. however i noticed that when selecting a wildmenu option 30 | // somehow causes the text input to lose focus (we need to update the 31 | // selected menu item in the text input field). i'm not sure why this is 32 | // different than the normal command update, since we do not use the text 33 | // input natively. we send input events directly to vim, vim sends cmd 34 | // updates back to us, and we update the text input field. 35 | hide: () => { 36 | enableCursor() 37 | showCursor() 38 | return { visible: false } 39 | }, 40 | updateCommand: ({ cmd, kind, position }: CommandUpdate) => (s: S) => { 41 | hideCursor() 42 | disableCursor() 43 | 44 | return { 45 | kind, 46 | position, 47 | visible: true, 48 | options: cmd ? s.options : [], 49 | value: is.string(cmd) && s.value !== cmd 50 | ? cmd 51 | : s.value 52 | } 53 | }, 54 | 55 | selectWildmenu: (ix: number) => ({ ix }), 56 | updateWildmenu: (options: string[]) => ({ 57 | options: [...new Set(options)] 58 | }), 59 | } 60 | 61 | type A = typeof actions 62 | 63 | const view = ($: S) => Plugin($.visible, [ 64 | 65 | ,Input({ 66 | focus: true, 67 | value: $.value, 68 | desc: 'command line', 69 | position: $.position, 70 | icon: modeSwitch.get($.kind) || Icon.Command, 71 | }) 72 | 73 | ,h('div', $.options.map((name, ix) => h(RowNormal, { 74 | active: ix === $.ix, 75 | }, [ 76 | ,h('div', name) 77 | ]))) 78 | 79 | ]) 80 | 81 | const ui = app({ name: 'command-line', state, actions, view }) 82 | 83 | // TODO: use export cns. this component is a high priority so it should be loaded early 84 | // because someone might open cmdline early 85 | sub('wildmenu.show', opts => ui.updateWildmenu(opts)) 86 | sub('wildmenu.select', ix => ui.selectWildmenu(ix)) 87 | sub('wildmenu.hide', () => ui.updateWildmenu([])) 88 | 89 | sub('cmd.hide', ui.hide) 90 | sub('cmd.update', ui.updateCommand) 91 | -------------------------------------------------------------------------------- /src/components/debug.ts: -------------------------------------------------------------------------------- 1 | import { DebuggerInfo, changeStack, changeScope } from '../ai/debug' 2 | import { PluginRight } from '../components/plugin-container' 3 | import { DebugProtocol as DP } from 'vscode-debugprotocol' 4 | import { Breakpoint } from '../ai/breakpoints' 5 | import { paddingVH } from '../ui/css' 6 | import { h, app } from '../ui/uikit' 7 | 8 | type Threads = DP.Thread[] 9 | type StackFrames = DP.StackFrame[] 10 | type Scopes = DP.Scope[] 11 | type Variables = DP.Variable[] 12 | 13 | const state = { 14 | id: '', 15 | type: '', 16 | visible: false, 17 | threads: [] as Threads, 18 | stackFrames: [] as StackFrames, 19 | scopes: [] as Scopes, 20 | variables: [] as Variables, 21 | activeThread: 0, 22 | activeStack: 0, 23 | activeScope: 0, 24 | debuggers: [] as DebuggerInfo[], 25 | breakpoints: [] as Breakpoint[], 26 | } 27 | 28 | type S = typeof state 29 | 30 | const actions = { 31 | show: () => ({ visible: true }), 32 | hide: () => ({ visible: false }), 33 | updateState: (state: Partial) => state, 34 | } 35 | 36 | type A = typeof actions 37 | 38 | const header = (title: string) => h('div', { 39 | style: { 40 | ...paddingVH(8, 8), 41 | background: 'rgba(255, 255, 255, 0.1)', 42 | fontWeight: 'bold', 43 | }, 44 | }, title) 45 | 46 | const ListItem = (name: string, active: boolean, clickFn: Function) => h('div', { 47 | style: { 48 | ...paddingVH(8, 4), 49 | background: active ? 'rgba(255, 255, 255, 0.05)' : undefined, 50 | }, 51 | onclick: clickFn, 52 | }, name) 53 | 54 | const view = ($: S) => PluginRight($.visible, { 55 | // TODO: TESTING ONLY 56 | zIndex: 99999999, 57 | }, [ 58 | 59 | ,h('div', [ 60 | ,header('Threads') 61 | ,h('div', $.threads.map(m => ListItem( 62 | m.name, 63 | $.activeThread === m.id, 64 | () => console.log('pls change thread to:', m.id), 65 | ))) 66 | ]) 67 | 68 | ,h('div', [ 69 | ,header('Stacks') 70 | ,h('div', $.stackFrames.map(m => ListItem( 71 | m.name, 72 | $.activeStack === m.id, 73 | () => changeStack(m.id), 74 | ))) 75 | ]) 76 | 77 | ,h('div', [ 78 | ,header('Scopes') 79 | ,h('div', $.scopes.map(m => ListItem( 80 | m.name, 81 | $.activeScope === m.variablesReference, 82 | () => changeScope(m.variablesReference), 83 | ))) 84 | ]) 85 | 86 | ,h('div', [ 87 | ,header('Variables') 88 | ,h('div', $.variables.map(m => ListItem( 89 | `${m.name} -> ${m.value}`, 90 | false, 91 | () => console.log('pls get var:', m.value), 92 | ))) 93 | ]) 94 | 95 | ,h('div', [ 96 | ,header('Breakpoints') 97 | ,h('div', $.breakpoints.map(m => ListItem( 98 | `${m.path}:${m.line}`, 99 | false, 100 | () => {}, 101 | ))) 102 | ]) 103 | 104 | ]) 105 | 106 | export default app({ name: 'debug', state, actions, view }) 107 | -------------------------------------------------------------------------------- /src/components/filetype-icon.ts: -------------------------------------------------------------------------------- 1 | import { getLanguageForExtension } from '../support/language-ids' 2 | import * as FeatherIcon from 'hyperapp-feather' 3 | import { pascalCase } from '../support/utils' 4 | import { basename, extname } from 'path' 5 | import * as Icons from 'hyperapp-seti' 6 | import { h } from '../ui/uikit' 7 | 8 | const findIcon = (id: string) => id && Reflect.get(Icons, pascalCase(id)) 9 | 10 | const customMappings = new Map([ 11 | [ 'readme.md', 'info' ], 12 | [ 'gif', 'image' ], 13 | [ 'jpg', 'image' ], 14 | [ 'jpeg', 'image' ], 15 | [ 'png', 'image' ], 16 | [ 'svg', 'image' ], 17 | ]) 18 | 19 | const findIconCustom = (filename: string, extension: string) => { 20 | const mapping = (customMappings.get(extension) || customMappings.get(filename)) 21 | return mapping && findIcon(mapping) 22 | } 23 | 24 | const getIcon = (path = '') => { 25 | const filename = basename(path).toLowerCase() 26 | const extension = extname(filename).replace(/^\./, '').toLowerCase() 27 | const langId = getLanguageForExtension(extension) 28 | 29 | return findIconCustom(filename, extension) 30 | || langId && findIcon(langId) 31 | || findIcon(extension) 32 | || findIcon(filename) 33 | || findIcon(path.toLowerCase()) 34 | || Icons.Shell 35 | } 36 | 37 | const featherStyle = { 38 | display: 'flex', 39 | justifyContent: 'center', 40 | alignItems: 'center', 41 | marginRight: '8px', 42 | marginLeft: '3px', 43 | fontSize: '1.1rem', 44 | } 45 | 46 | export const Folder = h('div', { 47 | style: featherStyle, 48 | }, [ 49 | ,h(FeatherIcon.Folder) 50 | ]) 51 | 52 | export const Terminal = h('div', { 53 | style: featherStyle, 54 | }, [ 55 | ,h(FeatherIcon.Terminal) 56 | ]) 57 | 58 | export default (fileTypeOrPath: string) => h('div', { 59 | style: { 60 | display: 'flex', 61 | justifyContent: 'center', 62 | alignItems: 'center', 63 | marginRight: '6px', 64 | marginTop: '2px', 65 | fontSize: '1.5rem', 66 | color: '#ccc', 67 | }, 68 | }, [ 69 | ,h(getIcon(fileTypeOrPath)) 70 | ]) 71 | -------------------------------------------------------------------------------- /src/components/generic-menu.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../components/plugin-container' 2 | import { RowNormal } from '../components/row-container' 3 | import { h, app, vimBlur, vimFocus } from '../ui/uikit' 4 | import { CreateTask } from '../support/utils' 5 | import Input from '../components/text-input' 6 | import { filter } from 'fuzzaldrin-plus' 7 | import * as Icon from 'hyperapp-feather' 8 | import { Component } from 'hyperapp' 9 | 10 | export interface MenuOption { 11 | key: any, 12 | value: string, 13 | } 14 | 15 | interface Props { 16 | description: string, 17 | options: MenuOption[], 18 | icon?: Component, 19 | } 20 | 21 | const state = { 22 | visible: false, 23 | value: '', 24 | options: [] as MenuOption[], 25 | cache: [] as MenuOption[], 26 | description: '', 27 | ix: 0, 28 | icon: Icon.User, 29 | task: CreateTask(), 30 | } 31 | 32 | type S = typeof state 33 | 34 | const resetState = { value: '', visible: false, ix: 0 } 35 | 36 | const actions = { 37 | select: () => (s: S) => { 38 | vimFocus() 39 | if (!s.options.length) return resetState 40 | s.task.done((s.options[s.ix] || {}).key) 41 | return resetState 42 | }, 43 | 44 | // TODO: not hardcoded 14 45 | change: (value: string) => (s: S) => ({ value, ix: 0, options: value 46 | ? filter(s.cache, value, { key: 'value' }).slice(0, 14) 47 | : s.cache.slice(0, 14) 48 | }), 49 | 50 | show: ({ options, description, icon, task }: any) => (vimBlur(), { 51 | description, 52 | options, 53 | task, 54 | icon, 55 | cache: options, 56 | visible: true 57 | }), 58 | 59 | hide: () => (vimFocus(), resetState), 60 | next: () => (s: S) => ({ ix: s.ix + 1 > Math.min(s.options.length - 1, 13) ? 0 : s.ix + 1 }), 61 | prev: () => (s: S) => ({ ix: s.ix - 1 < 0 ? Math.min(s.options.length - 1, 13) : s.ix - 1 }), 62 | } 63 | 64 | const view = ($: S, a: typeof actions) => Plugin($.visible, [ 65 | 66 | ,Input({ 67 | select: a.select, 68 | change: a.change, 69 | hide: a.hide, 70 | next: a.next, 71 | prev: a.prev, 72 | value: $.value, 73 | desc: $.description, 74 | focus: true, 75 | icon: $.icon, 76 | }) 77 | 78 | ,h('div', $.options.map(({ key, value }, id) => h(RowNormal, { 79 | key, 80 | active: id === $.ix 81 | }, [ 82 | ,h('span', value) 83 | ]))) 84 | 85 | ]) 86 | 87 | const ui = app({ name: 'generic-menu', state, actions, view }) 88 | 89 | export default (props: Props) => { 90 | const task = CreateTask() 91 | ui.show({ ...props, task }) 92 | return task.promise 93 | } 94 | -------------------------------------------------------------------------------- /src/components/generic-prompt.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../components/plugin-container' 2 | import { app, vimBlur, vimFocus } from '../ui/uikit' 3 | import { CreateTask } from '../support/utils' 4 | import Input from '../components/text-input' 5 | import * as Icon from 'hyperapp-feather' 6 | 7 | const state = { 8 | value: '', 9 | desc: '', 10 | visible: false, 11 | task: CreateTask(), 12 | } 13 | 14 | type S = typeof state 15 | 16 | const resetState = { value: '', visible: false, desc: '' } 17 | 18 | const actions = { 19 | show: ({ desc, task }: any) => (vimBlur(), { 20 | desc, 21 | task, 22 | value: '', 23 | visible: true, 24 | }), 25 | hide: () => (vimFocus(), resetState), 26 | change: (value: string) => ({ value }), 27 | select: () => (s: S) => { 28 | s.value && s.task.done(s.value) 29 | vimFocus() 30 | return resetState 31 | }, 32 | } 33 | 34 | type A = typeof actions 35 | 36 | const view = ($: S, a: A) => Plugin($.visible, [ 37 | 38 | ,Input({ 39 | focus: true, 40 | icon: Icon.HelpCircle, 41 | hide: a.hide, 42 | select: a.select, 43 | change: a.change, 44 | value: $.value, 45 | desc: $.desc, 46 | }) 47 | 48 | ]) 49 | 50 | const ui = app({ name: 'generic-prompt', state, actions, view }) 51 | 52 | export default (question: string) => { 53 | const task = CreateTask() 54 | ui.show({ task, desc: question }) 55 | return task.promise 56 | } 57 | -------------------------------------------------------------------------------- /src/components/hover.ts: -------------------------------------------------------------------------------- 1 | import { ColorData } from '../services/colorizer' 2 | import { activeWindow } from '../core/windows' 3 | import { sub } from '../messaging/dispatch' 4 | import { debounce } from '../support/utils' 5 | import Overlay from '../components/overlay' 6 | import { docStyle } from '../ui/styles' 7 | import { cursor } from '../core/cursor' 8 | import { h, app } from '../ui/uikit' 9 | import { cvar } from '../ui/css' 10 | 11 | interface ShowParams { 12 | data: ColorData[][], 13 | doc?: string, 14 | } 15 | 16 | const docs = (data: string) => h('div', { style: docStyle }, [ h('div', data) ]) 17 | 18 | const getPosition = (row: number, col: number) => ({ 19 | x: activeWindow() ? activeWindow()!.colToX(col - 1) : 0, 20 | y: activeWindow() ? activeWindow()!.rowToTransformY(row > 2 ? row : row + 1) : 0, 21 | anchorBottom: cursor.row > 2, 22 | }) 23 | 24 | const state = { 25 | value: [[]] as ColorData[][], 26 | visible: false, 27 | anchorBottom: true, 28 | doc: '', 29 | x: 0, 30 | y: 0, 31 | } 32 | 33 | type S = typeof state 34 | 35 | const actions = { 36 | hide: () => ({ visible: false }), 37 | show: ({ data, doc }: ShowParams) => ({ 38 | doc, 39 | value: data, 40 | visible: true, 41 | ...getPosition(cursor.row, cursor.col), 42 | }), 43 | updatePosition: () => (s: S) => s.visible 44 | ? getPosition(cursor.row, cursor.col) 45 | : undefined, 46 | } 47 | 48 | type A = typeof actions 49 | 50 | const view = ($: S) => Overlay({ 51 | x: $.x, 52 | y: $.y, 53 | maxWidth: 600, 54 | visible: $.visible, 55 | anchorAbove: $.anchorBottom, 56 | }, [ 57 | 58 | ,$.doc && !$.anchorBottom && docs($.doc) 59 | 60 | ,h('div', { 61 | style: { 62 | background: cvar('background-30'), 63 | padding: '8px', 64 | } 65 | }, $.value.map(m => h('div', { 66 | style: { 67 | display: 'flex', 68 | flexFlow: 'row wrap', 69 | } 70 | }, m.map(({ color, text }) => h('span', { 71 | style: { 72 | color: color || cvar('foreground'), 73 | whiteSpace: 'pre', 74 | } 75 | }, text))))) 76 | 77 | ,$.doc && $.anchorBottom && docs($.doc) 78 | 79 | ]) 80 | 81 | export const ui = app({ name: 'hover', state, actions, view }) 82 | 83 | sub('redraw', debounce(ui.updatePosition, 50)) 84 | -------------------------------------------------------------------------------- /src/components/inventory.ts: -------------------------------------------------------------------------------- 1 | import { h, app } from '../ui/uikit' 2 | import nvim from '../core/neovim' 3 | 4 | // TODO: show some layers as disabled? like the langserv layer would be disabled if there 5 | // are not language servers started. user can still see options and info, but visually 6 | // it appears non-functional 7 | // 8 | // can provide in layer description that 'this layer requires language server available' 9 | // the provide current status of lang serv. provide links to where to install langextensions 10 | 11 | const state = { 12 | visible: false, 13 | } 14 | 15 | type S = typeof state 16 | 17 | const resetState = { visible: false } 18 | 19 | const actions = { 20 | show: () => ({ visible: true }), 21 | hide: () => resetState, 22 | } 23 | 24 | type A = typeof actions 25 | 26 | const view = ($: S) => h('div', { 27 | style: { 28 | display: $.visible ? 'flex' : 'none', 29 | }, 30 | }, [ 31 | ,h('div', 'ur inventory got ninja looted luls') 32 | ]) 33 | 34 | const ui = app({ name: 'inventory', state, view, actions }) 35 | 36 | nvim.onAction('inventory', async () => { 37 | const timeoutLength = await nvim.options.timeoutlen 38 | console.log('timeoutLength', timeoutLength) 39 | ui.show() 40 | }) 41 | -------------------------------------------------------------------------------- /src/components/loading.ts: -------------------------------------------------------------------------------- 1 | import * as canvasContainer from '../core/canvas-container' 2 | import { Loader } from 'hyperapp-feather' 3 | import { h } from '../ui/uikit' 4 | 5 | interface LoaderParams { 6 | size?: number, 7 | color?: string, 8 | } 9 | 10 | export default ({ color, size = canvasContainer.font.size + 2 } = {} as LoaderParams) => h('div', { 11 | style: { 12 | color: color || 'rgba(255, 255, 255, 0.3)', 13 | animation: 'spin 2.5s linear infinite', 14 | height: `${size}px`, 15 | width: `${size}px`, 16 | } 17 | }, [ 18 | ,h(Loader, { size }) 19 | ]) 20 | -------------------------------------------------------------------------------- /src/components/lolglobal.ts: -------------------------------------------------------------------------------- 1 | import { currentWindowElement } from '../core/windows' 2 | import Input from '../components/text-input' 3 | import { rgba, paddingV } from '../ui/css' 4 | import * as Icon from 'hyperapp-feather' 5 | import { makel } from '../ui/vanilla' 6 | import { app, h } from '../ui/uikit' 7 | import nvim from '../core/neovim' 8 | 9 | const state = { 10 | value: '', 11 | focus: false, 12 | mode: 'n', 13 | } 14 | 15 | type S = typeof state 16 | 17 | const actions = { 18 | show: (mode: string) => { 19 | currentWindowElement.add(containerEl) 20 | return { mode, focus: true } 21 | }, 22 | hide: () => { 23 | currentWindowElement.remove(containerEl) 24 | nvim.cmd('undo') 25 | return { value: '', focus: false } 26 | }, 27 | // TODO: only works if a search pattern exists 28 | // how can we detect if there is a search pattern 29 | // and run :g//norm ??cmds?? 30 | // and if there isn't run :norm ??cmds?? 31 | // maybe separate binding?? 32 | // - need someway to escape special chars (enter, space, ctrl-key, etc.) 33 | 34 | // TODO: test to make sure it works with macros. are we 35 | // calling undo at the right time? 36 | change: (value: string) => (s: S) => { 37 | const preprocess = value.length ? 'undo |' : '' 38 | const mm = s.mode === 'v' ? `'<,'>` : '' 39 | nvim.cmd(`${preprocess} ${mm}g@@norm n${value}`) 40 | return { value } 41 | }, 42 | select: () => { 43 | currentWindowElement.remove(containerEl) 44 | return { value: '', focus: false } 45 | }, 46 | } 47 | 48 | type A = typeof actions 49 | 50 | const view = ($: S, a: A) => h('div', { 51 | style: { 52 | display: 'flex', 53 | flex: 1, 54 | }, 55 | }, [ 56 | 57 | ,h('div', { 58 | style: { 59 | ...paddingV(20), 60 | display: 'flex', 61 | alignItems: 'center', 62 | // TODO: figure out a good color from the colorscheme... StatusLine? 63 | background: rgba(217, 150, 255, 0.17), 64 | } 65 | }, [ 66 | ,h('span', 'multi normal') 67 | ]) 68 | 69 | ,Input({ 70 | small: true, 71 | focus: $.focus, 72 | value: $.value, 73 | desc: 'multi normal', 74 | icon: Icon.Camera, 75 | hide: a.hide, 76 | change: a.change, 77 | select: a.select, 78 | }) 79 | 80 | ]) 81 | 82 | const containerEl = makel({ 83 | position: 'absolute', 84 | width: '100%', 85 | display: 'flex', 86 | backdropFilter: 'blur(24px)', 87 | background: `rgba(var(--background-30-alpha), 0.6)`, 88 | }) 89 | 90 | const ui = app({ name: 'll', state, actions, view, element: containerEl }) 91 | 92 | nvim.onAction('llv', () => ui.show('v')) 93 | nvim.onAction('lln', () => ui.show('n')) 94 | -------------------------------------------------------------------------------- /src/components/nc.ts: -------------------------------------------------------------------------------- 1 | import { h, app } from '../ui/uikit' 2 | import nvim from '../core/neovim' 3 | 4 | const state = { 5 | visible: false, 6 | } 7 | 8 | type S = typeof state 9 | 10 | const actions = { 11 | show: () => ({ visible: true }), 12 | hide: () => (s: S) => { 13 | if (s.visible) return { visible: false } 14 | }, 15 | } 16 | 17 | const view = ($: S) => h('div', { 18 | style: { 19 | background: `url('../assets/nc.gif')`, 20 | display: $.visible ? 'block' : 'none', 21 | backgroundRepeat: 'no-repeat', 22 | backgroundSize: '75vw', 23 | position: 'absolute', 24 | height: '100%', 25 | width: '100%', 26 | }, 27 | }) 28 | 29 | const ui = app({ name: 'nc', state, actions, view }) 30 | 31 | nvim.onAction('nc', ui.show) 32 | nvim.on.cursorMove(ui.hide) 33 | -------------------------------------------------------------------------------- /src/components/overlay.ts: -------------------------------------------------------------------------------- 1 | import { h } from '../ui/uikit' 2 | 3 | interface Props { 4 | visible: boolean, 5 | x: number, 6 | y: number, 7 | maxWidth?: number, 8 | anchorAbove: boolean, 9 | zIndex?: number, 10 | onElement?: (element: HTMLElement) => void, 11 | } 12 | 13 | export default ($: Props, content: any[]) => h('div', { 14 | style: { 15 | zIndex: $.zIndex, 16 | display: $.visible ? 'flex' : 'none', 17 | height: '100%', 18 | width: '100%', 19 | flexFlow: $.anchorAbove ? 'column-reverse' : 'column', 20 | position: 'absolute', 21 | }, 22 | oncreate: $.onElement, 23 | }, [ 24 | 25 | ,h('.spacer', { 26 | style: { 27 | height: $.anchorAbove ? `calc(100% - ${$.y}px)` : `${$.y}px`, 28 | } 29 | }) 30 | 31 | ,h('div', { 32 | style: { 33 | display: 'flex', 34 | flexFlow: 'row nowrap', 35 | } 36 | }, [ 37 | 38 | ,h('.col', { 39 | style: { 40 | width: `${$.x}px`, 41 | } 42 | }) 43 | 44 | ,h('div', { 45 | style: { 46 | flexShrink: '0', 47 | maxWidth: $.maxWidth && `${$.maxWidth}px`, 48 | } 49 | }, content) 50 | 51 | ]) 52 | ]) 53 | -------------------------------------------------------------------------------- /src/components/plugin-container.ts: -------------------------------------------------------------------------------- 1 | import { is } from '../support/utils' 2 | import { h } from '../ui/uikit' 3 | 4 | type PluginFnNormal = (visible: boolean, children: any[]) => any 5 | type PluginFnWithStyles = (visible: boolean, styles: object, children: any[]) => any 6 | type PluginFn = PluginFnNormal & PluginFnWithStyles 7 | 8 | const base = { 9 | zIndex: 99, 10 | display: 'flex', 11 | width: '100%', 12 | height: '100%', 13 | justifyContent: 'center', 14 | } 15 | 16 | const normal = { ...base, alignItems: 'flex-start' } 17 | const top = { ...base, alignItems: 'flex-start' } 18 | const bottom = { ...base, alignItems: 'flex-end' } 19 | const right = { ...base, alignItems: 'stretch', justifyContent: 'flex-end' } 20 | 21 | const dialog = { 22 | background: `rgba(var(--background-45-alpha), 0.7)`, 23 | backdropFilter: 'blur(24px)', 24 | marginTop: '15%', 25 | flexFlow: 'column', 26 | } 27 | 28 | export const Plugin = (visible: boolean, children: any[]) => h('div', { 29 | style: normal, 30 | }, [ 31 | 32 | ,h('div', { 33 | style: { 34 | ...dialog, 35 | width: '600px', 36 | display: visible ? 'flex' : 'none', 37 | } 38 | }, children) 39 | 40 | ]) 41 | 42 | export const PluginTop: PluginFn = (visible: boolean, ...args: any[]) => h('div', { 43 | style: top, 44 | }, [ 45 | 46 | ,h('div', { 47 | style: { 48 | ...dialog, 49 | width: '400px', 50 | display: visible ? 'flex' : 'none', 51 | ...args.find(is.object) 52 | } 53 | }, args.find(is.array)) 54 | 55 | ]) 56 | 57 | export const PluginBottom: PluginFn = (visible: boolean, ...args: any[]) => h('div', { 58 | style: bottom, 59 | }, [ 60 | 61 | ,h('div', { 62 | style: { 63 | width: '100%', 64 | height: '100%', 65 | flexFlow: 'column', 66 | backdropFilter: 'blur(8px)', 67 | background: `rgba(var(--background-40-alpha), 0.8)`, 68 | display: visible ? 'flex' : 'none', 69 | ...args.find(is.object), 70 | } 71 | }, args.find(is.array)) 72 | 73 | ]) 74 | 75 | export const PluginRight = (visible: boolean, ...args: any[]) => h('div', { 76 | style: right, 77 | }, [ 78 | 79 | ,h('div', { 80 | style: { 81 | ...dialog, 82 | width: '500px', 83 | height: '100%', 84 | flexFlow: 'column', 85 | marginTop: 0, 86 | backdropFilter: 'blur(8px)', 87 | background: `rgba(var(--background-40-alpha), 0.8)`, 88 | display: visible ? 'flex' : 'none', 89 | ...args.find(is.object), 90 | } 91 | }, args.find(is.array)) 92 | 93 | ]) 94 | -------------------------------------------------------------------------------- /src/components/problem-info.ts: -------------------------------------------------------------------------------- 1 | import { activeWindow } from '../core/windows' 2 | import Overlay from '../components/overlay' 3 | import { sub } from '../messaging/dispatch' 4 | import { debounce } from '../support/utils' 5 | import * as Icon from 'hyperapp-feather' 6 | import { cursor } from '../core/cursor' 7 | import { h, app } from '../ui/uikit' 8 | import { cvar } from '../ui/css' 9 | 10 | const getPosition = (row: number, col: number) => ({ 11 | x: activeWindow() ? activeWindow()!.colToX(col - 1) : 0, 12 | y: activeWindow() ? activeWindow()!.rowToTransformY(row > 2 ? row : row + 1) : 0, 13 | anchorBottom: cursor.row > 2, 14 | }) 15 | 16 | const state = { 17 | x: 0, 18 | y: 0, 19 | value: '', 20 | visible: false, 21 | anchorBottom: true, 22 | } 23 | 24 | type S = typeof state 25 | 26 | const actions = { 27 | hide: () => ({ visible: false }), 28 | show: (value: string) => ({ 29 | value, 30 | visible: true, 31 | ...getPosition(cursor.row, cursor.col), 32 | }), 33 | updatePosition: () => (s: S) => s.visible 34 | ? getPosition(cursor.row, cursor.col) 35 | : undefined, 36 | } 37 | 38 | type A = typeof actions 39 | 40 | const view = ($: S) => Overlay({ 41 | x: $.x, 42 | y: $.y, 43 | maxWidth: 600, 44 | visible: $.visible, 45 | anchorAbove: $.anchorBottom, 46 | }, [ 47 | 48 | ,h('div', { 49 | style: { 50 | background: cvar('background-30'), 51 | color: cvar('foreground'), 52 | padding: '8px', 53 | display: 'flex', 54 | alignItems: 'center', 55 | } 56 | }, [ 57 | 58 | ,h('div', { 59 | style: { 60 | display: 'flex', 61 | alignItems: 'center', 62 | paddingRight: '8px', 63 | } 64 | }, [ 65 | ,h(Icon.XCircle, { 66 | style: { fontSize: '1.2rem' }, 67 | color: cvar('error'), 68 | }) 69 | ]) 70 | 71 | ,h('div', $.value) 72 | 73 | ]) 74 | 75 | ]) 76 | 77 | export const ui = app({ name: 'problem-info', state, actions, view }) 78 | 79 | sub('redraw', debounce(ui.updatePosition, 50)) 80 | -------------------------------------------------------------------------------- /src/components/row-container.ts: -------------------------------------------------------------------------------- 1 | import { cvar, paddingVH, paddingH } from '../ui/css' 2 | import { colors } from '../ui/styles' 3 | import { h } from '../ui/uikit' 4 | 5 | const row = { 6 | alignItems: 'center', 7 | paddingTop: '4px', 8 | paddingBottom: '4px', 9 | paddingLeft: '12px', 10 | paddingRight: '12px', 11 | whiteSpace: 'nowrap', 12 | overflow: 'hidden', 13 | textOverflow: 'ellipsis', 14 | display: 'flex', 15 | color: cvar('foreground-30'), 16 | minHeight: '1.4rem', 17 | } 18 | 19 | const activeRow = { 20 | ...row, 21 | fontWeight: 'bold', 22 | color: cvar('foreground-b20'), 23 | background: cvar('background-10'), 24 | } 25 | 26 | interface Options { 27 | key?: any, 28 | active: boolean, 29 | [key: string]: any, 30 | } 31 | 32 | const removePropsIntendedForThisComponent = (stuff: Options) => { 33 | const { active, ...rest } = stuff 34 | return rest 35 | } 36 | 37 | export const RowNormal = (o: Options, children: any[]) => h('div', { 38 | ...removePropsIntendedForThisComponent(o), 39 | style: { 40 | ...row, 41 | ...(o.active ? activeRow: undefined), 42 | ...o.style, 43 | } 44 | }, children) 45 | 46 | export const RowDesc = (o: Options, children: any[]) => h('div', { 47 | ...removePropsIntendedForThisComponent(o), 48 | style: { 49 | ...(o.active ? activeRow : row), 50 | whiteSpace: 'normal', 51 | overflow: 'normal', 52 | ...o.style, 53 | }, 54 | }, children) 55 | 56 | export const RowComplete = (o: Options, children: any[]) => h('div', { 57 | ...removePropsIntendedForThisComponent(o), 58 | style: { 59 | ...(o.active ? activeRow : row), 60 | ...paddingVH(0, 0), 61 | paddingRight: '8px', 62 | lineHeight: cvar('line-height'), 63 | ...o.style, 64 | } 65 | }, children) 66 | 67 | export const RowHeader = (o: Options, children: any[]) => h('div', { 68 | ...removePropsIntendedForThisComponent(o), 69 | style: { 70 | ...(o.active ? activeRow : row), 71 | ...paddingH(6), 72 | alignItems: 'center', 73 | color: colors.hint, 74 | background: cvar('background-20'), 75 | ...(o.active ? { 76 | color: '#fff', 77 | fontWeight: 'normal', 78 | background: cvar('background-b10'), 79 | }: 0), 80 | ...o.style, 81 | } 82 | }, children) 83 | 84 | export const RowImportant = (opts = {} as any, children: any[]) => h('div', { 85 | ...removePropsIntendedForThisComponent(opts), 86 | style: { 87 | ...opts.style, 88 | ...row, 89 | ...paddingH(8), 90 | color: cvar('important'), 91 | background: cvar('background-50'), 92 | } 93 | }, children) 94 | -------------------------------------------------------------------------------- /src/components/spell-check.ts: -------------------------------------------------------------------------------- 1 | import { sub } from '../messaging/dispatch' 2 | import { h, app } from '../ui/uikit' 3 | 4 | const state = { 5 | visible: false, 6 | content: '', 7 | } 8 | 9 | type S = typeof state 10 | 11 | const actions = { 12 | show: (msg: string) => { 13 | return ({ content: msg, visible: true }) 14 | }, 15 | hide: () => (s: S) => { 16 | if (s.visible) return { visible: false, content: '' } 17 | }, 18 | } 19 | 20 | let elref: HTMLElement 21 | const view = ($: S) => h('div', { 22 | oncreate: (e: any) => elref = e, 23 | style: { 24 | display: $.visible ? 'flex' : 'none', 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | position: 'absolute', 28 | height: '100%', 29 | width: '100%', 30 | }, 31 | }, [ 32 | ,h('pre', { 33 | style: { 34 | background: 'rgba(0, 0, 0, 0.8)', 35 | padding: '10px', 36 | color: '#fff', 37 | } 38 | }, $.content) 39 | ]) 40 | 41 | const ui = app({ name: 'spell-check', state, actions, view }) 42 | 43 | sub('msg:spell-check', msg => ui.show(msg)) 44 | 45 | sub('hack:input-keys', inputKeys => { 46 | const cancel = inputKeys === '' || inputKeys === '' 47 | if (elref.style.display === 'flex' && cancel) ui.hide() 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/user-menu.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../components/plugin-container' 2 | import { RowNormal } from '../components/row-container' 3 | import { h, app, vimBlur, vimFocus } from '../ui/uikit' 4 | import Input from '../components/text-input' 5 | import { filter } from 'fuzzaldrin-plus' 6 | import * as Icon from 'hyperapp-feather' 7 | import nvim from '../core/neovim' 8 | 9 | const state = { 10 | id: 0, 11 | visible: false, 12 | value: '', 13 | items: [] as string[], 14 | cache: [] as string[], 15 | desc: '', 16 | index: 0, 17 | } 18 | 19 | type S = typeof state 20 | 21 | const resetState = { value: '', visible: false, index: 0 } 22 | 23 | const actions = { 24 | select: () => (s: S) => { 25 | vimFocus() 26 | if (!s.items.length) return resetState 27 | const item = s.items[s.index] 28 | if (item) nvim.call.VeonimCallback(s.id, item) 29 | return resetState 30 | }, 31 | 32 | // TODO: not hardcoded 14 33 | change: (value: string) => (s: S) => ({ value, index: 0, items: value 34 | ? filter(s.cache, value).slice(0, 14) 35 | : s.cache.slice(0, 14) 36 | }), 37 | 38 | hide: () => (vimFocus(), resetState), 39 | show: ({ id, items, desc }: any) => (vimBlur(), { id, desc, items, cache: items, visible: true }), 40 | next: () => (s: S) => ({ index: s.index + 1 > Math.min(s.items.length - 1, 13) ? 0 : s.index + 1 }), 41 | prev: () => (s: S) => ({ index: s.index - 1 < 0 ? Math.min(s.items.length - 1, 13) : s.index - 1 }), 42 | } 43 | 44 | const view = ($: S, a: typeof actions) => Plugin($.visible, [ 45 | 46 | ,Input({ 47 | select: a.select, 48 | change: a.change, 49 | hide: a.hide, 50 | next: a.next, 51 | prev: a.prev, 52 | value: $.value, 53 | desc: $.desc, 54 | focus: true, 55 | icon: Icon.User, 56 | }) 57 | 58 | ,h('div', $.items.map((item, ix) => h(RowNormal, { 59 | key: item, 60 | active: ix === $.index, 61 | }, [ 62 | ,h('span', item) 63 | ]))) 64 | 65 | ]) 66 | 67 | const ui = app({ name: 'user-menu', state, actions, view }) 68 | 69 | nvim.onAction('user-menu', (id: number, desc: string, items = []) => items.length && ui.show({ id, items, desc })) 70 | -------------------------------------------------------------------------------- /src/components/user-overlay-menu.ts: -------------------------------------------------------------------------------- 1 | import { RowNormal } from '../components/row-container' 2 | import { h, app, vimBlur, vimFocus } from '../ui/uikit' 3 | import { activeWindow } from '../core/windows' 4 | import Input from '../components/text-input' 5 | import Overlay from '../components/overlay' 6 | import { filter } from 'fuzzaldrin-plus' 7 | import * as Icon from 'hyperapp-feather' 8 | import { cursor } from '../core/cursor' 9 | import nvim from '../core/neovim' 10 | import { cvar } from '../ui/css' 11 | 12 | const state = { 13 | id: 0, 14 | visible: false, 15 | value: '', 16 | items: [] as string[], 17 | cache: [] as string[], 18 | desc: '', 19 | index: 0, 20 | x: 0, 21 | y: 0, 22 | } 23 | 24 | type S = typeof state 25 | 26 | const actions = { 27 | select: () => (s: S) => { 28 | vimFocus() 29 | if (!s.items.length) return { value: '', visible: false, index: 0 } 30 | const item = s.items[s.index] 31 | if (item) nvim.call.VeonimCallback(s.id, item) 32 | return { value: '', visible: false, index: 0 } 33 | }, 34 | 35 | // TODO: not harcoded to 14 36 | change: (value: string) => (s: S) => ({ value, index: 0, items: value 37 | ? filter(s.cache, value).slice(0, 14) 38 | : s.cache.slice(0, 14) 39 | }), 40 | 41 | show: ({ x, y, id, items, desc }: any) => (vimBlur(), { x, y, id, desc, items, cache: items, visible: true }), 42 | hide: () => (vimFocus(), { value: '', visible: false, index: 0 }), 43 | // TODO: not hardcoded to 14 44 | next: () => (s: S) => ({ index: s.index + 1 > Math.min(s.items.length - 1, 13) ? 0 : s.index + 1 }), 45 | prev: () => (s: S) => ({ index: s.index - 1 < 0 ? Math.min(s.items.length - 1, 13) : s.index - 1 }), 46 | } 47 | 48 | const view = ($: S, a: typeof actions) => Overlay({ 49 | x: $.x, 50 | y: $.y, 51 | zIndex: 100, 52 | maxWidth: 600, 53 | visible: $.visible, 54 | anchorAbove: false, 55 | }, [ 56 | 57 | ,h('div', { 58 | style: { 59 | background: cvar('background-40'), 60 | } 61 | }, [ 62 | 63 | ,Input({ 64 | hide: a.hide, 65 | next: a.next, 66 | prev: a.prev, 67 | change: a.change, 68 | select: a.select, 69 | value: $.value, 70 | focus: true, 71 | small: true, 72 | icon: Icon.User, 73 | desc: $.desc, 74 | }) 75 | 76 | ,h('div', $.items.map((item, ix) => h(RowNormal, { 77 | key: item, 78 | active: ix === $.index, 79 | }, [ 80 | ,h('span', item) 81 | ]))) 82 | 83 | ]) 84 | 85 | ]) 86 | 87 | const ui = app({ name: 'user-overlay-menu', state, actions, view }) 88 | 89 | nvim.onAction('user-overlay-menu', (id: number, desc: string, items = []) => { 90 | if (!items.length) return 91 | const x = activeWindow() ? activeWindow()!.colToX(cursor.col) : 0 92 | // TODO: anchorBottom maybe? 93 | const y = activeWindow() ? activeWindow()!.rowToTransformY(cursor.row + 1) : 0 94 | ui.show({ x, y, id, items, desc }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/components/vim-create.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../components/plugin-container' 2 | import { app, vimBlur, vimFocus } from '../ui/uikit' 3 | import Input from '../components/text-input' 4 | import { createVim } from '../core/sessions' 5 | import * as Icon from 'hyperapp-feather' 6 | import nvim from '../core/neovim' 7 | 8 | const state = { 9 | value: '', 10 | visible: false, 11 | } 12 | 13 | type S = typeof state 14 | 15 | const actions = { 16 | show: () => (vimBlur(), { visible: true }), 17 | hide: () => (vimFocus(), { value: '', visible: false }), 18 | change: (value: string) => ({ value }), 19 | select: () => (s: S) => { 20 | vimFocus() 21 | s.value && createVim(s.value) 22 | return { value: '', visible: false } 23 | }, 24 | } 25 | 26 | const view = ($: S, a: typeof actions) => Plugin($.visible, [ 27 | 28 | ,Input({ 29 | hide: a.hide, 30 | select: a.select, 31 | change: a.change, 32 | value: $.value, 33 | focus: true, 34 | icon: Icon.FolderPlus, 35 | desc: 'create new vim session', 36 | }) 37 | 38 | ]) 39 | 40 | const ui = app({ name: 'vim-create', state, actions, view }) 41 | nvim.onAction('vim-create', ui.show) 42 | -------------------------------------------------------------------------------- /src/components/vim-rename.ts: -------------------------------------------------------------------------------- 1 | import { renameCurrent, getCurrentName } from '../core/sessions' 2 | import { Plugin } from '../components/plugin-container' 3 | import { app, vimBlur, vimFocus } from '../ui/uikit' 4 | import Input from '../components/text-input' 5 | import * as Icon from 'hyperapp-feather' 6 | import nvim from '../core/neovim' 7 | 8 | const state = { 9 | value: '', 10 | visible: false, 11 | } 12 | 13 | type S = typeof state 14 | 15 | const actions = { 16 | show: (value: string) => (vimBlur(), { value, visible: true }), 17 | hide: () => (vimFocus(), { value: '', visible: false }), 18 | change: (value: string) => ({ value }), 19 | select: () => (s: S) => { 20 | vimFocus() 21 | s.value && renameCurrent(s.value) 22 | return { value: '', visible: false } 23 | }, 24 | } 25 | 26 | const view = ($: S, a: typeof actions) => Plugin($.visible, [ 27 | 28 | ,Input({ 29 | hide: a.hide, 30 | select: a.select, 31 | change: a.change, 32 | value: $.value, 33 | focus: true, 34 | icon: Icon.Edit, 35 | desc: 'rename vim session', 36 | }) 37 | 38 | ]) 39 | 40 | const ui = app({ name: 'vim-rename', state, actions, view }) 41 | nvim.onAction('vim-rename', () => ui.show(getCurrentName())) 42 | -------------------------------------------------------------------------------- /src/components/vim-switch.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../components/plugin-container' 2 | import { RowNormal } from '../components/row-container' 3 | import { h, app, vimBlur, vimFocus } from '../ui/uikit' 4 | import { list, switchVim } from '../core/sessions' 5 | import Input from '../components/text-input' 6 | import { filter } from 'fuzzaldrin-plus' 7 | import * as Icon from 'hyperapp-feather' 8 | import nvim from '../core/neovim' 9 | 10 | interface Session { 11 | id: number, 12 | name: string, 13 | } 14 | 15 | const state = { 16 | value: '', 17 | visible: false, 18 | list: [] as Session[], 19 | cache: [] as Session[], 20 | index: 0, 21 | } 22 | 23 | type S = typeof state 24 | 25 | const actions = { 26 | show: (d: Session[]) => (vimBlur(), { list: d, cache: d, visible: true }), 27 | hide: () => (vimFocus(), { value: '', visible: false, index: 0 }), 28 | change: (value: string) => (s: S) => ({ value, index: 0, list: value 29 | ? filter(s.list, value, { key: 'name' }).slice(0, 10) 30 | : s.cache.slice(0, 10) 31 | }), 32 | 33 | 34 | select: () => (s: S) => { 35 | vimFocus() 36 | if (!s.list.length) return { value: '', visible: false, index: 0 } 37 | const { id } = s.list[s.index] 38 | if (id) switchVim(id) 39 | return { value: '', visible: false, index: 0 } 40 | }, 41 | 42 | // TODO: don't limit list to 10 entries and scroll instead! 43 | next: () => (s: S) => ({ index: s.index + 1 > Math.min(s.list.length - 1, 9) ? 0 : s.index + 1 }), 44 | prev: () => (s: S) => ({ index: s.index - 1 < 0 ? Math.min(s.list.length - 1, 9) : s.index - 1 }), 45 | } 46 | 47 | const view = ($: S, a: typeof actions) => Plugin($.visible, [ 48 | 49 | ,Input({ 50 | hide: a.hide, 51 | change: a.change, 52 | select: a.select, 53 | next: a.next, 54 | prev: a.prev, 55 | value: $.value, 56 | focus: true, 57 | icon: Icon.Grid, 58 | desc: 'switch vim session', 59 | }) 60 | 61 | ,h('div', $.list.map(({ id, name }, ix) => h(RowNormal, { 62 | key: `${id}-${name}`, 63 | active: ix === $.index, 64 | }, [ 65 | ,h('span', name) 66 | ]))) 67 | 68 | ]) 69 | 70 | const ui = app({ name: 'vim-switch', state, actions, view }) 71 | nvim.onAction('vim-switch', () => ui.show(list())) 72 | -------------------------------------------------------------------------------- /src/config/config-reader.ts: -------------------------------------------------------------------------------- 1 | import { log, exists, readFile, configPath as base, watchFile } from '../support/utils' 2 | 3 | type Config = Map 4 | type ConfigCallback = (config: Config) => void 5 | 6 | const loadConfig = async (path: string, notify: ConfigCallback) => { 7 | const pathExists = await exists(path) 8 | if (!pathExists) return log `config file at ${path} not found` 9 | 10 | const data = await readFile(path) 11 | const config = data 12 | .toString() 13 | .split('\n') 14 | .filter((line: string) => /^let g:vn_/.test(line)) 15 | .reduce((map: Config, line: string) => { 16 | const [ , key = '', dirtyVal = '' ] = line.match(/^let g:vn_(\S+)(?:\s*)\=(?:\s*)([\S\ ]+)/) || [] 17 | const cleanVal = dirtyVal.replace(/^(?:"|')(.*)(?:"|')$/, '$1') 18 | map.set(key, cleanVal) 19 | return map 20 | }, new Map()) 21 | 22 | notify(config) 23 | } 24 | 25 | export default async (location: string, cb: ConfigCallback) => { 26 | const path = `${base}/${location}` 27 | const pathExists = await exists(path) 28 | if (!pathExists) return log `config file at ${path} not found` 29 | 30 | loadConfig(path, cb).catch(e => log(e)) 31 | watchFile(path, () => loadConfig(path, cb).catch(e => log(e))) 32 | } 33 | 34 | export const watchConfig = async (location: string, cb: Function) => { 35 | const path = `${base}/${location}` 36 | const pathExists = await exists(path) 37 | if (!pathExists) return log `config file at ${path} not found` 38 | watchFile(path, () => cb()) 39 | } 40 | -------------------------------------------------------------------------------- /src/config/config-service.ts: -------------------------------------------------------------------------------- 1 | import * as defaultConfigs from '../config/default-configs' 2 | import { objDeepGet } from '../support/utils' 3 | import nvim from '../core/neovim' 4 | 5 | export default (configName: string, cb: (val: any) => void) => { 6 | const vimKey = `vn_${configName.split('.').join('_')}` 7 | Reflect.get(nvim.g, vimKey).then((val: any) => val && cb(val)) 8 | return objDeepGet(defaultConfigs)(configName) 9 | } 10 | -------------------------------------------------------------------------------- /src/config/default-configs.ts: -------------------------------------------------------------------------------- 1 | import { configPath } from '../support/utils' 2 | import { homedir } from 'os' 3 | import { join } from 'path' 4 | 5 | export const EXT_PATH = join(configPath, 'veonim', 'extensions') 6 | 7 | export const project = { 8 | root: homedir(), 9 | } 10 | 11 | export const explorer = { 12 | ignore: { 13 | dirs: ['.git'], 14 | files: ['.DS_Store'], 15 | }, 16 | } 17 | 18 | export const workspace = { 19 | ignore: { 20 | dirs: [], 21 | } 22 | } 23 | 24 | export const colorscheme = 'veonim' 25 | -------------------------------------------------------------------------------- /src/core/ai.ts: -------------------------------------------------------------------------------- 1 | import { filetypeDetectedStartServerMaybe } from '../langserv/director' 2 | import { getSignatureHint } from '../ai/signature-hint' 3 | import { getCompletions } from '../ai/completions' 4 | import colorizer from '../services/colorizer' 5 | import nvim from '../core/neovim' 6 | import '../ai/type-definition' 7 | import '../ai/implementation' 8 | import '../ai/diagnostics' 9 | import '../ai/references' 10 | import '../ai/definition' 11 | import '../ai/highlights' 12 | import '../ai/symbols' 13 | import '../ai/rename' 14 | import '../ai/hover' 15 | 16 | nvim.on.filetype(filetype => filetypeDetectedStartServerMaybe(nvim.state.cwd, filetype)) 17 | nvim.watchState.colorscheme((color: string) => colorizer.call.setColorScheme(color)) 18 | 19 | nvim.on.cursorMoveInsert(async () => { 20 | // tried to get the line contents from the render grid buffer, but it appears 21 | // this autocmd gets fired before the grid gets updated from the render event. 22 | // once we add a setImmediate to wait for render pass, we're back to the same 23 | // amount of time it took to simply query nvim with 'get_current_line' 24 | // 25 | // if we had a nvim notification for mode change, we could send events after 26 | // a render pass. this event would then contain both the current window grid 27 | // contents + current vim mode. we could then easily improve this action here 28 | // and perhaps others in the app 29 | const lineContent = await nvim.getCurrentLine() 30 | getCompletions(lineContent, nvim.state.line, nvim.state.column) 31 | getSignatureHint(lineContent) 32 | }) 33 | -------------------------------------------------------------------------------- /src/core/font-atlas.ts: -------------------------------------------------------------------------------- 1 | import * as canvasContainer from '../core/canvas-container' 2 | 3 | interface CharPosition { 4 | x: number, 5 | y: number, 6 | } 7 | 8 | interface FontAtlas { 9 | getCharPosition(char: string, color: string): CharPosition | undefined, 10 | bitmap: ImageBitmap, 11 | } 12 | 13 | const CHAR_START = 32 14 | const CHAR_END = 127 15 | const canvas = document.createElement('canvas') 16 | const ui = canvas.getContext('2d', { alpha: true }) as CanvasRenderingContext2D 17 | 18 | let atlas: FontAtlas 19 | 20 | const generate = async (colors: string[]): Promise => { 21 | if (!colors.length) throw new Error('cannot generate font atlas because no colors were given') 22 | const colorLines = new Map() 23 | 24 | const drawChar = (col: number, y: number, char: string) => { 25 | const { height, width } = canvasContainer.cell 26 | 27 | ui.save() 28 | ui.beginPath() 29 | ui.rect(col * width, y, width, height) 30 | ui.clip() 31 | ui.fillText(char, col * width, y) 32 | ui.restore() 33 | } 34 | 35 | const drawCharLine = (color: string, row: number) => { 36 | ui.fillStyle = color 37 | 38 | let column = 0 39 | for (let ix = CHAR_START; ix < CHAR_END; ix++) { 40 | drawChar(column, row, String.fromCharCode(ix)) 41 | column++ 42 | } 43 | } 44 | 45 | const height = canvasContainer.cell.height * colors.length 46 | const width = (CHAR_END - CHAR_START) * canvasContainer.cell.width 47 | 48 | canvas.height = height * window.devicePixelRatio 49 | canvas.width = width * window.devicePixelRatio 50 | 51 | ui.imageSmoothingEnabled = false 52 | ui.font = `${canvasContainer.font.size}px ${canvasContainer.font.face}` 53 | ui.scale(window.devicePixelRatio, window.devicePixelRatio) 54 | ui.textBaseline = 'top' 55 | 56 | colors.reduce((currentRow, color) => { 57 | drawCharLine(color, currentRow) 58 | colorLines.set(color, currentRow) 59 | return currentRow + canvasContainer.cell.height 60 | }, 0) 61 | 62 | const getCharPosition = (char: string, color: string) => { 63 | const code = char.charCodeAt(0) 64 | if (code < CHAR_START || code > CHAR_END) return 65 | const srcRow = colorLines.get(color) 66 | if (typeof srcRow !== 'number') return 67 | const x = (code - CHAR_START) * canvasContainer.cell.width * window.devicePixelRatio 68 | const y = srcRow * window.devicePixelRatio 69 | return { x, y } 70 | } 71 | 72 | return { 73 | getCharPosition, 74 | bitmap: await createImageBitmap(canvas), 75 | } 76 | } 77 | 78 | export default { 79 | get exists() { return !!atlas }, 80 | get bitmap() { return (atlas || {}).bitmap }, 81 | generate: (colors: string[]) => generate(colors).then(fa => atlas = fa), 82 | getCharPosition: (char: string, color: string) => { 83 | if (!atlas) return 84 | return atlas.getCharPosition(char, color) 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/core/inventory-layers.ts: -------------------------------------------------------------------------------- 1 | export enum InventoryLayerKind { 2 | Jump = 'jump', 3 | Debug = 'debug', 4 | Buffer = 'buffer', 5 | Search = 'search', 6 | Window = 'window', 7 | Project = 'project', 8 | Language = 'language', 9 | Instance = 'instance', 10 | /** Only available in Veonim DEV builds */ 11 | DEV = 'DEV', 12 | } 13 | 14 | export interface InventoryAction { 15 | /** So we don't break backwards compatibility for now */ 16 | legacyDeprecatedCommand: string 17 | /** Which layer this action belongs to */ 18 | layer: InventoryLayerKind 19 | /** Key binding to activate this action */ 20 | keybind: string 21 | /** Action name. Will be formatted and appended to layer name. Final command value would be :Veonim ${layer}-${command}*/ 22 | name: string 23 | /** User friendly description provided in the UI */ 24 | description: string 25 | /** Indicate to the user that this action is experimental. Default: FALSE */ 26 | experimental?: boolean 27 | } 28 | 29 | interface InventoryLayer { 30 | /** Layer name. Will be formatted and used for Vim command. */ 31 | name: string 32 | /** Key binding to activate this action */ 33 | keybind: string 34 | /** User friendly description provided in the UI */ 35 | description: string 36 | /** This layer is only available DEV builds. Default: FALSE */ 37 | devOnly?: boolean 38 | } 39 | 40 | type Layers = { [key in InventoryLayerKind]: InventoryLayer } 41 | 42 | export const layers: Layers = { 43 | language: { 44 | name: 'Language', 45 | keybind: 'l', 46 | description: 'Language server features', 47 | }, 48 | 49 | debug: { 50 | name: 'Debug', 51 | keybind: 'd', 52 | description: 'Start debugging and debugger controls', 53 | }, 54 | 55 | search: { 56 | name: 'Search', 57 | keybind: 's', 58 | description: 'Various search functions like grep, etc.', 59 | }, 60 | 61 | jump: { 62 | name: 'Jump', 63 | keybind: 'j', 64 | description: 'Access jump shortcuts', 65 | }, 66 | 67 | buffer: { 68 | name: 'Buffer', 69 | keybind: 'b', 70 | description: 'List and jump between buffers', 71 | }, 72 | 73 | window: { 74 | name: 'Window', 75 | keybind: 'w', 76 | description: 'Resize, split, and swap windows', 77 | }, 78 | 79 | project: { 80 | name: 'Project', 81 | keybind: 'p', 82 | description: 'Project management', 83 | }, 84 | 85 | instance: { 86 | name: 'Instance', 87 | keybind: 'i', 88 | description: 'Create and switch between multiple Neovim instances', 89 | }, 90 | 91 | DEV: { 92 | name: 'LOLHAX', 93 | keybind: '\'', 94 | description: 'if ur seein dis ur an ub3r 1337 h4x0rz', 95 | devOnly: true, 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /src/core/neovim.ts: -------------------------------------------------------------------------------- 1 | import { onCreateVim, onSwitchVim } from '../core/sessions' 2 | import setupRPC from '../messaging/rpc' 3 | import Neovim from '../neovim/api' 4 | 5 | export { NeovimState } from '../neovim/state' 6 | 7 | const io = new Worker(`${__dirname}/../workers/neovim-client.js`) 8 | const { onData, ...rpcAPI } = setupRPC(m => io.postMessage(m)) 9 | io.onmessage = ({ data: [kind, data] }: MessageEvent) => onData(kind, data) 10 | 11 | onCreateVim(info => io.postMessage([65, info])) 12 | onSwitchVim(id => io.postMessage([66, id])) 13 | 14 | export default Neovim({ ...rpcAPI, onCreateVim, onSwitchVim }) 15 | -------------------------------------------------------------------------------- /src/core/sessions.ts: -------------------------------------------------------------------------------- 1 | import { onExit, attachTo, switchTo, create } from '../core/master-control' 2 | import { EventEmitter } from 'events' 3 | import { remote } from 'electron' 4 | 5 | interface Vim { 6 | id: number, 7 | name: string, 8 | active: boolean, 9 | path: string, 10 | nameFollowsCwd: boolean, 11 | } 12 | 13 | interface VimInfo { 14 | id: number 15 | path: string 16 | } 17 | 18 | const watchers = new EventEmitter() 19 | watchers.setMaxListeners(200) 20 | const vims = new Map() 21 | let currentVimID = -1 22 | 23 | export default (id: number, path: string) => { 24 | vims.set(id, { id, path, name: 'main', active: true, nameFollowsCwd: true }) 25 | currentVimID = id 26 | watchers.emit('create', { id, path }) 27 | watchers.emit('switch', id) 28 | } 29 | 30 | export const createVim = async (name: string, dir?: string) => { 31 | const { id, path } = await create({ dir }) 32 | currentVimID = id 33 | watchers.emit('create', { id, path }) 34 | attachTo(id) 35 | switchTo(id) 36 | watchers.emit('switch', id) 37 | vims.forEach(v => v.active = false) 38 | vims.set(id, { id, path, name, active: true, nameFollowsCwd: !!dir }) 39 | } 40 | 41 | export const switchVim = async (id: number) => { 42 | if (!vims.has(id)) return 43 | currentVimID = id 44 | switchTo(id) 45 | watchers.emit('switch', id) 46 | vims.forEach(v => v.active = false) 47 | vims.get(id)!.active = true 48 | } 49 | 50 | const renameVim = (id: number, newName: string) => { 51 | if (!vims.has(id)) return 52 | const vim = vims.get(id)! 53 | vim.name = newName 54 | vim.nameFollowsCwd = false 55 | } 56 | 57 | export const getCurrentName = () => { 58 | const active = [...vims.values()].find(v => v.active) 59 | return active ? active.name : '' 60 | } 61 | 62 | export const renameCurrent = (name: string) => { 63 | const active = [...vims.values()].find(v => v.active) 64 | if (!active) return 65 | renameVim(active.id, name) 66 | } 67 | 68 | export const renameCurrentToCwd = (cwd: string) => { 69 | const active = [...vims.values()].find(v => v.active) 70 | if (!active) return 71 | if (active.nameFollowsCwd) active.name = cwd 72 | } 73 | 74 | export const list = () => [...vims.values()].filter(v => !v.active).map(v => ({ id: v.id, name: v.name })) 75 | 76 | export const sessions = { 77 | get current() { return currentVimID } 78 | } 79 | 80 | export const onCreateVim = (fn: (info: VimInfo) => void) => { 81 | watchers.on('create', (info: VimInfo) => fn(info)) 82 | ;[...vims.entries()].forEach(([ id, vim ]) => fn({ id, path: vim.path })) 83 | } 84 | 85 | export const onSwitchVim = (fn: (id: number) => void) => { 86 | watchers.on('switch', id => fn(id)) 87 | fn(currentVimID) 88 | } 89 | 90 | // because of circular dependency chain. master-control exports onExit. 91 | // master-control imports a series of dependencies which eventually 92 | // import this module. thus onExit will not be exported yet. 93 | setImmediate(() => onExit((id: number) => { 94 | if (!vims.has(id)) return 95 | vims.delete(id) 96 | 97 | if (!vims.size) return remote.app.quit() 98 | 99 | const next = Math.max(...vims.keys()) 100 | switchVim(next) 101 | })) 102 | -------------------------------------------------------------------------------- /src/core/shadow-buffers.ts: -------------------------------------------------------------------------------- 1 | import nvim from '../core/neovim' 2 | 3 | type Callback = () => void 4 | 5 | export interface ShadowBuffer { 6 | name: string, 7 | element: HTMLElement, 8 | onFocus?: Callback, 9 | onBlur?: Callback, 10 | onShow?: Callback, 11 | onHide?: Callback, 12 | } 13 | 14 | const shadowBuffers = new Map() 15 | 16 | // by using an initFn we allow the possibility in the future of generating more 17 | // than one instance of a shadow buffer. for example if a user wants to have two 18 | // explorer buffers, each one pointing to a separate directory? honestly, this is 19 | // probably a dumb use case, as plugins like NERDTree only allow one instance at 20 | // a time (that i'm aware of). honestly i'm still undecided if we should have one 21 | // instance only or allow multiple... 22 | export const registerShadowComponent = (initFn: () => ShadowBuffer) => { 23 | const shadowComponent = initFn() 24 | shadowBuffers.set(shadowComponent.name, shadowComponent) 25 | nvim.buffers.addShadow(shadowComponent.name) 26 | } 27 | 28 | export const getShadowBuffer = (name: string) => shadowBuffers.get(name) 29 | -------------------------------------------------------------------------------- /src/core/title.ts: -------------------------------------------------------------------------------- 1 | import * as canvasContainer from '../core/canvas-container' 2 | import { merge, simplifyPath } from '../support/utils' 3 | import * as dispatch from '../messaging/dispatch' 4 | import nvim from '../core/neovim' 5 | import { remote } from 'electron' 6 | 7 | const macos = process.platform === 'darwin' 8 | let titleBarVisible = false 9 | const titleBar = macos && document.createElement('div') 10 | const title = macos && document.createElement('div') 11 | 12 | export const setTitleVisibility = (visible: boolean) => { 13 | if (!titleBar) return 14 | titleBarVisible = visible 15 | titleBar.style.display = visible ? 'flex' : 'none' 16 | canvasContainer.resize() 17 | } 18 | 19 | const typescriptSucks = (el: any, bar: any) => el.prepend(bar) 20 | 21 | if (macos) { 22 | merge((title as HTMLElement).style, { 23 | marginLeft: '20%', 24 | marginRight: '20%', 25 | whiteSpace: 'nowrap', 26 | overflow: 'hidden', 27 | textOverflow: 'ellipsis', 28 | }) 29 | 30 | merge((titleBar as HTMLElement).style, { 31 | height: '22px', 32 | color: 'var(--foreground-60)', 33 | background: 'var(--background-15)', 34 | '-webkit-app-region': 'drag', 35 | '-webkit-user-select': 'none', 36 | width: '100%', 37 | display: 'flex', 38 | justifyContent: 'center', 39 | alignItems: 'center', 40 | }) 41 | 42 | ;(title as HTMLElement).innerText = 'veonim' 43 | ;(titleBar as HTMLElement).appendChild((title as HTMLElement)) 44 | typescriptSucks(document.body, titleBar) 45 | titleBarVisible = true 46 | 47 | remote.getCurrentWindow().on('enter-full-screen', () => { 48 | setTitleVisibility(false) 49 | dispatch.pub('window.change') 50 | }) 51 | 52 | remote.getCurrentWindow().on('leave-full-screen', () => { 53 | setTitleVisibility(true) 54 | dispatch.pub('window.change') 55 | }) 56 | 57 | nvim.watchState.file((file: string) => { 58 | const path = simplifyPath(file, nvim.state.cwd) 59 | ;(title as HTMLElement).innerText = `${path} - veonim` 60 | }) 61 | } 62 | 63 | else nvim.watchState.file((file: string) => { 64 | const path = simplifyPath(file, nvim.state.cwd) 65 | remote.getCurrentWindow().setTitle(`${path} - veonim`) 66 | }) 67 | 68 | export const specs = { 69 | get height() { 70 | return titleBarVisible ? 22 : 0 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/core/vim-functions.ts: -------------------------------------------------------------------------------- 1 | export enum BufferVar { 2 | TermAttached = 'veonim_term_attached', 3 | TermFormat = 'veonim_term_format', 4 | } 5 | 6 | interface VimBuffer { 7 | name: string, 8 | cur: boolean, 9 | mod: boolean, 10 | } 11 | 12 | export interface QuickFixList { 13 | text: string, 14 | lnum: number, 15 | col: number, 16 | vcol?: number, 17 | pattern?: string, 18 | nr?: number, 19 | bufnr?: number, 20 | filename?: string, 21 | type?: string, 22 | valid?: boolean, 23 | } 24 | 25 | interface State { 26 | filetype: string 27 | cwd: string 28 | file: string 29 | colorscheme: string 30 | revision: number 31 | bufferType: string 32 | line: number 33 | column: number 34 | editorTopLine: number 35 | editorBottomLine: number 36 | } 37 | 38 | interface Position { 39 | line: number 40 | column: number 41 | editorTopLine: number 42 | editorBottomLine: number 43 | } 44 | 45 | type WindowPosition = [ string, number, number, number ] 46 | 47 | export interface Functions { 48 | VeonimState(): Promise, 49 | VeonimPosition(): Promise, 50 | VeonimCallEvent(event: string): void, 51 | VeonimCallback(id: number, result: any): void, 52 | Buffers(): Promise, 53 | OpenPaths(): Promise, 54 | getcwd(): Promise, 55 | getline(type: string | number, end?: string): Promise, 56 | expand(type: string): Promise, 57 | synIDattr(id: number, type: string): Promise, 58 | getpos(where: string): Promise, 59 | setloclist(window: number, list: QuickFixList[]): Promise, 60 | getqflist(): Promise, 61 | cursor(line: number, column: number): Promise, 62 | bufname(expr: string | number): Promise, 63 | getbufline(expr: string | number, startLine: number, endLine?: number | string): Promise, 64 | getbufvar(expr: string | number, varname?: string, defaultValue?: any): Promise, 65 | termopen(cmd: string, options: object): void, 66 | } 67 | -------------------------------------------------------------------------------- /src/extensions/extensions.ts: -------------------------------------------------------------------------------- 1 | import { basename } from 'path' 2 | 3 | export interface ExtensionInfo { 4 | name: string 5 | publisher: string 6 | } 7 | 8 | export enum ActivationEventType { 9 | WorkspaceContains = 'workspaceContains', 10 | Language = 'onLanguage', 11 | Command = 'onCommand', 12 | Debug = 'onDebug', 13 | DebugInitialConfigs = 'onDebugInitialConfigurations', 14 | DebugResolve = 'onDebugResolve', 15 | View = 'onView', 16 | Always = '*', 17 | } 18 | 19 | interface ActivationEvent { 20 | type: ActivationEventType 21 | value: string 22 | } 23 | 24 | export interface Disposable { 25 | dispose: () => any 26 | [index: string]: any 27 | } 28 | 29 | export interface Extension extends ExtensionInfo { 30 | config: any 31 | packagePath: string 32 | requirePath: string 33 | extensionDependencies: string[] 34 | activationEvents: ActivationEvent[] 35 | subscriptions: Set 36 | localize: Function 37 | } 38 | 39 | export const activateExtension = async (e: Extension): Promise => { 40 | const requirePath = e.requirePath 41 | const extName = basename(requirePath) 42 | console.log('activating extension:', requirePath) 43 | 44 | const extension = require(requirePath) 45 | if (!extension.activate) { 46 | console.error(`extension ${extName} does not have a .activate() method`) 47 | return [] as any[] 48 | } 49 | 50 | const context = { subscriptions: [] as any[] } 51 | 52 | const maybePromise = extension.activate(context) 53 | const isPromise = maybePromise && maybePromise.then 54 | if (isPromise) await maybePromise.catch((err: any) => console.error(extName, err)) 55 | 56 | context.subscriptions.forEach(sub => e.subscriptions.add(sub)) 57 | 58 | return context.subscriptions 59 | } 60 | -------------------------------------------------------------------------------- /src/langserv/capabilities.ts: -------------------------------------------------------------------------------- 1 | import { SymbolKind, CompletionItemKind } from 'vscode-languageserver-protocol' 2 | 3 | export default (cwd: string) => ({ 4 | rootPath: cwd, 5 | rootUri: 'file://' + cwd, 6 | capabilities: { 7 | workspace: { 8 | applyEdit: true, 9 | workspaceEdit: { 10 | documentChanges: true 11 | }, 12 | didChangeConfiguration: { 13 | dynamicRegistration: true 14 | }, 15 | didChangeWatchedFiles: { 16 | dynamicRegistration: true 17 | }, 18 | symbol: { 19 | dynamicRegistration: true, 20 | symbolKind: { 21 | valueSet: SymbolKind, 22 | } 23 | }, 24 | executeCommand: { 25 | dynamicRegistration: true 26 | }, 27 | }, 28 | textDocument: { 29 | synchronization: { 30 | dynamicRegistration: true, 31 | willSave: true, 32 | willSaveWaitUntil: true, 33 | didSave: true 34 | }, 35 | completion: { 36 | dynamicRegistration: true, 37 | completionItem: { 38 | snippetSupport: true 39 | }, 40 | completionItemKind: { 41 | valueSet: CompletionItemKind, 42 | } 43 | }, 44 | hover: { 45 | dynamicRegistration: true 46 | }, 47 | signatureHelp: { 48 | dynamicRegistration: true 49 | }, 50 | references: { 51 | dynamicRegistration: true 52 | }, 53 | documentHighlight: { 54 | dynamicRegistration: true 55 | }, 56 | documentSymbol: { 57 | dynamicRegistration: true, 58 | symbolKind: { 59 | valueSet: SymbolKind, 60 | } 61 | }, 62 | formatting: { 63 | dynamicRegistration: true 64 | }, 65 | rangeFormatting: { 66 | dynamicRegistration: true 67 | }, 68 | onTypeFormatting: { 69 | dynamicRegistration: true 70 | }, 71 | definition: { 72 | dynamicRegistration: true 73 | }, 74 | codeAction: { 75 | dynamicRegistration: true 76 | }, 77 | codeLens: { 78 | dynamicRegistration: true 79 | }, 80 | documentLink: { 81 | dynamicRegistration: true 82 | }, 83 | rename: { 84 | dynamicRegistration: true 85 | } 86 | } 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /src/langserv/patch.ts: -------------------------------------------------------------------------------- 1 | import { Position, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol' 2 | import { uriAsCwd, uriAsFile } from '../support/utils' 3 | import { join } from 'path' 4 | 5 | const samePos = (s: Position, e: Position) => s.line === e.line && s.character === e.character 6 | 7 | enum Operation { 8 | Delete = 'delete', 9 | Append = 'append', 10 | Replace = 'replace', 11 | } 12 | 13 | interface PatchOperation { 14 | op: Operation, 15 | val: string, 16 | start: Position, 17 | end: Position, 18 | } 19 | 20 | export interface Patch { 21 | cwd: string, 22 | file: string, 23 | path: string, 24 | operations: PatchOperation[], 25 | } 26 | 27 | const asOperation = (edit: TextEdit): PatchOperation => { 28 | const { newText: val, range: { start, end } } = edit 29 | const meta = { val, start, end } 30 | 31 | if (!val) return { ...meta, op: Operation.Delete } 32 | if (samePos(start, end)) return { ...meta, op: Operation.Append } 33 | return { ...meta, op: Operation.Replace } 34 | } 35 | 36 | const asPatch = (uri: string, edits: TextEdit[]): Patch => { 37 | const cwd = uriAsCwd(uri) 38 | const file = uriAsFile(uri) 39 | return { 40 | cwd, 41 | file, 42 | path: join(cwd, file), 43 | operations: edits.map(asOperation), 44 | } 45 | } 46 | 47 | export const workspaceEditToPatch = ({ changes = {}, documentChanges }: WorkspaceEdit): Patch[] => documentChanges 48 | ? documentChanges.map(({ textDocument, edits }) => asPatch(textDocument.uri, edits)) 49 | : Object.entries(changes).map(([ file, edits ]) => asPatch(file, edits)) 50 | -------------------------------------------------------------------------------- /src/langserv/update-server.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolConnection, DidOpenTextDocumentParams, DidChangeTextDocumentParams, WillSaveTextDocumentParams, DidSaveTextDocumentParams, DidCloseTextDocumentParams, TextDocumentSyncKind } from 'vscode-languageserver-protocol' 2 | import { vscLanguageToFiletypes } from '../langserv/vsc-languages' 3 | import TextDocumentManager from '../neovim/text-document-manager' 4 | import FullDocumentManager from '../neovim/full-document-manager' 5 | import { traceLANGSERV as log } from '../support/trace' 6 | import { Buffer } from '../neovim/types' 7 | import nvim from '../vscode/neovim' 8 | 9 | interface LanguageServer extends ProtocolConnection { 10 | textSyncKind: TextDocumentSyncKind 11 | untilInitialized: Promise 12 | pauseTextSync: boolean 13 | } 14 | 15 | interface UpdaterParams { 16 | server: LanguageServer 17 | languageId: string 18 | incremental: boolean 19 | initialBuffer: Buffer 20 | } 21 | 22 | const updater = ({ server, languageId, initialBuffer, incremental }: UpdaterParams) => { 23 | const limitedFiletypes = vscLanguageToFiletypes(languageId) 24 | 25 | const send = (method: string, params: any) => { 26 | server.sendNotification(`textDocument/${method}`, params) 27 | log(`NOTIFY --> textDocument/${method}`, params) 28 | } 29 | 30 | const { on, dispose, manualBindBuffer } = incremental 31 | ? TextDocumentManager(nvim, limitedFiletypes) 32 | : FullDocumentManager(nvim, limitedFiletypes) 33 | 34 | manualBindBuffer(initialBuffer) 35 | 36 | on.didOpen(({ uri, version, languageId, text }) => send('didOpen', { 37 | textDocument: { uri, version, languageId, text }, 38 | } as DidOpenTextDocumentParams)) 39 | 40 | on.didChange(({ uri, version, contentChanges }) => { 41 | if (server.pauseTextSync) return 42 | 43 | send('didChange', { 44 | textDocument: { uri, version }, 45 | contentChanges, 46 | } as DidChangeTextDocumentParams) 47 | }) 48 | 49 | on.willSave(({ uri }) => send('willSave', { 50 | reason: 1, 51 | textDocument: { uri }, 52 | } as WillSaveTextDocumentParams)) 53 | 54 | on.didSave(({ uri }) => send('didSave', { 55 | textDocument: { uri }, 56 | } as DidSaveTextDocumentParams)) 57 | 58 | on.didClose(({ uri }) => send('didClose', { 59 | textDocument: { uri }, 60 | } as DidCloseTextDocumentParams)) 61 | 62 | return { dispose } 63 | } 64 | 65 | export default async (server: LanguageServer, languageId: string) => { 66 | await server.untilInitialized 67 | 68 | const params = { server, languageId, initialBuffer: nvim.current.buffer } 69 | 70 | if (server.textSyncKind === TextDocumentSyncKind.Incremental) { 71 | return updater(Object.assign(params, { incremental: true })) 72 | } 73 | 74 | if (server.textSyncKind === TextDocumentSyncKind.Full) { 75 | console.warn(`Warning: Language server for ${languageId} does not support incremental text synchronization. This is a negative performance impact - especially on large files.`) 76 | return updater(Object.assign(params, { incremental: false })) 77 | } 78 | 79 | return { dispose: () => {} } 80 | } 81 | -------------------------------------------------------------------------------- /src/langserv/vsc-languages.ts: -------------------------------------------------------------------------------- 1 | // this module exists because we can't always map 1:1 between vim 2 | // filetypes and vscode language ids. for example in vim we might 3 | // have javascript.jsx which the equivalent in vscode would be 4 | // javascriptreact 5 | 6 | // valid vscode identifiers 7 | // https://code.visualstudio.com/docs/languages/identifiers 8 | 9 | const filetypes = new Map([ 10 | ['javascript.jsx', 'javascript'], 11 | ['typescript.tsx', 'typescript'], 12 | ]) 13 | 14 | export default (filetype: string) => filetypes.get(filetype) || filetype 15 | export const vscLanguageToFiletypes = (languageId: string): string[] => [...filetypes] 16 | .reduce((res, [ ft, id ]) => { 17 | if (id === languageId) res.push(ft) 18 | return res 19 | }, [ languageId ]) 20 | -------------------------------------------------------------------------------- /src/messaging/dispatch.ts: -------------------------------------------------------------------------------- 1 | import { Watchers } from '../support/utils' 2 | 3 | const watchers = new Watchers() 4 | const cache = new Map() 5 | const buffer = (event: string, data: any) => cache.has(event) 6 | ? cache.get(event)!.push(data) 7 | : cache.set(event, [data]) 8 | 9 | export const unsub = (event: string, cb: Function) => watchers.remove(event, cb) 10 | export const sub = (event: string, cb: (...args: any[]) => void) => watchers.add(event, cb) 11 | 12 | export const pub = (event: string, ...args: any[]) => { 13 | if (!watchers.has(event)) return buffer(event, args) 14 | watchers.notify(event, ...args) 15 | } 16 | 17 | export const processAnyBuffered = (event: string) => { 18 | if (!cache.has(event)) return 19 | cache.get(event)!.forEach(d => watchers.notify(event, ...d)) 20 | cache.delete(event) 21 | } 22 | -------------------------------------------------------------------------------- /src/messaging/rpc.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { ID } from '../support/utils' 3 | 4 | export enum NeovimRPCDataType { 5 | onRequest, 6 | onResponse, 7 | onNotification, 8 | } 9 | 10 | export interface NeovimRPC { 11 | notify: (name: string, args: any[]) => void 12 | request: (name: string, args: any[]) => Promise 13 | onEvent: (event: string, fn: (data: any) => void) => void 14 | handleRequest: (event: string, fn: Function) => void 15 | } 16 | 17 | export interface NeovimRPCAPI extends NeovimRPC { 18 | onData: (dataType: NeovimRPCDataType, data: any) => void 19 | } 20 | 21 | export default (send: (data: any[]) => void): NeovimRPCAPI => { 22 | const requestHandlers = new Map() 23 | const pendingRequests = new Map() 24 | const watchers = new EventEmitter() 25 | const id = ID() 26 | let onRedrawFn = (_a: any[]) => {} 27 | 28 | const noRequestMethodFound = (id: number) => send([1, id, 'no one was listening for your request, sorry', null]) 29 | 30 | const onVimRequest = (id: number, method: string, args: any[]) => { 31 | const reqHandler = requestHandlers.get(method) 32 | if (!reqHandler) return noRequestMethodFound(id) 33 | 34 | const maybePromise = reqHandler(...args as any[]) 35 | 36 | if (maybePromise && maybePromise.then) maybePromise 37 | .then((result: any) => send([1, id, null, result])) 38 | .catch((err: string) => send([1, id, err, null])) 39 | } 40 | 41 | const onResponse = (id: number, error: string, result: any) => { 42 | if (!pendingRequests.has(id)) return 43 | 44 | const { done, fail } = pendingRequests.get(id) 45 | error ? fail(error) : done(result) 46 | pendingRequests.delete(id) 47 | } 48 | 49 | const onNotification = (method: string, args: any[]) => method === 'redraw' 50 | ? onRedrawFn(args) 51 | : watchers.emit(method, args) 52 | 53 | const api = {} as NeovimRPC 54 | api.request = (name, args) => { 55 | const reqId = id.next() 56 | send([0, reqId, name, args]) 57 | return new Promise((done, fail) => pendingRequests.set(reqId, { done, fail })) 58 | } 59 | 60 | api.notify = (name, args) => send([2, name, args]) 61 | 62 | // why === redraw? because there will only be one redraw fn and since it's a hot 63 | // path for perf, there is no need to iterate through the watchers to call redraw 64 | api.onEvent = (event, fn) => event === 'redraw' 65 | ? onRedrawFn = fn 66 | : watchers.on(event, fn) 67 | 68 | api.handleRequest = (event, fn) => requestHandlers.set(event, fn) 69 | 70 | const onData: NeovimRPCAPI['onData'] = (type, d) => { 71 | if (type === 0) onVimRequest(d[0] as number, d[1].toString(), d[2] as any[]) 72 | else if (type === 1) onResponse(d[0] as number, d[1] as string, d[2]) 73 | else if (type === 2) onNotification(d[0].toString(), d[1] as any[]) 74 | } 75 | 76 | return { ...api, onData } 77 | } 78 | -------------------------------------------------------------------------------- /src/messaging/session-transport.ts: -------------------------------------------------------------------------------- 1 | import CreateTransport from '../messaging/transport' 2 | import { createConnection } from 'net' 3 | 4 | interface Client { 5 | id: number 6 | path: string 7 | socket: NodeJS.Socket 8 | } 9 | 10 | export default (onDataSender?: (...args: any[]) => void) => { 11 | let sendRecvDataFn = (..._: any[]) => {} 12 | if (onDataSender) sendRecvDataFn = onDataSender 13 | 14 | const { encoder, decoder } = CreateTransport() 15 | const clients = new Map() 16 | const config = { current: -1 } 17 | let buffer: any[] = [] 18 | let connected = false 19 | 20 | const connectTo = ({ id, path }: { id: number, path: string }) => { 21 | connected = false 22 | const socket = createConnection(path) 23 | 24 | socket.on('end', () => { 25 | socket.unpipe() 26 | clients.delete(id) 27 | }) 28 | 29 | clients.set(id, { id, path, socket }) 30 | } 31 | 32 | const switchTo = (id: number) => { 33 | if (!clients.has(id)) return 34 | const { socket } = clients.get(id)! 35 | 36 | if (config.current > -1) { 37 | encoder.unpipe() 38 | const socketMaybe = clients.get(config.current) 39 | if (socketMaybe) socketMaybe.socket.unpipe() 40 | } 41 | 42 | encoder.pipe(socket) 43 | socket.pipe(decoder, { end: false }) 44 | 45 | if (buffer.length) { 46 | buffer.forEach(data => encoder.write(data)) 47 | buffer = [] 48 | } 49 | 50 | connected = true 51 | config.current = id 52 | } 53 | 54 | const send = (data: any) => { 55 | if (!connected) buffer.push(data) 56 | else encoder.write(data) 57 | } 58 | 59 | const onRecvData = (fn: (...args: any[]) => void) => sendRecvDataFn = fn 60 | decoder.on('data', ([type, ...d]: [number, any]) => sendRecvDataFn([ type, d ])) 61 | 62 | return { send, connectTo, switchTo, onRecvData } 63 | } 64 | -------------------------------------------------------------------------------- /src/messaging/transport.ts: -------------------------------------------------------------------------------- 1 | import { encode, decode, createEncodeStream, createDecodeStream, createCodec } from 'msgpack-lite' 2 | import { ExtType } from '../neovim/protocol' 3 | 4 | const ExtContainer = (kind: number, id: any) => ({ kind, id, extContainer: true }) 5 | 6 | interface Encoder { 7 | unpipe(): NodeJS.WritableStream, 8 | pipe(stdin: NodeJS.WritableStream): NodeJS.WritableStream, 9 | write(data: any): boolean, 10 | } 11 | 12 | export default () => { 13 | const codec = createCodec() 14 | 15 | codec.addExtUnpacker(ExtType.Buffer, data => ExtContainer(ExtType.Buffer, decode(data))) 16 | codec.addExtUnpacker(ExtType.Window, data => ExtContainer(ExtType.Window, decode(data))) 17 | codec.addExtUnpacker(ExtType.Tabpage, data => ExtContainer(ExtType.Tabpage, decode(data))) 18 | 19 | // TODO: figure out why peoples parents dropped them as babies 20 | let crustyJugglers: NodeJS.WritableStream // WTF x 8 21 | const cheekyBuffoons = createEncodeStream({ codec }) // WTF x 1 22 | 23 | const encoder: Encoder = { 24 | unpipe: () => cheekyBuffoons.unpipe(), 25 | pipe: (stdin: NodeJS.WritableStream) => crustyJugglers = cheekyBuffoons.pipe(stdin), // WTF x 999 26 | write: (data: any) => crustyJugglers.write(encode(data)) // WTF x 524 27 | } 28 | 29 | const decoder = createDecodeStream({ codec }) 30 | 31 | return { encoder, decoder } 32 | } 33 | -------------------------------------------------------------------------------- /src/messaging/worker-client.ts: -------------------------------------------------------------------------------- 1 | import { onFnCall, proxyFn, Watchers, uuid, CreateTask } from '../support/utils' 2 | import { EV_CREATE_VIM, EV_SWITCH_VIM } from '../support/constants' 3 | import { EventEmitter } from 'events' 4 | 5 | type EventFn = { [index: string]: (...args: any[]) => void } 6 | type RequestEventFn = { [index: string]: (...args: any[]) => Promise } 7 | 8 | // your linter may complain that postMessage has the wrong number of args. this 9 | // is probably because the linter does not understand that we are in a web 10 | // worker context. (assumes we are in the main web thread). i tried to make 11 | // this work with the tsconfigs, but alas: i am not clever enough 12 | const send = (data: any) => (postMessage as any)(data) 13 | 14 | const internalEvents = new EventEmitter() 15 | internalEvents.setMaxListeners(200) 16 | const watchers = new Watchers() 17 | const pendingRequests = new Map() 18 | 19 | onmessage = ({ data: [e, data = [], id] }: MessageEvent) => { 20 | if (e === EV_CREATE_VIM) return internalEvents.emit('vim.create', ...data) 21 | if (e === EV_SWITCH_VIM) return internalEvents.emit('vim.switch', ...data) 22 | 23 | if (!id) return watchers.notify(e, ...data) 24 | 25 | if (pendingRequests.has(id)) { 26 | pendingRequests.get(id)(data) 27 | pendingRequests.delete(id) 28 | return 29 | } 30 | 31 | watchers.notifyFn(e, cb => { 32 | const resultOrPromise = cb(...data) 33 | if (!resultOrPromise) return 34 | if (resultOrPromise.then) resultOrPromise.then((res: any) => send([e, res, id])) 35 | else send([e, resultOrPromise, id]) 36 | }) 37 | } 38 | 39 | export const onCreateVim = (fn: (info: any) => void) => internalEvents.on('vim.create', fn) 40 | export const onSwitchVim = (fn: (info: any) => void) => internalEvents.on('vim.switch', fn) 41 | 42 | export const call: EventFn = onFnCall((event: string, args: any[]) => send([event, args])) 43 | export const on = proxyFn((event: string, cb: (data: any) => void) => watchers.add(event, cb)) 44 | export const request: RequestEventFn = onFnCall((event: string, args: any[]) => { 45 | const task = CreateTask() 46 | const id = uuid() 47 | pendingRequests.set(id, task.done) 48 | send([event, args, id]) 49 | return task.promise 50 | }) 51 | -------------------------------------------------------------------------------- /src/messaging/worker.ts: -------------------------------------------------------------------------------- 1 | import { onFnCall, proxyFn, Watchers, uuid, CreateTask } from '../support/utils' 2 | import { EV_CREATE_VIM, EV_SWITCH_VIM } from '../support/constants' 3 | import { onCreateVim, onSwitchVim } from '../core/sessions' 4 | 5 | type EventFn = { [index: string]: (...args: any[]) => void } 6 | type RequestEventFn = { [index: string]: (...args: any[]) => Promise } 7 | 8 | export default (name: string) => { 9 | const worker = new Worker(`${__dirname}/../workers/${name}.js`) 10 | const watchers = new Watchers() 11 | const pendingRequests = new Map() 12 | 13 | const call: EventFn = onFnCall((event: string, args: any[]) => worker.postMessage([event, args])) 14 | const on = proxyFn((event: string, cb: (data: any) => void) => watchers.add(event, cb)) 15 | const request: RequestEventFn = onFnCall((event: string, args: any[]) => { 16 | const task = CreateTask() 17 | const id = uuid() 18 | pendingRequests.set(id, task.done) 19 | worker.postMessage([event, args, id]) 20 | return task.promise 21 | }) 22 | 23 | worker.onmessage = ({ data: [e, data = [], id] }: MessageEvent) => { 24 | if (!id) return watchers.notify(e, ...data) 25 | 26 | if (pendingRequests.has(id)) { 27 | pendingRequests.get(id)(data) 28 | pendingRequests.delete(id) 29 | return 30 | } 31 | 32 | watchers.notifyFn(e, cb => { 33 | const resultOrPromise = cb(...data) 34 | if (!resultOrPromise) return 35 | if (resultOrPromise.then) resultOrPromise.then((res: any) => worker.postMessage([e, res, id])) 36 | else worker.postMessage([e, resultOrPromise, id]) 37 | }) 38 | } 39 | 40 | onCreateVim(m => call[EV_CREATE_VIM](m)) 41 | onSwitchVim(m => call[EV_SWITCH_VIM](m)) 42 | 43 | return { on, call, request } 44 | } 45 | -------------------------------------------------------------------------------- /src/services/colorizer.ts: -------------------------------------------------------------------------------- 1 | import Worker from '../messaging/worker' 2 | 3 | export interface ColorData { 4 | color: string, 5 | text: string, 6 | highlight?: boolean, 7 | } 8 | 9 | const api = Worker('neovim-colorizer') 10 | export default api 11 | 12 | export const colorizeMarkdownToHTML = (markdown: string): Promise => { 13 | return api.request.colorizeMarkdownToHTML(markdown) 14 | } 15 | -------------------------------------------------------------------------------- /src/services/electron.ts: -------------------------------------------------------------------------------- 1 | import nvim from '../core/neovim' 2 | import { remote } from 'electron' 3 | 4 | nvim.onAction('version', () => nvim.cmd(`echo 'Veonim v${remote.app.getVersion()}'`)) 5 | nvim.onAction('hide', () => remote.app.hide()) 6 | nvim.onAction('quit', () => remote.app.quit()) 7 | nvim.onAction('maximize', () => remote.getCurrentWindow().maximize()) 8 | nvim.onAction('devtools', () => remote.getCurrentWebContents().toggleDevTools()) 9 | nvim.onAction('fullscreen', () => { 10 | const win = remote.getCurrentWindow() 11 | win.setFullScreen(!win.isFullScreen()) 12 | }) 13 | -------------------------------------------------------------------------------- /src/services/remote.ts: -------------------------------------------------------------------------------- 1 | import HttpServer from '../support/http-server' 2 | import { relative, join } from 'path' 3 | import nvim from '../core/neovim' 4 | 5 | interface RemoteRequest { cwd: string, file: string } 6 | 7 | const load = async ({ cwd, file }: RemoteRequest) => { 8 | if (!file) return 9 | const vimCwd = nvim.state.cwd 10 | const base = cwd.includes(vimCwd) ? relative(vimCwd, cwd) : cwd 11 | const path = join(base, file) 12 | nvim.cmd(`e ${path}`) 13 | } 14 | 15 | HttpServer(42320).then(({ port, onJsonRequest }) => { 16 | process.env.VEONIM_REMOTE_PORT = port + '' 17 | nvim.cmd(`let $VEONIM_REMOTE_PORT='${port}'`) 18 | onJsonRequest((data, reply) => (load(data), reply(201))) 19 | }) 20 | -------------------------------------------------------------------------------- /src/services/watch-reload.ts: -------------------------------------------------------------------------------- 1 | import { exists, watchFile } from '../support/utils' 2 | import { onSwitchVim } from '../core/sessions' 3 | import nvim from '../core/neovim' 4 | import { join } from 'path' 5 | 6 | const sessions = new Map>() 7 | const watchers = new Map() 8 | let currentSession = new Set() 9 | 10 | const anySessionsHaveFile = (file: string) => [...sessions.values()].some(s => s.has(file)) 11 | 12 | onSwitchVim(id => { 13 | if (sessions.has(id)) currentSession = sessions.get(id)! 14 | else sessions.set(id, currentSession = new Set()) 15 | nvim.cmd(`checktime`) 16 | }) 17 | 18 | nvim.on.bufLoad(async () => { 19 | const filepath = join(nvim.state.cwd, nvim.state.file) 20 | if (!filepath) return 21 | if (!await exists(filepath)) return 22 | currentSession.add(filepath) 23 | const w = await watchFile(filepath, () => currentSession.has(filepath) && nvim.cmd(`checktime ${filepath}`)) 24 | watchers.set(filepath, w) 25 | }) 26 | 27 | nvim.on.bufClose(() => { 28 | const filepath = join(nvim.state.cwd, nvim.state.file) 29 | if (!filepath) return 30 | currentSession.delete(filepath) 31 | if (anySessionsHaveFile(filepath)) return 32 | watchers.has(filepath) && watchers.get(filepath)!.close() 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /src/support/binaries.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptions, ChildProcess } from 'child_process' 2 | 3 | const platforms = new Map([ 4 | ['darwin', 'mac'], 5 | ['win32', 'win'], 6 | ['linux', 'linux'], 7 | ]) 8 | 9 | const suffix = platforms.get(process.platform) 10 | if (!suffix) throw new Error(`Unsupported platform ${process.platform}`) 11 | 12 | type Binary = (args?: string[], options?: SpawnOptions) => ChildProcess 13 | 14 | interface INeovim { 15 | run: Binary, 16 | runtime: string, 17 | path: string, 18 | } 19 | 20 | export const Neovim: INeovim = { 21 | run: require(`@veonim/neovim-${suffix}`).default, 22 | runtime: require(`@veonim/neovim-${suffix}`).vimruntime, 23 | path: require(`@veonim/neovim-${suffix}`).vimpath, 24 | } 25 | 26 | export const Ripgrep: Binary = require(`@veonim/ripgrep-${suffix}`).default 27 | export const Archiver: Binary = require(`all-other-unzip-libs-suck-${suffix}`).default 28 | -------------------------------------------------------------------------------- /src/support/colorize-with-highlight.ts: -------------------------------------------------------------------------------- 1 | import { ColorData } from '../services/colorizer' 2 | 3 | interface FilterResult { 4 | line: string, 5 | start: { 6 | line: number, 7 | column: number, 8 | }, 9 | end: { 10 | line: number, 11 | column: number, 12 | } 13 | } 14 | 15 | interface ColorizedFilterResult extends FilterResult { 16 | colorizedLine: ColorData[] 17 | } 18 | 19 | const recolor = (colors: ColorData[], start: number, end: number, color: string) => colors.reduce((res, m, ix) => { 20 | const prev: ColorData = res[res.length - 1] 21 | const needsRecolor = ix >= start && ix <= end 22 | const colorOfInterest = needsRecolor ? color : m.color 23 | const reusePreviousGroup = prev && prev.color === colorOfInterest 24 | 25 | reusePreviousGroup 26 | ? prev.text += m.text 27 | : res.push({ color: colorOfInterest, text: m.text }) 28 | 29 | return res 30 | }, [] as ColorData[]) 31 | 32 | const markAsHighlight = (colors: ColorData[], start: number, end: number) => colors.reduce((res, m, ix) => { 33 | const prev: ColorData = res[res.length - 1] 34 | const charIsHighlighted = ix >= start && ix <= end 35 | const reusePreviousGroup = prev && prev.color === m.color && prev.highlight === charIsHighlighted 36 | 37 | reusePreviousGroup 38 | ? prev.text += m.text 39 | : res.push({ color: m.color, text: m.text, highlight: charIsHighlighted }) 40 | 41 | return res 42 | }, [] as ColorData[]) 43 | 44 | export default (data: ColorizedFilterResult, highlightColor?: string) => highlightColor 45 | ? recolor(data.colorizedLine, data.start.column, data.end.column, highlightColor) 46 | : markAsHighlight(data.colorizedLine, data.start.column, data.end.column) 47 | -------------------------------------------------------------------------------- /src/support/constants.ts: -------------------------------------------------------------------------------- 1 | export const SHADOW_BUFFER_TYPE = 'veonim-shadow-buffer' 2 | export const EV_CREATE_VIM = '__internal__onCreateVim' 3 | export const EV_SWITCH_VIM = '__internal__onSwitchVim' 4 | -------------------------------------------------------------------------------- /src/support/dependency-manager.ts: -------------------------------------------------------------------------------- 1 | import { configPath, readFile, exists, isOnline } from '../support/utils' 2 | import installExtensions from '../support/manage-extensions' 3 | import installPlugins from '../support/manage-plugins' 4 | import { watchConfig } from '../config/config-reader' 5 | import { join } from 'path' 6 | 7 | const vimrcPath = join(configPath, 'nvim/init.vim') 8 | 9 | const getVimrcLines = async () => (await readFile(vimrcPath)) 10 | .toString() 11 | .split('\n') 12 | 13 | const refreshDependencies = async () => { 14 | const online = await isOnline('github.com') 15 | if (!online) return 16 | 17 | const vimrcExists = await exists(vimrcPath) 18 | if (!vimrcExists) return 19 | 20 | const configLines = await getVimrcLines() 21 | installExtensions(configLines) 22 | installPlugins(configLines) 23 | } 24 | 25 | export default () => { 26 | refreshDependencies() 27 | watchConfig('nvim/init.vim', refreshDependencies) 28 | } 29 | -------------------------------------------------------------------------------- /src/support/download.ts: -------------------------------------------------------------------------------- 1 | import Worker from '../messaging/worker' 2 | const { request } = Worker('download') 3 | 4 | export const url = { 5 | github: (user: string, repo: string) => `https://github.com/${user}/${repo}/archive/master.zip`, 6 | vscode: (author: string, name: string, version = 'latest') => `https://${author}.gallery.vsassets.io/_apis/public/gallery/publisher/${author}/extension/${name}/${version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage`, 7 | } 8 | 9 | export const download = (url: string, path: string): Promise => request.download(url, path) 10 | -------------------------------------------------------------------------------- /src/support/fake-module.ts: -------------------------------------------------------------------------------- 1 | import { hasUpperCase, is, objDeepGet } from '../support/utils' 2 | 3 | const Module = require('module') 4 | const originalLoader = Module._load 5 | const fakeModules = new Map() 6 | 7 | Module._load = (request: string, ...args: any[]) => fakeModules.has(request) 8 | ? fakeModules.get(request) 9 | : originalLoader(request, ...args) 10 | 11 | export default (moduleName: string, fakeImplementation: any, onMissing?: (name: string, path: string) => void) => { 12 | const getFake = objDeepGet(fakeImplementation) 13 | const notifyOfMissingPath = (path: string) => is.function(onMissing) && onMissing!(moduleName, path) 14 | 15 | const fake = (path = [] as string[]): any => new Proxy({}, { get: (_, key: string) => { 16 | const objPath = [ ...path, key ] 17 | const found = getFake(objPath) 18 | 19 | if (found) return found 20 | else notifyOfMissingPath(objPath.join('.')) 21 | 22 | if (hasUpperCase(key[0])) return class Anon {} 23 | return fake(objPath) 24 | } }) 25 | 26 | fakeModules.set(moduleName, fake()) 27 | } 28 | -------------------------------------------------------------------------------- /src/support/fetch.ts: -------------------------------------------------------------------------------- 1 | import { RequestOptions, IncomingMessage } from 'http' 2 | 3 | interface FetchOptions extends RequestOptions { 4 | data?: any 5 | } 6 | 7 | type FetchStreamFn = (url: string, options?: FetchOptions) => Promise 8 | 9 | export const fetchStream: FetchStreamFn = (url, options = { method: 'GET' }) => new Promise((done, fail) => { 10 | const { data, ...requestOptions } = options 11 | const opts = { ...require('url').parse(url), ...requestOptions } 12 | 13 | const req = require(url.startsWith('https://') ? 'https' : 'http').request(opts, (res: IncomingMessage) => done(res.statusCode! >= 300 && res.statusCode! < 400 14 | ? fetchStream(res.headers.location!, options) 15 | : res)) 16 | 17 | req.on('error', fail) 18 | if (data) req.write(data) 19 | req.end() 20 | }) 21 | -------------------------------------------------------------------------------- /src/support/fs-watch.ts: -------------------------------------------------------------------------------- 1 | import { throttle } from '../support/utils' 2 | import { promisify as P } from 'util' 3 | import { EventEmitter } from 'events' 4 | import { join } from 'path' 5 | import * as fs from 'fs' 6 | 7 | const watchers = new EventEmitter() 8 | const watchedParentPaths = new Map() 9 | 10 | const emptyStat = { isSymbolicLink: () => false } 11 | const getFSStat = async (path: string) => P(fs.lstat)(path).catch((_) => emptyStat) 12 | 13 | const getRealPath = async (path: string) => { 14 | const stat = await getFSStat(path) 15 | const isSymbolicLink = stat.isSymbolicLink() 16 | if (!isSymbolicLink) return path 17 | return P(fs.readlink)(path) 18 | } 19 | 20 | const watchDir = (path: string) => fs.watch(path, ((_, file) => { 21 | const fullpath = join(path, file) 22 | watchers.emit(fullpath) 23 | })) 24 | 25 | export const watchFile = async (path: string, callback: () => void) => { 26 | const realpath = await getRealPath(path) 27 | const parentPath = join(realpath, '../') 28 | const notifyCallback = throttle(callback, 15) 29 | watchers.on(realpath, notifyCallback) 30 | if (!watchedParentPaths.has(parentPath)) watchDir(parentPath) 31 | return { close: () => watchers.removeListener(realpath, notifyCallback) } 32 | } 33 | -------------------------------------------------------------------------------- /src/support/get-file-contents.ts: -------------------------------------------------------------------------------- 1 | import Worker from '../messaging/worker' 2 | import nvim from '../core/neovim' 3 | 4 | interface LineContents { 5 | ix: number, 6 | line: string, 7 | } 8 | 9 | const worker = Worker('get-file-lines') 10 | const isCurrentBuffer = (path: string) => path === nvim.state.absoluteFilepath 11 | 12 | const getFromCurrentBuffer = async (lines: number[]) => { 13 | const buffer = nvim.current.buffer 14 | const getLineRequests = lines.map(async ix => ({ 15 | ix, 16 | line: await buffer.getLine(ix), 17 | })) 18 | 19 | return Promise.all(getLineRequests) 20 | } 21 | 22 | export const getLines = (path: string, lines: number[]): Promise => isCurrentBuffer(path) 23 | ? getFromCurrentBuffer(lines) 24 | : worker.request.getLines(path, lines) 25 | -------------------------------------------------------------------------------- /src/support/git.ts: -------------------------------------------------------------------------------- 1 | import { shell, exists, watchFile } from '../support/utils' 2 | import * as dispatch from '../messaging/dispatch' 3 | import nvim from '../core/neovim' 4 | import * as path from 'path' 5 | 6 | const watchers: { branch: any, status: any } = { 7 | branch: undefined, 8 | status: undefined, 9 | } 10 | 11 | const getStatus = async (cwd: string) => { 12 | const res = await shell(`git diff --numstat`, { cwd }) 13 | const status = res 14 | .split('\n') 15 | .map(s => { 16 | const [ , additions, deletions ] = s.match(/^(\d+)\s+(\d+)\s+.*$/) || [] as any 17 | return { 18 | additions: parseInt(additions) || 0, 19 | deletions: parseInt(deletions) || 0, 20 | } 21 | }) 22 | .reduce((total, item) => { 23 | total.additions += item.additions 24 | total.deletions += item.deletions 25 | return total 26 | }, { additions: 0, deletions: 0 }) 27 | 28 | dispatch.pub('git:status', status) 29 | } 30 | 31 | const getBranch = async (cwd: string) => { 32 | const branch = await shell(`git rev-parse --abbrev-ref HEAD`, { cwd }) 33 | dispatch.pub('git:branch', branch) 34 | } 35 | 36 | nvim.on.bufWrite(() => getStatus(nvim.state.cwd)) 37 | 38 | nvim.watchState.cwd(async (cwd: string) => { 39 | getBranch(cwd) 40 | getStatus(cwd) 41 | 42 | if (watchers.branch) watchers.branch.close() 43 | if (watchers.status) watchers.status.close() 44 | 45 | const headPath = path.join(cwd, '.git/HEAD') 46 | const indexPath = path.join(cwd, '.git/index') 47 | 48 | if (await exists(headPath)) { 49 | watchers.branch = await watchFile(headPath, () => (getBranch(cwd), getStatus(cwd))) 50 | } 51 | if (await exists(indexPath)) { 52 | watchers.status = await watchFile(indexPath, () => getStatus(cwd)) 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /src/support/http-server.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server, IncomingMessage, ServerResponse } from 'http' 2 | 3 | interface HttpServer { 4 | server: Server, 5 | port: number, 6 | onRequest(handler: (req: IncomingMessage, res: ServerResponse) => void): void, 7 | onJsonRequest(handler: (req: T, res: (status: number, data?: any) => void) => void): void, 8 | } 9 | 10 | const reply = (res: ServerResponse) => (status = 200, data?: any) => { 11 | res.writeHead(status) 12 | res.end(data) 13 | } 14 | 15 | const attemptStart = (port = 8001, srv = createServer()): Promise => new Promise((fin, fail) => { 16 | srv.on('error', e => port < 65536 17 | ? attemptStart(port + 1, srv) 18 | : fail(`tried a bunch of ports but failed ${e}`)) 19 | 20 | srv.listen(port, () => fin({ 21 | server: srv, 22 | port: srv.address().port, 23 | onRequest: cb => srv.on('request', cb), 24 | onJsonRequest: cb => srv.on('request', (req: IncomingMessage, res: ServerResponse) => { 25 | let buf = '' 26 | req.on('data', m => buf += m) 27 | req.on('end', () => { try { cb(JSON.parse(buf), reply(res)) } catch (e) {} }) 28 | }) 29 | })) 30 | }) 31 | 32 | export default attemptStart 33 | -------------------------------------------------------------------------------- /src/support/local-storage.ts: -------------------------------------------------------------------------------- 1 | import { fromJSON } from '../support/utils' 2 | 3 | export const removeItem = (key: string) => localStorage.removeItem(key) 4 | export const setItem = (key: string, value: any) => localStorage.setItem(key, JSON.stringify(value)) 5 | export const getItem = (key: string, defaultValue = {} as T): T => { 6 | const raw = localStorage.getItem(key) 7 | if (!raw) return defaultValue 8 | return fromJSON(raw).or(defaultValue) 9 | } 10 | 11 | export const setTemp = (key: string, value: any) => sessionStorage.setItem(key, JSON.stringify(value)) 12 | export const getTemp = (key: string, defaultValue = {} as T): T => { 13 | const raw = sessionStorage.getItem(key) 14 | if (!raw) return defaultValue 15 | return fromJSON(raw).or(defaultValue) 16 | } 17 | -------------------------------------------------------------------------------- /src/support/localize.ts: -------------------------------------------------------------------------------- 1 | import { fromJSON, readFile, exists } from '../support/utils' 2 | 3 | const localize = (lang: any) => (value: string) => { 4 | // assumes that the entire value is a label. aka % at beginning 5 | // and end. this is from observations of package.nls.json 6 | const [ /*match*/, key = '' ] = value.match(/^%(.*?)%$/) || [] 7 | return Reflect.get(lang, key) 8 | } 9 | 10 | export default async (languageFilePath: string) => { 11 | const languageFileExists = await exists(languageFilePath) 12 | if (!languageFileExists) return (value: string) => value 13 | 14 | const languageRaw = await readFile(languageFilePath) 15 | const languageData = fromJSON(languageRaw).or({}) 16 | return localize(languageData) 17 | } 18 | -------------------------------------------------------------------------------- /src/support/manage-extensions.ts: -------------------------------------------------------------------------------- 1 | import { exists, getDirs, is, remove as removePath } from '../support/utils' 2 | import { load as loadExtensions } from '../core/extensions' 3 | import { NotifyKind, notify } from '../ui/notifications' 4 | import { EXT_PATH } from '../config/default-configs' 5 | import { url, download } from '../support/download' 6 | import { join } from 'path' 7 | 8 | interface Extension { 9 | name: string, 10 | user: string, 11 | repo: string, 12 | installed: boolean, 13 | } 14 | 15 | enum ExtensionKind { 16 | Github, 17 | VSCode, 18 | } 19 | 20 | const parseExtensionDefinition = (text: string) => { 21 | const isVscodeExt = text.toLowerCase().startsWith('vscode:extension') 22 | const [ , user = '', repo = '' ] = isVscodeExt 23 | ? (text.match(/^(?:vscode:extension\/)([^\.]+)\.(.*)/) || []) 24 | : (text.match(/^([^/]+)\/(.*)/) || []) 25 | 26 | return { user, repo, kind: isVscodeExt ? ExtensionKind.VSCode : ExtensionKind.Github } 27 | } 28 | 29 | const getExtensions = async (configLines: string[]) => Promise.all(configLines 30 | .filter(line => /^VeonimExt(\s*)/.test(line)) 31 | .map(line => (line.match(/^VeonimExt(\s*)(?:"|')(\S+)(?:"|')/) || [])[2]) 32 | .filter(is.string) 33 | .map(parseExtensionDefinition) 34 | .map(async m => { 35 | const name = `${m.user}--${m.repo}` 36 | 37 | return { 38 | ...m, 39 | name, 40 | installed: await exists(join(EXT_PATH, name)), 41 | } 42 | })) 43 | 44 | const removeExtraneous = async (extensions: Extension[]) => { 45 | const dirs = await getDirs(EXT_PATH) 46 | const extensionInstalled = (path: string) => extensions.some(e => e.name === path) 47 | const toRemove = dirs.filter(d => !extensionInstalled(d.name)) 48 | 49 | toRemove.forEach(dir => removePath(dir.path)) 50 | } 51 | 52 | export default async (configLines: string[]) => { 53 | const extensions = await getExtensions(configLines).catch() 54 | const extensionsNotInstalled = extensions.filter(ext => !ext.installed) 55 | if (!extensionsNotInstalled.length) return removeExtraneous(extensions) 56 | 57 | notify(`Found ${extensionsNotInstalled.length} extensions. Installing...`, NotifyKind.System) 58 | 59 | const installed = await Promise.all(extensions.map(e => { 60 | const isVscodeExt = e.kind === ExtensionKind.VSCode 61 | const destination = join(EXT_PATH, `${e.user}--${e.repo}`) 62 | const downloadUrl = isVscodeExt ? url.vscode(e.user, e.repo) : url.github(e.user, e.repo) 63 | 64 | return download(downloadUrl, destination) 65 | })) 66 | 67 | const installedOk = installed.filter(m => m).length 68 | const installedFail = installed.filter(m => !m).length 69 | 70 | if (installedOk) notify(`Installed ${installedOk} extensions!`, NotifyKind.Success) 71 | if (installedFail) notify(`Failed to install ${installedFail} extensions. See devtools console for more info.`, NotifyKind.Error) 72 | 73 | removeExtraneous(extensions) 74 | loadExtensions() 75 | } 76 | -------------------------------------------------------------------------------- /src/support/manage-plugins.ts: -------------------------------------------------------------------------------- 1 | import { exists, getDirs, is, configPath } from '../support/utils' 2 | import { NotifyKind, notify } from '../ui/notifications' 3 | import { url, download } from '../support/download' 4 | import nvim from '../core/neovim' 5 | import { join } from 'path' 6 | 7 | interface Plugin { 8 | name: string, 9 | user: string, 10 | repo: string, 11 | path: string, 12 | installed: boolean, 13 | } 14 | 15 | const packDir = join(configPath, 'nvim/pack') 16 | 17 | const splitUserRepo = (text: string) => { 18 | const [ , user = '', repo = '' ] = (text.match(/^([^/]+)\/(.*)/) || []) 19 | return { user, repo } 20 | } 21 | 22 | const getPlugins = async (configLines: string[]) => Promise.all(configLines 23 | .filter(line => /^Plug(\s*)/.test(line)) 24 | .map(line => (line.match(/^Plug(\s*)(?:"|')(\S+)(?:"|')/) || [])[2]) 25 | .filter(is.string) 26 | .map(splitUserRepo) 27 | .map(async m => { 28 | const name = `${m.user}-${m.repo}` 29 | const path = join(packDir, name) 30 | return { 31 | ...m, 32 | name, 33 | path: join(path, 'start'), 34 | installed: await exists(path), 35 | } 36 | })) 37 | 38 | const removeExtraneous = async (plugins: Plugin[]) => { 39 | const dirs = await getDirs(packDir) 40 | const pluginInstalled = (path: string) => plugins.some(e => e.name === path) 41 | const toRemove = dirs.filter(d => !pluginInstalled(d.name)) 42 | console.log('the following nvim packages should be removed from pack/ folder', toRemove) 43 | 44 | // TODO: we should only remove plugins veonim has installed. leave any 45 | // other plugins that were installed by user manually or other plugin 46 | // managers intact in the directory. 47 | // toRemove.forEach(dir => removePath(dir.path)) 48 | } 49 | 50 | export default async (configLines: string[]) => { 51 | const plugins = await getPlugins(configLines).catch() 52 | const pluginsNotInstalled = plugins.filter(plug => !plug.installed) 53 | if (!pluginsNotInstalled.length) return removeExtraneous(plugins) 54 | 55 | notify(`Found ${pluginsNotInstalled.length} Veonim plugins. Installing...`, NotifyKind.System) 56 | 57 | const installed = await Promise.all(plugins.map(p => download(url.github(p.user, p.repo), p.path))) 58 | const installedOk = installed.filter(m => m).length 59 | const installedFail = installed.filter(m => !m).length 60 | 61 | if (installedOk) notify(`Installed ${installedOk} plugins!`, NotifyKind.Success) 62 | if (installedFail) notify(`Failed to install ${installedFail} plugins. See devtools console for more info.`, NotifyKind.Error) 63 | 64 | removeExtraneous(plugins) 65 | nvim.cmd(`packloadall!`) 66 | } 67 | -------------------------------------------------------------------------------- /src/support/markdown.ts: -------------------------------------------------------------------------------- 1 | export { colorizeMarkdownToHTML as markdownToHTML } from '../services/colorizer' 2 | 3 | interface MarkdownOptions { 4 | listUnicodeChar?: boolean, 5 | stripListLeaders?: boolean, 6 | githubFlavoredMarkdown?: boolean, 7 | } 8 | 9 | const doStripListLeaders = (text: string, listUnicodeChar: boolean) => listUnicodeChar 10 | ? text.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, `${listUnicodeChar} $1`) 11 | : text.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, '$1') 12 | 13 | const doGithubMarkdown = (text: string) => text 14 | .replace(/\n={2,}/g, '\n') // header 15 | .replace(/~~/g, '') //strikethrough 16 | .replace(/`{3}.*\n/g, '') // fenced codeblocks 17 | 18 | // typescript/es-next version of stiang/remove-markdown 19 | export const remove = (markdown = '', options = {} as MarkdownOptions) => { 20 | const { listUnicodeChar = false, stripListLeaders = true, githubFlavoredMarkdown = true } = options 21 | 22 | // remove horizontal rules (stripListLeaders conflict with this rule, 23 | // which is why it has been moved to the top) 24 | const out1 = markdown.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, '') 25 | 26 | try { 27 | const out2 = stripListLeaders ? doStripListLeaders(out1, listUnicodeChar) : out1 28 | const out3 = githubFlavoredMarkdown ? doGithubMarkdown(out2) : out2 29 | return out3 30 | .replace(/<[^>]*>/g, '') // html tags 31 | .replace(/^[=\-]{2,}\s*$/g, '') // setext-style headers 32 | .replace(/\[\^.+?\](\: .*?$)?/g, '') // footnotes? 33 | .replace(/\s{0,2}\[.*?\]: .*?$/g, '') 34 | .replace(/\!\[.*?\][\[\(].*?[\]\)]/g, '') // images 35 | .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1') // inline links 36 | .replace(/^\s{0,3}>\s?/g, '') // blockquotes 37 | .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '') // reference-style links 38 | .replace(/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm, '$1$2$3') // atx-style headers 39 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2') // emphasis 40 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2') 41 | .replace(/(`{3,})(.*?)\1/gm, '$2') // code blocks 42 | .replace(/`(.+?)`/g, '$1') // inline code 43 | .replace(/\n{2,}/g, '\n\n') // 2+ newlines with 2 newlines. why? 44 | } 45 | 46 | catch(err) { 47 | console.error(err) 48 | return markdown 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/support/neovim-utils.ts: -------------------------------------------------------------------------------- 1 | import { delay, pascalCase, onProp } from '../support/utils' 2 | import { Range } from 'vscode-languageserver-protocol' 3 | import { Api } from '../neovim/protocol' 4 | 5 | type DefineFunction = { [index: string]: (fnBody: TemplateStringsArray, ...vars: any[]) => void } 6 | 7 | interface VimMode { 8 | blocking: boolean, 9 | mode: string, 10 | } 11 | 12 | // TODO: this getMode/blocking/input/capture :messages is kinda hack. 13 | // when neovim implements external dialogs, please revisit 14 | const unblock = (notify: Api, request: Api) => (): Promise => new Promise(fin => { 15 | let neverGonnaGiveYouUp = false 16 | const typescript_y_u_do_dis = (): Promise => request.getMode() as Promise 17 | 18 | const timer = setTimeout(() => { 19 | neverGonnaGiveYouUp = true // never gonna let you down 20 | fin([]) 21 | }, 2e3) 22 | 23 | const tryToUnblock = () => typescript_y_u_do_dis().then(mode => { 24 | if (!mode.blocking) { 25 | Promise.race([ 26 | request.commandOutput('messages').then(m => m.split('\n').filter(m => m)), 27 | delay(250).then(() => []) 28 | ]).then(fin) 29 | 30 | clearInterval(timer) 31 | return 32 | } 33 | 34 | notify.input(``) 35 | if (!neverGonnaGiveYouUp) setImmediate(() => tryToUnblock()) 36 | }) 37 | 38 | tryToUnblock() 39 | }) 40 | 41 | export default ({ notify, request }: { notify: Api, request: Api }) => ({ 42 | unblock: unblock(notify, request) 43 | }) 44 | 45 | export const FunctionGroup = () => { 46 | const fns: string[] = [] 47 | 48 | const defineFunc: DefineFunction = onProp((name: PropertyKey) => (strParts: TemplateStringsArray, ...vars: any[]) => { 49 | const expr = strParts 50 | .map((m, ix) => [m, vars[ix]].join('')) 51 | .join('') 52 | .split('\n') 53 | .filter(m => m) 54 | .map(m => m.trim()) 55 | .join('\\n') 56 | .replace(/"/g, '\\"') 57 | 58 | fns.push(`exe ":fun! ${pascalCase(name as string)}(...) range\\n${expr}\\nendfun"`) 59 | }) 60 | 61 | return { 62 | defineFunc, 63 | getFunctionsAsString: () => fns.join(' | '), 64 | } 65 | } 66 | 67 | export const CmdGroup = (strParts: TemplateStringsArray, ...vars: any[]) => strParts 68 | .map((m, ix) => [m, vars[ix]].join('')) 69 | .join('') 70 | .split('\n') 71 | .filter(m => m) 72 | .map(m => m.trim()) 73 | .map(m => m.replace(/\|/g, '\\|')) 74 | .join(' | ') 75 | .replace(/"/g, '\\"') 76 | 77 | export const positionWithinRange = (line: number, column: number, { start, end }: Range): boolean => { 78 | const startInRange = line >= start.line 79 | && (line !== start.line || column >= start.character) 80 | 81 | const endInRange = line <= end.line 82 | && (line !== end.line || column <= end.character) 83 | 84 | return startInRange && endInRange 85 | } 86 | -------------------------------------------------------------------------------- /src/support/please-get.ts: -------------------------------------------------------------------------------- 1 | export interface SafeObject { 2 | [index: string]: SafeObject, 3 | (defaultValue?: T): T, 4 | } 5 | 6 | const getPath = (obj: any, givenPath?: any[]) => { 7 | const path = givenPath || [] 8 | const length = path.length 9 | let hasOwn = true 10 | let key: string 11 | 12 | return (defaultValue: any) => { 13 | let index = 0 14 | while (obj != null && index < length) { 15 | key = path[index++] 16 | hasOwn = obj.hasOwnProperty(key) 17 | obj = obj[key] 18 | } 19 | 20 | return (hasOwn && index === length) ? obj : defaultValue 21 | } 22 | } 23 | 24 | export default (obj: any) => { 25 | const handler = (path: any[]) => ({ 26 | get: (_: any, key: string): any => { 27 | if (obj[key] && obj[key].isPrototypeOf(obj)) return obj[key] 28 | const newPath = [...path, key] 29 | return new Proxy(getPath(obj, newPath), handler(newPath)) as SafeObject 30 | } 31 | }) 32 | 33 | return new Proxy(getPath(obj), handler([])) as SafeObject 34 | } 35 | -------------------------------------------------------------------------------- /src/support/shell-env.ts: -------------------------------------------------------------------------------- 1 | import { NewlineSplitter } from '../support/utils' 2 | import { spawn } from 'child_process' 3 | 4 | const ansiEscape = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g') 5 | 6 | const defaultShell = () => { 7 | if (process.platform === 'darwin') return process.env.SHELL || '/bin/bash' 8 | if (process.platform === 'win32') return process.env.COMSPEC || 'cmd.exe' 9 | return process.env.SHELL || '/bin/sh' 10 | } 11 | 12 | const cleanup = (thing: string) => thing 13 | .replace(/^_SHELL_ENV_DELIMITER_/, '') 14 | .replace(ansiEscape, '') 15 | 16 | export default (shell = defaultShell()) => new Promise(done => { 17 | if (process.platform === 'win32') return done(process.env) 18 | 19 | const vars = process.env 20 | const proc = spawn(shell, [ 21 | '-ilc', 22 | 'echo -n "_SHELL_ENV_DELIMITER_"; env; echo -n "_SHELL_ENV_DELIMITER_"; exit', 23 | ]) 24 | 25 | proc.stdout.pipe(new NewlineSplitter()).on('data', (line: string) => { 26 | if (!line) return 27 | const cleanLine = cleanup(line) 28 | const [ key, ...valParts ] = cleanLine.split('=') 29 | const val = valParts.join('=') 30 | Reflect.set(vars, key, val) 31 | }) 32 | 33 | proc.on('exit', () => done(vars)) 34 | }) 35 | -------------------------------------------------------------------------------- /src/support/trace.ts: -------------------------------------------------------------------------------- 1 | const LANGSERV = process.env.VEONIM_TRACE_LANGSERV || false 2 | 3 | const log = (...msg: any[]) => console.debug(...msg) 4 | export const traceLANGSERV = LANGSERV ? log : () => {} 5 | -------------------------------------------------------------------------------- /src/support/vscode-shim.ts: -------------------------------------------------------------------------------- 1 | import fakeModule from '../support/fake-module' 2 | import vscode from '../vscode/api' 3 | 4 | type LogMissingModuleApi = (moduleName: string, apiPath: string) => void 5 | let logMissingModuleApiDuringDevelopment: LogMissingModuleApi = () => {} 6 | 7 | if (process.env.VEONIM_DEV) { 8 | logMissingModuleApiDuringDevelopment = (moduleName, apiPath) => console.warn(`fake module ${moduleName} is missing an implementation for: ${apiPath}`) 9 | } 10 | 11 | const LanguageClient = class LanguageClient { 12 | protected name: string 13 | protected serverActivator: Function 14 | 15 | constructor (name: string, serverActivator: Function) { 16 | this.name = name 17 | this.serverActivator = serverActivator 18 | } 19 | 20 | start () { 21 | console.log('starting extension:', this.name) 22 | return this.serverActivator() 23 | } 24 | 25 | error (...data: any[]) { 26 | console.error(this.name, ...data) 27 | } 28 | } 29 | 30 | fakeModule('vscode', vscode, logMissingModuleApiDuringDevelopment) 31 | 32 | fakeModule('vscode-languageclient', { 33 | LanguageClient, 34 | }, logMissingModuleApiDuringDevelopment) 35 | 36 | -------------------------------------------------------------------------------- /src/ui/checkboard.ts: -------------------------------------------------------------------------------- 1 | const checkboardCache = new Map() 2 | 3 | const render = (color1: string, color2: string, size: number) => { 4 | const canvas = document.createElement('canvas') 5 | canvas.width = size * 2 6 | canvas.height = size * 2 7 | const ctx = canvas.getContext('2d') 8 | if (!ctx) return null 9 | 10 | ctx.fillStyle = color1 11 | ctx.fillRect(0, 0, canvas.width, canvas.height) 12 | ctx.fillStyle = color2 13 | ctx.fillRect(0, 0, size, size) 14 | ctx.translate(size, size) 15 | ctx.fillRect(0, 0, size, size) 16 | return canvas.toDataURL() 17 | } 18 | 19 | export default (color1: string, color2: string, size: number) => { 20 | const scaledSize = size * window.devicePixelRatio 21 | const key = `${color1}-${color2}-${scaledSize}` 22 | if (checkboardCache.has(key)) return checkboardCache.get(key) 23 | 24 | const checkboard = render(color1, color2, scaledSize) 25 | checkboardCache.set(key, checkboard) 26 | return checkboard 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/hyperscript.ts: -------------------------------------------------------------------------------- 1 | const CLASS_SPLIT = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/ 2 | const ANY = /^\.|#/ 3 | const type = (m: any): string => Object.prototype.toString.call(m).slice(8, 11).toLowerCase() 4 | const argt = (args: any[]) => (t: string) => args.find(a => type(a) === t) 5 | const ex = (symbol: string, parts: string[]) => parts.map(p => p.charAt(0) === symbol && p.substring(1, p.length)).filter(p => p).join(' ') 6 | 7 | const parse = { 8 | selector: (selector: string): { id?: string, css?: string, tag?: string } => { 9 | if (!selector) return {} 10 | const p = selector.split(CLASS_SPLIT) 11 | return { id: ex('#', p), css: ex('.', p), tag: ANY.test(p[1]) ? 'div' : (p[1] || 'div') } 12 | }, 13 | 14 | css: (obj: any) => type(obj) === 'obj' ? Object.keys(obj).filter(k => obj[k]).join(' ') : '' 15 | } 16 | 17 | export default (hyper: any) => (...a: any[]) => { 18 | const $ = argt(a) 19 | const props = $('obj') || {} 20 | if (props.render === false) return 21 | 22 | const { tag = $('fun'), id, css } = parse.selector($('str')) 23 | const classes = [ css, props.class, parse.css(props.css) ].filter(c => c).join(' ').trim() 24 | 25 | if (id) props.id = id 26 | if (classes) props.class = classes 27 | if (props.hide != null) props.style = props.style || {} 28 | if (props.hide != null) props.style.display = props.hide ? 'none' : undefined 29 | 30 | Object.assign(props, { css: undefined, render: undefined, hide: undefined }) 31 | 32 | const notTag = argt(a.slice(1)) 33 | return hyper(tag, props, $('arr') || notTag('str') || notTag('num')) 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/lose-focus.ts: -------------------------------------------------------------------------------- 1 | const elements = new Map void>() 2 | 3 | document.addEventListener('click', e => { 4 | const items = [...elements.entries()] 5 | const done = items.filter(([ el ]) => !el.contains(e.target as Node)) 6 | 7 | done.forEach(([ el, callback ]) => { 8 | elements.delete(el) 9 | callback() 10 | }) 11 | }) 12 | 13 | export default (element: HTMLElement, callback: () => void) => elements.set(element, callback) 14 | -------------------------------------------------------------------------------- /src/ui/uikit.ts: -------------------------------------------------------------------------------- 1 | import { app as makeApp, h as makeHyperscript, ActionsType, View } from 'hyperapp' 2 | import { showCursor, hideCursor } from '../core/cursor' 3 | import { specs as titleSpecs } from '../core/title' 4 | import * as dispatch from '../messaging/dispatch' 5 | import { merge, uuid } from '../support/utils' 6 | import hyperscript from '../ui/hyperscript' 7 | import * as viminput from '../core/input' 8 | 9 | export const h = hyperscript(makeHyperscript) 10 | 11 | export const css = (builder: (classname: string) => string[]): string => { 12 | const id = `sc-${uuid()}` 13 | const styles = builder(id).join('\n') 14 | const style = document.createElement('style') 15 | style.type = 'text/css' 16 | style.appendChild(document.createTextNode(styles)) 17 | document.head.appendChild(style) 18 | return id 19 | } 20 | 21 | const hostElement = document.getElementById('plugins') as HTMLElement 22 | merge(hostElement.style, { 23 | position: 'absolute', 24 | display: 'flex', 25 | width: '100vw', 26 | zIndex: 420, // vape naysh yall 27 | // TODO: 24px for statusline. do it better 28 | // TODO: and title. bruv do i even know css? 29 | height: `calc(100vh - 24px - ${titleSpecs.height}px)`, 30 | }) 31 | 32 | dispatch.sub('window.change', () => { 33 | hostElement.style.height = `calc(100vh - 24px - ${titleSpecs.height}px)` 34 | }) 35 | 36 | export const vimFocus = () => { 37 | setImmediate(() => viminput.focus()) 38 | showCursor() 39 | } 40 | 41 | export const vimBlur = () => { 42 | viminput.blur() 43 | hideCursor() 44 | } 45 | 46 | const pluginsDiv = document.getElementById('plugins') as HTMLElement 47 | 48 | const prepareContainerElement = (name: string) => { 49 | const el = document.createElement('div') 50 | el.setAttribute('id', name) 51 | merge(el.style, { 52 | position: 'absolute', 53 | width: '100%', 54 | height: '100%', 55 | }) 56 | 57 | pluginsDiv.appendChild(el) 58 | return el 59 | } 60 | 61 | interface App { 62 | name: string, 63 | state: StateT, 64 | actions: ActionsType, 65 | view: View, 66 | element?: HTMLElement, 67 | } 68 | 69 | /** create app for cultural learnings of hyperapp for make benefit of glorious application veonim */ 70 | export const app = ({ state, actions, view, element, name }: App): ActionsT => { 71 | const containerElement = element || prepareContainerElement(name) 72 | 73 | if (process.env.VEONIM_DEV) { 74 | const devtools = require('@deomitrus/hyperapp-redux-devtools') 75 | return devtools(makeApp, { name })(state, actions, view, containerElement) 76 | } 77 | 78 | return makeApp(state, actions, view, containerElement) 79 | } 80 | -------------------------------------------------------------------------------- /src/ui/vanilla.ts: -------------------------------------------------------------------------------- 1 | import { is, merge } from '../support/utils' 2 | 3 | type EL1 = (tagName: string, style: object) => HTMLElement 4 | type EL2 = (style: object) => HTMLElement 5 | 6 | // TODO: what about a hyperscript to dom lib? that might be nice. you know for sciene. 7 | export const makel: EL1 & EL2 = (...args: any[]) => { 8 | const styleObject = args.find(is.object) 9 | 10 | const el = document.createElement(args.find(is.string) || 'div') 11 | styleObject && merge(el.style, styleObject) 12 | 13 | return el 14 | } 15 | -------------------------------------------------------------------------------- /src/vscode/api.ts: -------------------------------------------------------------------------------- 1 | import extensions from '../vscode/extensions' 2 | import languages from '../vscode/languages' 3 | import workspace from '../vscode/workspace' 4 | import commands from '../vscode/commands' 5 | import window from '../vscode/window' 6 | import debug from '../vscode/debug' 7 | import tasks from '../vscode/tasks' 8 | import scm from '../vscode/scm' 9 | import env from '../vscode/env' 10 | 11 | export default { 12 | extensions, 13 | languages, 14 | workspace, 15 | commands, 16 | window, 17 | debug, 18 | tasks, 19 | scm, 20 | env, 21 | } 22 | -------------------------------------------------------------------------------- /src/vscode/commands.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import * as vsc from 'vscode' 3 | 4 | const registeredCommands = new EventEmitter() 5 | 6 | const commands: typeof vsc.commands = { 7 | executeCommand: (command: string, ...args: any[]) => { 8 | registeredCommands.emit(command, ...args) 9 | return new Promise(() => {}) 10 | }, 11 | getCommands: async (filterInternal?: boolean) => { 12 | const events = registeredCommands.eventNames().map(m => m.toString()) 13 | if (filterInternal) return events.filter(e => !e.startsWith('_')) 14 | return events 15 | }, 16 | registerCommand: (command: string, callback: (...args: any[]) => void) => { 17 | registeredCommands.on(command, callback) 18 | return { dispose: () => registeredCommands.removeListener(command, callback) } 19 | }, 20 | registerTextEditorCommand: (command: string, callback: Function) => { 21 | // TODO: the callback needs to return a TextEditor object. where do we get that from? 22 | // TODO: are we supposed to return textEditorCommands in getCommands or executeCommand? 23 | console.warn('NYI: registerTextEditorCommand', command, callback) 24 | return { dispose: () => console.warn('NYI: registerTextEditorCommand disposable') } 25 | }, 26 | } 27 | 28 | export default commands 29 | -------------------------------------------------------------------------------- /src/vscode/debug.ts: -------------------------------------------------------------------------------- 1 | import { DebugConfigurationProvider, registerDebugConfigProvider } from '../extensions/debuggers' 2 | // import * as vsc from 'vscode' 3 | 4 | // const debug: typeof vsc.debug = { 5 | const debug: any = { 6 | registerDebugConfigurationProvider: (debugType: string, provider: DebugConfigurationProvider) => { 7 | const dispose = registerDebugConfigProvider(debugType, provider) 8 | return { dispose } 9 | }, 10 | } 11 | 12 | export default debug 13 | -------------------------------------------------------------------------------- /src/vscode/env.ts: -------------------------------------------------------------------------------- 1 | import * as vsc from 'vscode' 2 | import { hostname } from 'os' 3 | 4 | const env: typeof vsc.env = { 5 | appName: 'Veonim', 6 | appRoot: process.cwd(), 7 | language: 'en-US', 8 | machineId: hostname(), 9 | sessionId: `Veonim-${Date.now()}`, 10 | } 11 | 12 | export default env 13 | -------------------------------------------------------------------------------- /src/vscode/extensions.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/vscode/languages.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/vscode/neovim.ts: -------------------------------------------------------------------------------- 1 | import { onCreateVim, onSwitchVim } from '../messaging/worker-client' 2 | import SessionTransport from '../messaging/session-transport' 3 | import SetupRPC from '../messaging/rpc' 4 | import Neovim from '../neovim/api' 5 | 6 | const { send, connectTo, switchTo, onRecvData } = SessionTransport() 7 | const { onData, ...rpcAPI } = SetupRPC(send) 8 | 9 | onRecvData(([ type, d ]) => onData(type, d)) 10 | onCreateVim(connectTo) 11 | onSwitchVim(switchTo) 12 | 13 | export default Neovim({ ...rpcAPI, onCreateVim, onSwitchVim }) 14 | -------------------------------------------------------------------------------- /src/vscode/scm.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/vscode/tasks.ts: -------------------------------------------------------------------------------- 1 | // import { Watcher } from '../support/utils' 2 | import * as vsc from 'vscode' 3 | 4 | const activeTasks = new Set() 5 | // const watchers = Watcher() 6 | 7 | // const tasks: typeof vsc.tasks = { 8 | const tasks: any = { 9 | // var 10 | get taskExecutions() { return [...activeTasks] }, 11 | 12 | // events 13 | // onDidEndTask: fn => watchers.on('didEnd', fn), 14 | 15 | // functions 16 | } 17 | 18 | export default tasks 19 | -------------------------------------------------------------------------------- /src/vscode/window.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src/vscode/workspace.ts: -------------------------------------------------------------------------------- 1 | import nvim from '../vscode/neovim' 2 | import { basename } from 'path' 3 | // import * as vsc from 'vscode' 4 | 5 | // const workspace: typeof vsc.workspace = { 6 | const workspace: any = { 7 | get rootPath() { return nvim.state.cwd }, 8 | get workspaceFolders() { return [ nvim.state.cwd ] }, 9 | get name() { return basename(nvim.state.cwd) }, 10 | // TODO: NYI 11 | // TODO: how to make this sync? 12 | get textDocuments() { 13 | console.warn('NYI: vscode.workspace.textDocuments') 14 | return [] 15 | }, 16 | // TODO: events... 17 | // TODO: functions... 18 | } 19 | 20 | export default workspace 21 | -------------------------------------------------------------------------------- /src/workers/buffer-search.ts: -------------------------------------------------------------------------------- 1 | import { on, onCreateVim, onSwitchVim } from '../messaging/worker-client' 2 | import TextDocumentManager from '../neovim/text-document-manager' 3 | import SessionTransport from '../messaging/session-transport' 4 | import { filter as fuzzy, match } from 'fuzzaldrin-plus' 5 | import SetupRPC from '../messaging/rpc' 6 | import Neovim from '../neovim/api' 7 | 8 | const { send, connectTo, switchTo, onRecvData } = SessionTransport() 9 | const { onData, ...rpcAPI } = SetupRPC(send) 10 | 11 | onRecvData(([ type, d ]) => onData(type, d)) 12 | onCreateVim(connectTo) 13 | onSwitchVim(switchTo) 14 | 15 | const nvim = Neovim({ ...rpcAPI, onCreateVim, onSwitchVim }) 16 | const tdm = TextDocumentManager(nvim) 17 | 18 | interface FilterResult { 19 | line: string, 20 | start: { 21 | line: number, 22 | column: number, 23 | }, 24 | end: { 25 | line: number, 26 | column: number, 27 | } 28 | } 29 | 30 | const buffers = new Map() 31 | 32 | const getLocations = (str: string, query: string, buffer: string[]) => { 33 | const line = buffer.indexOf(str) 34 | const locations = match(str, query) 35 | 36 | return { 37 | start: { line, column: locations[0] }, 38 | end: { line, column: locations[locations.length - 1] }, 39 | } 40 | } 41 | 42 | const asFilterResults = (results: string[], lines: string[], query: string): FilterResult[] => [...new Set(results)] 43 | .map(m => ({ 44 | line: m, 45 | ...getLocations(m, query, lines), 46 | })) 47 | 48 | tdm.on.didOpen(({ name, textLines }) => buffers.set(name, textLines)) 49 | tdm.on.didClose(({ name }) => buffers.delete(name)) 50 | tdm.on.didChange(({ name, textLines, firstLine, lastLine }) => { 51 | const buf = buffers.get(name) || [] 52 | const affectAmount = lastLine - firstLine 53 | buf.splice(firstLine, affectAmount, ...textLines) 54 | }) 55 | 56 | on.fuzzy(async (file: string, query: string, maxResults = 20): Promise => { 57 | const bufferData = buffers.get(file) || [] 58 | const results = fuzzy(bufferData, query, { maxResults }) 59 | return asFilterResults(results, bufferData, query) 60 | }) 61 | 62 | on.visibleFuzzy(async (query: string): Promise => { 63 | const { editorTopLine: start, editorBottomLine: end } = nvim.state 64 | const visibleLines = await nvim.current.buffer.getLines(start, end) 65 | const results = fuzzy(visibleLines, query) 66 | return asFilterResults(results, visibleLines, query) 67 | }) 68 | -------------------------------------------------------------------------------- /src/workers/download.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir, remove } from '../support/utils' 2 | import { on } from '../messaging/worker-client' 3 | import { Archiver } from '../support/binaries' 4 | import { fetchStream } from '../support/fetch' 5 | import { createWriteStream } from 'fs' 6 | 7 | const downloadZip = (url: string, path: string) => new Promise(async done => { 8 | const downloadStream = await fetchStream(url) 9 | const fileStream = createWriteStream(`${path}.zip`) 10 | 11 | downloadStream 12 | .pipe(fileStream) 13 | .on('close', done) 14 | .on('error', done) 15 | }) 16 | 17 | const unzip = (path: string) => new Promise(done => Archiver(['open', `${path}.zip`, path]) 18 | .on('exit', done) 19 | .on('error', done)) 20 | 21 | on.download(async (url: string, path: string) => { 22 | await ensureDir(path) 23 | 24 | const downloadErr = await downloadZip(url, path).catch(console.error) 25 | if (downloadErr) { 26 | console.error(downloadErr) 27 | return false 28 | } 29 | 30 | const unzipErr = await unzip(path).catch(console.error) 31 | if (unzipErr) console.error(unzipErr) 32 | 33 | await remove(`${path}.zip`).catch(console.error) 34 | return !unzipErr 35 | }) 36 | -------------------------------------------------------------------------------- /src/workers/get-file-lines.ts: -------------------------------------------------------------------------------- 1 | import { on } from '../messaging/worker-client' 2 | import { exists } from '../support/utils' 3 | import { createReadStream } from 'fs' 4 | 5 | interface LineContents { 6 | ix: number, 7 | line: string, 8 | } 9 | 10 | const fileReader = (path: string, targetLines: number[]) => new Promise(done => { 11 | const collectedLines: LineContents[] = [] 12 | const linesOfInterest = new Set(targetLines) 13 | const maxLineIndex = Math.max(...targetLines) 14 | let currentLineIndex = 0 15 | let buffer = '' 16 | 17 | // not using NewlineSplitter here because it filters out empty lines 18 | // we need the empty lines, since we track the line index 19 | const readStream = createReadStream(path).on('data', raw => { 20 | const lines = (buffer + raw).split(/\r?\n/) 21 | buffer = lines.pop() || '' 22 | 23 | lines.forEach(line => { 24 | const needThisLine = linesOfInterest.has(currentLineIndex) 25 | if (needThisLine) collectedLines.push({ ix: currentLineIndex, line }) 26 | 27 | const reachedMaximumLine = currentLineIndex === maxLineIndex 28 | if (reachedMaximumLine) readStream.close() 29 | 30 | currentLineIndex++ 31 | }) 32 | }) 33 | 34 | readStream.on('close', () => done(collectedLines)) 35 | }) 36 | 37 | on.getLines(async (path: string, lines: number[]) => { 38 | const fileExists = await exists(path) 39 | 40 | // at the time of this writing, the only consumer of this piece of code is 41 | // find-references from the lang servers. the find references will return a 42 | // list of path locations. we want to get the text corresponding to those 43 | // path locations. i think it is extremely unlikely that we would get paths 44 | // from find-references and then the file goes missing from the FS (file 45 | // system). also, i assume that lang server would only return valid paths 46 | // that exist on the FS... 47 | // 48 | // although, i suppose there could be active buffers in the workspace that 49 | // have not been committed to the FS. and in the future lang servers might 50 | // support buffers that never exist on the FS (see new VSCode extensions for 51 | // in-memory files, etc.) 52 | if (!fileExists) { 53 | console.warn(`tried to read lines from ${path} that does not exist`) 54 | return [] 55 | } 56 | 57 | return fileReader(path, lines) 58 | }) 59 | -------------------------------------------------------------------------------- /src/workers/harvester.ts: -------------------------------------------------------------------------------- 1 | import { on, onCreateVim, onSwitchVim } from '../messaging/worker-client' 2 | import TextDocumentManager from '../neovim/text-document-manager' 3 | import SessionTransport from '../messaging/session-transport' 4 | import { filter as fuzzy } from 'fuzzaldrin-plus' 5 | import SetupRPC from '../messaging/rpc' 6 | import Neovim from '../neovim/api' 7 | 8 | const { send, connectTo, switchTo, onRecvData } = SessionTransport() 9 | const { onData, ...rpcAPI } = SetupRPC(send) 10 | 11 | onRecvData(([ type, d ]) => onData(type, d)) 12 | onCreateVim(connectTo) 13 | onSwitchVim(switchTo) 14 | 15 | const nvim = Neovim({ ...rpcAPI, onCreateVim, onSwitchVim }) 16 | const tdm = TextDocumentManager(nvim) 17 | const keywords = new Map() 18 | const last = { file: '', changedLine: '' } 19 | let isInsertMode = false 20 | 21 | const addKeywords = (file: string, words: string[]) => { 22 | const e = keywords.get(file) || [] 23 | words.forEach(word => { 24 | if (e.includes(word)) return 25 | keywords.set(file, (e.push(word), e)) 26 | }) 27 | } 28 | 29 | const harvestInsertMode = (file: string, textLines: string[]) => { 30 | const lastLine = textLines[textLines.length - 1] 31 | const lastChar = lastLine[lastLine.length - 1] 32 | Object.assign(last, { file, changedLine: lastLine }) 33 | 34 | const lastCharIsWord = /\w/.test(lastChar) 35 | const linesWithWords = textLines.map(line => line.match(/\w+/g) || []) 36 | 37 | const lastLineWithWords = linesWithWords[linesWithWords.length - 1] 38 | 39 | if (lastCharIsWord) lastLineWithWords.pop() 40 | 41 | const words = [...new Set(...linesWithWords)] 42 | const sizeableWords = words.filter(w => w.length > 2) 43 | 44 | addKeywords(file, sizeableWords) 45 | } 46 | 47 | const harvest = (file: string, textLines: string[]) => { 48 | if (isInsertMode) return harvestInsertMode(file, textLines) 49 | 50 | const harvested = new Set() 51 | const totalol = textLines.length 52 | 53 | for (let ix = 0; ix < totalol; ix++) { 54 | const words = textLines[ix].match(/\w+/g) || [] 55 | const wordsTotal = words.length 56 | 57 | for (let wix = 0; wix < wordsTotal; wix++) { 58 | const word = words[wix] 59 | if (word.length > 2) harvested.add(word) 60 | } 61 | } 62 | 63 | const nextKeywords = new Set([...keywords.get(file) || [], ...harvested]) 64 | keywords.set(file, [...nextKeywords]) 65 | } 66 | 67 | nvim.on.insertEnter(() => isInsertMode = true) 68 | nvim.on.insertLeave(async () => { 69 | isInsertMode = false 70 | const words = last.changedLine.match(/\w+/g) || [] 71 | addKeywords(last.file, words) 72 | }) 73 | 74 | tdm.on.didOpen(({ name, textLines }) => harvest(name, textLines)) 75 | tdm.on.didChange(({ name, textLines }) => harvest(name, textLines)) 76 | tdm.on.didClose(({ name }) => keywords.delete(name)) 77 | 78 | on.query(async (file: string, query: string, maxResults = 20) => { 79 | return fuzzy(keywords.get(file) || [], query, { maxResults }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/workers/neovim-client.ts: -------------------------------------------------------------------------------- 1 | import SessionTransport from '../messaging/session-transport' 2 | 3 | const { send, connectTo, switchTo } = SessionTransport(m => postMessage(m)) 4 | 5 | onmessage = ({ data }: MessageEvent) => { 6 | if (Array.isArray(data) && data[0] === 65) return connectTo(data[1]) 7 | if (Array.isArray(data) && data[0] === 66) return switchTo(data[1]) 8 | send(data) 9 | } 10 | -------------------------------------------------------------------------------- /src/workers/project-file-finder.ts: -------------------------------------------------------------------------------- 1 | import { on, call } from '../messaging/worker-client' 2 | import { NewlineSplitter } from '../support/utils' 3 | import { filter as fuzzy } from 'fuzzaldrin-plus' 4 | import { Ripgrep } from '../support/binaries' 5 | 6 | const INTERVAL = 250 7 | const AMOUNT = 10 8 | const TIMEOUT = 15e3 9 | const results = new Set() 10 | const cancelTokens = new Set() 11 | let query = '' 12 | 13 | const sendResults = ({ filter = true } = {}) => { 14 | if (!filter || !query) return call.results([...results].slice(0, AMOUNT)) 15 | 16 | const queries = query.split(' ').filter(m => m) 17 | // TODO: might be more performant to cache previous fuzzy results 18 | const items = queries.reduce((res, qry) => fuzzy(res, qry), [...results]) 19 | 20 | call.results(items.slice(0, AMOUNT)) 21 | } 22 | 23 | const getFilesWithRipgrep = (cwd: string) => { 24 | const timer = setInterval(sendResults, INTERVAL) 25 | const rg = Ripgrep(['--files', '--hidden', '--glob', '!node_modules', '--glob', '!.git'], { cwd }) 26 | let initialSent = false 27 | 28 | rg.stderr.pipe(new NewlineSplitter()).on('data', console.error) 29 | 30 | rg.stdout.pipe(new NewlineSplitter()).on('data', (path: string) => { 31 | const shouldSendInitialBatch = !initialSent && results.size >= AMOUNT 32 | results.add(path) 33 | 34 | if (shouldSendInitialBatch) { 35 | sendResults({ filter: false }) 36 | initialSent = true 37 | } 38 | }) 39 | 40 | rg.on('exit', () => { 41 | clearInterval(timer) 42 | sendResults({ filter: initialSent }) 43 | call.done() 44 | }) 45 | 46 | const reset = () => results.clear() 47 | const stop = () => { 48 | rg.kill() 49 | clearInterval(timer) 50 | } 51 | 52 | setImmediate(() => sendResults({ filter: false })) 53 | setTimeout(stop, TIMEOUT) 54 | return () => (stop(), reset()) 55 | } 56 | 57 | on.load((cwd: string) => { 58 | results.clear() 59 | query = '' 60 | 61 | const stopRipgrepSearch = getFilesWithRipgrep(cwd) 62 | cancelTokens.add(stopRipgrepSearch) 63 | }) 64 | 65 | on.stop(() => { 66 | query = '' 67 | cancelTokens.forEach(cancel => cancel()) 68 | cancelTokens.clear() 69 | }) 70 | 71 | on.query((data: string) => { 72 | query = data 73 | sendResults() 74 | }) 75 | -------------------------------------------------------------------------------- /src/workers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "exclude": [ 4 | "node_modules" 5 | ], 6 | "include": [ 7 | "./*.ts", 8 | "../messaging/worker-client.ts" 9 | ], 10 | "compilerOptions": { 11 | "skipLibCheck": true, 12 | "lib": ["es2017", "scripthost", "webworker"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/data/blarg.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stuff = { 4 | one: 1, 5 | two: 2, 6 | three: input => input + 3, 7 | } 8 | 9 | const res = stuff.three(stuff.one) 10 | const two = stuff.two 11 | 12 | console.log('res', res) 13 | console.log('two', two) 14 | -------------------------------------------------------------------------------- /test/data/src/blarg.ts: -------------------------------------------------------------------------------- 1 | import { externalFunction } from '../src/blarg2' 2 | 3 | const logger = (str: TemplateStringsArray | string, v: any[]) => Array.isArray(str) 4 | ? console.log((str as TemplateStringsArray).map((s, ix) => s + (v[ix] || '')).join('')) 5 | : console.log(str as string) 6 | 7 | export const log = (str: TemplateStringsArray | string, ...vars: any[]) => logger(str, vars) 8 | 9 | type TypeChecker = (thing: any) => boolean 10 | interface Types { string: TypeChecker, number: TypeChecker, array: TypeChecker, object: TypeChecker, null: TypeChecker, asyncfunction: TypeChecker, function: TypeChecker, promise: TypeChecker, map: TypeChecker, set: TypeChecker } 11 | 12 | export const snakeCase = (m: string) => m.split('').map(ch => /[A-Z]/.test(ch) ? '_' + ch.toLowerCase(): ch).join('') 13 | const type = (m: any) => (Object.prototype.toString.call(m).match(/^\[object (\w+)\]/) || [])[1].toLowerCase() 14 | 15 | export const fromJSON = (m: string) => ({ or: (defaultVal: any) => { try { return JSON.parse(m) } catch(_) { return defaultVal } }}) 16 | export const is = new Proxy({} as Types, { get: (_, key) => (val: any) => type(val) === key }) 17 | 18 | const doTheNeedful = (text: string) => externalFunction(text) 19 | 20 | export const emptyStat = { 21 | isDirectory: () => false, 22 | isFile: () => false, 23 | isSymbolicLink: () => false, 24 | } 25 | 26 | const result = doTheNeedful('42') 27 | console.log('result', result) 28 | -------------------------------------------------------------------------------- /test/data/src/blarg2.ts: -------------------------------------------------------------------------------- 1 | export const externalFunction = (someInput: string): number => someInput - 0 2 | -------------------------------------------------------------------------------- /test/data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.ts" 4 | ], 5 | "exclude": [ 6 | "node_modules", 7 | "src/workers/*.ts", 8 | "src/messaging/worker-client.ts" 9 | ], 10 | "compilerOptions": { 11 | "target": "es2017", 12 | "module": "commonjs", 13 | "outDir": "build", 14 | "strict": true, 15 | "lib": ["es2017", "dom"], 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "sourceMap": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/features.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { deepStrictEqual: eq } = require('assert') 4 | const launch = require('./launcher') 5 | const { delay } = require('../util') 6 | 7 | const snapshotTester = m => async name => { 8 | const diffAmount = await m.snapshotTest(name) 9 | eq(diffAmount < 1, true, `${name} image snapshot is different by ${diffAmount}% (diff of <1% is ok)`) 10 | } 11 | 12 | describe('features', () => { 13 | let m 14 | let testSnapshot 15 | 16 | before(async () => { 17 | m = await launch() 18 | testSnapshot = snapshotTester(m) 19 | }) 20 | 21 | after(() => m.stop()) 22 | 23 | it('fuzzy file finder', async () => { 24 | await m.veonimAction('files') 25 | await testSnapshot('files') 26 | await m.input.esc() 27 | }) 28 | 29 | it('explorer', async () => { 30 | await m.veonimAction('explorer') 31 | await testSnapshot('explorer') 32 | await m.input.esc() 33 | }) 34 | 35 | it('change dir', async () => { 36 | await m.veonimAction('change-dir') 37 | await testSnapshot('change-dir') 38 | await m.input.esc() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/e2e/launcher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const compareImages = require('resemblejs/compareImages') 4 | const { Application } = require('spectron') 5 | const { delay, pathExists } = require('../util') 6 | const fs = require('fs-extra') 7 | const path = require('path') 8 | 9 | const snapshotMode = process.argv.includes('--snapshot') 10 | console.log('snapshotMode', snapshotMode) 11 | 12 | module.exports = async () => { 13 | const projectPath = path.join(__dirname, '../data') 14 | const resultsPath = path.join(__dirname, '../../results') 15 | const snapshotsPath = path.join(__dirname, '../snapshots') 16 | 17 | fs.ensureDir(resultsPath) 18 | if (snapshotMode) fs.emptyDir(snapshotsPath) 19 | 20 | const app = new Application({ 21 | path: './node_modules/.bin/electron', 22 | args: [ path.join(__dirname, '../../build/bootstrap/main.js') ], 23 | }) 24 | 25 | await app.start() 26 | await app.client.waitUntilWindowLoaded() 27 | await delay(500) 28 | 29 | app.input = async m => { 30 | await delay(100) 31 | await app.client.keys(m) 32 | } 33 | 34 | app.input.enter = () => app.input('Enter') 35 | app.input.esc = () => app.input('Escape') 36 | 37 | app.input.meta = async m => { 38 | await app.input('\uE03D') 39 | await app.input(m) 40 | await app.input('\uE03D') 41 | } 42 | 43 | app.veonimAction = async cmd => { 44 | await app.input(`:Veonim ${cmd}`) 45 | await app.input.enter() 46 | } 47 | 48 | app.screencap = async name => { 49 | await delay(200) 50 | const imageBuf = await app.browserWindow.capturePage().catch(console.error) 51 | if (!imageBuf) return console.error(`faild to screencap "${name}"`) 52 | const location = path.join(resultsPath, `${name}.png`) 53 | fs.writeFile(location, imageBuf) 54 | await delay(100) 55 | return imageBuf 56 | } 57 | 58 | app.snapshotTest = async name => { 59 | const imageBuf = await app.screencap(name) 60 | const snapshotLocation = path.join(snapshotsPath, `${name}.png`) 61 | 62 | if (snapshotMode) { 63 | fs.writeFile(snapshotLocation, imageBuf) 64 | return 0 65 | } 66 | 67 | const snapshotExists = await pathExists(snapshotLocation) 68 | if (!snapshotExists) throw new Error(`snapshot "${name}" does not exist. generate snapshots with "--snapshot" flag`) 69 | 70 | const diff = await compareImages(imageBuf, await fs.readFile(snapshotLocation)) 71 | 72 | if (diff.rawMisMatchPercentage > 0) { 73 | fs.writeFile(path.join(resultsPath, `${name}-diff.png`), diff.getBuffer()) 74 | } 75 | 76 | return diff.rawMisMatchPercentage 77 | } 78 | 79 | await app.input(`:cd ${projectPath}`) 80 | await app.input.enter() 81 | 82 | return app 83 | } 84 | -------------------------------------------------------------------------------- /test/integration/neovim-colorizer.test.js: -------------------------------------------------------------------------------- 1 | const { src, same, globalProxy } = require('../util') 2 | const { EventEmitter } = require('events') 3 | const childProcess = require('child_process') 4 | 5 | describe.skip('markdown to HTML with syntax highlighting', () => { 6 | const watchers = new EventEmitter() 7 | 8 | const request = (method, ...data) => new Promise(done => { 9 | const reqId = Date.now() 10 | global.onmessage({ data: [ method, data, reqId ] }) 11 | watchers.once(reqId, done) 12 | }) 13 | 14 | let undoGlobalProxy 15 | let nvimProc 16 | 17 | before(() => { 18 | global.onmessage = () => {} 19 | global.postMessage = ([ ev, args, id ]) => watchers.emit(id, args) 20 | 21 | undoGlobalProxy = globalProxy('child_process', { 22 | ...childProcess, 23 | spawn: (...args) => { 24 | const proc = childProcess.spawn(...args) 25 | if (args[0].includes('nvim')) nvimProc = proc 26 | return proc 27 | } 28 | }) 29 | 30 | src('workers/neovim-colorizer') 31 | }) 32 | 33 | after(() => { 34 | nvimProc.kill('SIGKILL') 35 | undoGlobalProxy() 36 | delete global.onmessage 37 | delete global.postMessage 38 | }) 39 | 40 | it('happy path', async () => { 41 | const markdown = [ 42 | '# STAR WARS', 43 | '## ESB', 44 | '*italic* **bold** `code`', 45 | '```javascript', 46 | 'const generalKenobi = "hello there!"', 47 | 'console.log(generalKenobi)', 48 | '```', 49 | ].join('\n') 50 | 51 | const res = await request('colorizeMarkdownToHTML', markdown) 52 | 53 | // TODO: we are currently not getting colors back from nvim 54 | // this could be because: 55 | // - colorscheme is not loaded (checked $runtime but it seems correct) 56 | // - no syntax files for filetype 57 | // - wrong filetype 58 | const expected = [ 59 | '

STAR WARS

', 60 | '

ESB

', 61 | '

italic bold code

', 62 | '
const generalKenobi = "hello there!"console.log(generalKenobi)
', 63 | '', 64 | ].join('\n') 65 | 66 | same(res, expected) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/integration/vscode-api-commands.test.js: -------------------------------------------------------------------------------- 1 | const { src, same, spy, resetModule } = require('../util') 2 | 3 | describe('vscode api - commands', () => { 4 | let commands 5 | 6 | beforeEach(() => { 7 | commands = src('vscode/commands').default 8 | }) 9 | 10 | describe('func', () => { 11 | it('registerCommand', async () => { 12 | const command = commands.registerCommand('blarg', () => {}) 13 | const coms = await commands.getCommands() 14 | same(coms, ['blarg']) 15 | }) 16 | 17 | it('getCommands', async () => { 18 | const ayy = commands.registerCommand('ayy', () => {}) 19 | const lmao = commands.registerCommand('lmao', () => {}) 20 | const umad = commands.registerCommand('_umad', () => {}) 21 | 22 | const coms = await commands.getCommands() 23 | const comsNoInternal = await commands.getCommands(true) 24 | 25 | same(coms, ['ayy', 'lmao', '_umad']) 26 | same(comsNoInternal, ['ayy', 'lmao']) 27 | 28 | ayy.dispose() 29 | 30 | const coms2 = await commands.getCommands(true) 31 | 32 | same(coms2, ['lmao']) 33 | }) 34 | 35 | it('executeCommand', async () => { 36 | const callback = spy() 37 | const command = commands.registerCommand('blarg', callback) 38 | commands.executeCommand('blarg', 42) 39 | command.dispose() 40 | commands.executeCommand('blarg', 22) 41 | same(callback.calls, [ [42] ]) 42 | }) 43 | 44 | it('registerTextEditorCommand') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/integration/vscode-api-debug.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const debug = src('vscode/debug').default 4 | 5 | describe('vscode api - debug', () => { 6 | describe('var', () => { 7 | it('activeDebugConsole') 8 | it('activeDebugSession') 9 | it('breakpoints') 10 | }) 11 | 12 | describe('event', () => { 13 | it('onDidChangeActiveDebugSession') 14 | it('onDidChangeBreakpoints') 15 | it('onDidReceiveDebugSessionCustomEvent') 16 | it('onDidStartDebugSession') 17 | it('onDidTerminateDebugSession') 18 | }) 19 | 20 | describe('func', () => { 21 | it('addBreakpoints') 22 | it('registerDebugConfigurationProvider') 23 | it('removeBreakpoints') 24 | it('startDebugging') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/integration/vscode-api-env.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | describe('vscode api - env', () => { 4 | let env 5 | 6 | beforeEach(() => { 7 | env = src('vscode/env').default 8 | }) 9 | 10 | describe('var', () => { 11 | it('appName', () => { 12 | same(env.appName, 'Veonim') 13 | }) 14 | 15 | it('appRoot', () => { 16 | same(env.appRoot, process.cwd()) 17 | }) 18 | 19 | it('language', () => { 20 | same(env.language, 'en-US') 21 | }) 22 | 23 | it('machineId', () => { 24 | same(env.machineId, require('os').hostname()) 25 | }) 26 | 27 | it('sessionId', () => { 28 | const includesName = env.sessionId.includes('Veonim-') 29 | same(includesName, true, 'sessionId starts with Veonim-') 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/integration/vscode-api-extensions.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const extensions = src('vscode/extensions').default 4 | 5 | describe('vscode api - extensions', () => { 6 | describe('var', () => { 7 | it('all') 8 | }) 9 | 10 | describe('func', () => { 11 | it('getExtension') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/integration/vscode-api-languages.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const languages = src('vscode/languages').default 4 | 5 | describe('vscode api - languages', () => { 6 | describe('event', () => { 7 | it('onDidChangeDiagnostics') 8 | }) 9 | 10 | describe('func', () => { 11 | it('getLanguages') 12 | it('match') 13 | it('getDiagnostics') 14 | it('getDiagnostics') 15 | it('createDiagnosticCollection') 16 | it('registerCompletionItemProvider') 17 | it('registerCodeActionsProvider') 18 | it('registerCodeLensProvider') 19 | it('registerDefinitionProvider') 20 | it('registerImplementationProvider') 21 | it('registerTypeDefinitionProvider') 22 | it('registerHoverProvider') 23 | it('registerDocumentHighlightProvider') 24 | it('registerDocumentSymbolProvider') 25 | it('registerWorkspaceSymbolProvider') 26 | it('registerReferenceProvider') 27 | it('registerRenameProvider') 28 | it('registerDocumentFormattingEditProvider') 29 | it('registerDocumentRangeFormattingEditProvider') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/integration/vscode-api-scm.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const scm = src('vscode/scm').default 4 | 5 | describe('vscode api - scm', () => { 6 | describe('var', () => { 7 | it('inputBox') 8 | }) 9 | 10 | describe('func', () => { 11 | it('createSourceControl') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/integration/vscode-api-tasks.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const tasks = src('vscode/tasks').default 4 | 5 | describe('vscode api - tasks', () => { 6 | describe('var', () => { 7 | it('taskExecutions') 8 | }) 9 | 10 | describe('events', () => { 11 | it('onDidEndTask') 12 | it('onDidEndTaskProcess') 13 | it('onDidStartTask') 14 | it('onDidStartTaskProcess') 15 | }) 16 | 17 | describe('func', () => { 18 | it('executeTask') 19 | it('fetchTasks') 20 | it('registerTaskProvider') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/integration/vscode-api-window.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const window = src('vscode/window').default 4 | 5 | describe('vscode api - window', () => { 6 | describe('var', () => { 7 | it('activeTextEditor') 8 | it('visibleTextEditors') 9 | it('state') 10 | it('terminals') 11 | }) 12 | 13 | describe('events', () => { 14 | it('onDidChangeActiveTextEditor') 15 | it('onDidChangeVisibleTextEditors') 16 | it('onDidChangeTextEditorSelection') 17 | it('onDidChangeTextEditorVisibleRanges') 18 | it('onDidChangeTextEditorOptions') 19 | it('onDidChangeTextEditorViewColumn') 20 | it('onDidOpenTerminal') 21 | it('onDidCloseTerminal') 22 | it('onDidChangeWindowState') 23 | }) 24 | 25 | describe('func', () => { 26 | it('createInputBox') 27 | it('createOutputChannel') 28 | it('createQuickPick') 29 | it('createStatusBarItem') 30 | it('createTerminal') 31 | it('createTextEditorDecorationType') 32 | it('createTreeView') 33 | it('createWebviewPanel') 34 | it('registerTreeDataProvider') 35 | it('registerUriHandler') 36 | it('registerWebviewPanelSerializer') 37 | it('setStatusBarMessage') 38 | it('showErrorMessage') 39 | it('showInformationMessage') 40 | it('showInputBox') 41 | it('showOpenDialog') 42 | it('showQuickPick') 43 | it('showSaveDialog') 44 | it('showTextDocument') 45 | it('showWarningMessage') 46 | it('showWorkspaceFolderPick') 47 | it('withProgress') 48 | it('withScmProgress') 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/integration/vscode-api-workspace.test.js: -------------------------------------------------------------------------------- 1 | const { src, same, testDataPath } = require('../util') 2 | const startNeovim = require('../nvim-for-test') 3 | const path = require('path') 4 | const { EV_CREATE_VIM, EV_SWITCH_VIM } = src('support/constants') 5 | 6 | describe('vscode api - workspace', () => { 7 | let workspace 8 | let nvim 9 | let pipeName 10 | 11 | before(() => { 12 | global.onmessage = () => {} 13 | global.postMessage = () => {} 14 | }) 15 | 16 | after(() => { 17 | delete global.onmessage 18 | delete global.postMessage 19 | }) 20 | 21 | beforeEach(async () => { 22 | nvim = startNeovim() 23 | workspace = src('vscode/workspace').default 24 | pipeName = await nvim.request('eval', 'v:servername') 25 | 26 | global.onmessage({ data: [EV_CREATE_VIM, [{ id: 1, path: pipeName }]] }) 27 | global.onmessage({ data: [EV_SWITCH_VIM, [1]] }) 28 | 29 | const nvimSRC = src('vscode/neovim').default 30 | 31 | nvim.notify('command', `:cd ${testDataPath}`) 32 | 33 | // wait for NeovimState to be populated 34 | await new Promise((done, fail) => { 35 | const timer = setInterval(() => { 36 | if (nvimSRC.state.cwd === testDataPath) { 37 | clearInterval(timer) 38 | done() 39 | } 40 | }, 100) 41 | 42 | setTimeout(() => { 43 | clearInterval(timer) 44 | fail(`nvim cwd was never === ${testDataPath}`) 45 | }, 8e3) 46 | }) 47 | }) 48 | 49 | afterEach(() => { 50 | nvim.shutdown() 51 | }) 52 | 53 | describe('var', () => { 54 | it('rootPath', () => { 55 | same(workspace.rootPath, testDataPath) 56 | }) 57 | 58 | it('workspaceFolders', () => { 59 | same(workspace.workspaceFolders, [ testDataPath ]) 60 | }) 61 | 62 | it('name', () => { 63 | const baseFolderName = path.basename(testDataPath) 64 | same(workspace.name, baseFolderName) 65 | }) 66 | 67 | it('textDocuments') 68 | }) 69 | 70 | describe('events', () => { 71 | it('onWillSaveTextDocument') 72 | it('onDidChangeWorkspaceFolders') 73 | it('onDidOpenTextDocument') 74 | it('onDidCloseTextDocument') 75 | it('onDidChangeTextDocument') 76 | it('onDidSaveTextDocument') 77 | it('onDidChangeConfiguration') 78 | }) 79 | 80 | describe('func', () => { 81 | it('getWorkspaceFolder') 82 | it('asRelativePath') 83 | it('updateWorkspaceFolders') 84 | it('createFileSystemWatcher') 85 | it('findFiles') 86 | it('saveAll') 87 | it('applyEdit') 88 | it('openTextDocument') 89 | it('openTextDocument') 90 | it('openTextDocument') 91 | it('registerTextDocumentContentProvider') 92 | it('getConfiguration') 93 | it('registerTaskProvider') 94 | it('registerFileSystemProvider') 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /test/nvim-for-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { src } = require('./util') 4 | const msgpack = require('msgpack-lite') 5 | 6 | module.exports = () => { 7 | const { Neovim } = src('support/binaries') 8 | const { startupFuncs, startupCmds } = src('core/vim-startup') 9 | const CreateTransport = src('messaging/transport').default 10 | const SetupRPC = src('messaging/rpc').default 11 | let id = 1 12 | 13 | const proc = src('support/binaries').Neovim.run([ 14 | '--cmd', `${startupFuncs()} | ${startupCmds}`, 15 | '--cmd', `com! -nargs=* Plug 1`, 16 | '--cmd', `com! -nargs=* VeonimExt 1`, 17 | '--cmd', `com! -nargs=+ -range -complete=custom,VeonimCmdCompletions Veonim call Veonim()`, 18 | '--embed' 19 | ], { 20 | ...process.env, 21 | VIM: Neovim.path, 22 | VIMRUNTIME: Neovim.runtime, 23 | }) 24 | 25 | const { encoder, decoder } = CreateTransport() 26 | encoder.pipe(proc.stdin) 27 | proc.stdout.pipe(decoder) 28 | const { notify, request, onData } = SetupRPC(encoder.write) 29 | 30 | decoder.on('data', ([ type, ...d ]) => onData(type, d)) 31 | 32 | const shutdown = () => proc.kill() 33 | 34 | return { 35 | proc, 36 | shutdown, 37 | notify: (name, ...args) => notify(`nvim_${name}`, args), 38 | request: (name, ...args) => request(`nvim_${name}`, args), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/snapshots/change-dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/test/snapshots/change-dir.png -------------------------------------------------------------------------------- /test/snapshots/explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/test/snapshots/explorer.png -------------------------------------------------------------------------------- /test/snapshots/files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkishi/veonim/9ad1470b2f60dfd74b370b8830d2a39e6cf0bdbd/test/snapshots/files.png -------------------------------------------------------------------------------- /test/unit/config-service.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | 3 | const defaultConfig = { 4 | project: { 5 | root: '~/blarg' 6 | } 7 | } 8 | 9 | const setup = vimConfig => src('config/config-service', { 10 | 'config/default-configs': defaultConfig, 11 | 'core/neovim': { 12 | default: { 13 | g: vimConfig, 14 | } 15 | } 16 | }).default 17 | 18 | describe('config service', () => { 19 | it('do the needful', done => { 20 | const cs = setup({ 'vn_project_root': Promise.resolve('shittake') }) 21 | 22 | const val = cs('project.root', m => { 23 | same(m, 'shittake') 24 | done() 25 | }) 26 | 27 | same(val, '~/blarg') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/unit/relative-finder.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | const { findNext, findPrevious } = src('support/relative-finder') 3 | 4 | const getA = () => [{ 5 | path: '/main/a.ts', 6 | line: 4, 7 | column: 7, 8 | endLine: 4, 9 | endColumn: 9, 10 | }, { 11 | path: '/main/a.ts', 12 | line: 1, 13 | column: 1, 14 | endLine: 1, 15 | endColumn: 5, 16 | }, { 17 | path: '/main/a.ts', 18 | line: 9, 19 | column: 2, 20 | endLine: 9, 21 | endColumn: 4, 22 | }] 23 | 24 | const getC = () => [{ 25 | path: '/main/c.ts', 26 | line: 3, 27 | column: 1, 28 | endLine: 3, 29 | endColumn: 9, 30 | }, { 31 | path: '/main/c.ts', 32 | line: 1, 33 | column: 7, 34 | endLine: 1, 35 | endColumn: 9, 36 | }] 37 | 38 | const getItems = () => [ ...getA(), ...getC() ] 39 | 40 | describe('relative finder', () => { 41 | it('find next', () => { 42 | const next = findNext(getItems(), '/main/a.ts', 2, 1) 43 | 44 | same(next, { 45 | path: '/main/a.ts', 46 | line: 4, 47 | column: 7, 48 | endLine: 4, 49 | endColumn: 9, 50 | }) 51 | }) 52 | 53 | it('find next file (and first item) when none in current', () => { 54 | const next = findNext(getC(), '/main/a.ts', 9, 2) 55 | 56 | same(next, { 57 | path: '/main/c.ts', 58 | line: 1, 59 | column: 7, 60 | endLine: 1, 61 | endColumn: 9, 62 | }) 63 | }) 64 | 65 | it('when last loopback to first', () => { 66 | const next = findNext(getItems(), '/main/a.ts', 9, 2) 67 | 68 | same(next, { 69 | path: '/main/a.ts', 70 | line: 1, 71 | column: 1, 72 | endLine: 1, 73 | endColumn: 5, 74 | }) 75 | }) 76 | 77 | it('find previous', () => { 78 | const next = findPrevious(getItems(), '/main/a.ts', 2, 1) 79 | 80 | same(next, { 81 | path: '/main/a.ts', 82 | line: 1, 83 | column: 1, 84 | endLine: 1, 85 | endColumn: 5, 86 | }) 87 | }) 88 | 89 | it('find prev file (and last item) when none is current', () => { 90 | const next = findPrevious(getA(), '/main/c.ts', 1, 7) 91 | 92 | same(next, { 93 | path: '/main/a.ts', 94 | line: 9, 95 | column: 2, 96 | endLine: 9, 97 | endColumn: 4, 98 | }) 99 | }) 100 | 101 | it('when first loopback to last', () => { 102 | const next = findPrevious(getItems(), '/main/a.ts', 1, 1) 103 | 104 | same(next, { 105 | path: '/main/a.ts', 106 | line: 9, 107 | column: 2, 108 | endLine: 9, 109 | endColumn: 4, 110 | }) 111 | }) 112 | 113 | it('find previous when in middle of current item', () => { 114 | const next = findPrevious(getItems(), '/main/a.ts', 4, 8) 115 | 116 | same(next, { 117 | path: '/main/a.ts', 118 | line: 1, 119 | column: 1, 120 | endLine: 1, 121 | endColumn: 5, 122 | }) 123 | }) 124 | }) 125 | 126 | -------------------------------------------------------------------------------- /test/unit/utils.test.js: -------------------------------------------------------------------------------- 1 | const { src, same } = require('../util') 2 | const m = src('support/utils') 3 | 4 | const nix = process.platform === 'linux' || process.platform === 'darwin' 5 | 6 | if (nix) { 7 | 8 | describe('path parts', () => { 9 | it('root path', () => { 10 | const testpath = '/Users/a/veonim' 11 | const res = m.pathParts(testpath) 12 | same(res, ['/', 'Users', 'a', 'veonim']) 13 | }) 14 | 15 | it('relative dot path', () => { 16 | const testpath = './Users/a/veonim/' 17 | const res = m.pathParts(testpath) 18 | same(res, ['Users', 'a', 'veonim']) 19 | }) 20 | 21 | it('relative path', () => { 22 | const testpath = 'a/veonim/' 23 | const res = m.pathParts(testpath) 24 | same(res, ['a', 'veonim']) 25 | }) 26 | 27 | it('path segments', () => { 28 | const testpath = '/Users/a/../' 29 | const res = m.pathParts(testpath) 30 | same(res, ['/', 'Users']) 31 | }) 32 | }) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { deepStrictEqual: same } = require('assert') 4 | const proxyquire = require('proxyquire').noCallThru() 5 | const Module = require('module') 6 | const path = require('path') 7 | const fs = require('fs') 8 | const originalModuleLoader = Module._load 9 | 10 | const relativeFakes = obj => Object.keys(obj).reduce((res, key) => { 11 | const val = Reflect.get(obj, key) 12 | Reflect.set(res, `../${key}`, val) 13 | return res 14 | }, {}) 15 | 16 | const requireModule = (name, freshLoad) => { 17 | const modPath = require.resolve(`../build/${name}`) 18 | delete require.cache[modPath] 19 | return require(modPath) 20 | } 21 | 22 | const src = (name, fake, { noRelativeFake = false } = {}) => fake 23 | ? proxyquire(`../build/${name}`, noRelativeFake ? fake : relativeFakes(fake)) 24 | : requireModule(name) 25 | 26 | const resetModule = name => { 27 | delete require.cache[require.resolve(`../build/${name}`)] 28 | } 29 | 30 | const globalProxy = (name, implementation) => { 31 | Module._load = (request, ...args) => request === name 32 | ? implementation 33 | : originalModuleLoader(request, ...args) 34 | 35 | return () => Module._load = originalModuleLoader 36 | } 37 | 38 | const delay = time => new Promise(fin => setTimeout(fin, time)) 39 | 40 | const pathExists = path => new Promise(m => fs.access(path, e => m(!e))) 41 | 42 | const spy = returnValue => { 43 | const spyFn = (...args) => (spyFn.calls.push(args), returnValue) 44 | spyFn.calls = [] 45 | return spyFn 46 | } 47 | 48 | global.localStorage = { 49 | getItem: () => {}, 50 | setItem: () => {}, 51 | } 52 | 53 | const testDataPath = path.join(process.cwd(), 'test', 'data') 54 | 55 | module.exports = { src, same, globalProxy, delay, pathExists, spy, resetModule, testDataPath } 56 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { $, go, run, fromRoot } = require('./runner') 4 | const fs = require('fs-extra') 5 | 6 | const paths = { 7 | index: 'src/bootstrap/index.html', 8 | processExplorer: 'src/bootstrap/process-explorer.html', 9 | } 10 | 11 | const copy = { 12 | index: () => { 13 | $`copying index html` 14 | return fs.copy(fromRoot(paths.index), fromRoot('build/bootstrap/index.html')) 15 | }, 16 | processExplorer: () => { 17 | $`copying process-explorer html` 18 | return fs.copy(fromRoot(paths.processExplorer), fromRoot('build/bootstrap/process-explorer.html')) 19 | }, 20 | assets: () => { 21 | $`copying assets` 22 | return fs.copy(fromRoot('src/assets'), fromRoot('build/assets')) 23 | }, 24 | runtime: () => { 25 | $`copying runtime files` 26 | return fs.copy(fromRoot('runtime'), fromRoot('build/runtime')) 27 | }, 28 | } 29 | 30 | const codemod = { 31 | workerExports: () => { 32 | $`adding exports objects to web workers to work in electron context` 33 | return run('jscodeshift -t tools/dummy-exports.js build/workers') 34 | }, 35 | removeDebug: () => { 36 | $`removing debug code from release build` 37 | return run('jscodeshift -t tools/remove-debug.js build') 38 | }, 39 | } 40 | 41 | require.main === module && go(async () => { 42 | $`cleaning build folder` 43 | await fs.emptyDir(fromRoot('build')) 44 | 45 | const tscMain = run('tsc -p tsconfig.json') 46 | const tscWorkers = run('tsc -p src/workers/tsconfig.json') 47 | 48 | await Promise.all([ tscMain, tscWorkers ]) 49 | 50 | await codemod.workerExports() 51 | await codemod.removeDebug() 52 | 53 | await Promise.all([ 54 | copy.index(), 55 | copy.processExplorer(), 56 | copy.assets(), 57 | copy.runtime(), 58 | ]) 59 | }) 60 | 61 | module.exports = { paths, copy, codemod } 62 | -------------------------------------------------------------------------------- /tools/devdocs/range.md: -------------------------------------------------------------------------------- 1 | i think the range logic will be 2 | 3 | - if nvim end - start = 1 && lineData 4 | ``` 5 | const changes = [ 6 | { start, end, text: '' }, 7 | { start, end: start, text: lineData[0] + '\n' } 8 | ] 9 | ``` 10 | 11 | - if lineData 12 | ``` 13 | const changes = [ 14 | { start, end, text: lineData.map(line => line + '\n').join('') } 15 | ] 16 | ``` 17 | 18 | otherwise, passthru, no changes 19 | 20 | ## delete 21 | 22 | ### delete line 2 23 | - s 1/0, e 2/0, t: '' 24 | 25 | -- s 1/0, e 2/0, t: '' 26 | 27 | ### delete line 2,3 28 | - s 1/0, e 3/0, t: '' 29 | 30 | -- s 1/0, e 3/0, t: '' 31 | 32 | ### delete first word on line 2 33 | - s 1/0, e 1/6, t: '' 34 | 35 | -- s 1/0, e 2/0, t: ' lineWithout firstWord' 36 | 37 | 38 | ## insert 39 | 40 | ### insert text on line 2 41 | - s 1/0, e 1/0, t: 'c' 42 | 43 | -- s 1/0, e 2/0, t: 'c restOfLine' 44 | 45 | ## paste 46 | 47 | ### paste 1 line after line 2 48 | - s 1/{lastChar}, e 1/{lastChar}, t: '\n pastedLine' 49 | 50 | -- s 2/0, e 2/0, t: 'pastedLine' 51 | 52 | ### paste 2 lines after line 2 53 | - s 1/{lastChar}, e 1/{lastChar}, t: '\n pastedLine1\n pastedLine2' 54 | 55 | -- s 2/0, e 2/0, t: 'pastedLine1', 'pastedLine2' 56 | 57 | ### paste a word on line 2 58 | - s 1/0, e 1/0, t: 'pastedWord' 59 | 60 | -- s 1/0, e 2/0, t: 'pastedWord restOfLine2' 61 | 62 | 63 | ## replace (select text and paste or insert) 64 | 65 | ### replace line 2 with 1 line 66 | - s 1/0, e 2/0, t: '' 67 | - s 1/0, e 1/0, t: 'pastedLine\n' 68 | 69 | -- s 1/0, e 2/0, t: '' 70 | -- s 1/0, e 1/0, t: 'pastedLine' 71 | 72 | ### replace lines 2,3 with 1 line 73 | - s 1/0, e 3/0, t: '' 74 | - s 1/0, e 1/0, t: 'pastedLine\n' 75 | 76 | -- s 1/0, e 3/0, t: '' 77 | -- s 1/0, e 1/0, t: 'pastedLine' 78 | 79 | ### replace line 2 with 2 lines 80 | - s 1/0, e 2/0, t: '' 81 | - s 1/0, e 1/0, t: 'pastedLine\n pastedLine2\n' 82 | 83 | -- s 1/0, e 2/0, t: '' 84 | -- s 1/0, e 1/0, t: 'pastedLine1', 'pastedline2' 85 | 86 | ### replace lines 2,3 with 2 lines 87 | - s 1/0, e 3/0, t: '' 88 | - s 1/0, e 1/0, t: 'pastedLine\n pastedLine2\n' 89 | 90 | -- s 1/0, e 3/0, t: '' 91 | -- s 1/0, e 1/0, t: 'pastedLine1', 'pastedLine2' 92 | 93 | ### replace first word on line 2 94 | - s 1/0, e 1/5, t: '' 95 | - s 1/0, e 1/0, t: 'pastedWord' 96 | 97 | -- s 1/0, e 2/0, t: ' restOfLine withoutReplacedWord' 98 | -- s 1/0, e 2/0, t: 'pastedWord restOfLine' 99 | -------------------------------------------------------------------------------- /tools/dummy-exports.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ({ source }, { jscodeshift: j }) => j(source) 4 | .forEach(path => { 5 | path.value.program.body.unshift( 6 | j.variableDeclaration('var', [ 7 | j.variableDeclarator( 8 | j.identifier('exports'), 9 | j.objectExpression([]), 10 | ) 11 | ])) 12 | }) 13 | .toSource() 14 | 15 | // run with jscodeshift (npm i -g jscodeshift) 16 | // jscodeshift -t cleaner.js main.js another.js 17 | // or a folder 18 | // jscodeshift -t cleaner.js src 19 | // dry run: 20 | // append args: -d -p 21 | -------------------------------------------------------------------------------- /tools/font-sizer/calc-sizer.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('electron') 2 | const { join } = require('path') 3 | const { createWriteStream } = require('fs') 4 | const out = createWriteStream(join(__dirname, '../../src/assets/roboto-sizes.json')) 5 | const leftPad = (str, amt) => Array(amt).fill(' ').join('') + str 6 | const write = (m = '', pad = 0) => out.write(leftPad(`${m}\n`, pad)) 7 | 8 | console.log('waiting for fonts') 9 | 10 | const canvasEl = document.getElementById('canvas') 11 | const canvas = canvasEl.getContext('2d', { alpha: false }) 12 | 13 | const fontSizer = face => size => { 14 | canvas.font = `${size}px ${face}` 15 | return Math.floor(canvas.measureText('m').width) 16 | } 17 | 18 | 19 | const main = () => { 20 | console.log('fonts loaded k thx') 21 | const getWidth = fontSizer('Roboto Mono Builtin') 22 | 23 | const points = [...Array(50)].map((_, ix) => ix + 4) 24 | const widths = points.map(p => ({ size: p, width: getWidth(p) })) 25 | const lastIx = widths.length - 1 26 | 27 | write('{') 28 | widths.forEach((m, ix) => write(`"${m.size}": ${m.width}${ix === lastIx ? '' : ','}`, 2)) 29 | write('}') 30 | 31 | console.log('wrote the sizes, done here!') 32 | remote.process.exit(0) 33 | } 34 | 35 | document.fonts.onloadingdone = main 36 | -------------------------------------------------------------------------------- /tools/font-sizer/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron') 2 | let win 3 | 4 | app.on('ready', () => { 5 | win = new BrowserWindow({ focus: true }) 6 | win.loadURL(`file:///${__dirname}/sizer.html`) 7 | win.webContents.openDevTools() 8 | }) 9 | -------------------------------------------------------------------------------- /tools/font-sizer/sizer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sizer 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tools/package.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { $, go, run, fromRoot } = require('./runner') 4 | const { copy, remove, ensureDir } = require('fs-extra') 5 | const { build } = require('electron-builder') 6 | const { deps } = require('./postinstall') 7 | 8 | const config = { 9 | productName: 'veonim', 10 | appId: 'com.veonim.veonim', 11 | directories: { 12 | buildResources: 'art' 13 | }, 14 | files: [ 15 | 'build/**', 16 | '!**/*.map' 17 | ], 18 | mac: { 19 | target: ['dmg', 'zip'], 20 | files: [{ 21 | from: 'bindeps/node_modules', 22 | to: 'node_modules' 23 | }] 24 | }, 25 | linux: { 26 | target: ['appimage', 'zip'], 27 | files: [{ 28 | from: 'bindeps/node_modules', 29 | to: 'node_modules' 30 | }] 31 | }, 32 | win: { 33 | target: ['portable', 'zip'], 34 | files: [{ 35 | from: 'bindeps/node_modules', 36 | to: 'node_modules' 37 | }] 38 | }, 39 | asar: false, 40 | publish: false, 41 | } 42 | 43 | go(async () => { 44 | $`cleaning dist (release) folder` 45 | await remove(fromRoot('dist')) 46 | 47 | $`installing binary dependencies for dist` 48 | await ensureDir(fromRoot('bindeps')) 49 | 50 | for (const [ dependency, version ] of Object.entries(deps)) { 51 | await run(`npm i ${dependency}@${version} --force --no-save --prefix ./bindeps`) 52 | } 53 | 54 | $`building veonim for operating system: ${process.platform}` 55 | await build({ config }).catch(console.error) 56 | 57 | await remove(fromRoot('bindeps')) 58 | $`fin dist pack` 59 | }) 60 | -------------------------------------------------------------------------------- /tools/postinstall.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const { $, go, run, fromRoot, fetch } = require('./runner') 3 | const fs = require('fs-extra') 4 | const pkgPath = fromRoot('package.json') 5 | const pkg = require(pkgPath) 6 | const os = process.platform 7 | const deps = Reflect.get(pkg, `bindeps-${os}`) 8 | 9 | const binaryDependencies = async () => { 10 | if (!deps) return 11 | 12 | for (const [ dependency, version ] of Object.entries(deps)) { 13 | await run(`npm i ${dependency}@${version} --no-save --no-package-lock --no-audit`) 14 | } 15 | 16 | const pkgData = JSON.stringify(pkg, null, 2) 17 | await fs.writeFile(pkgPath, pkgData) 18 | } 19 | 20 | const vscodeTypings = () => new Promise(async (done, fail) => { 21 | const vscodeApiVersion = Reflect.get(pkg, 'vscode-api-version') 22 | const modulePath = 'node_modules/@types/vscode' 23 | const vscodeTypingsUrl = version => `https://raw.githubusercontent.com/Microsoft/vscode/${version}/src/vs/vscode.d.ts` 24 | 25 | await fs.ensureDir(fromRoot(modulePath)) 26 | await fs.writeFile(fromRoot(modulePath, 'package.json'), `{ 27 | "name": "@types/vscode", 28 | "main": "", 29 | "version": "${vscodeApiVersion}", 30 | "typings": "index.d.ts" 31 | }\n`) 32 | 33 | const downloadStream = await fetch(vscodeTypingsUrl(vscodeApiVersion)) 34 | const fileStream = fs.createWriteStream(fromRoot(modulePath, 'index.d.ts')) 35 | 36 | downloadStream 37 | .pipe(fileStream) 38 | .on('close', done) 39 | .on('error', fail) 40 | }) 41 | 42 | require.main === module && go(async () => { 43 | $`installing binary dependencies` 44 | await binaryDependencies() 45 | $`installed binary dependencies` 46 | 47 | $`installing vscode extension api typings` 48 | await vscodeTypings().catch(err => console.log('failed to install vscode typings', err)) 49 | $`installed vscode extension api typings` 50 | }) 51 | 52 | module.exports = { deps } 53 | -------------------------------------------------------------------------------- /tools/remove-debug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ({ source }, { jscodeshift: j }) => j(source) 4 | .find(j.IfStatement) 5 | .forEach(path => { 6 | try { 7 | const m = path.value.test 8 | if (m.object.object.name === 'process' 9 | && m.object.property.name === 'env' 10 | && m.property.name === 'VEONIM_DEV' 11 | ) path.replace() 12 | } catch(e) {} 13 | }) 14 | .toSource() 15 | 16 | // run with jscodeshift (npm i -g jscodeshift) 17 | // jscodeshift -t cleaner.js main.js another.js 18 | // or a folder 19 | // jscodeshift -t cleaner.js src 20 | // dry run: 21 | // append args: -d -p 22 | -------------------------------------------------------------------------------- /tools/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { spawn } = require('child_process') 4 | const { join } = require('path') 5 | 6 | const root = join(__dirname, '..') 7 | const fromRoot = (...paths) => join(root, ...paths) 8 | 9 | const run = (cmd, opts = {}) => new Promise(done => { 10 | console.log(cmd) 11 | 12 | const proc = spawn('npx', cmd.split(' '), { ...opts, cwd: root, shell: true }) 13 | const exit = () => (proc.kill(), process.exit()) 14 | 15 | process.on('SIGBREAK', exit) 16 | process.on('SIGTERM', exit) 17 | process.on('SIGHUP', exit) 18 | process.on('SIGINT', exit) 19 | 20 | proc.stdout.pipe(process.stdout) 21 | proc.stderr.pipe(process.stderr) 22 | proc.on('exit', done) 23 | 24 | if (opts.outputMatch) proc.stdout.on('data', data => { 25 | const outputHas = data 26 | .toString() 27 | .toLowerCase() 28 | .includes(opts.outputMatch) 29 | 30 | if (outputHas && typeof opts.onOutputMatch === 'function') opts.onOutputMatch() 31 | }) 32 | }) 33 | 34 | const $ = (s, ...v) => Array.isArray(s) ? console.log(s.map((s, ix) => s + (v[ix] || '')).join('')) : console.log(s) 35 | const go = fn => fn().catch(e => (console.error(e), process.exit(1))) 36 | const createTask = () => ( (done = () => {}, promise = new Promise(m => done = m)) => ({ done, promise }) )() 37 | 38 | const fetch = (url, options = { method: 'GET' }) => new Promise((done, fail) => { 39 | const { data, ...requestOptions } = options 40 | const opts = { ...require('url').parse(url), ...requestOptions } 41 | 42 | const { request } = url.startsWith('https://') 43 | ? require('https') 44 | : require('http') 45 | 46 | const req = request(opts, res => done(res.statusCode >= 300 && res.statusCode < 400 47 | ? fetchStream(res.headers.location, options) 48 | : res)) 49 | 50 | req.on('error', fail) 51 | if (data) req.write(data) 52 | req.end() 53 | }) 54 | 55 | module.exports = { $, go, run, root, fromRoot, createTask, fetch } 56 | -------------------------------------------------------------------------------- /tools/setup-mirrors.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const { $, go, run, fromRoot } = require('./runner') 3 | const fs = require('fs-extra') 4 | const pkgPath = fromRoot('package.json') 5 | const pkg = require(pkgPath) 6 | 7 | const mirrors = Reflect.get(pkg, 'repository-mirrors') 8 | 9 | go(async () => { 10 | if (!mirrors || !mirrors.length) return 11 | $`setting up git mirrors` 12 | 13 | for (const mirror of mirrors) { 14 | await run(`git remote set-url origin --push --add ${mirror}`) 15 | } 16 | 17 | $`enchanted mirror who is the fairest of them all?` 18 | }) 19 | -------------------------------------------------------------------------------- /tools/start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { $, go, run, fromRoot, createTask } = require('./runner') 4 | const { copy, codemod, paths } = require('./build') 5 | const fs = require('fs-extra') 6 | 7 | const devConfig = fromRoot('xdg_config') 8 | 9 | go(async () => { 10 | $`local dev XDG_CONFIG_HOME dir: ${devConfig}` 11 | await fs.ensureDir(devConfig) 12 | 13 | await Promise.all([ 14 | copy.index(), 15 | copy.processExplorer(), 16 | copy.assets(), 17 | copy.runtime(), 18 | ]) 19 | 20 | const tsc = { main: createTask(), workers: createTask() } 21 | 22 | run('tsc -p tsconfig.json --watch --preserveWatchOutput', { 23 | outputMatch: 'compilation complete', 24 | onOutputMatch: tsc.main.done, 25 | }) 26 | 27 | run('tsc -p src/workers/tsconfig.json --watch --preserveWatchOutput', { 28 | outputMatch: 'compilation complete', 29 | onOutputMatch: () => codemod.workerExports().then(tsc.workers.done), 30 | }) 31 | 32 | await Promise.all([ tsc.main.promise, tsc.workers.promise ]) 33 | 34 | run('electron build/bootstrap/main.js', { 35 | env: { 36 | ...process.env, 37 | VEONIM_DEV: 42, 38 | XDG_CONFIG_HOME: devConfig, 39 | } 40 | }) 41 | 42 | $`watching index.html for changes...` 43 | fs.watch(fromRoot(paths.index), copy.index) 44 | fs.watch(fromRoot(paths.processExplorer), copy.processExplorer) 45 | }) 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.ts" 4 | ], 5 | "exclude": [ 6 | "node_modules", 7 | "src/workers/*.ts", 8 | "src/messaging/worker-client.ts" 9 | ], 10 | "compilerOptions": { 11 | "target": "es2017", 12 | "module": "commonjs", 13 | "outDir": "build", 14 | "strict": true, 15 | "lib": ["es2017", "dom"], 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "sourceMap": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------