├── docs ├── images ├── dist │ ├── normal.css.gz │ ├── normal.js.gz │ ├── user-theme.css.gz │ ├── user-theme.css │ ├── normal.js │ └── normal.css ├── index.html ├── .vscode │ └── settings.html ├── dependencies.json ├── toc.json ├── LICENSE.html ├── src │ └── extension.html └── CHANGELOG.html ├── .gitignore ├── images ├── pooh.jpg ├── favicon.png ├── keymap.png ├── normal.jpg ├── sharks.png ├── output-log.png ├── searching.gif ├── status-bar.gif ├── import-preset.png ├── quick-snippet.gif ├── selected-text.png ├── vim-uppercase.gif ├── wood-planes.jpg ├── cursor-movement.gif ├── keyboard-layout.png ├── mode-switching.gif ├── recursive-keymap-example.png ├── escape.svg └── favicon.svg ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── index.html ├── .vscodeignore ├── tsconfig.json ├── litsconfig.json ├── user-theme.css ├── LICENSE.md ├── src ├── extension.ts └── actions.ts ├── package.json ├── CHANGELOG.md └── README.md /docs/images: -------------------------------------------------------------------------------- 1 | ../images -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | out 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /images/pooh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/pooh.jpg -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/favicon.png -------------------------------------------------------------------------------- /images/keymap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/keymap.png -------------------------------------------------------------------------------- /images/normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/normal.jpg -------------------------------------------------------------------------------- /images/sharks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/sharks.png -------------------------------------------------------------------------------- /images/output-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/output-log.png -------------------------------------------------------------------------------- /images/searching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/searching.gif -------------------------------------------------------------------------------- /images/status-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/status-bar.gif -------------------------------------------------------------------------------- /docs/dist/normal.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/docs/dist/normal.css.gz -------------------------------------------------------------------------------- /docs/dist/normal.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/docs/dist/normal.js.gz -------------------------------------------------------------------------------- /images/import-preset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/import-preset.png -------------------------------------------------------------------------------- /images/quick-snippet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/quick-snippet.gif -------------------------------------------------------------------------------- /images/selected-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/selected-text.png -------------------------------------------------------------------------------- /images/vim-uppercase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/vim-uppercase.gif -------------------------------------------------------------------------------- /images/wood-planes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/wood-planes.jpg -------------------------------------------------------------------------------- /images/cursor-movement.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/cursor-movement.gif -------------------------------------------------------------------------------- /images/keyboard-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/keyboard-layout.png -------------------------------------------------------------------------------- /images/mode-switching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/mode-switching.gif -------------------------------------------------------------------------------- /docs/dist/user-theme.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/docs/dist/user-theme.css.gz -------------------------------------------------------------------------------- /images/recursive-keymap-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johtela/vscode-modaledit/HEAD/images/recursive-keymap-example.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false 4 | }, 5 | "search.exclude": { 6 | "out": true 7 | }, 8 | "typescript.tsc.autoDetect": "off" 9 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to README.html 4 | 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to README.html 4 | 5 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | docs/** 4 | out/** 5 | src/** 6 | .gitignore 7 | **/*tsconfig.json 8 | **/*.map 9 | **/*.ts 10 | **/*.html 11 | **/*.css 12 | images/** 13 | !images/normal.jpg 14 | -------------------------------------------------------------------------------- /docs/.vscode/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to tutorial.html 4 | 5 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile", 9 | "problemMatcher": "$tsc", 10 | "isBackground": false, 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "webpack", 22 | "problemMatcher": [ 23 | "$tsc" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /docs/dist/user-theme.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Sanchez:ital@0;1&display=swap";body{--sans-font: "Noto Sans";--serif-font: "Sanchez";--mono-font: "Roboto Mono";--cnt-font-size: 17px;--cnt-width: 75ch}key{display:inline;display:inline-block;min-width:2em;padding:0 2px;margin:0 2px;font-family:var(--sans-font);font-size:.75em;text-align:center;text-decoration:none;border-radius:.3em;border:2px outset #f6e4db;cursor:default;user-select:none;background:linear-gradient(to bottom right,#e6b99e,#f6e4db);color:#323232} 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/actions": { 3 | "url": "src/actions.html", 4 | "dependencies": [ 5 | "vscode" 6 | ] 7 | }, 8 | "vscode": { 9 | "dependencies": [] 10 | }, 11 | "src/commands": { 12 | "url": "src/commands.html", 13 | "dependencies": [ 14 | "vscode", 15 | "src/actions", 16 | "util" 17 | ] 18 | }, 19 | "util": { 20 | "dependencies": [] 21 | }, 22 | "src/extension": { 23 | "url": "src/extension.html", 24 | "dependencies": [ 25 | "vscode", 26 | "src/actions", 27 | "src/commands" 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /litsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ "*.md", "presets/*" ], 3 | "updateToc": true, 4 | "nodeModules": [ 5 | { 6 | "path": "./src/extension.ts", 7 | "outDir": "./dist", 8 | "external": ["vscode"] 9 | } 10 | ], 11 | "frontMatter": { 12 | "projectName": "ModalEdit", 13 | "repository": "https://github.com/johtela/vscode-modaledit", 14 | "download": "https://marketplace.visualstudio.com/items?itemName=johtela.vscode-modaledit", 15 | "license": "LICENSE.html", 16 | "footer": "Copyright © 2025 Tommi Johtela", 17 | "logo": "images/escape.svg", 18 | "styles": [ "./user-theme.css" ], 19 | "theme": "red-alert" 20 | } 21 | } -------------------------------------------------------------------------------- /user-theme.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Sanchez:ital@0;1&display=swap'); 2 | 3 | body { 4 | --sans-font: "Noto Sans"; 5 | --serif-font: "Sanchez"; 6 | --mono-font: "Roboto Mono"; 7 | 8 | --cnt-font-size: 17px; 9 | --cnt-width: 75ch; 10 | } 11 | /* Custom style for keyboard keys. */ 12 | key { 13 | display: inline; 14 | display: inline-block; 15 | min-width: 2em; 16 | padding: 0 2px; 17 | margin: 0 2px; 18 | font-family: var(--sans-font); 19 | font-size: 0.75em; 20 | text-align: center; 21 | text-decoration: none; 22 | border-radius: .3em; 23 | border: 2px outset #f6e4db; 24 | cursor: default; 25 | user-select: none; 26 | background: linear-gradient(to bottom right, #e6b99e, #f6e4db); 27 | color: rgb(50, 50, 50); 28 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | **Copyright © 2020 Tommi Johtela** 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /docs/toc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "page": "Home", 4 | "file": "README.html", 5 | "desc": "Home page", 6 | "bullet": "🏠" 7 | }, 8 | { 9 | "page": "Tutorial", 10 | "file": "tutorial.html", 11 | "desc": "How to create your own keymaps", 12 | "bullet": "🎓" 13 | }, 14 | { 15 | "page": "Vim Presets", 16 | "file": "presets/vim.html", 17 | "desc": "Built-in Vim presets", 18 | "bullet": "🦈" 19 | }, 20 | { 21 | "page": "Implementation", 22 | "subs": [ 23 | { 24 | "page": "Extension", 25 | "file": "src/extension.html", 26 | "desc": "Main extension module", 27 | "bullet": "🔌" 28 | }, 29 | { 30 | "page": "Actions", 31 | "file": "src/actions.html", 32 | "desc": "Keymap management", 33 | "bullet": "⌨️" 34 | }, 35 | { 36 | "page": "Commands", 37 | "file": "src/commands.html", 38 | "desc": "Registered commands", 39 | "bullet": "📢" 40 | } 41 | ] 42 | }, 43 | { 44 | "page": "Change Log", 45 | "file": "CHANGELOG.html", 46 | "desc": "Version history", 47 | "bullet": "📅" 48 | }, 49 | { 50 | "page": "License", 51 | "file": "LICENSE.html", 52 | "desc": "MIT license", 53 | "bullet": "📜" 54 | } 55 | ] -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * # Extension Entry Point 3 | * 4 | * The module `vscode` contains the VS Code extensibility API. The other 5 | * modules are part of the extension. 6 | */ 7 | import * as vscode from 'vscode' 8 | import * as actions from './actions' 9 | import * as commands from './commands' 10 | /** 11 | * ## Activation 12 | * 13 | * This method is called when the extension is activated. The activation events 14 | * are set in the `package.json` like this: 15 | * ```js 16 | * "activationEvents": [ "*" ], 17 | * ``` 18 | * which means that the extension is activated as soon as VS Code is running. 19 | */ 20 | export function activate(context: vscode.ExtensionContext) { 21 | /** 22 | * The commands are defined in the `package.json` file. We register them 23 | * with function defined in the `commands` module. 24 | */ 25 | commands.register(context) 26 | /** 27 | * We create an output channel for diagnostic messages and pass it to the 28 | * `actions` module. 29 | */ 30 | let channel = vscode.window.createOutputChannel("ModalEdit") 31 | actions.setOutputChannel(channel) 32 | /** 33 | * Then we subscribe to events we want to react to. 34 | */ 35 | context.subscriptions.push( 36 | channel, 37 | vscode.workspace.onDidChangeConfiguration(actions.updateFromConfig), 38 | vscode.window.onDidChangeVisibleTextEditors(commands.resetSelecting), 39 | vscode.window.onDidChangeTextEditorSelection(e => 40 | commands.updateCursorAndStatusBar(e.textEditor)), 41 | vscode.workspace.onDidChangeTextDocument(commands.onTextChanged)) 42 | /** 43 | * Next we update the active settings from the config file, and at last, 44 | * we enter into normal or edit mode depending on the settings. 45 | */ 46 | actions.updateFromConfig() 47 | if (actions.getStartInNormalMode()) 48 | commands.enterNormal() 49 | else 50 | commands.enterInsert() 51 | } 52 | /** 53 | * ## Deactivation 54 | * 55 | * This method is called when your extension is deactivated 56 | */ 57 | export function deactivate() { 58 | commands.enterInsert() 59 | } -------------------------------------------------------------------------------- /images/escape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 32 | 36 | 37 | 47 | 48 | 70 | 72 | 73 | 75 | image/svg+xml 76 | 78 | 79 | 80 | 81 | 82 | 87 | 96 | Esc 108 | Esc 120 | 121 | 122 | -------------------------------------------------------------------------------- /images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 32 | 36 | 37 | 47 | 48 | 70 | 72 | 73 | 75 | image/svg+xml 76 | 78 | 79 | 80 | 81 | 82 | 87 | 96 | Esc 108 | Esc 120 | 121 | 122 | -------------------------------------------------------------------------------- /docs/dist/normal.js: -------------------------------------------------------------------------------- 1 | "use strict";(()=>{var n=(e,o)=>()=>(o||e((o={exports:{}}).exports,o),o.exports);var H=n((Re,ve)=>{ve.exports={}});var _=n((De,pe)=>{pe.exports={}});var g=n(r=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});r.infobox=r.closepopups=r.contentarea=r.navmenu=r.navbar=r.hamburger=r.accordion=r.collapsed=r.expanded=void 0;r.elementWithId=fe;r.firstElementWithClass=I;r.elementsWithClass=be;r.elementsWithTag=ye;r.isHTMLCollection=A;r.each=f;r.create=xe;r.attr=we;r.initAccordions=ke;r.popupOnClick=M;r.toggleClassOnClick=ze;r.expanded="expanded";r.collapsed="collapsed";r.accordion="accordion";r.hamburger="hamburger";r.navbar="navbar";r.navmenu="navmenu";r.contentarea="contentarea";r.closepopups="closepopups";r.infobox="info-box";function fe(e){return document.getElementById(e)}function I(e,o=document){let t=o.getElementsByClassName(e)[0];if(!t)throw ReferenceError(`Cannot find element with class "${e}".`);return t}function be(e,o=document){return o.getElementsByClassName(e)}function ye(e,o=document){return o.getElementsByTagName(e)}function A(e){return e.length!==void 0}function f(e,o){if(A(e))for(let t=0;tt.appendChild(a))),t}function we(e,o,t){return f(e,a=>a.setAttribute(o,t)),e}function ke(e){let o=e.getElementsByClassName(r.accordion);for(let t=0;t{a.classList.toggle(r.collapsed),i.style.maxHeight=i.style.maxHeight==="0px"?s:"0px"}}}function M(e,o,t){e.addEventListener("click",o),I(r.closepopups).addEventListener("mouseup",t),document.addEventListener("keydown",i=>{i.key==="Escape"&&t()})}function ze(e,o,t=e,a){M(e,()=>{f(t,i=>i.classList.toggle(o)),a?.()},()=>{f(t,i=>i.classList.remove(o)),a?.()})}});var C=n(z=>{"use strict";Object.defineProperty(z,"__esModule",{value:!0});z.initAccordions=P;var u=g(),k=u.firstElementWithClass("tocmenu");k&&setTimeout(P,1e3);function P(){var e;u.each(u.elementsWithClass(u.accordion,k),o=>{let t=o.nextElementSibling;Ce(o,t),o.onclick=()=>{o.classList.toggle(u.collapsed),Te(o,t)}}),(e=k.querySelector(".highlight"))===null||e===void 0||e.scrollIntoView({block:"nearest",behavior:"smooth"})}function Ce(e,o){let t=N(o);o.style.maxHeight=o.scrollHeight+"px",t&&F(e,o)}function Te(e,o){let t=N(o);o.style.maxHeight=t?o.scrollHeight+"px":"0px",t&&F(e,o)}function N(e){return e.style.maxHeight=="0px"}function F(e,o){let t=O(e);for(;t;)t.style.maxHeight=t.scrollHeight+o.scrollHeight+"px",t=O(t)}function O(e){let o=e.parentElement;for(;o&&o.tagName=="UL"||o.tagName=="LI";){if(o.tagName=="UL"&&o.previousElementSibling.classList.contains(u.accordion))return o;o=o.parentElement}return null}});var S=n(T=>{"use strict";Object.defineProperty(T,"__esModule",{value:!0});T.activateItem=qe;var d=g();Se();function Se(){let e=d.elementWithId(d.navbar);if(!e)return;let o=d.firstElementWithClass(d.navmenu,e),t=d.firstElementWithClass(d.hamburger,e),a=!1;d.toggleClassOnClick(t,d.expanded,e,h),h();let i=window.scrollY;window.addEventListener("scroll",()=>{var c=window.scrollY;s(i>c?0:-e.offsetHeight+1),i=c}),e.addEventListener("mouseenter",()=>{a&&s(0)});function s(c){a=c!==0,e.classList.contains(d.expanded)||(e.style.top=`${c}px`)}function h(){e.style.height=o.scrollHeight+"px"}}function qe(e,o){d.each(d.elementsWithClass("navitem",e.parentElement),t=>t.classList.remove("active")),e.classList.add("active"),window.localStorage.setItem(o,e.id)}});var D=n(q=>{"use strict";Object.defineProperty(q,"__esModule",{value:!0});q.initializeTheme=Ee;var $=g(),U=S(),G="syntaxHighlight",R="theme";function B(e){document.body.setAttribute("data-syntax-highlight",e);let o=$.elementWithId(e);o&&(0,U.activateItem)(o,G)}function Y(e){document.body.setAttribute("data-theme",e);let o=$.elementWithId(e);o&&(0,U.activateItem)(o,R)}function Ee(){document.body.syntaxHighlight=B;let e=window.localStorage.getItem(G);e&&B(e),document.body.theme=Y;let o=window.localStorage.getItem(R);o&&Y(o)}});var J=n(V=>{"use strict";Object.defineProperty(V,"__esModule",{value:!0});var E=g(),je=C(),Le=D();(0,Le.initializeTheme)();var We=E.elementsWithClass("toc-button")[0],x=E.elementsWithClass("layout")[0],He=E.elementsWithClass("contentarea")[0],K="toc-open";We.onmousedown=()=>{x.classList.add(K),x.ontransitionend=()=>{(0,je.initAccordions)(),x.ontransitionend=null}};He.addEventListener("mousedown",()=>{x.classList.remove(K)},{capture:!0})});var X=n((Ze,_e)=>{_e.exports={}});var Q=n((eo,Ie)=>{Ie.exports={}});var ee=n(Z=>{"use strict";Object.defineProperty(Z,"__esModule",{value:!0});var w=g();w.each(w.elementsWithClass(w.hamburger),e=>w.toggleClassOnClick(e,"open"))});var oe=n((to,Ae)=>{Ae.exports={}});var ne=n(L=>{"use strict";Object.defineProperty(L,"__esModule",{value:!0});L.tooltip=re;var Me=g(),te="tooltip",j;document.querySelectorAll('[data-toggle="tooltip"]').forEach(e=>re(e,e.getAttribute("data-title")));function re(e,o){e.addEventListener("mouseenter",()=>Oe(e,o)),e.addEventListener("mouseleave",ae)}function Oe(e,o){ae(),o&&(j=e,setTimeout(()=>Pe(o,e),500))}function Pe(e,o){if(j!=o)return;let t=Me.create("legend");document.body.appendChild(t),t.id=te,t.innerHTML=e.replace(/=>/g,"\u21D2");let a=o.getBoundingClientRect();t.style.left=`${Math.round(a.left)+window.scrollX}px`,t.style.top=`${Math.round(a.top)+window.scrollY}px`,t.style.opacity="95%"}function ae(){let e=document.getElementById(te);e&&e.remove(),j=void 0}});var ie=n((ao,Ne)=>{Ne.exports={}});var le=n((no,Fe)=>{Fe.exports={}});var ce=n((io,Be)=>{Be.exports={}});var he=n(de=>{"use strict";Object.defineProperty(de,"__esModule",{value:!0});var v=g(),se=v.elementsWithClass("pagemenu")[0];if(se){let i=function(s,h,c,m,l){for(;lc)if(h){let p=document.createElement("ul");h.appendChild(p),l=i(p,null,y,m,l)}else l=i(s,null,y,m,l);else{let p=v.attr(v.create("a",b.textContent),"href","#"+b.id),W=v.create("li",p);s.appendChild(W),e[l]={heading:b,link:p},l=i(s,W,c,m,l+1)}}return l},e=[],o=v.firstElementWithClass("contentarea"),t=v.firstElementWithClass("pagetree",se),a=o.querySelectorAll("h1, h2, h3, h4, h5");i(t,null,1,a,0),window.addEventListener("scroll",()=>{let s=window.scrollY,h=!1,c=null;for(let m=0;ms+l.heading.offsetHeight&&((c||l).link.classList.add("highlight"),h=!0),c=l}!h&&c&&c.link.classList.add("highlight")})}});var me=n((co,Ye)=>{Ye.exports={}});var ge=n((so,$e)=>{$e.exports={}});var Ue=n(ue=>{Object.defineProperty(ue,"__esModule",{value:!0});H();_();J();X();S();Q();ee();oe();ne();ie();C();le();ce();he();me();ge()});Ue();})(); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-modaledit", 3 | "displayName": "ModalEdit", 4 | "description": "Configurable modal editing engine with built-in Vim keymaps", 5 | "version": "2.2.0", 6 | "publisher": "johtela", 7 | "engines": { 8 | "vscode": "^1.103.0" 9 | }, 10 | "repository": { 11 | "url": "https://github.com/johtela/vscode-modaledit" 12 | }, 13 | "license": "MPL-2.0", 14 | "categories": [ 15 | "Keymaps", 16 | "Other" 17 | ], 18 | "homepage": "https://johtela.github.io/vscode-modaledit", 19 | "activationEvents": [ 20 | "onStartupFinished" 21 | ], 22 | "icon": "images/normal.jpg", 23 | "main": "./dist/extension.js", 24 | "contributes": { 25 | "configuration": { 26 | "type": "object", 27 | "title": "ModalEdit", 28 | "properties": { 29 | "modaledit.keybindings": { 30 | "type": "object", 31 | "description": "Keybindings map key → VS Code commands", 32 | "default": {}, 33 | "patternProperties": { 34 | "^.([\\-,].)*$": { 35 | "anyOf": [ 36 | { 37 | "type": "string", 38 | "description": "VS Code command" 39 | }, 40 | { 41 | "type": "array", 42 | "description": "Sequence of commands", 43 | "items": { 44 | "anyOf": [ 45 | { 46 | "type": "object", 47 | "description": "Action" 48 | }, 49 | { 50 | "type": "string", 51 | "description": "VS Code command" 52 | } 53 | ] 54 | } 55 | }, 56 | { 57 | "type": "object", 58 | "description": "VS Code command with arguments", 59 | "properties": { 60 | "command": { 61 | "type": "string", 62 | "description": "VS Code command" 63 | }, 64 | "args": { 65 | "description": "Command arguments", 66 | "anyOf": [ 67 | { 68 | "type": "object" 69 | }, 70 | { 71 | "type": "string" 72 | } 73 | ] 74 | } 75 | } 76 | }, 77 | { 78 | "type": "object", 79 | "description": "Conditional command", 80 | "properties": { 81 | "condition": { 82 | "type": "string", 83 | "description": "JavaScript expression that is evaluated" 84 | } 85 | } 86 | }, 87 | { 88 | "type": "number", 89 | "description": "Keymap id" 90 | } 91 | ] 92 | } 93 | } 94 | }, 95 | "modaledit.selectbindings": { 96 | "type": "object", 97 | "description": "Keybindings used when selection is active", 98 | "default": {}, 99 | "patternProperties": { 100 | "^.([\\-,].)*$": { 101 | "anyOf": [ 102 | { 103 | "type": "string", 104 | "description": "VS Code command" 105 | }, 106 | { 107 | "type": "array", 108 | "description": "Sequence of commands", 109 | "items": { 110 | "anyOf": [ 111 | { 112 | "type": "object", 113 | "description": "Action" 114 | }, 115 | { 116 | "type": "string", 117 | "description": "VS Code command" 118 | } 119 | ] 120 | } 121 | }, 122 | { 123 | "type": "object", 124 | "description": "VS Code command with arguments", 125 | "properties": { 126 | "command": { 127 | "type": "string", 128 | "description": "VS Code command" 129 | }, 130 | "args": { 131 | "description": "Command arguments", 132 | "anyOf": [ 133 | { 134 | "type": "object" 135 | }, 136 | { 137 | "type": "string" 138 | } 139 | ] 140 | } 141 | } 142 | }, 143 | { 144 | "type": "object", 145 | "description": "Conditional command", 146 | "properties": { 147 | "condition": { 148 | "type": "string", 149 | "description": "JavaScript expression that is evaluated" 150 | } 151 | } 152 | }, 153 | { 154 | "type": "number", 155 | "description": "Keymap id" 156 | } 157 | ] 158 | } 159 | } 160 | }, 161 | "modaledit.insertCursorStyle": { 162 | "type": "string", 163 | "enum": [ 164 | "block", 165 | "block-outline", 166 | "line", 167 | "line-thin", 168 | "underline", 169 | "underline-thin" 170 | ], 171 | "default": "line", 172 | "description": "Shape of the cursor when in insert mode." 173 | }, 174 | "modaledit.normalCursorStyle": { 175 | "type": "string", 176 | "enum": [ 177 | "block", 178 | "block-outline", 179 | "line", 180 | "line-thin", 181 | "underline", 182 | "underline-thin" 183 | ], 184 | "default": "block", 185 | "description": "Shape of the cursor when in normal mode." 186 | }, 187 | "modaledit.searchCursorStyle": { 188 | "type": "string", 189 | "enum": [ 190 | "block", 191 | "block-outline", 192 | "line", 193 | "line-thin", 194 | "underline", 195 | "underline-thin" 196 | ], 197 | "default": "underline", 198 | "description": "Shape of the cursor when incremental search is active." 199 | }, 200 | "modaledit.selectCursorStyle": { 201 | "type": "string", 202 | "enum": [ 203 | "block", 204 | "block-outline", 205 | "line", 206 | "line-thin", 207 | "underline", 208 | "underline-thin" 209 | ], 210 | "default": "line-thin", 211 | "description": "Shape of the cursor when selection is active in normal mode." 212 | }, 213 | "modaledit.insertStatusText": { 214 | "type": "string", 215 | "default": "-- $(edit) INSERT --", 216 | "description": "Mode text (and icons) shown in status bar in insert mode." 217 | }, 218 | "modaledit.insertStatusColor": { 219 | "type": "string", 220 | "description": "Color of the status bar mode text in insert mode (in HTML format)." 221 | }, 222 | "modaledit.normalStatusText": { 223 | "type": "string", 224 | "default": "-- $(move) NORMAL --", 225 | "description": "Mode text (and icons) shown in status bar in normal mode." 226 | }, 227 | "modaledit.normalStatusColor": { 228 | "type": "string", 229 | "description": "Color of the status bar mode text in normal mode (in HTML format)." 230 | }, 231 | "modaledit.searchStatusText": { 232 | "type": "string", 233 | "default": "$(search) SEARCH", 234 | "description": "Mode text (and icons) shown in status bar in search mode." 235 | }, 236 | "modaledit.searchStatusColor": { 237 | "type": "string", 238 | "description": "Color of the status bar mode text when in search mode (in HTML format)." 239 | }, 240 | "modaledit.selectStatusText": { 241 | "type": "string", 242 | "default": "-- $(paintcan) VISUAL --", 243 | "description": "Mode text (and icons) shown in status bar selection is active in normal mode." 244 | }, 245 | "modaledit.selectStatusColor": { 246 | "type": "string", 247 | "description": "Color of the status bar mode text when selection is active in normal mode (in HTML format)." 248 | }, 249 | "modaledit.startInNormalMode": { 250 | "type": "boolean", 251 | "default": true, 252 | "description": "Is editor initially in normal mode?" 253 | } 254 | } 255 | }, 256 | "commands": [ 257 | { 258 | "command": "modaledit.toggle", 259 | "title": "ModalEdit: Toggle normal / insert mode" 260 | }, 261 | { 262 | "command": "modaledit.enterNormal", 263 | "title": "ModalEdit: Normal mode" 264 | }, 265 | { 266 | "command": "modaledit.enterInsert", 267 | "title": "ModalEdit: Insert mode" 268 | }, 269 | { 270 | "command": "modaledit.defineBookmark", 271 | "title": "ModalEdit: Define bookmark" 272 | }, 273 | { 274 | "command": "modaledit.goToBookmark", 275 | "title": "ModalEdit: Go to bookmark" 276 | }, 277 | { 278 | "command": "modaledit.showBookmarks", 279 | "title": "ModalEdit: Show bookmarks" 280 | }, 281 | { 282 | "command": "modaledit.cancelSearch", 283 | "title": "ModalEdit: Cancel search mode" 284 | }, 285 | { 286 | "command": "modaledit.deleteCharFromSearch", 287 | "title": "ModalEdit: Delete the last search character" 288 | }, 289 | { 290 | "command": "modaledit.importPresets", 291 | "title": "ModalEdit: Import preset keybindings" 292 | } 293 | ], 294 | "keybindings": [ 295 | { 296 | "key": "Escape", 297 | "command": "modaledit.enterNormal", 298 | "when": "editorTextFocus && !suggestWidgetMultipleSuggestions && !suggestWidgetVisible" 299 | }, 300 | { 301 | "key": "Escape", 302 | "command": "modaledit.cancelSearch", 303 | "when": "editorTextFocus && modaledit.searching" 304 | }, 305 | { 306 | "key": "Backspace", 307 | "command": "modaledit.deleteCharFromSearch", 308 | "when": "editorTextFocus && modaledit.searching" 309 | } 310 | ] 311 | }, 312 | "scripts": { 313 | "vscode:prepublish": "lits --deployMode prod", 314 | "compile": "tsc -p ./", 315 | "watch": "tsc -watch -p ./", 316 | "deploy": "lits --deployMode prod", 317 | "lits-watch": "lits --serve", 318 | "postversion": "git push && git push --tags" 319 | }, 320 | "devDependencies": { 321 | "@types/node": "^24.3.0", 322 | "@types/vscode": "^1.103.0", 323 | "litscript": "^2.3.0" 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the ModalEdit extension will be documented in this file. 4 | 5 | ## Version 1.0 6 | 7 | - Initial release 8 | 9 | ## Version 1.1 10 | 11 | - Added `selectTillMatch` argument to `modalEdit.search` command. 12 | - Editor does not automatically revert back to normal mode when changing window. 13 | 14 | ## Version 1.2 15 | 16 | - Added `startInNormalMode` setting, resolving issue 17 | [#1](https://github.com/johtela/vscode-modaledit/issues/1) 18 | 19 | 20 | ## Version 1.3 21 | 22 | - Incremental search now returns to insert mode, if it was invoked from there 23 | [#4](https://github.com/johtela/vscode-modaledit/issues/4) 24 | - Added new command `modaledit.typeNormalKeys` which can be used to "call" 25 | key bindings. Also fixes issue 26 | [#3](https://github.com/johtela/vscode-modaledit/issues/3) 27 | - Added new argument `typeAfterAccept` to `modaledit.search` command. This 28 | invokes normal mode key bindings (using `modaledit.typeNormalKeys`) after 29 | successful search. The argument can be used to enter insert mode, or clear 30 | selection after search, for example. 31 | 32 | ## Version 1.4 33 | 34 | - Fixed few issues with `modaledit.search` command. 35 | - You can use `__selection` variable in JS expressions to access currently 36 | selected text. 37 | 38 | ## Version 1.5 39 | 40 | Update that was sparked by issue [#6](https://github.com/johtela/vscode-modaledit/issues/6). 41 | Contains multiple new features: 42 | 43 | - `repeat` attribute added to commands with parameters. 44 | - Keymaps can contain [key ranges](https://johtela.github.io/vscode-modaledit/docs/README.html#key-ranges). 45 | - Support for [recursive keymaps](https://johtela.github.io/vscode-modaledit/docs/README.html#defining-recursive-keymaps). 46 | - New `__keySequence` variable added to JS expressions. Contains the key 47 | sequence that was used to invoke a command. 48 | - New property `help` added to keymaps. The help string is shown in the status 49 | bar when the associated keymap is active. 50 | - Added ModalEdit log to [output window](https://johtela.github.io/vscode-modaledit/docs/README.html#debugging-keybindings). 51 | - Semi-large refactoring of type definitions in the [actions module](https://johtela.github.io/vscode-modaledit/docs/src/actions.html). 52 | 53 | ## Version 1.6 54 | 55 | - [New command `modaledit.selectBetween`](https://johtela.github.io/vscode-modaledit/docs/README.html#selecting-text-between-delimiters) 56 | selects text between two delimiter strings. Especially useful when combined 57 | with the key ranges and recursive keymaps introduced in version 1.5. 58 | - Added a shorter alias `__keys` to the `__keySequence` variable available in 59 | JS expressions. 60 | 61 | ## Version 1.7 62 | 63 | Two "repeat" related bigger improvements: 64 | 65 | - New [`modaledit.repeatLastChange` command](https://johtela.github.io/vscode-modaledit/docs/README.html#repeat-last-change) 66 | emulates Vim's dot `.` command quite faithfully. 67 | - The `repeat` property used in context with 68 | [commands taking arguments](https://johtela.github.io/vscode-modaledit/docs/README.html#commands-with-arguments) can now also contain a JS expression that 69 | returns a boolean value. In this case, the value is used as a condition that 70 | tells if the command should be repeated. The command is repeated as long as 71 | the expression returns a truthy value. 72 | 73 | And some minor changes: 74 | 75 | - New variable `__rkeys` available for use in JS expressions. It contains the 76 | keys pressed to invoke a command in reverse order. This is handy if you need 77 | to access the last keys in the sequence. They are conveniently the first ones 78 | in `__rkeys`. 79 | - Removed unneeded images from the extension package. The package is now 3 MBs 80 | smaller. 81 | 82 | ## Version 2.0 83 | 84 | Major release containing lot of new features and improvements. 85 | 86 | ### Preset keybindings 87 | 88 | It is possible now to import keybindings through the `modaledit.importPresets` 89 | command. [Vim presets](preset/vim.html) are included in the extension 90 | ([#7](https://github.com/johtela/vscode-modaledit/issues/7)). The presets can be 91 | also defined as JavaScript ([#9](https://github.com/johtela/vscode-modaledit/issues/9)). 92 | They are evaluated or "compiled" to JSON when import is run. 93 | 94 | ### Improvements to Search 95 | 96 | Search command has several new features: 97 | 98 | - Multicursor search ([#5](https://github.com/johtela/vscode-modaledit/issues/5), 99 | [#12](https://github.com/johtela/vscode-modaledit/pull/12)) is now working. 100 | 101 | - There are four new parameters: `typeBeforeNextMatch`, `typeAfterNextMatch`, 102 | `typeBeforePreviousMatch`, and `typeAfterPreviousMatch`. These can be used to 103 | run key commands after `modaledit.nextMatch` and `modaledit.previousMathch` 104 | commands. The need for these parameters arose when implementing Vim's `t` and 105 | `f` key commands. These commands look for specified character and place the 106 | cursor either on it or before it. Without the new parameters, it would not 107 | be possible to emulate Vim's behavior using `modaledit.search`. This is 108 | because by default, it selects the search string and search always starts from 109 | the current cursor position. To make jumping to next and previous character 110 | possible, we need to adjust the cursor position before and after the commands. 111 | 112 | - New parameter `wrapAround` causes the search to jump the beginning/end of 113 | the file if it hits bottom/top. This closes issue [#8](https://github.com/johtela/vscode-modaledit/issues/8) 114 | 115 | - The implementation of `modaledit.search` and `modaledit.selectBetween` 116 | commands have been refactored. Adding the new parameters described above made 117 | it possible to simplify the implementation and remove hacky code. The changes 118 | should not break existing functionality but make these commands work more 119 | logically and consistently. For example, both of the commands now work 120 | properly when they are used to extend the existing selection. 121 | 122 | ### Cursor and Status Bar Configuration 123 | 124 | You can now define a different cursor shape when selection is active in normal 125 | mode using the `selectCursorStyle`. Also, you can 126 | [change the status bar text](README.html#changing-status-bar) shown in each 127 | mode. It is possible to include icons in the status bar, if you like. This 128 | should be sufficient to close issue 129 | [#13](https://github.com/johtela/vscode-modaledit/issues/13) 130 | 131 | A secondary status bar was added to show the keys that have been pressed so far. 132 | It also shows help messages defined in bindings and warnings from the search 133 | command. 134 | 135 | ### Bookmark Improvements 136 | 137 | There is a new command `modaledit.showBookmarks` that shows all the defined 138 | bookmarks. You can jump to any of them by selecting one in the list. Also, 139 | the bookmark can now be any string instead of a number. This actually worked 140 | previously, but now documentation about this is updated too. 141 | 142 | New parameter `select` in the `modaledit.goToBookmark` command extends selection 143 | till the bookmark instead of putting the cursor on it. This makes it possible to 144 | use bookmarks as selection scoping mechanism à la Vim. 145 | 146 | ### Changes Concerning JS Expressions 147 | 148 | New variables `__cmd` and `__rcmd` can be used in JS expressions. They contain 149 | the key sequence that was pressed as a string. They correspond to expressions: 150 | ```js 151 | __cmd = __keys.join('') 152 | __rcmd = __rkeys.join('') 153 | ``` 154 | In many cases, these variables allow you to write expressions that inspect the 155 | key sequence in a shorter form. 156 | 157 | All the variants of `__keys` or `__keySequence` variables now contain the actual 158 | key sequence that was used to invoke the command. Previously they contained the 159 | sequence that _the user_ pressed. Since you can also invoke key commands 160 | programmatically using `modaledit.typeNormalKeys`, this made implementing 161 | reusable commands more difficult. You could not rely on the key sequence to 162 | correspond to the path to the keybinding you were defining. Now you can rely 163 | that the `__keys` variable and its variants contain the path to the binding, 164 | which should make sure that commands work correctly when invoked through 165 | `modaledit.typeNormalKeys`. 166 | 167 | ### Other Changes 168 | 169 | - Configuration section called `selectbindings` can be used to define key 170 | bindings that are in effect when selection is active. This allows you to 171 | define different key sequences for same leader keys in normal mode and 172 | selecion mode. 173 | 174 | For example in Vim, the `d` key is in normal mode the leader key for sequences 175 | such as `dw` (delete word) or `dip` (delete inside paragraph). In visual 176 | (selection) mode, `d` deletes the selected text. Previously it was not 177 | possible to have such keymaps, but with `selectbindings` you can now define 178 | them. 179 | 180 | - `selecting` flag is now refreshed when you switch between files. The select 181 | mode no longer "sticks" between tabs. 182 | 183 | ## Version 2.1 184 | 185 | Two new commands (courtesy of [David Little](https://github.com/haberdashPI)): 186 | 187 | - `modaledit.enableSelection` turns selection mode on. 188 | - `modaledit.cancelMultipleSelections` cancels selection mode preserving 189 | multiple cursors. 190 | 191 | ## Version 2.2 192 | 193 | - Added a possibility to abort long running commands by pressing `Esc`. 194 | - `modaledit.selectBetween` command now supports the `nested` parameter, which 195 | correctly handles selection between nested brackets, and resembles Vim's 196 | behavior. Also updated Vim bindings accordingly. 197 | [#39](https://github.com/johtela/vscode-modaledit/issues/39) 198 | - Will later upload extension also to 199 | [Open VSIX Registry](https://open-vsx.org/) so that it can be used also with 200 | VSCodium and other open-source forks. -------------------------------------------------------------------------------- /docs/LICENSE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ModalEdit - License 10 | 11 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 295 | 296 |
297 |
298 | 299 |
300 |

Table of Contents

301 | 302 | 359 |
360 |
361 | 362 | 363 | 364 |
365 |
366 | 367 |
368 |

The MIT License (MIT)

369 |

Copyright © 2020 Tommi Johtela

370 |

Permission is hereby granted, free of charge, to any person obtaining a copy 371 | of this software and associated documentation files (the "Software"), to deal 372 | in the Software without restriction, including without limitation the rights 373 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 374 | copies of the Software, and to permit persons to whom the Software is 375 | furnished to do so, subject to the following conditions:

376 |

The above copyright notice and this permission notice shall be included in 377 | all copies or substantial portions of the Software.

378 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 379 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 380 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 381 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 382 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 383 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 384 | THE SOFTWARE.

385 | 386 |
387 |
388 | 389 | 394 |
395 |
396 | 397 | 412 | 413 | 414 | 415 | -------------------------------------------------------------------------------- /docs/dist/normal.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital@0;1&family=Cousine:wght@400;700&family=Nunito+Sans:opsz,wght@6..12,500;6..12,700&display=swap";.font-sample-sans{font-family:var(--sans-font)}.font-sample-serif{font-family:var(--serif-font)}.font-sample-mono{font-family:var(--mono-font)}.color-samples{display:flex;height:4rem;line-height:4rem;text-align:center;font-family:var(--sans-font)}.color-samples div{flex-grow:2}.color-samples .prim{color:var(--col-sec);background-color:var(--col-prim)}.color-samples .prim-light{color:var(--col-sec-light);background-color:var(--col-prim-light)}.color-samples .prim-dark{color:var(--col-sec-dark);background-color:var(--col-prim-dark)}.color-samples .sec{color:var(--col-prim);background-color:var(--col-sec)}.color-samples .sec-light{color:var(--col-prim-light);background-color:var(--col-sec-light)}.color-samples .sec-dark{color:var(--col-prim-dark);background-color:var(--col-sec-dark)}[data-theme=ice-age]{--col-sec: hsl(222, 40%, 90%);--col-sec-dark: hsl(222, 40%, 99%);--col-sec-light: hsl(222, 40%, 81%);--col-prim: hsl(223, 15%, 37%);--col-prim-dark: hsl(223, 15%, 46%);--col-prim-light: hsl(223, 15%, 28%);--cnt-link-color: lightsteelblue;--cnt-link-visited-color: steelblue}[data-theme=ultra-violet]{--col-prim: hsl(185, 13%, 81%);--col-prim-light: hsl(185, 13%, 90%);--col-prim-dark: hsl(185, 13%, 72%);--col-sec: hsl(269, 35%, 38%);--col-sec-light: hsl(269, 35%, 47%);--col-sec-dark: hsl(269, 35%, 29%)}[data-theme=chocolate]{--col-prim: hsl(29, 69%, 92%);--col-prim-light: hsl(29, 69%, 100%);--col-prim-dark: hsl(29, 69%, 83%);--col-sec: hsl(5, 21%, 42%);--col-sec-light: hsl(5, 21%, 51%);--col-sec-dark: hsl(5, 21%, 33%)}[data-theme=red-alert]{--col-prim: hsl(9, 54%, 97%);--col-prim-light: hsl(9, 54%, 100%);--col-prim-dark: hsl(9, 54%, 88%);--col-sec: hsl(353, 100%, 30%);--col-sec-light: hsl(353, 100%, 39%);--col-sec-dark: hsl(353, 100%, 21%)}[data-theme=blueberry]{--col-prim: hsl(40, 42%, 82%);--col-prim-light: hsl(40, 42%, 91%);--col-prim-dark: hsl(40, 42%, 73%);--col-sec: hsl(248, 19%, 24%);--col-sec-light: hsl(248, 19%, 33%);--col-sec-dark: hsl(248, 19%, 15%)}[data-theme=book-cover]{--col-sec: hsl(33, 87%, 63%);--col-sec-dark: hsl(33, 87%, 72%);--col-sec-light: hsl(33, 87%, 54%);--col-prim: hsl(213, 20%, 11%);--col-prim-dark: hsl(213, 20%, 20%);--col-prim-light: hsl(213, 20%, 2%);--toc-link-color: lightgray;--cnt-color: lightgray;--cnt-link-color: lightsteelblue;--cnt-link-visited-color: steelblue}[data-theme=vintage]{--col-prim: hsl(345, 6%, 14%);--col-prim-light: hsl(345, 6%, 5%);--col-prim-dark: hsl(345, 6%, 23%);--col-sec: hsl(25, 36%, 79%);--col-sec-light: hsl(25, 36%, 70%);--col-sec-dark: hsl(25, 36%, 88%);--cnt-header-color: lightblue;--cnt-link-color: lightsteelblue;--cnt-link-visited-color: steelblue}[data-theme=greyhound]{--col-prim: hsl(0, 0%, 92%);--col-prim-light: hsl(0, 0%, 100%);--col-prim-dark: hsl(0, 0%, 83%);--col-sec: hsl(222, 29%, 27%);--col-sec-light: hsl(222, 29%, 36%);--col-sec-dark: hsl(222, 29%, 18%)}[data-theme=mustard]{--col-sec: hsl(42, 73%, 57%);--col-sec-dark: hsl(42, 73%, 66%);--col-sec-light: hsl(42, 73%, 48%);--col-prim: hsl(352, 53%, 19%);--col-prim-dark: hsl(352, 53%, 28%);--col-prim-light: hsl(352, 53%, 10%);--cnt-link-color: lightsteelblue;--cnt-link-visited-color: steelblue}[data-theme=peachy]{--col-prim: hsl(19, 79%, 65%);--col-prim-light: hsl(19, 79%, 74%);--col-prim-dark: hsl(19, 79%, 56%);--col-sec: hsl(233, 56%, 18%);--col-sec-light: hsl(233, 56%, 27%);--col-sec-dark: hsl(233, 56%, 9%)}[data-theme=brownie]{--col-prim: hsl(33, 51%, 71%);--col-prim-light: hsl(33, 51%, 80%);--col-prim-dark: hsl(33, 51%, 62%);--col-sec: hsl(11, 66%, 35%);--col-sec-light: hsl(11, 66%, 44%);--col-sec-dark: hsl(11, 66%, 26%)}[data-theme=camouflage]{--col-prim: hsl(77, 11%, 87%);--col-prim-light: hsl(77, 11%, 96%);--col-prim-dark: hsl(77, 11%, 78%);--col-sec: hsl(154, 7%, 19%);--col-sec-light: hsl(154, 7%, 28%);--col-sec-dark: hsl(154, 7%, 10%)}body{--sans-font: "Nunito Sans";--serif-font: "Source Serif 4";--mono-font: "Cousine";--col-prim: hsl(29, 69%, 92%);--col-prim-light: hsl(29, 69%, 100%);--col-prim-dark: hsl(29, 69%, 83%);--col-sec: hsl(5, 21%, 42%);--col-sec-light: hsl(5, 21%, 51%);--col-sec-dark: hsl(5, 21%, 33%);--col-prim-text: var(--col-sec);--col-prim-text-light: var(--col-sec-light);--col-prim-text-dark: var(--col-sec-dark);--col-sec-text: var(--col-prim);--col-sec-text-light: var(--col-prim-dark);--col-sec-text-dark: var(--col-prim-light);--nav-text-transform: capitalize;--nav-horz-padding: .5rem;--nav-vert-padding: .5rem;--nav-font-size: 1.2rem;--nav-transition: .3s;--nav-button-justify: flex-start;--nav-button-spacing: .5rem;--nav-border-radius: 4px;--nav-bg-color: var(--col-sec-light);--nav-border-color: var(--col-sec);--nav-text-color: var(--col-prim);--nav-title-color: var(--col-prim-light);--nav-text-hover: var(--col-prim-light);--nav-icon-color: var(--col-prim-dark);--nav-text-height: calc(1.3 * var(--nav-font-size));--nav-height: calc(var(--nav-text-height) + (var(--nav-vert-padding) * 2));--hamburger-height: var(--nav-text-height);--toc-transition: .3s;--scrollbar-width: 4px;--scrollbar-color: var(--col-prim);--scrollbar-hover: var(--col-prim-dark);--toc-header-font-size: 1rem;--toc-font-size: .9rem;--toc-border-width: 2px;--toc-sideicon-radius: @nav-button-radius;--toc-bg-color: var(--col-prim);--toc-text-color: var(--col-sec);--toc-link-color: var(--toc-text-color);--toc-ruler-color: var(--col-prim-dark);--toc-hover-color: var(--col-sec-light);--toc-hover-bg: var(--col-prim-light);--toc-scrollbar-color: var(--col-prim-dark);--toc-scrollbar-hover: var(--col-sec-light);--pm-transition: .3s;--pm-header-font-size: 1rem;--pm-font-size: .9rem;--pm-bg-color: var(--cnt-bg-color);--pm-text-color: var(--col-sec);--pm-ruler-color: var(--col-prim-dark);--pm-hover-color: var(--col-sec-dark);--cnt-font: var(--serif-font);--cnt-header-font: var(--sans-font);--cnt-font-size: 1.1rem;--cnt-mono-font-size: .9rem;--cnt-width: 80ch;--cnt-margin: 1rem;--cnt-color: var(--col-sec-dark);--cnt-header-color: var(--col-sec);--cnt-bg-color: var(--col-prim-light);--cnt-pre-color: var(--col-prim);--cnt-border-color: var(--col-prim);--cnt-dark-border: var(--col-prim-dark);--cnt-link-color: blue;--cnt-link-visited-color: darkblue;--cnt-link-hover-color: dodgerblue;--cnt-link-active-color: orange;--ftr-color: var(--col-prim);--ftr-dark-color: var(--col-prim-dark);--ftr-bg-color: var(--col-sec);--ftr-font-family: var(--sans-font);--ftr-font-size: 1rem;--ftr-padding: 1rem;--ftr-border-color: var(--col-sec-light);--ftr-text-hover: var(--col-prim-light);--tooltip-color: var(--col-sec-text);--tooltip-bg-color: var(--col-sec);--tooltip-border-color: var(--col-sec-light);--lnd-background: var(--col-prim);--lnd-background-attachment: fixed;--lnd-background-repeat: no-repeat;--lnd-title-color: var(--col-prim-text);--lnd-title-font-size: 12vw;--lnd-title-font-weight: bold;--lnd-info-background-color: @color-white;--lnd-info-text-color: @color-text}.narrow-scrollbars::-webkit-scrollbar{background:transparent}.narrow-scrollbars::-webkit-scrollbar:horizontal{height:var(--scrollbar-width)}.narrow-scrollbars::-webkit-scrollbar:vertical{width:var(--scrollbar-width)}.narrow-scrollbars::-webkit-scrollbar-thumb{border-radius:calc(var(--scrollbar-width) / 2)}.narrow-scrollbars::-webkit-scrollbar-thumb:hover{background-color:var(--scrollbar-hover)!important}.narrow-scrollbars:hover::-webkit-scrollbar-thumb{background-color:var(--scrollbar-color)}pre.syntaxhighlight{--code-tab-size: 4;color:var(--code-text-color);background:var(--code-background-color);-moz-tab-size:var(--code-tab-size);-o-tab-size:var(--code-tab-size);tab-size:var(--code-tab-size);-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;overflow-x:auto;overflow-y:hidden}pre.syntaxhighlight .tooltip-inner{max-width:1000px}pre.syntaxhighlight .punctuation{color:var(--punctuation-color)}pre.syntaxhighlight .keyword,pre.syntaxhighlight .attribute,pre.syntaxhighlight .attr{color:var(--keyword-color);font-weight:var(--keyword-font-weight, normal)}pre.syntaxhighlight .comment{color:var(--comment-color)}pre.syntaxhighlight .member,pre.syntaxhighlight .selector-class{color:var(--member-color)}pre.syntaxhighlight .string{color:var(--string-color);background:var(--string-background-color, var(--code-background-color))}pre.syntaxhighlight .number,pre.syntaxhighlight .literal{color:var(--number-color)}pre.syntaxhighlight .typename,pre.syntaxhighlight .variable{color:var(--typename-color);font-weight:var(--typename-font-weight, normal)}pre.syntaxhighlight a,pre.syntaxhighlight a:visited{color:inherit}pre.syntaxhighlight a:hover span{color:var(--link-hover-color);text-decoration:underline}pre.syntaxhighlight a:active span{color:var(--link-active-color);text-decoration:underline}[data-syntax-highlight=monokai]{--code-text-color: #f8f8f2;--code-background-color: #272822;--link-hover-color: blue;--link-active-color: blueviolet;--punctuation-color: #d0d0c8;--keyword-color: #66d9ef;--comment-color: slategray;--member-color: #a6e22e;--string-color: #f92672;--number-color: #ae81ff;--typename-color: #e6db74}[data-syntax-highlight=coding-horror]{--code-text-color: #000000;--code-background-color: #F8F8F8;--link-hover-color: blue;--link-active-color: blueviolet;--punctuation-color:#000000;--keyword-color: #000080;--keyword-font-weight: bold;--comment-color: #008000;--member-color: slategray;--string-color: #000000;--string-background-color: #ffffe6;--number-color: #800000;--typename-color: #a65300}[data-syntax-highlight=solarized-light]{--code-text-color: #657b83;--code-background-color: #fdf6e3;--link-hover-color: blue;--link-active-color: blueviolet;--punctuation-color:#719a07;--keyword-color: #859900;--comment-color: #93a1a1;--member-color: #cb4b16;--string-color: #2aa198;--number-color: #2aa14e;--typename-color: #b58900;--typename-font-weight: bold}[data-syntax-highlight=son-of-obsidian]{--code-text-color: #f1f2f2;--code-background-color: #22282a;--link-hover-color: deepskyblue;--link-active-color: blueviolet;--punctuation-color: #e8e2b7;--keyword-color: #93c763;--comment-color: #66747b;--member-color: #a082bd;--string-color: #ec7600;--number-color: #ffcd22;--typename-color: #678cb1}@layer components,site,content;@layer site{body{margin:0}.layout{--toc-width: 20%;--pm-width: 20%;display:flex;flex-direction:row;justify-content:space-between;margin-top:calc(var(--nav-height));background-color:var(--cnt-bg-color)}.layout>div{flex-grow:0;flex-shrink:0}.sidepane{--scrollbar-color: transparent;position:sticky;top:0;height:100dvh;height:100vh;overflow-x:hidden;overflow-y:auto;transition:width var(--toc-transition)}.sidepane{--scrollbar-color: var(--toc-scrollbar-color);--scrollbar-hover: var(--toc-scrollbar-hover)}.sidepane:first-of-type{background-color:var(--toc-bg-color);border:solid var(--toc-border-width) var(--toc-ruler-color);width:var(--toc-width)}.sidepane:last-of-type{width:var(--pm-width)}.toc-button{display:none;height:100%;justify-content:center;align-items:center;cursor:pointer}.toc-button svg{stroke:var(--col-sec-light)}.layout.toc-open .toc-button{display:none}.layout.toc-open .tocmenu{display:block}@media only screen and (max-width: 1300px){.layout{--toc-width: min(24px, 5%) ;--pm-width: 25%}.tocmenu{display:none}.toc-button{display:flex}.layout.toc-open{--toc-width: min(75%, 35ch) ;--pm-width: 0px}}@media only screen and (max-width: 1100px){.layout{--pm-width: 0px}}@media only screen and (max-width: 850px){.layout{--cnt-width: 85%}}.contentarea{width:var(--cnt-width);margin:var(--cnt-margin);scroll-behavior:smooth}}@layer components{#navbar{--title-font-size: var(--nav-font-size);font-family:var(--sans-font);position:fixed;top:0;left:0;width:100%;display:flex;align-items:flex-start;z-index:101;transition:top var(--nav-transition),height var(--nav-transition)}#navbar,.navitem>.navmenu{background-color:var(--nav-bg-color);color:var(--nav-icon-color);border:solid 1px var(--nav-border-color);border-radius:var(--nav-border-radius)}.navmenu{display:flex;flex-grow:100;align-items:stretch}.navitem{position:relative;cursor:pointer;display:flex;align-items:center}.navitem>.navmenu{display:none;position:absolute;overflow:auto;flex-direction:column;flex-wrap:wrap;min-width:max-content;max-height:50vh;align-items:stretch;top:100%;left:0;z-index:102}.navitem:hover>.navmenu{display:flex}.navitem.active>a{color:var(--nav-text-hover)}.navitem.active:after{content:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='icon icon-tabler icon-tabler-check' viewBox='0 0 24 24' stroke-width='1.5' stroke='lime' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M5 12l5 5l10 -10' /%3E%3C/svg%3E");width:2ch;height:2ch;padding-right:1ch}.navitem a{text-wrap:nowrap;flex-grow:1;display:flex;align-items:center;text-decoration:none;user-select:none;color:var(--nav-text-color);font-size:calc(var(--nav-font-size) - 3px);text-transform:var(--nav-text-transform);padding:var(--nav-vert-padding) var(--nav-horz-padding)}#navbar>.navmenu>.navitem>a{margin-right:var(--nav-button-spacing)}.navitem a:hover{text-shadow:0 0 1px var(--nav-text-hover)}.navitem a svg,.navitem a img{height:var(--title-font-size);stroke:var(--nav-icon-color)}.navitem a span{margin-left:4px}.navitem a.title{color:var(--nav-title-color);font-size:var(--title-font-size)}.navitem a.title svg,.navitem a.title img{height:var(--nav-text-height)}#navbar .hamburger{display:none;margin:var(--nav-vert-padding) var(--nav-horz-padding)}@media only screen and (max-width: 800px){#navbar:not(.expanded)>.navmenu>.navitem:not(:first-child){display:none}#navbar .hamburger{display:block}.navitem>.navmenu{left:4ch}#navbar.expanded>.navmenu a{display:flex;font-size:var(--title-font-size)}#navbar.expanded>.navmenu{flex-direction:column;align-items:stretch;padding-bottom:var(--nav-vert-padding)}}}@layer components{.hamburger{--hamburger-size: calc((var(--hamburger-height) / 6) + 1px);--hamburger-offs: calc(var(--hamburger-size) * 1.4);--hamburger-width: calc(var(--hamburger-height) + 6px);display:block;cursor:pointer}.bar1,.bar2,.bar3{width:var(--hamburger-width);height:var(--hamburger-size);background-color:currentColor;border-radius:2px;transition:transform var(--nav-transition)}.bar2{margin:var(--hamburger-size) 0}.hamburger.open .bar1{transform:rotate(-45deg) translate(calc(0px - var(--hamburger-offs)),var(--hamburger-offs))}.hamburger.open .bar2{opacity:0}.hamburger.open .bar3{transform:rotate(45deg) translate(calc(0px - var(--hamburger-offs)),calc(0px - var(--hamburger-offs)))}}@layer components{#tooltip{--font: var(--sans-font);--arrow-size: 4px;--arrow-border-size: calc(var(--arrow-size) + 1px);position:absolute;padding:4px;z-index:9999;font-family:var(--font);font-size:14px;background-color:var(--tooltip-bg-color);border-radius:4px;color:var(--tooltip-color);border:1px solid var(--tooltip-border-color);opacity:0%;transform:translateY(-110%);transition:opacity .5s}#tooltip:after,#tooltip:before{top:100%;left:min(1em,50%);border:solid transparent;content:"";height:0;width:0;position:absolute;pointer-events:none}#tooltip:after{border-top-color:var(--tooltip-bg-color);border-width:var(--arrow-size);margin-left:calc(0 - var(--arrow-size))}#tooltip:before{border-top-color:var(--tooltip-border-color);border-width:var(--arrow-border-size);margin-left:calc(0 - var(--arrow-border-size))}}@layer components{.tocmenu{--highlight-bar-width: 3px;--highlight-margin: 10px;--padding-vert: calc(var(--toc-font-size) * .2);margin:1rem;color:var(--toc-text-color);font-family:var(--sans-font);font-size:var(--toc-font-size)}.tocmenu h3{font-size:var(--toc-header-font-size);font-weight:700;margin-block:.5rem;padding-inline:0;--divider-color: var(--toc-ruler-color);border-bottom:solid calc(var(--toc-font-size) / 8) var(--toc-ruler-color)}.tocmenu ul{list-style:none;overflow:hidden;margin-block:0;padding-inline:0;transition:max-height var(--toc-transition)}.tocmenu ul ul{margin-left:var(--toc-font-size)}.tocmenu a{text-decoration:none;color:var(--toc-link-color);display:grid;grid-template-columns:3ch auto;align-items:baseline;padding:var(--padding-vert) 0 var(--padding-vert) var(--highlight-margin)}.tocmenu a.highlight{border-left:var(--highlight-bar-width) solid var(--toc-ruler-color);padding-left:calc(var(--highlight-margin) - var(--highlight-bar-width));font-weight:700}.tocmenu a:hover{background:linear-gradient(to right,var(--toc-hover-bg),transparent)}.accordion{display:flex;justify-content:space-between;border-top:2px solid var(--toc-ruler-color);cursor:pointer;padding:var(--padding-vert) 0;margin-top:var(--padding-vert);margin-left:var(--highlight-margin);width:100%;font-weight:700;font-size:var(--toc-header-font-size)}.accordion>svg{width:var(--toc-font-size);stroke:var(--toc-ruler-color);margin:0 var(--highlight-margin) 0 5px;transition:transform var(--toc-transition)}.accordion.collapsed>svg{transform:rotate(180deg)}.accordion:hover{border-top-color:var(--toc-hover-color)}.accordion:hover>svg{stroke:var(--toc-hover-color)}}@layer content{.contentarea{--radius: 4px;font-family:var(--cnt-font);font-size:var(--cnt-font-size);color:var(--cnt-color);line-height:1.6em}.contentarea a{color:var(--cnt-link-color);text-decoration:none}.contentarea a:visited{color:var(--cnt-link-visited-color)}.contentarea a:hover{color:var(--cnt-link-hover-color)}.contentarea a:active{color:var(--cnt-link-active-color)}.contentarea a:focus{outline:thin dotted}.contentarea a:hover,.contentarea a:active{outline:0}.contentarea p{margin:1rem 0;margin-block-start:.5rem;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.contentarea p code,.contentarea ul code,.contentarea ol code{color:var(--cnt-header-color);background-color:var(--cnt-border-color);padding:3px 4px;border-radius:var(--radius)}.contentarea img{max-width:100%;float:right;margin:0 0 1rem 1rem}.contentarea h1,.contentarea h2,.contentarea h3,.contentarea h4,.contentarea h5,.contentarea h6{font-family:var(--cnt-header-font);font-weight:400;color:var(--cnt-header-color);line-height:1em;margin-block:0;padding-inline:0;clear:both}.contentarea h1{margin:1rem 0 2rem;font-size:2rem;font-weight:700;border-bottom:.1em var(--cnt-border-color) solid}.contentarea h2{font-size:1.6rem}.contentarea h3{font-size:1.4rem}.contentarea h2,.contentarea h3{margin:2.5rem 0 0;border-bottom:.1em var(--cnt-border-color) solid}.contentarea blockquote{font-style:italic;font-size:1rem;margin:1rem;text-align:justify;padding:1rem 2rem;box-shadow:4px 4px 0 var(--cnt-dark-border);background-color:var(--col-prim);max-width:90%}.contentarea blockquote:before{display:block;padding-left:10px;content:"\201c";font-size:3rem;position:relative;left:-2.5rem;top:.5rem;height:0}.contentarea hr{display:block;height:2px;border:0;border-top:1px solid #aaa;border-bottom:1px solid #eee;margin:1em 0;padding:0}.contentarea code,.contentarea pre{font-family:var(--mono-font);font-size:var(--cnt-mono-font-size);line-height:1.5em;tab-size:4;hyphens:none}.contentarea pre:not(.syntaxhighlight){background-color:var(--cnt-pre-color)}.contentarea pre{padding:1em;border-radius:var(--radius);overflow-x:auto;overflow-y:hidden;clear:both}.contentarea pre.console{background-color:#000;color:#d3d3d3}.contentarea pre.console:before{content:"> ";color:#a9a9a9}.contentarea b,.contentarea strong{font-weight:700}.contentarea ins{background:#ff9;color:#000;text-decoration:none}.contentarea mark{background:#ff0;color:#000;font-style:italic;font-weight:700}.contentarea sub,.contentarea sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.contentarea sup{top:-.5em}.contentarea sub{bottom:-.25em}.contentarea ul,.contentarea ol{margin:1em 0;padding:0 0 0 2em}.contentarea p+ul,.contentarea p+ol{margin-top:-.5em}.contentarea li{padding-left:.5em}.contentarea dd{margin:0 0 0 2em}.contentarea table{border-collapse:collapse;border:1px solid var(--col-prim-dark);border-radius:.5em;margin-inline:auto;overflow:hidden;color:var(--col-prim-text);padding:1em}.contentarea tbody tr:nth-child(odd){background-color:var(--col-prim-dark)}.contentarea tbody tr:nth-child(2n){background-color:var(--col-prim)}.contentarea th,.contentarea td{padding:.5em 1em}.contentarea thead{background-color:var(--col-sec-light);color:var(--col-sec-text-light);text-align:left;font-family:var(--sans-font)}.contentarea td{vertical-align:top}.contentarea svg{display:block;margin-left:auto;margin-right:auto}.contentarea summary{font-family:var(--cnt-header-font);font-weight:400;color:#888;cursor:pointer}.contentarea summary:focus{outline:none}.contentarea summary:hover{color:var(--cnt-color)}.contentarea details{padding:0 8px;border:#AAA dashed 1px;border-radius:var(--radius);max-height:2em;overflow:auto;transition:max-height 2s}.contentarea details[open]{max-height:95vh}.contentarea .alert{font-family:var(--sans-font);font-weight:600;display:flex;align-items:center;border-radius:var(--radius);padding:1em;background-color:var(--bgColor, #f8d7da);color:var(--textColor, #721c24)}.contentarea .alert:before{content:var(--icon);font-size:24px;margin-right:.5em}.contentarea .alert.error{--bgColor: #f8d7da;--textColor: #721c24;--icon: "\26d4"}.contentarea .alert.warning{--bgColor: #fff3cd;--textColor: #856404;--icon: "\26a0\fe0f"}.contentarea .alert.info{--bgColor: #d1ecf1;--textColor: #0c5460;--icon: "\2139\fe0f"}.contentarea .alert.success{--bgColor: #d4edda;--textColor: #155724;--icon: "\2705"}@media only screen and (max-width: 850px){.contentarea img{float:none;display:block;max-width:90%;margin:1rem auto}}}@layer components{.pagemenu{margin:1rem;color:var(--pm-text-color);font-family:var(--sans-font);font-size:var(--pm-font-size)}.pagemenu h3{font-size:var(--pm-header-font-size);border-bottom:solid calc(var(--pm-font-size) / 8) var(--pm-ruler-color);margin-block:.5rem;padding-inline:0}.pagemenu ul{list-style-type:none;overflow:hidden;margin-block:0;padding-inline:0}.pagemenu ul ul{margin-left:var(--pm-font-size)}.pagemenu li{margin:calc(var(--pm-font-size) * .4) 0}.pagemenu a{font-family:var(--serif-font);display:inline-block;text-decoration:none;color:var(--pm-text-color);width:90%;padding-left:10px}.pagemenu a.highlight{border-left:solid 3px var(--pm-ruler-color);padding-left:7px;font-weight:700}.pagemenu a:active{color:var(--pm-hover-color)}.pagemenu a:hover{text-decoration:underline}}@layer components{.footer{box-sizing:border-box;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;width:100%;color:var(--ftr-color);background-color:var(--ftr-bg-color);font-family:var(--sans-font);font-size:var(--ftr-font-size);padding:var(--ftr-padding)}.footer-button{color:var(--ftr-color);text-decoration:none;padding:calc(var(--ftr-padding) / 2);border:solid 2px var(--ftr-border-color);border-radius:4px;margin:2px}.footer-button.disabled{color:var(--ftr-border-color);cursor:not-allowed}.footer p{margin:0;padding:0}.footer-button p:first-of-type{color:var(--ftr-dark-color);font-weight:400;font-size:calc(var(--ftr-font-size) - 2px)}.footer-text{margin:var(--ftr-padding);text-align:center;color:var(--ftr-dark-color)}.footer-text a{color:var(--ftr-color);text-decoration:none}.footer-text p:last-of-type{font-size:calc(var(--ftr-font-size) - 2px)}.footer a:hover{text-shadow:0 0 1px var(--ftr-text-hover)}@media only screen and (max-width: 850px){.footer{justify-content:space-around}.footer-text{order:1}}} 2 | -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * # Converting Keybindings to Actions 3 | * 4 | * This module defines the schema of the configuration file using TypeScript 5 | * interfaces. We parse the configuration JSON to TypeScript objects which 6 | * directly define all the valid keyboard sequences and the commands that these 7 | * will invoke. 8 | */ 9 | //#region -c action.ts imports 10 | import * as vscode from 'vscode' 11 | //#endregion 12 | /** 13 | * ## Action Definitions 14 | * 15 | * The keybinding configuration consist of _actions_ that can take three forms: 16 | * an action can be a command (defined later), a keymap, or a number that refers 17 | * to a keymap defined earlier. 18 | */ 19 | export type Action = Command | Keymap | number 20 | /** 21 | * Commands can be invoked in four ways: by specifying just command a name 22 | * (string), or using a conditional command, a command with parameters, or a 23 | * sequence (array) of commands. The definition is recursive, meaning that a 24 | * sequence can contain all four types of commands. 25 | */ 26 | export type Command = string | Conditional | Parameterized | Command[] 27 | /** 28 | * A conditional command consist of condition (a JavaScript expression) and set 29 | * of branches to take depending on the result of the condition. Each branch can 30 | * be any type of command defined above. 31 | */ 32 | export interface Conditional { 33 | condition: string 34 | [branch: string]: Command 35 | } 36 | /** 37 | * A command that takes arguments can be specified using the `Parameterized` 38 | * interface. Arguments can be given either as an object or a string, which 39 | * is assumed to contain a valid JS expression. Additionally, you can specify 40 | * that the command is run multiple times by setting the `repeat` property. The 41 | * property must be either a number, or a JS expression that evaluates to a 42 | * number. If it evaluates to some other type, the expression is used as a 43 | * condition that is evaluated after the command is run. If the expression 44 | * returns a truthy value, the command is repeated. 45 | */ 46 | export interface Parameterized { 47 | command: string 48 | args?: {} | string 49 | repeat?: number | string 50 | } 51 | /** 52 | * A keymap is a dictionary of keys (characters) to actions. Keys are either 53 | * single characters or character ranges, denoted by sequences of `,` 54 | * and `-`. Values of the dictionary can be also nested keymaps. 55 | * This is how you can define commands that require multiple keypresses. 56 | * 57 | * ![keymap example](../images/keymap.png) 58 | * When the value of a key is number, it refers to another keymap whose `id` 59 | * equals the number. The number can also point to the same keymap where it 60 | * resides. With this mechanism, you can define _recursive_ keymaps that can 61 | * take (theoretically) infinitely long key sequences. The picture on the right 62 | * illustrates this. 63 | * 64 | * The `help` field contains help text that is shown in the status bar when the 65 | * keymap is active. 66 | */ 67 | export interface Keymap { 68 | id: number 69 | help: string 70 | [key: string]: Action 71 | } 72 | /** 73 | * ## Cursor Shapes 74 | * 75 | * You can use various cursor shapes in different modes. The list of available 76 | * shapes is defined below. 77 | */ 78 | type Cursor = 79 | | "block" 80 | | "block-outline" 81 | | "line" 82 | | "line-thin" 83 | | "underline" 84 | | "underline-thin" 85 | | undefined 86 | /** 87 | * ## Configuration State 88 | * 89 | * The variables below contain the current cursor configuration. 90 | */ 91 | let insertCursorStyle: vscode.TextEditorCursorStyle 92 | let normalCursorStyle: vscode.TextEditorCursorStyle 93 | let searchCursorStyle: vscode.TextEditorCursorStyle 94 | let selectCursorStyle: vscode.TextEditorCursorStyle 95 | let insertStatusText: string 96 | let normalStatusText: string 97 | let searchStatusText: string 98 | let selectStatusText: string 99 | let insertStatusColor: string | undefined 100 | let normalStatusColor: string | undefined 101 | let searchStatusColor: string | undefined 102 | let selectStatusColor: string | undefined 103 | /** 104 | * Another thing you can set in config, is whether ModalEdit starts in normal 105 | * mode. 106 | */ 107 | let startInNormalMode: boolean 108 | /** 109 | * The root of the action configuration is keymap. This defines what key 110 | * sequences will be run when keys are pressed in normal mode. 111 | */ 112 | let baseKeymap: Keymap 113 | let selectKeymap: Keymap 114 | /** 115 | * The current active keymap is stored here. The active keymap changes when the 116 | * user invokes a multi-key action sequence. 117 | */ 118 | let currentKeymap: Keymap | null = null 119 | /** 120 | * The last command run is also stored. This is needed to run commands which 121 | * capture the keyboard. 122 | */ 123 | let lastCommand: string 124 | /** 125 | * The key sequence that user has pressed is stored for error reporting 126 | * purposes and to make it available to command arguments. Since commands 127 | * can invoke other commands through `typeNormalKeys` command, we need to 128 | * maintain the key sequences in a stack. Both current key sequence and the 129 | * stack is initialized to empty array. 130 | */ 131 | let keySequence: string[] = [] 132 | let keySeqStack: string[][] = [] 133 | /** 134 | * We need a dictionary that returns a keymap for given id. 135 | */ 136 | let keymapsById: { [id: number]: Keymap } 137 | /** 138 | * ## Configuration Accessors 139 | * 140 | * The following functions return the current configuration settings. 141 | */ 142 | export function getInsertStyles(): 143 | [vscode.TextEditorCursorStyle, string, string | undefined] { 144 | return [ insertCursorStyle, insertStatusText, insertStatusColor ] 145 | } 146 | 147 | export function getNormalStyles(): 148 | [vscode.TextEditorCursorStyle, string, string | undefined ] { 149 | return [ normalCursorStyle, normalStatusText, normalStatusColor ] 150 | } 151 | 152 | export function getSearchStyles(): 153 | [vscode.TextEditorCursorStyle, string, string | undefined] { 154 | return [ searchCursorStyle, searchStatusText, searchStatusColor ] 155 | } 156 | 157 | export function getSelectStyles(): 158 | [vscode.TextEditorCursorStyle, string, string | undefined] { 159 | return [ selectCursorStyle, selectStatusText, selectStatusColor ] 160 | } 161 | 162 | export function getStartInNormalMode(): boolean { 163 | return startInNormalMode 164 | } 165 | /** 166 | * You can also set the last command from outside the module. 167 | */ 168 | export function setLastCommand(command: string) { 169 | lastCommand = command 170 | } 171 | /** 172 | * ## Logging 173 | * 174 | * To enable logging and error reporting ModalEdit creates an output channel 175 | * that is visible in the output pane. The channel is created in the extension 176 | * activation hook, but it is passed to this module using the `setOutputChannel` 177 | * function. 178 | */ 179 | let outputChannel: vscode.OutputChannel 180 | 181 | export function setOutputChannel(channel: vscode.OutputChannel) { 182 | outputChannel = channel 183 | } 184 | /** 185 | * Once the channel is set, we can output messages to it using the `log` 186 | * function. 187 | */ 188 | export function log(message: string) { 189 | outputChannel.appendLine(message) 190 | } 191 | /** 192 | * ## Updating Configuration from settings.json 193 | * 194 | * Whenever you save the user-level `settings.json` or the one located in the 195 | * `.vsode` directory VS Code calls this function that updates the current 196 | * configuration. 197 | */ 198 | export function updateFromConfig(): void { 199 | const config = vscode.workspace.getConfiguration("modaledit") 200 | UpdateKeybindings(config) 201 | insertCursorStyle = toVSCursorStyle( 202 | config.get("insertCursorStyle", "line")) 203 | normalCursorStyle = toVSCursorStyle( 204 | config.get("normalCursorStyle", "block")) 205 | searchCursorStyle = toVSCursorStyle( 206 | config.get("searchCursorStyle", "underline")) 207 | selectCursorStyle = toVSCursorStyle( 208 | config.get("selectCursorStyle", "line-thin")) 209 | insertStatusText = config.get("insertStatusText", "-- $(edit) INSERT --") 210 | normalStatusText = config.get("normalStatusText", "-- $(move) NORMAL --") 211 | searchStatusText = config.get("searchStatusText", "$(search) SEARCH") 212 | selectStatusText = config.get("selectStatusText", "-- $(paintcan) VISUAL --") 213 | insertStatusColor = config.get("insertStatusColor") || undefined 214 | normalStatusColor = config.get("normalStatusColor") || undefined 215 | searchStatusColor = config.get("searchStatusColor") || undefined 216 | selectStatusColor = config.get("selectStatusColor") || undefined 217 | startInNormalMode = config.get("startInNormalMode", true) 218 | } 219 | /** 220 | * The following function updates base keymap and select-mode keymap. 221 | */ 222 | function UpdateKeybindings(config: vscode.WorkspaceConfiguration) { 223 | log("Validating keybindings in 'settings.json'...") 224 | keymapsById = {} 225 | errors = 0 226 | let sel = config.get("selectbindings") 227 | if (isKeymap(sel)) { 228 | selectKeymap = sel 229 | validateAndResolveKeymaps(sel) 230 | } 231 | let base = config.get("keybindings") 232 | if (isKeymap(base)) { 233 | baseKeymap = base 234 | validateAndResolveKeymaps(base) 235 | } 236 | else 237 | log("ERROR: Invalid configuration structure. Keybindings not updated.") 238 | if (errors > 0) 239 | log(`Found ${errors} error${errors > 1 ? "s" : ""}. ` + 240 | "Keybindings might not work correctly.") 241 | else 242 | log("Validation completed successfully.") 243 | } 244 | /** 245 | * To make sure that the keybinding section is valid, we define a function that 246 | * checks it. At the same time the function resolves all the keymaps that are 247 | * referred by an id. It records the number of errors. 248 | */ 249 | let errors: number 250 | /** 251 | * The keymap ranges are recognized with the following regular expression. 252 | * Examples of valid key sequences include: 253 | * 254 | * - `0-9` 255 | * - `a,b,c` 256 | * - `d,e-h,l` 257 | * 258 | * Basically you can add individual characters to the range with a comma `,` and 259 | * an ASCII range with dash `-`. The ASCII code of the first character must be 260 | * smaller than the second one's. 261 | */ 262 | let keyRE = /^.([\-,].)+$/ 263 | /** 264 | * The function itself is recursive; it calls itself, if it finds a nested 265 | * keymap. It stores all the keymaps it encounters in the `keymapsById` 266 | * dictionary. 267 | */ 268 | function validateAndResolveKeymaps(keybindings: Keymap) { 269 | function error(message: string) { 270 | log("ERROR: " + message) 271 | errors++ 272 | } 273 | if (typeof keybindings.id === 'number') 274 | keymapsById[keybindings.id] = keybindings 275 | for (let key in keybindings) { 276 | if (keybindings.hasOwnProperty(key) && key != "id" && key != "help") { 277 | let target = keybindings[key] 278 | if (isKeymap(target)) 279 | validateAndResolveKeymaps(target) 280 | else if (typeof target === 'number') { 281 | let id = target 282 | target = keymapsById[id] 283 | if (!target) 284 | error(`Undefined keymap id: ${id}`) 285 | else 286 | keybindings[key] = target 287 | } 288 | if (key.match(keyRE)) 289 | for (let i = 1; i < key.length; i += 2) { 290 | if (key[i] == '-') { 291 | let first = key.charCodeAt(i - 1) 292 | let last = key.charCodeAt(i + 1) 293 | if (first > last) 294 | error(`Invalid key range: "${key}"`) 295 | else 296 | for (let i = first; i <= last; i++) 297 | keybindings[String.fromCharCode(i)] = target 298 | } 299 | else { 300 | keybindings[key[i - 1]] = target 301 | keybindings[key[i + 1]] = target 302 | } 303 | } 304 | else if (key.length != 1) 305 | error(`Invalid key binding: "${key}"`) 306 | } 307 | } 308 | } 309 | /** 310 | * The helper function below converts cursor styles specified in configuration 311 | * to enumeration members used by VS Code. 312 | */ 313 | function toVSCursorStyle(cursor: Cursor): vscode.TextEditorCursorStyle { 314 | switch (cursor) { 315 | case "line": return vscode.TextEditorCursorStyle.Line 316 | case "block": return vscode.TextEditorCursorStyle.Block 317 | case "underline": return vscode.TextEditorCursorStyle.Underline 318 | case "line-thin": return vscode.TextEditorCursorStyle.LineThin 319 | case "block-outline": return vscode.TextEditorCursorStyle.BlockOutline 320 | case "underline-thin": return vscode.TextEditorCursorStyle.UnderlineThin 321 | default: return vscode.TextEditorCursorStyle.Line 322 | } 323 | } 324 | /** 325 | * ## Type Predicates 326 | * 327 | * Since JavaScript does not have dynamic type information we need to write 328 | * functions that check which type of action we get from the configuration. 329 | * First we define a high-level type predicate that checks if a value is 330 | * an action. 331 | */ 332 | function isAction(x: any): x is Action { 333 | return isCommand(x) || isKeymap(x) || isNumber(x) 334 | } 335 | /** 336 | * This one checks whether a value is a string. 337 | */ 338 | function isString(x: any): x is string { 339 | return x && typeof x === "string" 340 | } 341 | /** 342 | * This one checks whether a value is a number. 343 | */ 344 | function isNumber(x: any): x is number { 345 | return x && typeof x === "number" 346 | } 347 | /** 348 | * This one identifies an object. 349 | */ 350 | function isObject(x: any): boolean { 351 | return x && typeof x === "object" 352 | } 353 | /** 354 | * This checks if a value is a command. 355 | */ 356 | function isCommand(x: any): x is Action { 357 | return isString(x) || isParameterized(x) || isConditional(x) || 358 | isCommandSequence(x) 359 | } 360 | /** 361 | * This checks if a value is an array of commands. 362 | */ 363 | function isCommandSequence(x: any): x is Command[] { 364 | return Array.isArray(x) && x.every(isCommand) 365 | } 366 | /** 367 | * This recognizes a conditional action. 368 | */ 369 | function isConditional(x: any): x is Conditional { 370 | return isObject(x) && isString(x.condition) && 371 | Object.keys(x).every(key => 372 | key === "condition" || isCommand(x[key])) 373 | } 374 | /** 375 | * This asserts that a value is a parameterized command. 376 | */ 377 | function isParameterized(x: any): x is Parameterized { 378 | return isObject(x) && isString(x.command) && 379 | (!x.args || isObject(x.args) || isString(x.args)) && 380 | (!x.repeat || isNumber(x.repeat) || isString(x.repeat)) 381 | } 382 | /** 383 | * And finally this one checks if a value is a keymap. 384 | */ 385 | function isKeymap(x: any): x is Keymap { 386 | return isObject(x) && !isConditional(x) && !isParameterized(x) && 387 | Object.values(x).every(isAction) 388 | } 389 | /** 390 | * ## Executing Commands 391 | * 392 | * In the end all keybindings will invoke one or more VS Code commands. The 393 | * following function runs a command whose name and arguments are given as 394 | * parameters. If the command throws an exception because of invalid arguments, 395 | * for example, the error is shown in the popup window at the corner of the 396 | * screen. 397 | */ 398 | async function executeVSCommand(command: string, ...rest: any[]): Promise { 399 | try { 400 | await vscode.commands.executeCommand(command, ...rest) 401 | lastCommand = command 402 | } 403 | catch (error) { 404 | vscode.window.showErrorMessage(getMessage(error)) 405 | } 406 | } 407 | /** 408 | * Dig out the error message from unknown error object. 409 | */ 410 | function getMessage(error: unknown) { 411 | return error instanceof Error ? error.message : 412 | error instanceof Object ? error.toString() : 413 | typeof error == 'string' ? error : "Unknown error" 414 | } 415 | 416 | /** 417 | * `evalString` function evaluates JavaScript expressions. Before doing so, it 418 | * defines some variables that can be used in the evaluated text. 419 | */ 420 | function evalString(str: string, __selecting: boolean): any { 421 | let __file = undefined 422 | let __line = undefined 423 | let __col = undefined 424 | let __char = undefined 425 | let __selection = undefined 426 | let __keySequence = keySequence 427 | let __keys = keySequence 428 | let __rkeys = keySequence.slice().reverse() 429 | let __cmd = __keys.join('') 430 | let __rcmd = __rkeys.join('') 431 | let editor = vscode.window.activeTextEditor 432 | if (editor) { 433 | let cursor = editor.selection.active 434 | __file = editor.document.fileName 435 | __line = cursor.line 436 | __col = cursor.character 437 | __char = editor.document.getText(new vscode.Range(cursor, 438 | cursor.translate({ characterDelta: 1 }))) 439 | __selection = editor.document.getText(editor.selection) 440 | } 441 | try { 442 | return eval(`(${str})`) 443 | } 444 | catch (error) { 445 | vscode.window.showErrorMessage("Evaluation error: " + getMessage(error)) 446 | } 447 | } 448 | /** 449 | * We need the evaluation function when executing conditional command. The 450 | * condition is evaluated and if a key is found that matches the result, it is 451 | * executed. 452 | */ 453 | async function executeConditional(cond: Conditional, selecting: boolean): 454 | Promise { 455 | let res = evalString(cond.condition, selecting) 456 | let branch = isString(res) ? res : JSON.stringify(res) 457 | if (branch && isAction(cond[branch])) 458 | await execute(cond[branch], selecting) 459 | } 460 | /** 461 | * Since repeated commands can potentially put VSCode in a busy loop, we provide 462 | * a way to abort them. For this purpose, we define the `abort' flag. It's 463 | * reset when an action starts and ends. 464 | * 465 | * The `abortActions` function sets the flag, and effectively aborts the repeat 466 | * loop, if we are inside it. 467 | */ 468 | let abort = false 469 | export function abortActions() { 470 | abort = true 471 | } 472 | /** 473 | * Parameterized commands can get their arguments in two forms: as a string 474 | * that is evaluated to get the actual arguments, or as an object. Before 475 | * executing the command, we inspect the `repeat` property. If it is string 476 | * we evaluate it, and check if the result is a number. If so, we update the 477 | * `repeat` variable that designates repetition count. If not, we treate it as 478 | * a continue condition. The subroutine `exec` runs the command either `repeat` 479 | * times or as long as the expression in the `repeat` property returns a truthy 480 | * value. 481 | */ 482 | async function executeParameterized(action: Parameterized, selecting: boolean) { 483 | let repeat: string | number = 1 484 | async function exec(args?: any) { 485 | abort = false 486 | let cont = true 487 | if (isString(repeat)) 488 | do { 489 | await executeVSCommand(action.command, args) 490 | cont = !abort && evalString(repeat, selecting) 491 | } 492 | while (cont) 493 | else 494 | for (let i = 0; i < repeat && !abort; i++) 495 | await executeVSCommand(action.command, args) 496 | abort = false 497 | } 498 | if (action.repeat) { 499 | if (isString(action.repeat)) { 500 | let val = evalString(action.repeat, selecting) 501 | if (typeof val === 'number') 502 | repeat = Math.max(1, val) 503 | else 504 | repeat = action.repeat 505 | } 506 | else 507 | repeat = Math.max(1, action.repeat) 508 | } 509 | if (action.args) { 510 | if (typeof action.args === 'string') 511 | await exec(evalString(action.args, selecting)) 512 | else 513 | await exec(action.args) 514 | } 515 | else 516 | await exec() 517 | } 518 | /** 519 | * ## Executing Actions 520 | * 521 | * Before running any commands, we need to identify which type of action we got. 522 | * Depending on the type we use different function to execute the command. If 523 | * the action is not a command, it has to be a keymap. Since we resolved `id` 524 | * referenences in `validateAndResolveKeymaps`, an action has to be a keymap 525 | * object at this point. We set the new keymap as the active one. 526 | */ 527 | async function execute(action: Action, selecting: boolean): Promise { 528 | currentKeymap = null 529 | if (isString(action)) 530 | await executeVSCommand(action) 531 | else if (isCommandSequence(action)) 532 | for (const command of action) 533 | await execute(command, selecting) 534 | else if (isConditional(action)) 535 | await executeConditional(action, selecting) 536 | else if (isParameterized(action)) 537 | await executeParameterized(action, selecting) 538 | else 539 | currentKeymap = action 540 | } 541 | /** 542 | * ## Key Press Handler 543 | * 544 | * Now that the plumbing of actions is implemented, it is straightforward to 545 | * map the pressed key to an action. The special case occurs when a command 546 | * captures the keyboard. Then we rerun the previous command and give the key 547 | * to it as an argument. 548 | * 549 | * Otherwise we just check if the current keymap contains binding for the key 550 | * pressed, and execute the action. If not, we present an error to the user. 551 | * 552 | * As a last step the function returns `true`, if the current keymap is `null`. 553 | * This indicates that the key invoked a command instead of just changing the 554 | * active keymap. 555 | */ 556 | export async function handleKey(key: string, selecting: boolean, 557 | capture: boolean): Promise { 558 | 559 | function error() { 560 | vscode.window.showWarningMessage("ModalEdit: Undefined key binding: " + 561 | keySequence.join(" - ")) 562 | currentKeymap = null 563 | } 564 | 565 | if (!currentKeymap) { 566 | keySeqStack.push(keySequence) 567 | keySequence = [] 568 | } 569 | keySequence.push(key) 570 | if (capture && lastCommand) 571 | await executeVSCommand(lastCommand, key) 572 | else if (currentKeymap) { 573 | if (currentKeymap[key]) 574 | await execute(currentKeymap[key], selecting) 575 | else 576 | error() 577 | } 578 | else { 579 | if (selecting && selectKeymap[key]) 580 | await execute(selectKeymap[key], selecting) 581 | else if (baseKeymap[key]) 582 | await execute(baseKeymap[key], selecting) 583 | else 584 | error() 585 | } 586 | if (!currentKeymap) 587 | keySequence = keySeqStack.pop()! 588 | return !currentKeymap 589 | } 590 | /** 591 | * ## Keymap Help 592 | * 593 | * When defining complex key sequences you can help the user by defining what 594 | * keys she can press next and what they do. If the help is defined, it is shown 595 | * in the status bar. 596 | */ 597 | export function getHelp(): string | undefined { 598 | return currentKeymap?.help 599 | } 600 | -------------------------------------------------------------------------------- /docs/src/extension.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ModalEdit - Extension 10 | 11 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 295 | 296 |
297 |
298 | 299 |
300 |

Table of Contents

301 | 302 | 359 |
360 |
361 | 362 | 363 | 364 |
365 |
366 | 367 |
368 |

Extension Entry Point

369 |

The module vscode contains the VS Code extensibility API. The other 370 | modules are part of the extension.

371 |
import * as vscode from 'vscode'
372 | import * as actions from './actions'
373 | import * as commands from './commands'
374 | 
375 |

Activation

376 |

This method is called when the extension is activated. The activation events 377 | are set in the package.json like this:

378 | 379 |
"activationEvents": [ "*" ],
380 | 
381 |

which means that the extension is activated as soon as VS Code is running.

382 |
export function activate(context: vscode.ExtensionContext) {
383 | 
384 |

The commands are defined in the package.json file. We register them 385 | with function defined in the commands module.

386 |
	commands.register(context)
387 | 
388 |

We create an output channel for diagnostic messages and pass it to the 389 | actions module.

390 |
	let channel = vscode.window.createOutputChannel("ModalEdit")
391 | 	actions.setOutputChannel(channel)
392 | 
393 |

Then we subscribe to events we want to react to.

394 |
	context.subscriptions.push(
395 | 		channel,
396 | 		vscode.workspace.onDidChangeConfiguration(actions.updateFromConfig),
397 | 		vscode.window.onDidChangeVisibleTextEditors(commands.resetSelecting),
398 | 		vscode.window.onDidChangeTextEditorSelection(e =>
399 | 			commands.updateCursorAndStatusBar(e.textEditor)),
400 | 		vscode.workspace.onDidChangeTextDocument(commands.onTextChanged))
401 | 
402 |

Next we update the active settings from the config file, and at last, 403 | we enter into normal or edit mode depending on the settings.

404 |
	actions.updateFromConfig()
405 | 	if (actions.getStartInNormalMode())
406 | 		commands.enterNormal()
407 | 	else
408 | 		commands.enterInsert()
409 | }
410 | 
411 |

Deactivation

412 |

This method is called when your extension is deactivated

413 |
export function deactivate() {
414 | 	commands.enterInsert()
415 | }
416 | 
417 | 418 |
419 |
420 | 421 | 426 |
427 |
428 | 429 | 445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modal Editing in VS Code 2 | 3 | ModalEdit is a simple but powerful extension that adds configurable "normal" 4 | mode to VS Code. The most prominent [modal editor][1] is [Vim][2], which also 5 | inspired the development of ModalEdit. It includes Vim commands as presets 6 | you can import, but ModalEdit's true power comes with its configurability. You 7 | can emulate existing editors like Vim or [Kakoune][8] or build your keyboard 8 | layout from ground up and add exactly the features you need. 9 | 10 | As in Vim, the goal of the extension is to save your keystrokes and make 11 | editing as efficient as possible. Unlike most Vim emulators, ModalEdit leverages 12 | the built-in features of VS Code. You define your keybindings using commands 13 | provided by VS Code and other extensions. You can build complex operations by 14 | arranging commands into sequences. You can define conditional commands that do 15 | different things based on editor state. Also, you can map these commands to 16 | arbitrarily long keyboard sequences. 17 | 18 | > Version 2.0 is the latest major release, and it contains many improvements 19 | > and new features that make ModalEdit more robust and flexible than before. 20 | > See the [change log][12] for full list of enhancements and changes in the 21 | > functionality. 22 | 23 | ## Getting Started 24 | 25 | When extension is installed text documents will open in normal mode. The 26 | current mode is shown in the status bar. You can switch between modes by 27 | clicking the pane in the status bar. 28 | 29 | ![Status bar](images/status-bar.gif) 30 | 31 | In normal mode keys don't output characters but invoke commands. You can 32 | specify these commands in the `settings.json` file. To edit your user-level 33 | settings file, open command palette with `Ctrl+Shift+P` and look up command 34 | **Preferences: Open Settings (JSON)**. If you want the configuration to be 35 | project specific, edit the `settings.json` that is located in the `.vscode` 36 | directory under your project directory. 37 | 38 | > You might want to skip to the [tutorial][9], if you prefer learning by 39 | > example. If you want to start with Vim keybindings, you'll find the 40 | > instructions [here][14]. Otherwise keep reading this document. 41 | 42 | To define the key mappings used in normal mode, add a property named 43 | `modaledit.keybindings`. You should define at least one binding that will switch 44 | the editor to the *insert mode*, which is the same as VS Code's default mode. 45 | ```js 46 | "modaledit.keybindings": { 47 | "i": "modaledit.enterInsert" 48 | } 49 | ``` 50 | When you save the `settings.json` file, keybindings take effect immediately. 51 | 52 | ModalEdit adds a regular VS Code keyboard shortcut for `Esc` to return back to 53 | normal mode. If you wish, you can remap this command to another key by 54 | pressing `Ctrl+K Ctrl+S`. 55 | 56 | ### Selections/Visual Mode 57 | 58 | ModalEdit does not have a separate selection/visual mode as Vim has. It is 59 | possible to select text both in normal mode and insert mode. However, since it 60 | is typical that commands in normal have different behavior when selection is 61 | active, the status bar text changes to indicate that. You can change the text 62 | shown in status bar using [configuration parameters](#changing-status-bar) 63 | 64 | ![Selection active](images/selected-text.png) 65 | 66 | ModalEdit defines a new command `modaledit.toggleSelection` which allows 67 | you to start selecting text in normal mode without holding down the shift key. 68 | This imitates Vim's visual mode. 69 | 70 | ## Configuration 71 | 72 | You can define the normal mode commands in four different ways. It is also 73 | possible to combine them freely. 74 | 75 | ### Single Command 76 | 77 | The simplest way is to map a key to a single command. This has the format: 78 | ```js 79 | "": "" 80 | ``` 81 | The `` needs to be a single character and `` any valid VS Code 82 | command. You can see the list of all of the available commands by opening 83 | global settings with command **Preferences: Open Default Keyboard Shortcuts (JSON)**. 84 | 85 | The example in the previous section maps the `i` key to the 86 | `modaledit.enterInsert` command. 87 | 88 | ### Commands with Arguments 89 | 90 | Some [commands][6] take arguments. For example `cursorMove` which allows you 91 | to specify which direction and how much cursor moves. These commands can be 92 | executed by defining an object with prefined properties: 93 | ```js 94 | "": { 95 | "command": "", 96 | "args": { ... } | "{ ... }" 97 | "repeat": number | "" 98 | } 99 | ``` 100 | The `` is again a valid VS Code command. The `args` property contains 101 | whatever arguments the command takes. It can be specified as a JSON object 102 | or as a string. If the value of the `args` property is a string, ModalEdit 103 | treats it as a JavaScript expression. It evaluates the expression and passes the 104 | result to the command. The following variables can be used inside expression 105 | strings: 106 | 107 | | Variable | Type | Description 108 | | --------------- | ---------- | ------------------------------------------------- 109 | | `__file` | `string` | The file name of the document that is edited. 110 | | `__line` | `number` | The line number where the cursor is currently on. 111 | | `__col` | `number` | The column number where the cursor is currently on. 112 | | `__char` | `string` | The character under the cursor. 113 | | `__selection` | `string` | Currently selected text. 114 | | `__selecting` | `boolean` | Flag that indicates whether selection is active. 115 | | `__keySequence` | `string[]` | Array of keys that were pressed to invoke the command. 116 | | `__keys` | `string[]` | Alias to the `__keySequence` variable. 117 | | `__rkeys` | `string[]` | Contains the `__keys` array reversed. This is handy when you want to access the last characters of the array as they will be first in `__rkeys`. 118 | | `__cmd` | `string` | Containst the `__keys` array joined together to a string. Now you don't have to do this explicitly in you expressions. 119 | | `__rcmd` | `string` | Containst the `__rkeys` array joined together to a string. 120 | 121 | The `repeat` property allows you to run the command multiple times. If the value 122 | of the property is a number, it directly determines the repetition count. If it 123 | is a string, ModalEdit evaluates it as JS expression and checks if the result is 124 | a number. In that case the returned number is used as the repeat count. Note 125 | that numbers smaller than 1 will be ignored, and the command is always run at 126 | least once. 127 | 128 | If returned value is not a number, the expression is treated as a condition that 129 | is evaluated after the command has run. The command is repeated as long as the 130 | expression returns a truthy value. 131 | 132 | Below is an example that maps key `o` to a command that moves the cursor to the 133 | end of line. It also selects the jumped range, if we have selection active. 134 | ```js 135 | "o": { 136 | "command": "cursorMove", 137 | "args": "{ to: 'wrappedLineEnd', select: __selecting }" 138 | }, 139 | ``` 140 | 141 | ### Sequence of Commands 142 | 143 | To construct more complex operations consisting of multiple steps, you can 144 | define command sequences. Commands in a sequence will be run one after another. 145 | A sequence is defined as an array. 146 | ```js 147 | "": [ , , ... ] 148 | ``` 149 | In above, `` can assume any of the supported forms: single command, 150 | one with arguments, or conditional command (see below). 151 | 152 | The next example maps the `f` key to a command sequence that first deletes the 153 | selected text and then switch to insert mode. It corresponds to the `c` command 154 | in Vim. 155 | ```js 156 | "f": [ 157 | "deleteRight", 158 | "modaledit.enterInsert" 159 | ], 160 | ``` 161 | 162 | ### Conditional Commands 163 | 164 | For even more complex scenarios, you can define commands that run different 165 | commands depending on a specified condition. The most common use case for this 166 | is to run a different command when selection is active. The format of a 167 | conditional commands is: 168 | ```js 169 | "": { 170 | "condition": "", 171 | "": , 172 | "": , 173 | ... 174 | } 175 | ``` 176 | Here `` can be any valid JavaScript expression. You can use 177 | variables listed in the "Commands with Arguments" section in the expression. If 178 | the expression evaluates to ``, `` will be executed, if to 179 | ``, `` will be run, and so forth. If none of the defined 180 | properties match the expression result, nothing is done. Commands can be of any 181 | kind: a single command, sequence, or command with arguments. 182 | 183 | Below is an example that moves cursor one word forward with `w` key. We use 184 | the `__selecting` variable to determine if a selection is active. If so, we 185 | extend the selection using `cursorWordStartRightSelect` command, otherwise we 186 | just jump to next word with `cursorWordStartRight`. 187 | ```js 188 | "w": { 189 | "condition": "__selecting", 190 | "true": "cursorWordStartRightSelect", 191 | "false": "cursorWordStartRight" 192 | }, 193 | ``` 194 | 195 | ### Binding Key Sequences 196 | 197 | When you want to define a multi-key sequence, nest the key bindings. You can 198 | define a two key command using the following format. 199 | ```js 200 | "": { 201 | "": 202 | }, 203 | ``` 204 | Again, the `` can be in any of the forms described above. To invoke 205 | the command you first press `` in normal mode followed by ``. 206 | 207 | The example below defines two commands that are bound to key sequences `g - f` 208 | (search forwards) and `g - b` (search backwards). 209 | ```js 210 | "g": { 211 | "f": { 212 | "command": "modaledit.search", 213 | "args": {} 214 | }, 215 | "b": 216 | "command": "modaledit.search", 217 | "args": { 218 | "backwards": true 219 | } 220 | } 221 | } 222 | ``` 223 | 224 | ### Defining Recursive Keymaps 225 | 226 | Version 1.5 of ModalEdit introduced the possibility to create recursive keymaps. 227 | With this feature you can define arbitrarily long keyboard sequences. This is 228 | useful, for example, for creating commands that you can repeat by entering first 229 | a number followed by a command key. Keymaps got two new features to enable this 230 | functionality. 231 | 232 | #### Key Ranges 233 | 234 | You can add multiple characters to a keybinding comma `,` and dash `-`. For 235 | example, `a,b` bind both `a` and `b` to the same action. You can also add ranges 236 | like any numeric character `0-9`. The ASCII code of the first character must be 237 | smaller than the second one's. You can also combine these notations; for 238 | instance, range `a,d-f` maps keys `a`, `d`, `e`, and `f` to a same action. 239 | 240 | #### Keymap IDs 241 | 242 | By giving keymap a numeric ID, you can refer to it in another (or same) keymap. 243 | With key ranges, this allows you to create a binding that can take theoretically 244 | infinitely long key sequence. The example below shows how you can define 245 | commands like `3w` that moves the cursor forward by three words. First we define 246 | the commands that moves or select the previous/next word (with keys `b` and `w`), 247 | and then we create a binding that matches a positive number using key ranges and 248 | a recursive keymap. We also use the 249 | [`modaledit.typeNormalKeys` command](#invoking-key-bindings) to invoke the 250 | existing key bindings and the [`repeat` property](#commands-with-arguments) to 251 | repeat the command. 252 | ```js 253 | "w": { 254 | "condition": "__selecting", 255 | "true": "cursorWordStartRightSelect", 256 | "false": "cursorWordStartRight" 257 | }, 258 | "b": { 259 | "condition": "__selecting", 260 | "true": "cursorWordStartLeftSelect", 261 | "false": "cursorWordStartLeft" 262 | }, 263 | "1-9": { 264 | "id": 1, 265 | "help": "Enter count followed by [w, b]", 266 | "0-9": 1, 267 | "w,b": { 268 | "command": "modaledit.typeNormalKeys", 269 | "args": "{ keys: __rkeys[0] }", 270 | "repeat": "Number(__keys.slice(0, -1).join(''))" 271 | } 272 | } 273 | ``` 274 | We give the keymap attached to key range `1-9` the `id` of 1. When that keymap 275 | is active pressing key `0-9` will "jump" back to the same keymap. That is 276 | designated by the number `1` in the key binding. Only when the user presses 277 | some other key we get out of this keymap. If the user presses `w` or `b`, we 278 | run the command bound to the respective key. We get the repetition count by 279 | slicing the all but last character from the `__keys` array and converting that 280 | to a number. The command key is the last item of the `__keys` array. We can 281 | access it more easily using the reversed `__rkeys` array. The item is first in 282 | that array. 283 | 284 | The picture below illustrates how keymap and command objects are stored in 285 | memory. 286 | 287 | ![recursive keymap](images/recursive-keymap-example.png) 288 | 289 | It is also possible to jump to another keymap, which enables even more 290 | complicated keyboard sequences. The only restriction is that you can only jump 291 | to a key binding which is already defined. I.e. you cannot refer to an ID of a 292 | keymap that appears later in the configuration. 293 | 294 | > To better understand how keymaps work behind the scenes check the source 295 | > [documentation][10]. 296 | 297 | Another new feature used in the example above is the optional `help` property 298 | in the keymap. The contents of the property is shown in the status bar when the 299 | keymap is active. It makes using long keyboard sequences easier by providing a 300 | hint what keys you can press next. 301 | 302 | ### Keybindings in Selection/Visual Mode 303 | 304 | ModalEdit 2.0 adds a new configuration section called `selectbidings` that has 305 | the same structure as the `keybindings` section. With it you can now map keys 306 | that act as the lead key of a normal mode sequence to run a commands when 307 | pressed in visual mode. 308 | 309 | For example, you might want the `d` key to be the leader key for sequence 310 | "delete word" `dw` in normal mode, but in selection mode `d` should delete the selection 311 | without expecting any following keys. Previously it was not possible to define 312 | this behavior, but now you can do it with `selectbindings`. 313 | 314 | `selectbindings` section is always checked first when ModalEdit looks for a 315 | mapping for a keypress. If there is no binding defined in `selectbindings` 316 | then it checks the `keybindings` section. Note that you can still define normal 317 | mode commands that work differently when selection is active. You can use either 318 | a conditional or parameterized command to check the `__selecting` flag, and 319 | perform a different action based on that. 320 | 321 | ### Debugging Keybindings 322 | 323 | If you are not sure that your bindings are correct, check the ModalEdit's 324 | output log. You can find it by opening **View - Output** and then choosing the 325 | **ModalEdit** from the drop-down menu. Errors in configuration will be reported 326 | there. If your configuration is ok, you should see the following message. 327 | 328 | ![output log](images/output-log.png) 329 | 330 | ### Changing Cursors 331 | 332 | You can set the cursor shape shown in each mode by changing the following 333 | settings. 334 | 335 | | Setting | Default | Description 336 | | --------------------- | ------------- | ------------------------------------- 337 | | `insertCursorStyle` | `line` | Cursor shown in insert mode. 338 | | `normalCursorStyle` | `block` | Cursor shown in normal mode. 339 | | `searchCursorStyle` | `underline` | Cursor shown when incremental search is on. 340 | | `selectCursorStyle` | `line-thin` | Cursor shown when selection is active in normal mode. 341 | 342 | The possible values are: 343 | 344 | - `block` 345 | - `block-outline` 346 | - `line` 347 | - `line-thin` 348 | - `underline` 349 | - `underline-thin` 350 | 351 | ### Changing Status Bar 352 | 353 | With version 2.0, you can also change the text shown in status bar in each mode 354 | along with the text color. Note that you can add icons in the text by using 355 | syntax `$(icon-name)` where `icon-name` is a valid name from the gallery of 356 | [built-in icons][15]. 357 | 358 | The color of the status text is specified in HTML format, such as `#ffeeff`, 359 | `cyan`, or `rgb(50, 50, 50)`. By default these colors are not defined, and thus 360 | they are same as the rest of text in the status bar. 361 | 362 | | Setting | Default | Description 363 | | ------------------ | ------------------------- | ------------------------------------- 364 | | `insertStatusText` | `-- $(edit) INSERT --` | Status text shown in insert mode 365 | | `normalStatusText` | `-- $(move) NORMAL --` | Status text shown in normal mode 366 | | `searchStatusText` | `$(search) SEARCH` | Status text shown when search is active 367 | | `selectStatusText` | `-- $(paintcan) VISUAL --`| Status text shown when selection is active in normal mode 368 | | `insertStatusColor`| `undefined` | Status text color in insert mode 369 | | `normalStatusColor`| `undefined` | Status text color in normal mode 370 | | `searchStatusColor`| `undefined` | Status text color when search is active 371 | | `selectStatusColor`| `undefined` | Status text color when selection is active in normal mode 372 | 373 | ### Start in Normal Mode 374 | 375 | If you want VS Code to be in insert mode when it starts, set the 376 | `startInNormalMode` setting to `false`. By default, editor is in normal mode 377 | when you open it. 378 | 379 | ### Example Configurations 380 | 381 | You can find example key bindings [here][7]. These are my own settings. The 382 | cheat sheet for my keyboard layout is shown below. I have created it in 383 | . Please note that my keyboard layout 384 | is Finnish, so the non-alphanumeric keys might be in strange places. 385 | 386 | ![My keyboard layout](images/keyboard-layout.png) 387 | 388 | As you can see, I haven't followed Vim conventions but rather tailored the 389 | keyboard layout according to my own preferences. I encourage you to do the same. 390 | 391 | In general, you should not try to convert VS Code into a Vim clone. The editing 392 | philosophies of Vim and VS Code are quite dissimilar. Targets of Vim operations 393 | are defined with special range commands, whereas VS Code's commands operate on 394 | selected text. For example, to delete a word in Vim, you first press `d` to 395 | delete and then `w` for word. In VS Code you first select the word (with `W` or 396 | `e` key in my configuration) then you delete the selection with `d` key. 397 | 398 | To better understand the difference, check out [Kakoune editor's documentation][8]. 399 | ModalEdit extends VS Code with normal mode editing, so you have more or less 400 | the same capabilities as in Kakoune. 401 | 402 | ## Additional VS Code Commands 403 | 404 | ModalEdit adds few useful commands to VS Code's repertoire. They help you 405 | create more Vim-like workflow for searching and navigation. 406 | 407 | ### Switching between Modes 408 | 409 | Use the following commands to change the current editor mode. None of the 410 | commands require any arguments. 411 | 412 | | Command | Description 413 | | ------------------------------------- | ---------------------------------------------- 414 | | `modaledit.toggle` | Toggles between modes. 415 | | `modaledit.enterNormal` | Switches to normal mode. 416 | | `modaledit.enterInsert` | Switches to insert mode. 417 | | `modaledit.toggleSelection` | Toggles selection mode on or off. Selection mode is implicitly on whenever editor has text selected. 418 | | `modaledit.enableSelection` | Turn selection mode on. 419 | | `modaledit.cancelSelection` | Cancel selection mode and clear selection. 420 | | `modaledit.cancelMultipleSelections` | Cancel selection mode and clear selections, but preserve multiple cursors. 421 | 422 | ### Incremental Search 423 | 424 | The standard search functionality in VS Code is quite clunky as it opens a 425 | dialog which takes you out of the editor. To achieve more fluid searching 426 | experience ModalEdit provides incremental search commands that mimic Vim's 427 | corresponding operations. 428 | 429 | > There are lot of new parameters in the `search` command that were added in 430 | > version 2.0. Specifically, `typeAfter...` and `typeBefore...` arguments might 431 | > seem odd at first glance. Please see the [change log](CHANGELOG.html) to 432 | > understand the rationale why they are needed. 433 | 434 | #### `modaledit.search` 435 | 436 | Starts incremental search. The cursor is changed to indicate that editor is in 437 | search mode. Normal mode commands are suppressed while incremental search is 438 | active. Just type the search string directly without leaving the editor. You 439 | can see the searched string in the status bar as well as the search parameters. 440 | 441 | ![Searching](images/searching.gif) 442 | 443 | The command takes following arguments. All of them are optional. 444 | 445 | | Argument | Type | Default | Description 446 | | ------------------------- | --------- | ----------- | --------------------------------- 447 | | `backwards` | `boolean` | `false` | Search backwards. Default is forwards 448 | | `caseSensitive` | `boolean` | `false` | Search is case-sensitive. Default is case-insensitive 449 | | `wrapAround` | `boolean` | `false` | Search wraps around to top/bottom depending on search direction. Default is off. 450 | | `acceptAfter` | `number` | `undefined` | Accept search automatically after _x_ characters has been entered. This helps implementing quick one or two character search operations. 451 | | `selectTillMatch` | `boolean` | `false` | Select the range from current position till the match instead of just the match. Useful with `acceptAfter` to quickly extend selection till the specified character(s). 452 | | `typeAfterAccept` | `string` | `undefined` | Allows to run normal mode commands through key bindings (see `modaledit.typeNormalKeys` command) after successful search. The argument can be used to enter insert mode, or clear selection after search, for example. 453 | | `typeBeforeNextMatch` | `string` | `undefined` | Run the specified key commands *before* searhing for the next match. 454 | | `typeAfterNextMatch` | `string` | `undefined` | Run the specified key commands *after* the next match command is executed. 455 | | `typeBeforePreviousMatch` | `string` | `undefined` | Run the specified key commands *before* searhing for the previous match. 456 | | `typeAfterPreviousMatch` | `string` | `undefined` | Run the specified key commands *after* the previous match command executed. 457 | 458 | #### `modaledit.cancelSearch` 459 | 460 | Cancels the incremental search, returns the cursor to the starting position, 461 | and switches back to normal mode. 462 | 463 | #### `modaledit.deleteCharFromSearch` 464 | 465 | Deletes the last character of the search string. By default the backspace key 466 | is bound to this command when ModalEdit is active and in search mode. 467 | 468 | #### `modaledit.nextMatch` 469 | 470 | Moves to the next match and selectes it. Which way to search depends on the 471 | search direction. 472 | 473 | #### `modaledit.previousMatch` 474 | 475 | Moves to the previous match and selectes it. Which way to search depends on the 476 | search direction. 477 | 478 | ### Bookmarks 479 | 480 | To quickly jump inside documents ModalEdit provides two bookmark commands: 481 | 482 | - `modaledit.defineBookmark` stores the current position in a bookmark, and 483 | - `modaledit.goToBookmark` jumps to the given bookmark. 484 | - `modaledit.showBookmarks` shows the defined bookmarks in the command bar and 485 | allows jumping to them by selecting one. 486 | 487 | The first two commands take one argument which contains the bookmark name. It 488 | can be any string (or number), so you can define unlimited number of bookmarks. 489 | If the argument is omitted, default value `0` is assumed. 490 | ```js 491 | { 492 | "command": "modaledit.defineBookmark", 493 | "args": { 494 | "bookmark": "0" 495 | } 496 | } 497 | ``` 498 | 499 | ### Quick Snippets 500 | 501 | Snippets come in handy when you need to insert boilerplate text. However, the 502 | problem with snippets is that very seldom one bothers to create a new one. If a 503 | snippet is used only a couple of times in a specific situation, the effort of 504 | defining it nullifies the advantage. 505 | 506 | With ModalEdit, you can create snippets quickly by selecting a region of text 507 | and invoking command `modaledit.defineQuickSnippet`. You can assign the snippet 508 | to a register by specifying its index as an argument. 509 | ```js 510 | { 511 | "command": "modaledit.defineQuickSnippet", 512 | "args": { 513 | "snippet": 1 514 | } 515 | } 516 | ``` 517 | Use the `modaledit.insertQuickSnippet` command to insert the defined snippet at 518 | the cursor position. It takes the same argument as `modaledit.defineQuickSnippet`. 519 | 520 | A snippet can have arguments or placeholders which you can fill in after 521 | inserting it. These are written as `$1`, `$2`, ... inside the snippet. You can 522 | quickly define the arguments with the `modaledit.fillSnippetArgs` command. First 523 | multi-select all the arguments (by pressing `Alt` while selecting with a mouse), 524 | then run the command. After that, select the snippet itself and run the 525 | `modaledit.defineQuickSnippet` command. 526 | 527 | In the following example key sequence `q - a` fills snippet arguments, 528 | `q - w - 1` defines snippet in register 1, and `q - 1` inserts it. 529 | 530 | ![Quick snippet](images/quick-snippet.gif) 531 | 532 | It is usually a good idea to run `editor.action.formatDocument` after inserting 533 | a snippet to clean up whitespace. You can do this automatically adding it 534 | to the command sequence. 535 | ```js 536 | "q": { 537 | "1": [ 538 | { 539 | "command": "modaledit.insertQuickSnippet", 540 | "args": { 541 | "snippet": 1 542 | } 543 | }, 544 | "editor.action.formatDocument" 545 | ], 546 | } 547 | ``` 548 | 549 | ### Invoking Key Bindings 550 | 551 | The new command `modaledit.typeNormalKeys` invokes commands through key 552 | bindings. Calling this command with a key sequence has the same effect as 553 | pressing the keys in normal mode. This allows you to treat key bindings as 554 | subroutines that can be called using this command. 555 | 556 | The command has one argument `keys` which contains the key sequence as string. 557 | Assuming that keys `k` and `u` are bound to some commands, the following 558 | example runs them both one after another. 559 | ```js 560 | { 561 | "command": "modaledit.typeNormalKeys", 562 | "args": { 563 | "keys": "ku" 564 | } 565 | } 566 | ``` 567 | 568 | ### Selecting Text Between Delimiters 569 | 570 | The `modaledit.selectBetween` command helps implement advanced selection 571 | operations. The command takes as arguments two strings/regular expressions that 572 | delimit the text to be selected. Both of them are optional, but in order for the 573 | command to do anything one of them needs to be defined. If the `from` argument 574 | is missing, the selection goes from the cursor position forwards to the `to` 575 | string. If the `to` is missing the selection goes backwards till the `from` 576 | string. In addition to these parameters, the command has four flags: 577 | 578 | - If the `regex` flag is on, `from` and `to` strings are treated as regular 579 | expressions in the search. 580 | - The `inclusive` flag tells if the delimiter strings are included in the 581 | selection or not. By default the delimiter strings are not part of the 582 | selection. 583 | - The `caseSensitive` flag makes the search case-sensitive. When this flag is 584 | missing or false the search is case-insensitive. 585 | - By default the search scope is the current line. If you want search inside 586 | the whole document, set the `docScope` flag. 587 | - The `nested` makes sure that `from` and `to` are balanced. I.e. if there are 588 | nested `from` → `to` blocks, the command selects the block where the cursor 589 | currently resides. Deeper level blocks will be included in the selection. 590 | 591 | Below is an example that selects all text inside matching parentheses. For more 592 | advanced examples check the [tutorial][9]. 593 | ```js 594 | { 595 | "command": "modaledit.selectBetween", 596 | "args": { 597 | "from": "(", 598 | "to": ")" 599 | "nested": true 600 | } 601 | } 602 | ``` 603 | 604 | ### Repeat Last Change 605 | 606 | `modaledit.repeatLastChange` command repeats the last command (sequence) that 607 | caused text in the editor to change. It corresponds to the [dot `.` command][13] 608 | in Vim. The command takes no arguments. 609 | 610 | ### Importing Presets 611 | 612 | In version 2.0, new command `modaledit.importPresets` was introduced. It reads 613 | keybindings from a file and copies them to the global `settings.json` file. It 614 | overrides existing keybindings, so back them up somewhere before running the 615 | command, if you want to preserve them. 616 | 617 | In 2.0, also Vim keybindings were added as built-in presets. You can learn more 618 | about Vim bindings [here][14]. Built-in presets are located under the `presets` 619 | folder under the extension installation folder. The command scans and lists all 620 | the files there. It also provides an option to browse for any other file you 621 | want to import. 622 | 623 | Presets are stored either in a JSON or JavaScript file. In either case, the 624 | file to be imported should evaluate to an object which should have at least one 625 | of the following properties: 626 | ```json 627 | { 628 | "keybindings": { ... }, 629 | "selectbindings": { ... } 630 | } 631 | ``` 632 | Both of the properties must follow the configuration structure defined above. 633 | It is also possible to define the object in JS. In that case the object should 634 | be the expression that the whole script evaluates to. 635 | 636 | ## Acknowledgements 637 | 638 | I was using the [Simple Vim][3] extension for a long time, but was never fully 639 | happy with it. It shares the idea of being a simple extension reusing VS Code's 640 | functionality, but it is sorely lacking in configurability. If you don't like 641 | its default key mappings, you are out of luck. 642 | 643 | Then I found extension called [Vimspired][4] which has a really great idea for 644 | implementing modal editing: just add a section in the `settings.json` which 645 | contains the keymap for normal mode. This allows you to mimic Vim behavior, if 646 | you wish to do so, or take a completely different approach. For example, don't 647 | use `h`, `j`, `k`, `l` keys to move the cursor but `w`, `a`, `s`, `d` keys 648 | instead. 649 | 650 | I really like Vimspired, but still wanted to change some of its core behavior 651 | and add many additional features. I didn't want to harass the author with 652 | extensive pull requests, so I decided to implement my own take of the theme. I 653 | shameleslly copied the core parts of Vimspired and then changed them beyond 654 | recognition. Anyway, credit goes to [Brian Malehorn][5] for coming up with the 655 | great idea and helping me jump start my project. 656 | 657 | [1]: https://unix.stackexchange.com/questions/57705/modeless-vs-modal-editors 658 | [2]: https://www.vim.org/ 659 | [3]: https://marketplace.visualstudio.com/items?itemName=jpotterm.simple-vim 660 | [4]: https://marketplace.visualstudio.com/items?itemName=bmalehorn.vimspired 661 | [5]: https://marketplace.visualstudio.com/publishers/bmalehorn 662 | [6]: https://code.visualstudio.com/api/references/commands#commands 663 | [7]: https://gist.github.com/johtela/b63232747fdd465748fedb9ca6422c84 664 | [8]: https://kakoune.org/why-kakoune/why-kakoune.html 665 | [9]: https://johtela.github.io/vscode-modaledit/docs/.vscode/settings.html 666 | [10]: https://johtela.github.io/vscode-modaledit/docs/src/actions.html 667 | [11]: https://johtela.github.io/vscode-modaledit/docs/CHANGELOG.html#version-1-5 668 | [12]: https://johtela.github.io/vscode-modaledit/docs/CHANGELOG.html#version-2-0 669 | [13]: https://vim.fandom.com/wiki/Repeat_last_change 670 | [14]: https://johtela.github.io/vscode-modaledit/docs/presets/vim.html 671 | [15]: https://microsoft.github.io/vscode-codicons/dist/codicon.html -------------------------------------------------------------------------------- /docs/CHANGELOG.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ModalEdit - Change Log 10 | 11 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 295 | 296 |
297 |
298 | 299 |
300 |

Table of Contents

301 | 302 | 359 |
360 |
361 | 362 | 363 | 364 |
365 |
366 | 367 |
368 |

Change Log

369 |

All notable changes to the ModalEdit extension will be documented in this file.

370 |

Version 1.0

371 |
    372 |
  • Initial release
  • 373 |
374 |

Version 1.1

375 |
    376 |
  • Added selectTillMatch argument to modalEdit.search command.
  • 377 |
  • Editor does not automatically revert back to normal mode when changing window.
  • 378 |
379 |

Version 1.2

380 |
    381 |
  • Added startInNormalMode setting, resolving issue 382 | #1
  • 383 |
384 |

Version 1.3

385 |
    386 |
  • Incremental search now returns to insert mode, if it was invoked from there 387 | #4
  • 388 |
  • Added new command modaledit.typeNormalKeys which can be used to "call" 389 | key bindings. Also fixes issue 390 | #3
  • 391 |
  • Added new argument typeAfterAccept to modaledit.search command. This 392 | invokes normal mode key bindings (using modaledit.typeNormalKeys) after 393 | successful search. The argument can be used to enter insert mode, or clear 394 | selection after search, for example.
  • 395 |
396 |

Version 1.4

397 |
    398 |
  • Fixed few issues with modaledit.search command.
  • 399 |
  • You can use __selection variable in JS expressions to access currently 400 | selected text.
  • 401 |
402 |

Version 1.5

403 |

Update that was sparked by issue #6. 404 | Contains multiple new features:

405 |
    406 |
  • repeat attribute added to commands with parameters.
  • 407 |
  • Keymaps can contain key ranges.
  • 408 |
  • Support for recursive keymaps.
  • 409 |
  • New __keySequence variable added to JS expressions. Contains the key 410 | sequence that was used to invoke a command.
  • 411 |
  • New property help added to keymaps. The help string is shown in the status 412 | bar when the associated keymap is active.
  • 413 |
  • Added ModalEdit log to output window.
  • 414 |
  • Semi-large refactoring of type definitions in the actions module.
  • 415 |
416 |

Version 1.6

417 |
    418 |
  • New command modaledit.selectBetween 419 | selects text between two delimiter strings. Especially useful when combined 420 | with the key ranges and recursive keymaps introduced in version 1.5.
  • 421 |
  • Added a shorter alias __keys to the __keySequence variable available in 422 | JS expressions.
  • 423 |
424 |

Version 1.7

425 |

Two "repeat" related bigger improvements:

426 |
    427 |
  • New modaledit.repeatLastChange command 428 | emulates Vim's dot . command quite faithfully.
  • 429 |
  • The repeat property used in context with 430 | commands taking arguments can now also contain a JS expression that 431 | returns a boolean value. In this case, the value is used as a condition that 432 | tells if the command should be repeated. The command is repeated as long as 433 | the expression returns a truthy value.
  • 434 |
435 |

And some minor changes:

436 |
    437 |
  • New variable __rkeys available for use in JS expressions. It contains the 438 | keys pressed to invoke a command in reverse order. This is handy if you need 439 | to access the last keys in the sequence. They are conveniently the first ones 440 | in __rkeys.
  • 441 |
  • Removed unneeded images from the extension package. The package is now 3 MBs 442 | smaller.
  • 443 |
444 |

Version 2.0

445 |

Major release containing lot of new features and improvements.

446 |

Preset keybindings

447 |

It is possible now to import keybindings through the modaledit.importPresets 448 | command. Vim presets are included in the extension 449 | (#7). The presets can be 450 | also defined as JavaScript (#9). 451 | They are evaluated or "compiled" to JSON when import is run.

452 | 453 |

Search command has several new features:

454 |
    455 |
  • 456 |

    Multicursor search (#5, 457 | #12) is now working.

    458 |
  • 459 |
  • 460 |

    There are four new parameters: typeBeforeNextMatch, typeAfterNextMatch, 461 | typeBeforePreviousMatch, and typeAfterPreviousMatch. These can be used to 462 | run key commands after modaledit.nextMatch and modaledit.previousMathch 463 | commands. The need for these parameters arose when implementing Vim's t and 464 | f key commands. These commands look for specified character and place the 465 | cursor either on it or before it. Without the new parameters, it would not 466 | be possible to emulate Vim's behavior using modaledit.search. This is 467 | because by default, it selects the search string and search always starts from 468 | the current cursor position. To make jumping to next and previous character 469 | possible, we need to adjust the cursor position before and after the commands.

    470 |
  • 471 |
  • 472 |

    New parameter wrapAround causes the search to jump the beginning/end of 473 | the file if it hits bottom/top. This closes issue #8

    474 |
  • 475 |
  • 476 |

    The implementation of modaledit.search and modaledit.selectBetween 477 | commands have been refactored. Adding the new parameters described above made 478 | it possible to simplify the implementation and remove hacky code. The changes 479 | should not break existing functionality but make these commands work more 480 | logically and consistently. For example, both of the commands now work 481 | properly when they are used to extend the existing selection.

    482 |
  • 483 |
484 |

Cursor and Status Bar Configuration

485 |

You can now define a different cursor shape when selection is active in normal 486 | mode using the selectCursorStyle. Also, you can 487 | change the status bar text shown in each 488 | mode. It is possible to include icons in the status bar, if you like. This 489 | should be sufficient to close issue 490 | #13

491 |

A secondary status bar was added to show the keys that have been pressed so far. 492 | It also shows help messages defined in bindings and warnings from the search 493 | command.

494 |

Bookmark Improvements

495 |

There is a new command modaledit.showBookmarks that shows all the defined 496 | bookmarks. You can jump to any of them by selecting one in the list. Also, 497 | the bookmark can now be any string instead of a number. This actually worked 498 | previously, but now documentation about this is updated too.

499 |

New parameter select in the modaledit.goToBookmark command extends selection 500 | till the bookmark instead of putting the cursor on it. This makes it possible to 501 | use bookmarks as selection scoping mechanism à la Vim.

502 |

Changes Concerning JS Expressions

503 |

New variables __cmd and __rcmd can be used in JS expressions. They contain 504 | the key sequence that was pressed as a string. They correspond to expressions:

505 | 506 |
__cmd = __keys.join('')
507 | __rcmd = __rkeys.join('')
508 | 
509 |

In many cases, these variables allow you to write expressions that inspect the 510 | key sequence in a shorter form.

511 |

All the variants of __keys or __keySequence variables now contain the actual 512 | key sequence that was used to invoke the command. Previously they contained the 513 | sequence that the user pressed. Since you can also invoke key commands 514 | programmatically using modaledit.typeNormalKeys, this made implementing 515 | reusable commands more difficult. You could not rely on the key sequence to 516 | correspond to the path to the keybinding you were defining. Now you can rely 517 | that the __keys variable and its variants contain the path to the binding, 518 | which should make sure that commands work correctly when invoked through 519 | modaledit.typeNormalKeys.

520 |

Other Changes

521 |
    522 |
  • 523 |

    Configuration section called selectbindings can be used to define key 524 | bindings that are in effect when selection is active. This allows you to 525 | define different key sequences for same leader keys in normal mode and 526 | selecion mode.

    527 |

    For example in Vim, the d key is in normal mode the leader key for sequences 528 | such as dw (delete word) or dip (delete inside paragraph). In visual 529 | (selection) mode, d deletes the selected text. Previously it was not 530 | possible to have such keymaps, but with selectbindings you can now define 531 | them.

    532 |
  • 533 |
  • 534 |

    selecting flag is now refreshed when you switch between files. The select 535 | mode no longer "sticks" between tabs.

    536 |
  • 537 |
538 |

Version 2.1

539 |

Two new commands (courtesy of David Little):

540 |
    541 |
  • modaledit.enableSelection turns selection mode on.
  • 542 |
  • modaledit.cancelMultipleSelections cancels selection mode preserving 543 | multiple cursors.
  • 544 |
545 |

Version 2.2

546 |
    547 |
  • Added a possibility to abort long running commands by pressing Esc.
  • 548 |
  • modaledit.selectBetween command now supports the nested parameter, which 549 | correctly handles selection between nested brackets, and resembles Vim's 550 | behavior. Also updated Vim bindings accordingly. 551 | #39
  • 552 |
  • Will later upload extension also to 553 | Open VSIX Registry so that it can be used also with 554 | VSCodium and other open-source forks.
  • 555 |
556 | 557 |
558 |
559 | 560 | 565 |
566 |
567 | 568 | 584 | 585 | 586 | 587 | --------------------------------------------------------------------------------