├── .babelrc.json ├── .browserslistrc ├── .eslintrc.json ├── .github ├── main.workflow └── workflows │ └── nodejs.yml ├── .gitignore ├── .madrun.mjs ├── .npmignore ├── .putout.json ├── ChangeLog ├── LICENSE ├── README.md ├── bin └── deepword.js ├── client ├── api │ ├── _add-commands.js │ ├── _init-socket.js │ ├── _on-save.js │ ├── clipboard.js │ ├── evaluate.js │ ├── go-to-line.js │ ├── index.js │ ├── save.js │ ├── set-mode-for-path.js │ ├── set-mode.js │ ├── set-mode.spec.js │ ├── show-message │ │ ├── index.js │ │ └── show-message.css │ ├── story.js │ └── vim.js └── index.js ├── common ├── mode-for-ext.js └── mode-for-ext.spec.js ├── html └── index.html ├── img └── deepword.png ├── json ├── bin.json └── edit.json ├── package.json ├── server ├── edit.js ├── index.js ├── resolve-path.js └── resolve-path.spec.js └── webpack.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": ["@babel/plugin-proposal-numeric-separator"] 6 | } 7 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Safari versions 3 | Firefox ESR 4 | not dead 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:putout/recommended" 4 | ], 5 | "plugins": [ 6 | "putout", 7 | "n" 8 | ], 9 | "overrides": [{ 10 | "files": ["bin/**/*.js"], 11 | "rules": { 12 | "no-console": 0 13 | }, 14 | "extends": [ 15 | "plugin:n/recommended" 16 | ] 17 | }, { 18 | "files": ["client/**/*.js"], 19 | "env": { 20 | "browser": true 21 | } 22 | }] 23 | } 24 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Push" { 2 | resolves = ["lint", "coverage"] 3 | on = "push" 4 | } 5 | 6 | action "lint" { 7 | uses = "gimenete/eslint-action@1.0" 8 | } 9 | 10 | action "coverage" { 11 | uses = "coverallsapp/github-action@v1.0.1" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | env: 9 | NAME: deepword 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 18.x 14 | - 20.x 15 | - 21.x 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: oven-sh/setup-bun@v1 19 | with: 20 | bun-version: latest 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install Redrun 26 | run: bun i redrun -g --no-save 27 | - name: Install 28 | run: bun i --no-save 29 | - name: Lint 30 | run: redrun fix:lint 31 | - name: Install Rust 32 | run: rustup update 33 | - uses: actions/cache@v4 34 | with: 35 | path: | 36 | ~/.cargo/bin/ 37 | ~/.cargo/registry/index/ 38 | ~/.cargo/registry/cache/ 39 | ~/.cargo/git/db/ 40 | target/ 41 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 42 | - name: Typos Install 43 | run: which typos || cargo install typos-cli 44 | - name: Typos 45 | run: typos --write-changes 46 | - name: Commit fixes 47 | continue-on-error: true 48 | uses: EndBug/add-and-commit@v9 49 | with: 50 | fetch: --force 51 | message: "chore: ${{ env.NAME }}: actions: lint ☘️" 52 | pull: --rebase --autostash 53 | - name: Coverage 54 | run: redrun coverage report 55 | - name: Coveralls 56 | continue-on-error: true 57 | uses: coverallsapp/github-action@v2 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | npm-debug.log 4 | dist 5 | dist-dev 6 | .nyc_output 7 | yarn-error.log 8 | 9 | # vim 10 | *.swp.* 11 | *.swp 12 | coverage 13 | .idea 14 | -------------------------------------------------------------------------------- /.madrun.mjs: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'start': () => 'bin/deepword.js package.json', 5 | 'start:dev': () => 'NODE_ENV=development npm start', 6 | 'test': () => `tape '{client,common,server}/**/*.spec.js'`, 7 | 'coverage': () => 'c8 npm test', 8 | 'report': () => 'c8 report --reporter=lcov', 9 | 'watcher:test': () => 'nodemon -e spec.js -w client -w server -w common -x ', 10 | 'fix:lint': () => run('lint', '--fix'), 11 | 'lint': () => 'putout .', 12 | 'fresh:lint': () => run('lint', '--fresh'), 13 | 'lint:fresh': () => run('lint', '--fresh'), 14 | 'wisdom': () => run('build'), 15 | 'build': () => run([ 16 | 'clean', 17 | 'mkdir', 18 | 'build:client*', 19 | 'cp:*', 20 | ]), 21 | 'cp:socket.io:dist': () => 'cp node_modules/socket.io-client/dist/socket.io.js dist/socket.io.js', 22 | 'cp:socket.io:dist-dev': () => 'cp node_modules/socket.io-client/dist/socket.io.js dist-dev/socket.io.js', 23 | 'cp:socket.io.map:dist': () => 'cp node_modules/socket.io-client/dist/socket.io.js.map dist/socket.io.js.map', 24 | 'cp:socket.io.map:dist-dev': () => 'cp node_modules/socket.io-client/dist/socket.io.js.map dist-dev/socket.io.js.map', 25 | 'build-progress': () => 'webpack --progress', 26 | 'build:client': () => run('build-progress', '--mode production'), 27 | 'build:start': () => run(['build:client', 'start']), 28 | 'build:client:dev': async () => `NODE_ENV=development ${await run('build-progress')} --mode development`, 29 | 'build:start:dev': () => run(['build:client:dev', 'start:dev']), 30 | 'watch:client': async () => `nodemon -w client -x "${await run('build:client')}"`, 31 | 'watch:client:dev': async () => `NODE_ENV=development "${await run('watch:client')}"`, 32 | 'mkdir': () => 'mkdirp dist dist-dev', 33 | 'clean': () => 'rimraf dist dist-dev', 34 | }; 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | client 3 | common 4 | *.spec.js 5 | img 6 | yarn-error.log 7 | 8 | # vim files 9 | *.swp.* 10 | *.swp 11 | 12 | webpack.config.js 13 | 14 | 15 | coverage 16 | -------------------------------------------------------------------------------- /.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "client/index.js": { 4 | "remove-console": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2024.04.03, v10.0.0 2 | 3 | feature: 4 | - 8580f82 deepword: c8 v9.1.0 5 | - c1a3256 deepword: convert to ESM 6 | 7 | 2024.04.03, v9.0.3 8 | 9 | feature: 10 | - bb4ff7b deepword: monaco-editor v0.47.0 11 | 12 | 2024.03.16, v9.0.2 13 | 14 | feature: 15 | - 8ff4ae7 deepword: restbox v4.0.1 16 | 17 | 2024.03.06, v9.0.1 18 | 19 | feature: 20 | - e19a03d deepword: webpack-cli v5.1.4 21 | - 4bec74c deepword: css-loader v6.10.0 22 | - 9c992ee deepword: monaco-editor v0.46.0 23 | - bb9a55c deepword: socket-file v7.0.0 24 | 25 | 2024.02.02, v9.0.0 26 | 27 | feature: 28 | - e47b52b deepword: drop support of node < 18 29 | - d469aa6 deepword: keybindings 30 | - 2d4a540 deepword: eslint-plugin-n v16.6.2 31 | - e9a488e bin: socket.io 32 | - fe3a949 deepword: socket-file v6.1.0 33 | - 4f95b95 deepword: babel-loader v9.1.3 34 | - abfd8d8 deepword: cssnano v6.0.3 35 | - 4e1d81f deepword: eslint-plugin-putout v22.3.1 36 | - b2f39a3 deepword: eslint v8.56.0 37 | - 89458b9 deepword: rimraf v5.0.5 38 | - 5d1bc63 deepword: restafary v12.0.0 39 | - 0f8973d deepword: nodemon v3.0.3 40 | - ab72967 deepword: monaco-vim v0.4.1 41 | - 5067c3a deepword: es6-promisify v7.0.0 42 | - e3b9688 deepword: monaco-editor v0.45.0 43 | - 5414b97 deepword: madrun v10.0.1 44 | - dc67634 deepword: clean-css-loader v4.2.1 45 | - 4853146 deepword: mkdirp v3.0.1 46 | - cc1120f deepword: socket.io-client v4.7.4 47 | - 77215e8 deepword: socket.io v4.7.4 48 | - 9b05f16 deepword: putout v35.0.4 49 | - a5e77a4 deepword: supertape v10.0.0 50 | - 13414ab deepword: style-loader v3.3.4 51 | 52 | 2021.03.02, v8.0.1 53 | 54 | feature: 55 | - feature(package) clean-css-loader v3.0.0 56 | - feature(package) monaco-editor v0.22.3 57 | - feature(package) monaco-editor v0.22.3 58 | - feature(package) supertape v5.0.0 59 | - feature(package) restbox v3.0.0 60 | - feature(package) eslint-plugin-putout v7.2.0 61 | - feature(package) putout v15.3.1 62 | - feature(package) restafary v10.1.0 63 | 64 | 65 | 66 | 2021.01.19, v8.0.0 67 | 68 | feature: 69 | - feature(package) socket.io-client v3.1.0 70 | - feature(deepword) drop support of node < 14 71 | - feature(package) socket.io v3.1.0 72 | - feature(package) restafary v9.1.1 73 | - feature(package) madrun v8.6.0 74 | - feature(package) putout v13.7.1 75 | - feature(package) socket-file v5.0.0 76 | - feature(package) putout v11.0.4 77 | - feature(package) eslint-plugin-putout v6.0.1 78 | - feature(package) webpack-cli v4.1.0 79 | - feature(package) webpack v5.2.0 80 | - feature(package) style-loader v2.0.0 81 | - feature(package) css-loader v5.0.0 82 | - feature(package) monaco-editor v0.21.2 83 | - feature(package) putout v10.0.3 84 | 85 | 86 | 87 | 2020.09.06, v7.0.15 88 | 89 | feature: 90 | - (package) css-loader v4.2.2 91 | - (package) monaco-vim v0.1.8 92 | 93 | 94 | 2020.07.02, v7.0.14 95 | 96 | fix: 97 | - (deepword) edward -> deepword 98 | 99 | feature: 100 | - (package) madrun v7.0.0 101 | - (package) putout v9.1.0 102 | - (package) eslint-plugin-putout v5.0.1 103 | 104 | 105 | 2020.06.22, v7.0.13 106 | 107 | fix: 108 | - (deepword) server: edit: data -> Edit 109 | 110 | feature: 111 | - (package) eslint-plugin-putout v4.1.0 112 | - (package) supertape v2.0.1 113 | 114 | 115 | 2020.06.06, v7.0.12 116 | 117 | feature: 118 | - (server) edit: simplify 119 | - (package) eslint v7.2.0 120 | - (package) putout v8.15.0 121 | - (package) madrun v6.0.1 122 | - (package) @cloudcmd/stub v3.1.0 123 | 124 | 125 | 2020.04.16, v7.0.11 126 | 127 | feature: 128 | - (package) jssha v3.1.0 129 | 130 | 131 | 2020.03.31, v7.0.10 132 | 133 | feature: 134 | - (package) url-loader v4.0.0 135 | - (package) file-loader v6.0.0 136 | 137 | 138 | 2020.03.10, v7.0.9 139 | 140 | fix: 141 | - (deepword) readjson 142 | 143 | 144 | 2020.03.03, v7.0.8 145 | 146 | feature: 147 | - (package) readjson v2.0.1 148 | - (package) monaco-vim v0.1.7 149 | 150 | 151 | 2020.02.29, v7.0.7 152 | 153 | feature: 154 | - (package) socket-file v4.0.0 155 | 156 | 157 | 2020.02.26, v7.0.6 158 | 159 | feature: 160 | - (package) try-to-catch v3.0.0 161 | - (package) try-catch v3.0.0 162 | 163 | 164 | 2020.02.17, v7.0.5 165 | 166 | feature: 167 | - (package) monaco-editor v0.20.0 168 | - (package) mkdirp v1.0.3 169 | 170 | 171 | 2020.01.17, v7.0.4 172 | 173 | feature: 174 | - (package) monaco-editor v0.19.3 175 | - (package) eslint-plugin-node v11.0.0 176 | - (package) emitify v4.0.1 177 | - (package) nyc v15.0.0 178 | - (package) url-loader v3.0.0 179 | - (package) nodemon v2.0.1 180 | - (package) madrun v5.0.0 181 | - (package) eslint-plugin-putout v3.0.0 182 | - (package) putout v7.0.2 183 | 184 | 185 | 2019.11.04, v7.0.3 186 | 187 | feature: 188 | - (package) madrun v4.1.4 189 | - (package) monaco-vim v0.1.4 190 | 191 | 192 | 2019.10.16, v7.0.2 193 | 194 | feature: 195 | - (package) try-to-catch v2.0.0 196 | 197 | 198 | 2019.10.02, v7.0.1 199 | 200 | feature: 201 | - (deepword) upgrade resolve-path 202 | 203 | 204 | 2019.09.25, v7.0.0 205 | 206 | fix: 207 | - (deepword) singleton (coderaiser/cloudcmd/issues/254) 208 | 209 | feature: 210 | - (deepword) drop support of node < 10 211 | - (package) restafary v8.2.0 212 | - (package) restbox v2.0.0 213 | 214 | 215 | 2019.09.20, v6.2.3 216 | 217 | feature: 218 | - (package) load.js v3.0.2 219 | - (package) smalltalk v4.0.2 220 | - (package) currify v4.0.0 221 | 222 | 223 | 2019.09.18, v6.2.2 224 | 225 | feature: 226 | - (package) putout v6.0.0 227 | - (package) fullstore v2.0.2 228 | - (package) rm unused ashify 229 | 230 | 231 | 2019.09.09, v6.2.1 232 | 233 | feature: 234 | - (package) monaco-editor v0.18.0 235 | - (package) eslint-plugin-node v10.0.0 236 | 237 | 238 | 2019.09.04, v6.2.0 239 | 240 | feature: 241 | - (deepword) add browserlist 242 | - (package) madrun v3.0.1 243 | - (package) restafary v7.0.0 244 | 245 | 246 | 2019.08.21, v6.1.6 247 | 248 | feature: 249 | - (package) monaco-vim v0.1.1 250 | - (package) rimraf v3.0.0 251 | - (package) eslint-plugin-putout v2.0.0 252 | - (package) putout v5.7.1 253 | - (package) style-loader v1.0.0 254 | 255 | 256 | 2019.08.02, v6.1.5 257 | 258 | feature: 259 | - (package) monaco-vim v0.1.0 260 | 261 | 262 | 2019.07.24, v6.1.4 263 | 264 | feature: 265 | - (package) monaco-vim v0.0.15 266 | 267 | 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Coderaiser 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deepword [![License][LicenseIMGURL]][LicenseURL] [![NPM version][NPMIMGURL]][NPMURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Coverage Status][CoverageIMGURL]][CoverageURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/deepword.svg?style=flat 4 | [BuildStatusURL]: https://github.com/cloudcmd/deepword/actions?query=workflow%3A%22Node+CI%22 "Build Status" 5 | [BuildStatusIMGURL]: https://github.com/cloudcmd/deepword/workflows/Node%20CI/badge.svg 6 | [LicenseIMGURL]: https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat 7 | [NPM_INFO_IMG]: https://nodei.co/npm/deepword.png?downloads=true&&stars&&downloadRank "npm install deepword" 8 | [NPMURL]: https://npmjs.org/package/deepword "npm" 9 | [LicenseURL]: https://tldrlegal.com/license/mit-license "MIT License" 10 | [CoverageURL]: https://coveralls.io/github/cloudcmd/deepword?branch=master 11 | [CoverageIMGURL]: https://coveralls.io/repos/cloudcmd/deepword/badge.svg?branch=master&service=github 12 | 13 | Web editor used in [Cloud Commander](http://cloudcmd.io) based on [Monaco](https://microsoft.github.io/monaco-editor/ "Monaco"). 14 | 15 |  16 | 17 | ## Features 18 | 19 | - Syntax highlighting based on extension of file for over 30 languages. 20 | - Configurable options ([edit.json](json/edit.json) could be overriden by `~/.deepword.json`) according to [monaco editor options](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditoroptions.html) 21 | 22 | ## Install 23 | 24 | ``` 25 | npm i deepword -g 26 | ``` 27 | 28 | ![NPM\_INFO][NPM_INFO_IMG] 29 | 30 | ## Command line parameters 31 | 32 | Usage: `deepword [filename]` 33 | 34 | |Parameter |Operation 35 | |:----------------------|:-------------------------------------------- 36 | | `-h, --help` | display help and exit 37 | | `-v, --version` | output version information and exit 38 | 39 | ## Hot keys 40 | 41 | |Key |Operation 42 | |:----------------------|:-------------------------------------------- 43 | | `Ctrl + s` | save 44 | | `Ctrl + f` | find 45 | | `Ctrl + h` | replace 46 | | `Ctrl + g` | go to line 47 | | `Ctrl + e` | evaluate (JavaScript only supported) 48 | 49 | For more details see [Ace keyboard shortcuts](https://github.com/ajaxorg/ace/wiki/Default-Keyboard-Shortcuts "Ace keyboard shortcuts"). 50 | 51 | ## Options 52 | 53 | You can override [monaco editor options](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditoroptions.html) in `~/.deepword.json`. 54 | 55 | ### Theme 56 | 57 | To override `theme` use `theme` options in `~/.deepword.json`. Themes can be: 58 | 59 | - `vs` 60 | - `vs-dark` 61 | 62 | ## API 63 | 64 | Deepword could be used as middleware for [express](http://expressjs.com "Express"). 65 | For this purpuse API could be used. 66 | 67 | ### Server 68 | 69 | #### deepword(options) 70 | 71 | Middleware of `deepword`. Options could be omitted. 72 | 73 | ```js 74 | import deepword from 'deepword'; 75 | import express from 'express'; 76 | 77 | const app = express(); 78 | 79 | app.use(deepword({ 80 | root: '/', // default 81 | diff: true, // default 82 | zip: true, // default 83 | dropbox: false, // optional 84 | dropboxToken: 'token', // optional 85 | })); 86 | 87 | app.listen(31_337); 88 | ``` 89 | 90 | #### deepword.listen(socket) 91 | 92 | Could be used with [socket.io](http://socket.io "Socket.io") to handle editor events with. 93 | 94 | ```js 95 | import {Server} from 'socket.io'; 96 | const socket = new Server(server); 97 | 98 | deepword.listen(socket, { 99 | prefixSocket: '/deepword', // optional 100 | auth: (accept, reject) => (username, password) => { 101 | // optional 102 | accept(); 103 | }, 104 | }); 105 | ``` 106 | 107 | ### Client 108 | 109 | Deepword uses [Monaco](https://microsoft.github.io/monaco-editor/ "Monaco") on client side, so API is similar. 110 | All you need is put minimal `html`, `css`, and `js` into your page. 111 | 112 | Minimal html: 113 | 114 | ```html 115 |
116 | 117 | ``` 118 | 119 | Minimal css: 120 | 121 | ```css 122 | html, body, .edit { 123 | height: 100%; 124 | width: 100%; 125 | } 126 | 127 | body { 128 | margin: 0; 129 | } 130 | 131 | .edit { 132 | overflow: hidden; 133 | } 134 | 135 | ``` 136 | 137 | Minimal js: 138 | 139 | ```js 140 | deepword('[data-name="js-edit"]', (editor) => { 141 | editor.setValue('Hello deepword!'); 142 | }); 143 | ``` 144 | 145 | For more information you could always look into `html` and `bin` directory. 146 | 147 | ## Related 148 | 149 | - [Edward](https://github.com/cloudcmd/edward "Edwdard") - web editor based on [Ace](https://ace.c9.io "Ace"). 150 | - [Dword](https://github.com/cloudcmd/dword "Dword") - web editor based on [Codemirror](https://codemirror.net "Codemirror"). 151 | 152 | ## License 153 | 154 | MIT 155 | -------------------------------------------------------------------------------- /bin/deepword.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {createRequire} from 'node:module'; 4 | import {dirname} from 'node:path'; 5 | import process from 'node:process'; 6 | import fs from 'node:fs'; 7 | import {fileURLToPath} from 'node:url'; 8 | import http from 'node:http'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | const require = createRequire(import.meta.url); 13 | const [name] = process.argv.slice(2); 14 | 15 | if (!name) 16 | usage(); 17 | else if (/^(-v|--v)$/.test(name)) 18 | version(); 19 | else if (/^(-h|--help)$/.test(name)) 20 | help(); 21 | else 22 | checkFile(name, async (error) => { 23 | if (!error) 24 | return await main(name); 25 | 26 | console.error(error.message); 27 | }); 28 | 29 | function getPath(name) { 30 | const reg = /^(~|\/)/; 31 | 32 | if (!reg.test(name)) 33 | name = process.cwd() + '/' + name; 34 | 35 | return name; 36 | } 37 | 38 | async function main(name) { 39 | const filename = getPath(name); 40 | const DIR = `${__dirname}/../html/`; 41 | const {default: deepword} = await import('../server/index.js'); 42 | const {default: express} = await import('express'); 43 | const {Server} = await import('socket.io'); 44 | 45 | const app = express(); 46 | const server = http.createServer(app); 47 | 48 | const {env} = process; 49 | const port = env.PORT || 1337; 50 | const ip = env.IP || '0.0.0.0'; 51 | 52 | app 53 | .use(express.static(DIR)) 54 | .use(deepword({ 55 | diff: true, 56 | zip: true, 57 | })); 58 | 59 | server.listen(port, ip); 60 | 61 | const socket = new Server(server); 62 | const edSocket = deepword.listen(socket); 63 | 64 | edSocket.on('connection', () => { 65 | fs.readFile(name, 'utf8', (error, data) => { 66 | if (error) 67 | console.error(error.message); 68 | else 69 | edSocket.emit('file', filename, data); 70 | }); 71 | }); 72 | 73 | console.log('url: http://' + ip + ':' + port); 74 | } 75 | 76 | function checkFile(name, callback) { 77 | fs.stat(name, (error, stat) => { 78 | let msg; 79 | 80 | if (error && error.code === 'ENOENT') 81 | msg = Error(`no such file or directory: '${name}'`); 82 | else if (stat.isDirectory()) 83 | msg = Error(`'${name}' is directory`); 84 | 85 | callback(msg); 86 | }); 87 | } 88 | 89 | function version() { 90 | console.log(`v${info().version}`); 91 | } 92 | 93 | function info() { 94 | return require('../package'); 95 | } 96 | 97 | function usage() { 98 | console.log(`Usage: ${info().name} [filename]`); 99 | } 100 | 101 | function help() { 102 | const bin = require('../json/bin'); 103 | 104 | usage(); 105 | console.log('Options:'); 106 | 107 | for (const name of Object.keys(bin)) { 108 | console.log(` ${name} ${bin[name]}`); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /client/api/_add-commands.js: -------------------------------------------------------------------------------- 1 | const addId = (a) => { 2 | a.id = `deepword.action.${a.id}`; 3 | return a; 4 | }; 5 | 6 | export default function _addCommands() { 7 | const {_monaco, _eddy} = this; 8 | const {KeyCode, KeyMod} = _monaco; 9 | 10 | const addAction = _eddy.addAction.bind(_eddy); 11 | const evaluate = this.evaluate.bind(this); 12 | const goToLine = this.goToLine.bind(this); 13 | const save = this.save.bind(this); 14 | 15 | const run = (fn) => () => this.isKey() && fn(); 16 | 17 | const { 18 | KeyE, 19 | KeyG, 20 | KeyS, 21 | } = KeyCode; 22 | 23 | const {CtrlCmd, WinCtrl} = KeyMod; 24 | 25 | const actions = [{ 26 | id: 'evaluate', 27 | label: 'Evaluate', 28 | keybindings: [CtrlCmd | KeyE, WinCtrl | KeyE], 29 | run: run(evaluate), 30 | }, { 31 | id: 'goToLine', 32 | label: 'Go To Line', 33 | keybindings: [CtrlCmd | KeyG, WinCtrl | KeyG], 34 | run: goToLine, 35 | }, { 36 | id: 'save', 37 | label: 'Save', 38 | keybindings: [CtrlCmd | KeyS, WinCtrl | KeyS], 39 | run: run(save), 40 | }]; 41 | 42 | actions 43 | .map(addId) 44 | .forEach(addAction); 45 | } 46 | -------------------------------------------------------------------------------- /client/api/_init-socket.js: -------------------------------------------------------------------------------- 1 | /* global io*/ 2 | import {alert} from 'smalltalk'; 3 | import {applyPatch} from 'daffy'; 4 | import {promisify} from 'es6-promisify'; 5 | 6 | const getHost = () => { 7 | const l = location; 8 | 9 | return l.origin || l.protocol + '//' + l.host; 10 | }; 11 | 12 | export default function _initSocket(prefix = '', socketPath = '') { 13 | const href = `${getHost()}${prefix}`; 14 | const FIVE_SECONDS = 5000; 15 | const socketPatch = promisify((name, data, fn) => { 16 | socket.emit('patch', name, data); 17 | fn(); 18 | }); 19 | 20 | this.on('auth', (username, password) => { 21 | socket.emit('auth', username, password); 22 | }); 23 | 24 | const socket = io.connect(href, { 25 | 'max reconnection attempts': 2 ** 32, 26 | 'reconnection limit': FIVE_SECONDS, 27 | 'path': `${socketPath}/socket.io`, 28 | }); 29 | 30 | socket.on('reject', () => { 31 | this.emit('reject'); 32 | }); 33 | 34 | socket.on('connect', () => { 35 | this._patch = socketPatch; 36 | }); 37 | 38 | socket.on('disconnect', () => { 39 | this._patch = this._patchHTTP; 40 | }); 41 | 42 | socket.on('message', (msg) => { 43 | this._onSave(null, msg); 44 | }); 45 | 46 | socket.on('patch', (name, patch, hash) => { 47 | const wrongHash = hash !== _story.getHash(name); 48 | 49 | const { 50 | _filename, 51 | _story, 52 | getValue, 53 | setValue, 54 | getCursor, 55 | moveCursorTo, 56 | sha, 57 | } = this; 58 | 59 | const wrongFile = name !== _filename; 60 | 61 | if (wrongFile || wrongHash) 62 | return; 63 | 64 | const value = applyPatch(getValue(), patch); 65 | 66 | setValue(value); 67 | 68 | _story 69 | .setData(name, value) 70 | .setHash(name, sha()); 71 | 72 | const {row, column} = getCursor(); 73 | moveCursorTo(row, column); 74 | }); 75 | 76 | socket.on('file', (filename, value) => { 77 | this._filename = filename; 78 | this._value = value; 79 | 80 | this.setValue(value); 81 | this.setModeForPath(filename); 82 | }); 83 | 84 | socket.on('err', (error) => { 85 | alert(this._TITLE, error); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /client/api/_on-save.js: -------------------------------------------------------------------------------- 1 | import {confirm} from 'smalltalk'; 2 | import {write} from 'restafary/client'; 3 | 4 | const empty = () => {}; 5 | 6 | export default function _onSave(error, text) { 7 | let msg = 'Try again?'; 8 | 9 | const onSave = this._onSave.bind(this); 10 | const {_filename} = this; 11 | const _value = this.getValue(); 12 | 13 | if (error) { 14 | if (error.message) 15 | msg = error.message + '\n' + msg; 16 | else 17 | msg = `Can't save.${msg}`; 18 | 19 | confirm(this._TITLE, msg) 20 | .then(() => { 21 | write(_filename, _value, onSave); 22 | }) 23 | .catch(empty) 24 | .then(() => { 25 | this.focus(); 26 | }); 27 | } else { 28 | this.showMessage(text); 29 | 30 | this 31 | ._story 32 | .setData(_filename, _value) 33 | .setHash(_filename, this.sha()); 34 | 35 | this.emit('save', _value.length); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/api/clipboard.js: -------------------------------------------------------------------------------- 1 | export function copyToClipboard() { 2 | document.execComand('copy'); 3 | return this; 4 | } 5 | 6 | export function cutToClipboard() { 7 | this.focus(); 8 | document.execComand('cut'); 9 | 10 | return this; 11 | } 12 | 13 | export function pastFromClipboard() { 14 | this.showMessage('pastFromClipboard: Not implemented'); 15 | } 16 | -------------------------------------------------------------------------------- /client/api/evaluate.js: -------------------------------------------------------------------------------- 1 | import {alert} from 'smalltalk'; 2 | import tryCatch from 'try-catch'; 3 | 4 | const getErrorMsg = (isJS, value) => { 5 | if (!isJS) 6 | return 'Evaluation supported for JavaScript only'; 7 | 8 | const [error] = tryCatch(Function, value); 9 | 10 | return error; 11 | }; 12 | 13 | export default function evaluate() { 14 | const isJS = /\.js$/.test(this._filename); 15 | 16 | const getValue = this.getValue.bind(this); 17 | const focus = this.focus.bind(this); 18 | 19 | const msg = getErrorMsg(isJS, getValue()); 20 | 21 | if (!msg) 22 | return; 23 | 24 | alert(this._TITLE, msg).then(focus); 25 | } 26 | -------------------------------------------------------------------------------- /client/api/go-to-line.js: -------------------------------------------------------------------------------- 1 | import {prompt} from 'smalltalk'; 2 | 3 | export default function goToLine() { 4 | const empty = (e) => { 5 | if (e) 6 | throw e; 7 | }; 8 | 9 | const msg = 'Enter line number:'; 10 | const {_eddy, _TITLE} = this; 11 | const {lineNumber} = _eddy.getPosition(); 12 | 13 | prompt(_TITLE, msg, lineNumber) 14 | .then((line) => { 15 | const lineNumber = Number(line); 16 | const column = 0; 17 | 18 | const reveal = true; 19 | const revealVerticalInCenter = true; 20 | 21 | _eddy.setPosition({ 22 | lineNumber, 23 | column, 24 | }, reveal, revealVerticalInCenter); 25 | }) 26 | .catch(empty) 27 | .then(() => { 28 | _eddy.focus(); 29 | }); 30 | 31 | return this; 32 | } 33 | -------------------------------------------------------------------------------- /client/api/index.js: -------------------------------------------------------------------------------- 1 | import {inherits} from 'node:util'; 2 | import { 3 | patch, 4 | write, 5 | prefix, 6 | } from 'restafary/client'; 7 | import * as load from 'load.js'; 8 | import Emitify from 'emitify'; 9 | import {createPatch} from 'daffy'; 10 | import jssha from 'jssha'; 11 | import currify from 'currify'; 12 | import {enableVim, disableVim} from './vim.js'; 13 | import goToLine from './go-to-line.js'; 14 | import _initSocket from './_init-socket.js'; 15 | import showMessage from './show-message/index.js'; 16 | import setMode from './set-mode.js'; 17 | import setModeForPath from './set-mode-for-path.js'; 18 | import save from './save.js'; 19 | import _onSave from './_on-save.js'; 20 | import _addCommands from './_add-commands.js'; 21 | import evaluate from './evaluate.js'; 22 | import { 23 | copyToClipboard, 24 | cutToClipboard, 25 | pastFromClipboard, 26 | } from './clipboard.js'; 27 | import story from './story.js'; 28 | 29 | export default currify(Deepword); 30 | 31 | inherits(Deepword, Emitify); 32 | 33 | function Deepword(element, options, eddy) { 34 | if (!(this instanceof Deepword)) 35 | return new Deepword(element, options, eddy); 36 | 37 | Emitify.call(this); 38 | 39 | const {monaco} = window; 40 | 41 | this._monaco = monaco; 42 | this._TITLE = 'Deepword'; 43 | this._element = element; 44 | /* monaco editor bigger then element */ 45 | this._element.style.overflow = 'hidden'; 46 | 47 | this._eddy = eddy; 48 | 49 | const { 50 | maxSize, 51 | socketPath, 52 | prefixSocket = '/deepword', 53 | } = options; 54 | 55 | this._maxSize = maxSize || 512_000; 56 | this._prefix = options.prefix || '/deepword'; 57 | 58 | prefix(`${this._prefix}/api/v1/fs`); 59 | 60 | this._initSocket(prefixSocket, socketPath); 61 | this._addCommands(); 62 | this._story = story(); 63 | 64 | this._isKey = true; 65 | 66 | this._write = this._writeHTTP; 67 | this._patch = this._patchHTTP; 68 | } 69 | 70 | Deepword.prototype.goToLine = goToLine; 71 | Deepword.prototype._initSocket = _initSocket; 72 | Deepword.prototype.showMessage = showMessage; 73 | Deepword.prototype.setMode = setMode; 74 | Deepword.prototype.setModeForPath = setModeForPath; 75 | Deepword.prototype.save = save; 76 | Deepword.prototype._onSave = _onSave; 77 | Deepword.prototype._addCommands = _addCommands; 78 | Deepword.prototype.evaluate = evaluate; 79 | 80 | Deepword.prototype.copyToClipboard = copyToClipboard; 81 | Deepword.prototype.cutToClipboard = cutToClipboard; 82 | Deepword.prototype.pastFromClipboard = pastFromClipboard; 83 | 84 | Deepword.prototype.isKey = function() { 85 | return this._isKey; 86 | }; 87 | 88 | Deepword.prototype.enableKey = function() { 89 | this._isKey = true; 90 | return this; 91 | }; 92 | 93 | Deepword.prototype.disableKey = function() { 94 | this._isKey = false; 95 | return this; 96 | }; 97 | 98 | Deepword.prototype.setValue = function(value) { 99 | this._eddy.setValue(value); 100 | 101 | return this; 102 | }; 103 | 104 | Deepword.prototype.setValueFirst = function(name, value) { 105 | this.setValue(value); 106 | 107 | this._filename = name; 108 | this._value = value; 109 | 110 | return this; 111 | }; 112 | 113 | Deepword.prototype.getValue = function() { 114 | return this._eddy.getValue(); 115 | }; 116 | 117 | Deepword.prototype.getCursor = function() { 118 | const {column, lineNumber} = this._eddy.getPosition(); 119 | 120 | return { 121 | column, 122 | row: lineNumber, 123 | }; 124 | }; 125 | 126 | Deepword.prototype.moveCursorTo = function(lineNumber, column) { 127 | this._eddy.setPosition({ 128 | column, 129 | lineNumber, 130 | }); 131 | 132 | return this; 133 | }; 134 | 135 | Deepword.prototype.focus = function() { 136 | this._eddy.focus(); 137 | return this; 138 | }; 139 | 140 | Deepword.prototype._patchHTTP = function(path, value) { 141 | const onSave = this._onSave.bind(this); 142 | patch(path, value, onSave); 143 | }; 144 | 145 | Deepword.prototype._writeHTTP = function(path, data) { 146 | const onSave = this._onSave.bind(this); 147 | write(path, data, onSave); 148 | }; 149 | 150 | Deepword.prototype._loadOptions = async function() { 151 | const {_prefix, _options} = this; 152 | 153 | this._options = _options || await load.json(`${_prefix}/options.json`); 154 | //return Promise.resolve(this._options); 155 | 156 | return this._options; 157 | }; 158 | 159 | Deepword.prototype.setOption = function(name, value) { 160 | const options = {}; 161 | 162 | options[name] = value; 163 | 164 | if (name === 'keyMap') 165 | this.setKeyMap(value); 166 | 167 | this._eddy.updateOptions(options); 168 | 169 | return this; 170 | }; 171 | 172 | Deepword.prototype.setKeyMap = function(name) { 173 | if (name === 'vim') 174 | return enableVim(this._eddy, this._element); 175 | 176 | disableVim(); 177 | }; 178 | 179 | Deepword.prototype.setOptions = function(options) { 180 | for (const name of Object.keys(options)) { 181 | this.setOption(name, options[name]); 182 | } 183 | 184 | return this; 185 | }; 186 | 187 | Deepword.prototype.isChanged = function() { 188 | return this._value !== this.getValue(); 189 | }; 190 | 191 | Deepword.prototype.sha = function() { 192 | const value = this.getValue(); 193 | const sha = new jssha('SHA-1', 'TEXT'); 194 | 195 | sha.update(value); 196 | 197 | return sha.getHash('HEX'); 198 | }; 199 | 200 | Deepword.prototype._diff = function(value) { 201 | return createPatch(value, this.getValue()); 202 | }; 203 | 204 | Deepword.prototype._doDiff = async function(path) { 205 | const {_value, _story} = this; 206 | 207 | const ifEqual = (equal) => !equal ? '' : this._diff(_value); 208 | 209 | return _story 210 | .checkHash(path) 211 | .then(ifEqual) 212 | .catch(ifEqual); 213 | }; 214 | 215 | Deepword.prototype.selectAll = function() { 216 | const {_eddy} = this; 217 | const {model} = _eddy; 218 | const getLinesCount = model.getLineCount.bind(model); 219 | 220 | _eddy.setSelection({ 221 | startLineNumber: 1, 222 | startColumn: 1, 223 | endLineNumber: getLinesCount(), 224 | endColumn: Infinity, 225 | }); 226 | 227 | return this; 228 | }; 229 | 230 | Deepword.prototype.remove = function() { 231 | this.showMessage('remove: Not implemented'); 232 | }; 233 | 234 | Deepword.prototype._getSelected = function() { 235 | const {_eddy} = this; 236 | const selection = _eddy.getSelection(); 237 | 238 | return _eddy.model.getValueInRange(selection); 239 | }; 240 | 241 | Deepword.prototype._showMessageOnce = function(msg) { 242 | if (this._showedOnce) 243 | return; 244 | 245 | this.showMessage(msg); 246 | this._showedOnce = true; 247 | }; 248 | -------------------------------------------------------------------------------- /client/api/save.js: -------------------------------------------------------------------------------- 1 | import wraptile from 'wraptile'; 2 | import {promisify} from 'es6-promisify'; 3 | import _zipio from 'zipio'; 4 | 5 | const isString = (a) => typeof a === 'string'; 6 | const zipio = promisify(_zipio); 7 | 8 | const setValue = wraptile(_setValue); 9 | 10 | export default function() { 11 | save 12 | .call(this) 13 | .then(setValue(this)); 14 | 15 | return this; 16 | } 17 | 18 | async function save() { 19 | const value = this.getValue(); 20 | const {length} = value; 21 | const {_filename, _maxSize} = this; 22 | 23 | const {diff, zip} = await this._loadOptions(); 24 | 25 | if (diff) { 26 | const patch = await this._doDiff(_filename); 27 | const isPatch = checkPatch(length, _maxSize, patch); 28 | 29 | if (isPatch) 30 | return this._patch(_filename, patch); 31 | } 32 | 33 | if (!zip) 34 | return this._write(_filename, value); 35 | 36 | const zipedValue = await zipio(value); 37 | 38 | return this._write(`${_filename}?unzip`, zipedValue); 39 | } 40 | 41 | function _setValue(ctx) { 42 | ctx._value = ctx.getValue(); 43 | } 44 | 45 | function checkPatch(length, maxSize, patch) { 46 | const patchLength = patch?.length || 0; 47 | const isLessMaxLength = length < maxSize; 48 | const isLessLength = isLessMaxLength && patchLength < length; 49 | const isStr = isString(patch); 50 | 51 | return patch && isStr && isLessLength; 52 | } 53 | -------------------------------------------------------------------------------- /client/api/set-mode-for-path.js: -------------------------------------------------------------------------------- 1 | import {extname} from 'node:path'; 2 | import modeForExt from '../../common/mode-for-ext.js'; 3 | 4 | const modeForPath = (name, langs) => { 5 | return modeForExt(extname(name), langs); 6 | }; 7 | 8 | export default function setModeForPath(name) { 9 | const {_monaco} = this; 10 | 11 | const {languages} = _monaco; 12 | 13 | const mode = modeForPath(name, languages.getLanguages()); 14 | 15 | this.setMode(mode); 16 | 17 | return this; 18 | } 19 | -------------------------------------------------------------------------------- /client/api/set-mode.js: -------------------------------------------------------------------------------- 1 | export default function setMode(mode) { 2 | const {_monaco, _eddy} = this; 3 | 4 | const {createModel} = _monaco.editor; 5 | 6 | const value = this.getValue(); 7 | const model = createModel(value, mode); 8 | 9 | _eddy.setModel(model); 10 | _eddy.focus(); 11 | 12 | return this; 13 | } 14 | -------------------------------------------------------------------------------- /client/api/set-mode.spec.js: -------------------------------------------------------------------------------- 1 | import {test, stub} from 'supertape'; 2 | import setMode from './set-mode.js'; 3 | 4 | test('client: setMode: return this', (t) => { 5 | const ctx = getContext(); 6 | 7 | t.equal(setMode.call(ctx), ctx, 'should return this'); 8 | t.end(); 9 | }); 10 | 11 | test('client: setMode: getValue', (t) => { 12 | const ctx = getContext(); 13 | 14 | const {getValue} = ctx; 15 | setMode.call(ctx); 16 | 17 | t.calledWithNoArgs(getValue, 'should call getValue'); 18 | t.end(); 19 | }); 20 | 21 | test('client: setMode: createModel', (t) => { 22 | const ctx = getContext(); 23 | const value = 'hello'; 24 | 25 | ctx.getValue.returns(value); 26 | 27 | const {createModel} = ctx._monaco.editor; 28 | const mode = 'javascript'; 29 | 30 | setMode.call(ctx, mode); 31 | 32 | t.calledWith(createModel, [value, mode], 'should call createModel'); 33 | t.end(); 34 | }); 35 | 36 | test('client: setMode: setModel', (t) => { 37 | const ctx = getContext(); 38 | const model = 'model'; 39 | 40 | ctx._monaco.editor.createModel.returns(model); 41 | 42 | const {setModel} = ctx._eddy; 43 | 44 | setMode.call(ctx); 45 | 46 | t.calledWith(setModel, [model], 'should call setModel'); 47 | t.end(); 48 | }); 49 | 50 | test('client: setMode: focus', (t) => { 51 | const ctx = getContext(); 52 | const {focus} = ctx._eddy; 53 | 54 | setMode.call(ctx); 55 | 56 | t.calledWithNoArgs(focus, 'should call focus'); 57 | t.end(); 58 | }); 59 | 60 | function getContext() { 61 | const focus = stub(); 62 | const setModel = stub(); 63 | const _eddy = { 64 | setModel, 65 | focus, 66 | }; 67 | 68 | const createModel = stub(); 69 | 70 | const editor = { 71 | createModel, 72 | }; 73 | 74 | const _monaco = { 75 | editor, 76 | }; 77 | 78 | const getValue = stub(); 79 | 80 | return { 81 | _monaco, 82 | _eddy, 83 | getValue, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /client/api/show-message/index.js: -------------------------------------------------------------------------------- 1 | import './show-message.css'; 2 | 3 | export default function showMessage(text) { 4 | const HIDE_TIME = 2000; 5 | let {_elementMsg} = this; 6 | 7 | if (!_elementMsg) { 8 | this._elementMsg = _elementMsg = createMsg(); 9 | this._element.appendChild(this._elementMsg); 10 | } 11 | 12 | _elementMsg.textContent = text; 13 | _elementMsg.hidden = false; 14 | 15 | setTimeout(() => { 16 | _elementMsg.hidden = true; 17 | }, HIDE_TIME); 18 | 19 | return this; 20 | } 21 | 22 | function createMsg() { 23 | const wrapper = document.createElement('div'); 24 | 25 | wrapper.innerHTML = '