├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── .prettierrc.yaml
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.ts
├── package.json
├── schema
└── plugin.json
├── src
├── __tests__
│ └── utils.ts
├── index.ts
└── utils.ts
├── style
└── index.css
├── tsconfig.json
└── yarn.lock
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - "**" # matches every branch
7 | - "!master" # excludes master
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-18.04
12 |
13 | name: n${{ matrix.node-version }}/py${{ matrix.python-version }}/j${{ matrix.jupyterlab-version}}
14 |
15 | strategy:
16 | matrix:
17 | node-version: [12, 16]
18 | python-version: [3.7, 3.8]
19 | jupyterlab-version: [">=2.0.0,<4.0.0"]
20 |
21 | steps:
22 | - name: Install node
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 |
27 | - name: Install Python
28 | uses: actions/setup-python@v1
29 | with:
30 | python-version: ${{ matrix.python-version }}
31 |
32 | - name: Install jupyterlab
33 | run: python -m pip install "jupyterlab${{ matrix.jupyterlab-version }}"
34 |
35 | - uses: actions/checkout@v1
36 |
37 | - name: Install JS dependencies, build & test
38 | run: |
39 | jlpm --frozen-lockfile
40 | jlpm prepare
41 | jlpm test
42 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches: [master]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-18.04
10 | steps:
11 | - name: Install node
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: 12
15 | registry-url: https://registry.npmjs.org/
16 |
17 | - name: Install Python
18 | uses: actions/setup-python@v1
19 | with:
20 | python-version: "3.7"
21 |
22 | - name: Install jupyterlab
23 | run: python -m pip install jupyterlab
24 |
25 | - uses: actions/checkout@v1
26 |
27 | - name: Install JS dependencies and build
28 | run: jlpm
29 |
30 | - name: Automated Version Bump
31 | uses: "phips28/gh-action-bump-version@v6.0.2"
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 |
35 | - run: npm publish --access public
36 | env:
37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bundle.*
2 | lib/
3 | node_modules/
4 | *.egg-info/
5 | .ipynb_checkpoints
6 | tsconfig.tsbuildinfo
7 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | trailingComma: "es5"
2 | tabWidth: 4
3 | semi: true
4 | singleQuote: false
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Ryan Homer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # text-shortcuts
2 |
3 | A jupyterlab extension to insert text via keyboard shortcuts.
4 |
5 | 
6 |
7 | ## Pre-requisites
8 |
9 | - JupyterLab 2.x, 3.x
10 | - [node 12+](https://nodejs.org)
11 |
12 | ## Installation
13 |
14 | ```bash
15 | jupyter labextension install @techrah/text-shortcuts
16 | ```
17 |
18 | or add it through your Jupyter Lab **Extensions** tab.
19 |
20 | Then, add some user shortcuts:
21 |
22 | - In Jupyter Lab, select **Settings** / **Advanced Settings Editor** from the menu.
23 |
24 | - Select the **Keyboard Shortcuts** tab.
25 |
26 | - In the **User Preferences** section, add your shortcuts configuration and click the "save" icon.
27 |
28 | Here are two useful shortcuts for programming in R:
29 |
30 | ```json
31 | {
32 | "shortcuts": [
33 | {
34 | "command": "text-shortcuts:insert-text",
35 | "args": {
36 | "kernel": "ir",
37 | "text": "|>",
38 | "autoPad": true
39 | },
40 | "keys": ["Accel Shift M"],
41 | "selector": "body"
42 | },
43 | {
44 | "command": "text-shortcuts:insert-text",
45 | "args": {
46 | "kernel": "ir",
47 | "text": "<-",
48 | "autoPad": true
49 | },
50 | "keys": ["Alt -"],
51 | "selector": "body"
52 | }
53 | ]
54 | }
55 | ```
56 |
57 | **NOTE: As of version 0.1.x You do NOT need to add the above shortcuts to _User Preferences_ unless you want to override the default behaviour.** These two shortcuts are now installed by default. They can be found in _Keyboard Shortcuts / System Defaults_.
58 |
59 |
60 |
61 | ### Anatomy of a Text Shortcut
62 |
63 | ```
64 | {
65 | ...
66 | "command": "text-shortcuts:insert-text"
67 | ...
68 | }
69 | ```
70 |
71 | Identifies the keyboard shortcut as a text shortcut that is intercepted by this extension.
72 |
73 | ```
74 | {
75 | ...
76 | "keys": [
77 | "Accel Shift M"
78 | ],
79 | ...
80 | }
81 | ```
82 |
83 | `keys` is an array of keyboard shortcuts that activate the insertion of the text snippet. Each entry can be a combination of one or more of the following modifiers, ending with a text character. For example, "Accel Shift M" represents Command-Shift-M on macOS.
84 |
85 | - `Accel` : Command (macOS) / Ctrl (Windows)
86 | - `Alt` : Option (macOS) / Alt (Windows)
87 | - `Shift` : Shift
88 | - `Ctrl` : Control
89 |
90 | ```
91 | {
92 | ...
93 | "args": {
94 | "kernel": "ir",
95 | "text": "|>",
96 | "autoPad": true
97 | }
98 | ...
99 | }
100 | ```
101 |
102 | - `kernel` (optional): If you specify a `kernel`, the shortcut will only work in notebooks that are running the specified kernel. Examples of kernel names are `ir` and `python3`. For a list of installed kernels, use `jupyter kernelspec list`.
103 |
104 | - `text`: This is the actual text that you want inserted.
105 |
106 | - `autoPad`: (`true` | `false`). If `true`, will add spacing either before, after, or both before and after so that there is a single space on each side of the text.
107 |
108 | ```
109 | {
110 | ...
111 | "selector": "body"
112 | ...
113 | }
114 | ```
115 |
116 | CSS selector. Always use `"body"` for this extension.
117 |
118 | ## Development
119 |
120 | ### Pre-requisites
121 |
122 | - node 5+
123 | - Python 3.6+
124 |
125 | It is strongly recommended that you set up a virtual Python environment. These instructions will assume that Anaconda is already installed.
126 |
127 | - Create a new virtual environment and activate it.
128 |
129 | ```bash
130 | conda create --name text-shortcuts
131 | conda activate text-shortcuts
132 | ```
133 |
134 | - Install jupyterlab
135 |
136 | ```bash
137 | conda install jupyterlab
138 | ```
139 |
140 | - Clone this project and in the root of the project folder, install dependencies with the JupyterLab Package Manager
141 |
142 | ```bash
143 | jlpm
144 | ```
145 |
146 | - Install the extension
147 |
148 | ```bash
149 | jupyter labextension install . --no-build
150 | ```
151 |
152 | - Start up jupyter lab in watch mode. Don't forget to activate your virtual environment. If you want to use a different browser for development, specify that with the `--browser` switch. If you want to use a custom port, specify that with the `--port` switch.
153 |
154 | ```bash
155 | conda activate text-shortcuts
156 | jupyter lab --watch --browser="chrome" --port=8889
157 | ```
158 |
159 | - In another terminal, run the TypeScript compiler in watch mode.
160 |
161 | ```bash
162 | conda activate text-shortcuts
163 | jlpm tsc -w
164 | ```
165 |
166 | For more information on developing JupyterLab extensions, here are some helpful resources:
167 |
168 | - [Extension Developer Guide][1]
169 | - [Common Extension Points: Keyboard Shortcuts][2]
170 | - [JupyterLab Extensions by Examples][3]
171 | - [CodeMirror: Document management methods][4]
172 | - [Interface INotebookTracker][5]
173 |
174 | Pull requests are welcome!
175 |
176 | [1]: https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html
177 | [2]: https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#keyboard-shortcuts
178 | [3]: https://github.com/jupyterlab/extension-examples
179 | [4]: https://codemirror.net/doc/manual.html#api_doc
180 | [5]: https://jupyterlab.github.io/jupyterlab/interfaces/_notebook_src_index_.inotebooktracker.html
181 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | "@babel/preset-typescript",
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property and type check, visit:
3 | * https://jestjs.io/docs/en/configuration.html
4 | */
5 |
6 | export default {
7 | transformIgnorePatterns: ["/node_modules/(?!(@jupyterlab/.*)/)"],
8 | };
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@techrah/text-shortcuts",
3 | "version": "1.0.4",
4 | "description": "Insert text via shortcut keys in Jupyter Lab.",
5 | "keywords": [
6 | "jupyter",
7 | "jupyterlab",
8 | "jupyterlab-extension"
9 | ],
10 | "homepage": "https://github.com/techrah/jupyterext-text-shortcuts",
11 | "bugs": {
12 | "url": "https://github.com/techrah/jupyterext-text-shortcuts/issues"
13 | },
14 | "license": "MIT",
15 | "author": "Ryan Homer",
16 | "files": [
17 | "lib/**/*.{d.ts,js}",
18 | "style/**/*.css",
19 | "schema/**/*.json"
20 | ],
21 | "main": "lib/index.js",
22 | "types": "lib/index.d.ts",
23 | "style": "style/index.css",
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/techrah/jupyterext-text-shortcuts.git"
27 | },
28 | "scripts": {
29 | "build": "tsc",
30 | "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
31 | "prepare": "npm run clean && npm run build",
32 | "test": "jest",
33 | "watch": "tsc -w"
34 | },
35 | "dependencies": {
36 | "@jupyterlab/application": "^3.0.0",
37 | "@jupyterlab/codemirror": "^3.0.0",
38 | "@jupyterlab/notebook": "^3.0.0",
39 | "lodash": "4.17.21"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.14.0",
43 | "@babel/preset-env": "^7.14.1",
44 | "@babel/preset-typescript": "^7.13.0",
45 | "@types/codemirror": "^0.0.88",
46 | "@types/jest": "^26.0.23",
47 | "@types/lodash": "^4.0.0",
48 | "babel-jest": "^26.6.3",
49 | "jest": "^26.6.3",
50 | "prettier": "2.2.1",
51 | "rimraf": "^3.0.0",
52 | "ts-node": "^9.1.1",
53 | "typescript": "^3.8.3"
54 | },
55 | "sideEffects": [
56 | "style/*.css"
57 | ],
58 | "jupyterlab": {
59 | "extension": true,
60 | "schemaDir": "schema"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/schema/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Text Shortcuts",
3 | "description": "Text Shortcuts plugin settings.",
4 | "type": "object",
5 | "jupyter.lab.shortcuts": [
6 | {
7 | "command": "text-shortcuts:insert-text",
8 | "args": {
9 | "kernel": "ir",
10 | "text": "|>",
11 | "autoPad": true
12 | },
13 | "keys": ["Accel Shift M"],
14 | "selector": "body"
15 | },
16 | {
17 | "command": "text-shortcuts:insert-text",
18 | "args": {
19 | "kernel": "ir",
20 | "text": "<-",
21 | "autoPad": true
22 | },
23 | "keys": ["Alt -"],
24 | "selector": "body"
25 | }
26 | ],
27 | "properties": {},
28 | "additionalProperties": false
29 | }
30 |
--------------------------------------------------------------------------------
/src/__tests__/utils.ts:
--------------------------------------------------------------------------------
1 | import CodeMirror from "codemirror";
2 | import _ from "lodash/fp";
3 |
4 | import { getPaddedTextToInsert } from "../utils";
5 |
6 | describe("getPaddedTextToInsert", () => {
7 | const docTemplate =
8 | "The quick brown fox jumps over the lazy dog." +
9 | "\nThis is line two." +
10 | "\nThis is line three.";
11 | const textToInsert = "|>";
12 | const textToInsertLeftPadded = ` ${textToInsert}`;
13 | const textToInsertRightPadded = `${textToInsert} `;
14 | const textToInsertPadded = ` ${textToInsert} `;
15 |
16 | let doc;
17 |
18 | beforeEach(() => {
19 | doc = new CodeMirror.Doc(docTemplate);
20 | });
21 |
22 | it("should right pad at start of text, character after cursor", () => {
23 | doc.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 0 });
24 | expect(getPaddedTextToInsert(doc, textToInsert)).toBe(
25 | textToInsertRightPadded
26 | );
27 | });
28 |
29 | it("shouldn't right pad at start of text, space after cursor", () => {
30 | doc.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 3 });
31 | expect(getPaddedTextToInsert(doc, textToInsert)).toBe(textToInsert);
32 | });
33 |
34 | it("should right pad at end of text", () => {
35 | doc.setSelection({ line: doc.lastLine(), ch: docTemplate.length });
36 | expect(doc.getSelection()).toBe("");
37 | expect(getPaddedTextToInsert(doc, textToInsert)).toBe(
38 | textToInsertPadded
39 | );
40 | });
41 |
42 | it("shoudn't left pad at start of text", () => {
43 | doc.setSelection({ line: 0, ch: 0 });
44 | expect(doc.getSelection()).toBe("");
45 | expect(getPaddedTextToInsert(doc, textToInsert)).toBe(
46 | textToInsertRightPadded
47 | );
48 | });
49 |
50 | it("should left pad just after a word", () => {
51 | doc.setSelection({ line: 0, ch: 3 });
52 | expect(doc.getSelection()).toBe("");
53 | expect(getPaddedTextToInsert(doc, textToInsert)).toBe(
54 | textToInsertLeftPadded
55 | );
56 | });
57 |
58 | it("should pad in the middle of a word", () => {
59 | doc.setSelection({ line: 0, ch: 5 });
60 | expect(doc.getSelection()).toBe("");
61 | expect(getPaddedTextToInsert(doc, textToInsert)).toBe(
62 | textToInsertPadded
63 | );
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { get } from "lodash/fp";
2 | import CodeMirror from "codemirror";
3 | import {
4 | JupyterFrontEnd,
5 | JupyterFrontEndPlugin,
6 | } from "@jupyterlab/application";
7 | import { INotebookTracker } from "@jupyterlab/notebook";
8 | import { getPaddedTextToInsert } from "./utils";
9 |
10 | const PLUGIN_ID = "@techrah/text-shortcuts:plugin";
11 |
12 | const insertText = (tracker: INotebookTracker) => (args: any) => {
13 | const widget = tracker.currentWidget;
14 | if (!widget) return;
15 |
16 | // If a kernel name is specified in args, compare with current kernel name.
17 | const kernel = get("sessionContext.session.kernel", widget);
18 | if (args.kernel && kernel.name !== args.kernel) return;
19 |
20 | const doc = get("content.activeCell.editor.doc", widget) as CodeMirror.Doc;
21 | if (!doc) return;
22 |
23 | const { text, autoPad } = args;
24 | const insertionText = autoPad ? getPaddedTextToInsert(doc, text) : text;
25 |
26 | doc.replaceSelection(insertionText);
27 | };
28 |
29 | const handleActivation = (app: JupyterFrontEnd, tracker: INotebookTracker) => {
30 | app.commands.addCommand("text-shortcuts:insert-text", {
31 | label: "Insert Text",
32 | execute: insertText(tracker),
33 | });
34 | };
35 |
36 | /**
37 | * text-shortcuts extension.
38 | */
39 | const extension: JupyterFrontEndPlugin = {
40 | id: PLUGIN_ID,
41 | autoStart: true,
42 | requires: [INotebookTracker],
43 | activate: handleActivation,
44 | };
45 |
46 | export default extension;
47 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { isEqual, pick } from "lodash/fp";
2 | import CodeMirror from "codemirror";
3 |
4 | const getPaddingText = (
5 | pos1: CodeMirror.Position,
6 | pos2: CodeMirror.Position,
7 | lastPos: CodeMirror.Position,
8 | ch: string
9 | ): string => {
10 | const pickPos = pick(["line", "ch"]);
11 |
12 | // End of line
13 | if (isEqual(pickPos(pos2), pickPos(lastPos))) return " ";
14 |
15 | // Nothing selected
16 | if (isEqual(pickPos(pos1), pickPos(pos2))) return "";
17 |
18 | // Already padded
19 | if (ch === " ") return "";
20 |
21 | // Not yet padded
22 | return " ";
23 | };
24 |
25 | export const getPaddedTextToInsert = (
26 | doc: CodeMirror.Doc,
27 | textToInsert: string
28 | ): string => {
29 | const _doc = doc.copy(false);
30 |
31 | const from = _doc.getCursor("from");
32 | const to = _doc.getCursor("to");
33 |
34 | _doc.extendSelection(
35 | { ...from, ch: from.ch - 1 },
36 | { ...to, ch: to.ch + 1 }
37 | );
38 |
39 | const extSelectedText = _doc.getSelection();
40 | const extFrom = _doc.getCursor("from");
41 | const extTo = _doc.getCursor("to");
42 |
43 | const lastLine = _doc.lastLine();
44 | const lastPos: CodeMirror.Position = {
45 | line: lastLine,
46 | ch: _doc.getLine(lastLine).length,
47 | };
48 |
49 | const leftCh = getPaddingText(
50 | from,
51 | extFrom,
52 | lastPos,
53 | extSelectedText.slice(0, 1)
54 | );
55 | const rightCh = getPaddingText(
56 | to,
57 | extTo,
58 | lastPos,
59 | extSelectedText.slice(-1)
60 | );
61 | const paddedTextToInsert = `${leftCh}${textToInsert}${rightCh}`;
62 |
63 | return paddedTextToInsert;
64 | };
65 |
--------------------------------------------------------------------------------
/style/index.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techrah/jupyterext-text-shortcuts/852463e2fb6fd6ec82e40630b27a42ce3f269bb4/style/index.css
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "composite": false,
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "incremental": true,
8 | "jsx": "react",
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "noEmitOnError": true,
12 | "noImplicitAny": true,
13 | "noUnusedLocals": true,
14 | "preserveWatchOutput": true,
15 | "resolveJsonModule": true,
16 | "outDir": "lib",
17 | "strict": true,
18 | "strictNullChecks": false,
19 | "target": "es2017",
20 | "types": []
21 | },
22 | "include": ["src/*"],
23 | "exclude": ["src/tests/*"]
24 | }
25 |
--------------------------------------------------------------------------------