├── .babelrc ├── .github └── workflows │ ├── production-build.yaml │ └── publish-to-market.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── .vscodeignore ├── .yarnrc ├── LICENSE ├── README.md ├── contributing.md ├── docs └── assets │ ├── Paper File bookmark.gif │ ├── Paper Filetree.gif │ ├── Paper Selection bookmark.gif │ └── PaperBanner.png ├── icons ├── icon.png └── tiny.png ├── package.json ├── prettier.config.js ├── src ├── extension │ ├── ContentProvider.ts │ ├── api │ │ ├── DocsManager.ts │ │ ├── index.ts │ │ ├── static-files.ts │ │ └── utils.ts │ ├── extension.ts │ └── typings.d.ts └── ui │ ├── App.tsx │ ├── Editor.tsx │ ├── EditorFloatingButton.tsx │ ├── EventsListener.ts │ ├── assets │ ├── JsBubblesLogo.png │ └── logo.png │ ├── docs-list │ ├── DocItem.tsx │ └── index.tsx │ ├── editor-views │ ├── file-bookmark │ │ ├── Component.tsx │ │ ├── index.tsx │ │ └── styles.css │ ├── file-tree │ │ ├── Component.tsx │ │ ├── Node.tsx │ │ ├── index.tsx │ │ └── styles.css │ └── selection-bookmark │ │ ├── Component.tsx │ │ ├── index.tsx │ │ └── styles.css │ ├── index.tsx │ ├── styles.css │ ├── theme.ts │ ├── types.ts │ └── typings.d.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-typescript", 6 | { 7 | "isTSX": true, 8 | "allExtensions": true 9 | } 10 | ], 11 | "@babel/preset-env" 12 | ], 13 | "plugins": [ 14 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 15 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 16 | "@babel/plugin-proposal-object-rest-spread", 17 | "babel-plugin-styled-components", 18 | "@babel/plugin-transform-runtime", 19 | "@babel/plugin-syntax-optional-chaining", 20 | "@babel/plugin-proposal-optional-chaining" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/production-build.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | node-version: [10.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn install and build 21 | run: | 22 | yarn install --ignore-engines 23 | yarn build 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-market.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*' 5 | name: Publish to market 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 10.x 16 | - name: yarn install, build, and test 17 | run: | 18 | yarn install --ignore-engines 19 | yarn build 20 | - uses: lannonbr/vsce-action@master 21 | with: 22 | args: 'publish -p $VSCE_TOKEN --yarn' 23 | env: 24 | CI: true 25 | VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | .vscode-test 4 | cypress/videos 5 | cypress/screenshots 6 | -------------------------------------------------------------------------------- /.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": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "env": { 16 | "dev": "true" 17 | } 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "runtimeExecutable": "${execPath}", 24 | "args": [ 25 | "${workspaceFolder}/src/test/dummy-project-used-by-tests", 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 28 | ], 29 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"] 30 | }, 31 | { 32 | "type": "node", 33 | "request": "launch", 34 | "name": "Jest Current File", 35 | "program": "${workspaceFolder}/node_modules/.bin/jest", 36 | "args": ["${fileBasename}"], 37 | "console": "integratedTerminal", 38 | "internalConsoleOptions": "neverOpen", 39 | "disableOptimisticBPs": true, 40 | "windows": { 41 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "on", 11 | "typescript.tsdk": "node_modules\\typescript\\lib" 12 | } 13 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | **/tsconfig.json 7 | **/*.map 8 | **/*.ts 9 | **/webpack.config.js 10 | website 11 | docs 12 | .babelrc 13 | cypress -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | network-timeout 600000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Raathi Kugarajan 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 | 2 | 3 |
4 |
5 | 6 | Paper is a note taking tool for VSCode. The motivation behind Paper is to create a tool that will let developers keep frequently accessed files and locations within files (functions, classes and etc) bookmarked for easy access. 7 | 8 |
9 | 10 | ### Install the extension 11 | 12 | You can install the [extension](https://marketplace.visualstudio.com/items?itemName=Raathigeshan.paper) from VSCode marketplace. 13 | 14 |
15 | 16 | ### Shortcuts 17 | 18 | - `# ` - Creates a H1 heading 19 | - `## ` - Creates a H2 heading 20 | - `### ` - Creates a H3 heading 21 | - `- ` - Creates a list 22 | 23 |
24 | 25 | ### Features 26 | 27 | #### Bookmark active file 28 | 29 | 30 | 31 | Click the file icon in the floating menu to create a bookmark for the active file. 32 | 33 | This is helpful if you want to bookmark a file that you frequently visit while implementing a feature. 34 | 35 |
36 | 37 | #### Bookmark selection 38 | 39 | 40 | 41 | Select text in a file and click the the pointer icon in the floating menu to create a bookmark to the selection. 42 | 43 | Using this feature you can bookmark any number of locations in your codebase you visit often. 44 | 45 |
46 | 47 | #### File tree 48 | 49 | 50 | 51 | A file tree widget shows you the files under a particular path as a tree view. 52 | 53 | This helps to keep a particular directory in-sight if don't want to scroll through in VSCode's tree view. 54 | 55 |
56 | 57 |
58 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/contributing.md -------------------------------------------------------------------------------- /docs/assets/Paper File bookmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/docs/assets/Paper File bookmark.gif -------------------------------------------------------------------------------- /docs/assets/Paper Filetree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/docs/assets/Paper Filetree.gif -------------------------------------------------------------------------------- /docs/assets/Paper Selection bookmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/docs/assets/Paper Selection bookmark.gif -------------------------------------------------------------------------------- /docs/assets/PaperBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/docs/assets/PaperBanner.png -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/icons/icon.png -------------------------------------------------------------------------------- /icons/tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/icons/tiny.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paper", 3 | "displayName": "Paper", 4 | "description": "Take code notes", 5 | "publisher": "raathigeshan", 6 | "version": "0.11.0", 7 | "engines": { 8 | "vscode": "^1.50.0" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Raathigesh/paper" 16 | }, 17 | "icon": "icons/icon.png", 18 | "galleryBanner": { 19 | "color": "#ffff", 20 | "theme": "light" 21 | }, 22 | "activationEvents": [ 23 | "*" 24 | ], 25 | "main": "./out/extension/extension", 26 | "contributes": { 27 | "viewsContainers": { 28 | "activitybar": [ 29 | { 30 | "id": "paper-view", 31 | "title": "Paper", 32 | "icon": "icons/icon.png" 33 | } 34 | ] 35 | }, 36 | "views": { 37 | "paper-view": [ 38 | { 39 | "type": "webview", 40 | "id": "paper", 41 | "name": "Paper" 42 | } 43 | ] 44 | } 45 | }, 46 | "scripts": { 47 | "vscode:prepublish": "yarn run build", 48 | "compile": "tsc -p ./", 49 | "watch": "tsc -watch -p ./src/extension --project ./tsconfig.json", 50 | "postinstall": "node ./node_modules/vscode/bin/install", 51 | "ui": "webpack-dev-server --env.development --config ./webpack.config.js", 52 | "build-ui": "webpack --env.production --config ./webpack.config.js", 53 | "build": "rimraf out && yarn compile && yarn build-ui", 54 | "package-ext": "vsce package --yarn", 55 | "publish-ext": "vsce publish --yarn", 56 | "pretty": "prettier --write \"src/**/*.{ts,tsx}\"", 57 | "test": "jest", 58 | "ship": "yarn version && git push origin --tag" 59 | }, 60 | "devDependencies": { 61 | "@babel/core": "^7.7.2", 62 | "@babel/plugin-proposal-class-properties": "^7.4.0", 63 | "@babel/plugin-proposal-decorators": "^7.6.0", 64 | "@babel/plugin-proposal-object-rest-spread": "^7.4.3", 65 | "@babel/plugin-proposal-optional-chaining": "^7.6.0", 66 | "@babel/plugin-syntax-optional-chaining": "^7.2.0", 67 | "@babel/plugin-transform-runtime": "^7.5.0", 68 | "@babel/preset-env": "^7.4.3", 69 | "@babel/preset-react": "^7.0.0", 70 | "@babel/preset-typescript": "^7.7.2", 71 | "@chakra-ui/react": "^1.6.5", 72 | "@devtools-ds/dom-inspector": "^1.1.2", 73 | "@emotion/core": "^10.0.22", 74 | "@emotion/react": "11", 75 | "@emotion/styled": "11", 76 | "@tiptap/extension-link": "^2.0.0-beta.19", 77 | "@tiptap/react": "^2.0.0-beta.55", 78 | "@tiptap/starter-kit": "^2.0.0-beta.89", 79 | "@types/babel-traverse": "^6.25.5", 80 | "@types/cors": "^2.8.12", 81 | "@types/express": "^4.17.13", 82 | "@types/jest": "^24.0.21", 83 | "@types/mocha": "^2.2.42", 84 | "@types/node": "^10.12.21", 85 | "@types/react": "^16.8.14", 86 | "@types/react-dom": "^16.8.4", 87 | "@types/react-resizable": "^1.7.2", 88 | "@types/react-resize-detector": "^4.2.0", 89 | "@types/react-router-dom": "^4.3.3", 90 | "all-contributors-cli": "^6.17.0", 91 | "babel-loader": "^8.0.5", 92 | "babel-plugin-styled-components": "^1.10.0", 93 | "concurrently": "^5.2.0", 94 | "copy-to-clipboard": "^3.3.1", 95 | "cross-env": "^5.2.0", 96 | "css-loader": "^2.1.1", 97 | "emotion-theming": "^10.0.19", 98 | "file-loader": "^3.0.1", 99 | "framer-motion": "4", 100 | "html-webpack-plugin": "^3.2.0", 101 | "html-webpack-template": "^6.2.0", 102 | "husky": "^3.0.9", 103 | "jest": "^24.9.0", 104 | "prettier": "^1.19.1", 105 | "pretty-quick": "^2.0.0", 106 | "react": "^16.8.6", 107 | "react-dom": "^16.8.6", 108 | "react-feather": "^2.0.9", 109 | "rimraf": "^2.6.3", 110 | "style-loader": "^0.23.1", 111 | "terser-webpack-plugin": "^2.3.5", 112 | "typescript": "^4.1.3", 113 | "url-loader": "^3.0.0", 114 | "vscode": "^1.1.28", 115 | "vscode-test": "^1.4.0", 116 | "wait-on": "^5.1.0", 117 | "webpack": "^4.30.0", 118 | "webpack-cli": "^3.3.1", 119 | "webpack-dev-server": "^3.3.1" 120 | }, 121 | "dependencies": { 122 | "body-parser": "^1.19.0", 123 | "cors": "^2.8.5", 124 | "directory-tree": "^2.2.9", 125 | "express": "^4.17.1", 126 | "get-port": "^5.1.1" 127 | }, 128 | "husky": { 129 | "hooks": { 130 | "pre-commit": "pretty-quick --staged" 131 | } 132 | }, 133 | "jest": { 134 | "testPathIgnorePatterns": [ 135 | "/node_modules/", 136 | "/out/" 137 | ], 138 | "moduleNameMapper": { 139 | "^common(.*)$": "/src/common$1", 140 | "^entities(.*)$": "/src/entities$1", 141 | "^indexer(.*)$": "/src/indexer$1" 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 4, 4 | semi: true, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /src/extension/ContentProvider.ts: -------------------------------------------------------------------------------- 1 | export default class ContentProvider { 2 | getDevServerContent() { 3 | return ` 4 | 5 | 6 | 7 | 8 | 9 | Waypoint 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | `; 18 | } 19 | 20 | getProdContent(port: number) { 21 | return ` 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | Waypoint 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 | `; 40 | } 41 | 42 | getContent(port: number) { 43 | if (process.env.dev) { 44 | return this.getDevServerContent(); 45 | } 46 | 47 | return this.getProdContent(port); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/extension/api/DocsManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | const StateKey = 'paper-documents'; 4 | 5 | interface Document { 6 | content: string; 7 | id: string; 8 | type: 'doc'; 9 | name: string; 10 | } 11 | 12 | interface State { 13 | activeDocument: null | string; 14 | documents: Document[]; 15 | } 16 | 17 | export class DocsManager { 18 | context: vscode.ExtensionContext; 19 | 20 | constructor(context: vscode.ExtensionContext) { 21 | this.context = context; 22 | } 23 | 24 | getActiveDocument(): Document | null { 25 | if (this.context.workspaceState.get(StateKey) === undefined) { 26 | const newDocId = new Date().valueOf().toString(); 27 | this.context.workspaceState.update(StateKey, { 28 | activeDocument: newDocId, 29 | documents: [ 30 | { 31 | content: 32 | this.context.workspaceState.get('paper-content') || 33 | '', 34 | id: newDocId, 35 | type: 'doc', 36 | name: 'Untitled', 37 | }, 38 | ], 39 | } as State); 40 | } 41 | 42 | const state: State = this.context.workspaceState.get(StateKey) as any; 43 | const activeDocument = state.documents.find( 44 | item => item.id === state.activeDocument 45 | ); 46 | if (!activeDocument) { 47 | return null; 48 | } 49 | return activeDocument; 50 | } 51 | 52 | setActiveDocument(id: string) { 53 | const state: State = this.context.workspaceState.get(StateKey) as any; 54 | this.context.workspaceState.update(StateKey, { 55 | ...state, 56 | activeDocument: id, 57 | } as State); 58 | } 59 | 60 | updateDocument(id: string, content: string) { 61 | const state: State = this.context.workspaceState.get(StateKey) as any; 62 | const updatedDocuments = state.documents.map(item => { 63 | if (item.id === id) { 64 | return { 65 | ...item, 66 | content, 67 | }; 68 | } 69 | return item; 70 | }); 71 | 72 | const nextState: State = { 73 | ...state, 74 | documents: updatedDocuments, 75 | }; 76 | this.context.workspaceState.update(StateKey, nextState); 77 | } 78 | 79 | createDocument(name: string, content: string, type: 'doc') { 80 | const state: State = this.context.workspaceState.get(StateKey) as any; 81 | const id = new Date().valueOf().toString(); 82 | const nextState: State = { 83 | ...state, 84 | documents: [ 85 | ...state.documents, 86 | { 87 | id, 88 | content, 89 | type, 90 | name, 91 | }, 92 | ], 93 | }; 94 | this.context.workspaceState.update(StateKey, nextState); 95 | 96 | return id; 97 | } 98 | 99 | deleteDocument(id: string) { 100 | const state: State = this.context.workspaceState.get(StateKey) as any; 101 | const nextState: State = { 102 | ...state, 103 | activeDocument: 104 | id === state.activeDocument ? null : state.activeDocument, 105 | documents: state.documents.filter(item => item.id !== id), 106 | }; 107 | this.context.workspaceState.update(StateKey, nextState); 108 | } 109 | 110 | getDocumentsList(): Document[] { 111 | const state: State = this.context.workspaceState.get(StateKey) as any; 112 | return state.documents; 113 | } 114 | 115 | renameDoc(id: string, name: string) { 116 | const state: State = this.context.workspaceState.get(StateKey) as any; 117 | const updatedDocuments = state.documents.map(item => { 118 | if (item.id === id) { 119 | return { 120 | ...item, 121 | name, 122 | }; 123 | } 124 | return item; 125 | }); 126 | 127 | const nextState: State = { 128 | ...state, 129 | documents: updatedDocuments, 130 | }; 131 | this.context.workspaceState.update(StateKey, nextState); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/extension/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as vscode from 'vscode'; 3 | import * as bodyParser from 'body-parser'; 4 | import * as cors from 'cors'; 5 | import * as dirTree from 'directory-tree'; 6 | 7 | import { initializeStaticRoutes } from './static-files'; 8 | import { isAbsolute, join, relative } from 'path'; 9 | import { DocsManager } from './DocsManager'; 10 | import { resolvePath } from './utils'; 11 | 12 | export async function startApiServer( 13 | port: number, 14 | context: vscode.ExtensionContext 15 | ) { 16 | const docsManager = new DocsManager(context); 17 | 18 | const app = express(); 19 | app.use(bodyParser()); 20 | app.use(cors()); 21 | initializeStaticRoutes(app, port); 22 | 23 | app.post('/open-file', (req, res) => { 24 | const path = req.body.filePath; 25 | const fullPath = resolvePath(vscode.workspace.rootPath || '', path); 26 | 27 | let selection = undefined; 28 | if (req.body.selection) { 29 | selection = new vscode.Selection( 30 | new vscode.Position( 31 | req.body.selection.start.line, 32 | req.body.selection.start.column 33 | ), 34 | new vscode.Position( 35 | req.body.selection.end.line, 36 | req.body.selection.end.column 37 | ) 38 | ); 39 | } 40 | vscode.window.showTextDocument(vscode.Uri.file(fullPath), { 41 | selection, 42 | }); 43 | res.send('OK'); 44 | }); 45 | 46 | app.post('/create', (req, res) => { 47 | const name = req.body.name; 48 | const type = req.body.type; 49 | 50 | const id = docsManager.createDocument(name, '', type); 51 | const documents = docsManager.getDocumentsList(); 52 | res.json({ 53 | id, 54 | documents, 55 | }); 56 | }); 57 | 58 | app.get('/content', (req, res) => { 59 | const document = docsManager.getActiveDocument(); 60 | res.json(document); 61 | }); 62 | 63 | app.post('/content', (req, res) => { 64 | const content = req.body.content; 65 | const id = req.body.id; 66 | 67 | if (!id) { 68 | res.sendStatus(500); 69 | } else { 70 | docsManager.updateDocument(id, content); 71 | res.send('OK'); 72 | } 73 | }); 74 | 75 | app.get('/documents', (req, res) => { 76 | const documents = docsManager.getDocumentsList(); 77 | res.json(documents); 78 | }); 79 | 80 | app.post('/changeActiveDocument', (req, res) => { 81 | const id = req.body.id; 82 | docsManager.setActiveDocument(id); 83 | res.send('OK'); 84 | }); 85 | 86 | app.post('/renameDocument', (req, res) => { 87 | const id = req.body.id; 88 | const newName = req.body.newName; 89 | 90 | docsManager.renameDoc(id, newName); 91 | res.send('OK'); 92 | }); 93 | 94 | app.post('/deleteDocument', (req, res) => { 95 | const id = req.body.id; 96 | docsManager.deleteDocument(id); 97 | 98 | res.send('OK'); 99 | }); 100 | 101 | app.post('/tree', (req, res) => { 102 | const fullPath = resolvePath( 103 | vscode.workspace.rootPath || '', 104 | req.body.directoryPath 105 | ); 106 | const tree = dirTree(fullPath); 107 | res.json(tree); 108 | }); 109 | 110 | app.get('/events', async function(req, res) { 111 | res.set({ 112 | 'Cache-Control': 'no-cache', 113 | 'Content-Type': 'text/event-stream', 114 | Connection: 'keep-alive', 115 | }); 116 | res.flushHeaders(); 117 | 118 | // Tell the client to retry every 10 seconds if connectivity is lost 119 | res.write('retry: 10000\n\n'); 120 | 121 | vscode.window.onDidChangeActiveTextEditor(event => { 122 | if (event) { 123 | res.write( 124 | `data: ${JSON.stringify({ 125 | activeEditor: event?.document.fileName, 126 | })}\n\n` 127 | ); 128 | } 129 | }); 130 | }); 131 | 132 | // View specific endpoints 133 | app.get('/activeFilePath', (req, res) => { 134 | res.json({ 135 | activeFilePath: relative( 136 | vscode.workspace.rootPath || '', 137 | vscode.window.activeTextEditor?.document.fileName || '' 138 | ), 139 | }); 140 | }); 141 | 142 | app.get('/getSelection', (req, res) => { 143 | if (vscode.window.activeTextEditor?.selection.start) { 144 | const selectionRange = new vscode.Range( 145 | vscode.window.activeTextEditor?.selection.start, 146 | vscode.window.activeTextEditor?.selection.end 147 | ); 148 | const text = vscode.window.activeTextEditor?.document.getText( 149 | selectionRange 150 | ); 151 | 152 | const fullTxt = `${text}|${vscode.window.activeTextEditor?.document.fileName}|${selectionRange.start.line}|${selectionRange.start.character}|${selectionRange.end.line}|${selectionRange.end.character}`; 153 | res.json({ 154 | selection: fullTxt, 155 | }); 156 | } 157 | 158 | res.json({ 159 | selection: null, 160 | }); 161 | }); 162 | 163 | return new Promise((resolve, reject) => { 164 | app.listen(port, () => { 165 | const url = `http://localhost:${port}`; 166 | console.log(`⚡ Insight is running at ${url} `); 167 | resolve(null); 168 | }); 169 | }); 170 | } 171 | -------------------------------------------------------------------------------- /src/extension/api/static-files.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { readdirSync } from 'fs'; 3 | 4 | export function initializeStaticRoutes(express: any, port: number) { 5 | const uiDirectory = join(__dirname, '../../ui'); 6 | const files = readdirSync(uiDirectory); 7 | 8 | const htmlContent = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | Waypoint 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | `; 27 | 28 | express.get('/', (req: any, res: any) => { 29 | res.set('Content-Type', 'text/html'); 30 | res.send(htmlContent); 31 | }); 32 | 33 | files.forEach(fileName => { 34 | express.get(`/${fileName}`, (req: any, res: any) => { 35 | return res.sendFile(join(uiDirectory, fileName)); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/extension/api/utils.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join, normalize, resolve } from 'path'; 2 | 3 | export function resolvePath(workspaceRoot: string, path: string) { 4 | if (resolve(path) === normalize(path)) { 5 | return path; 6 | } 7 | 8 | return join(workspaceRoot, path); 9 | } 10 | -------------------------------------------------------------------------------- /src/extension/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import ContentProvider from './ContentProvider'; 3 | import { startApiServer } from './api'; 4 | import { join } from 'path'; 5 | const getPort = require('get-port'); 6 | 7 | let isServerRunning = false; 8 | let port = 0; 9 | 10 | export function activate(context: vscode.ExtensionContext) { 11 | initialize(context); 12 | } 13 | 14 | export function deactivate() {} 15 | 16 | async function initialize(context: vscode.ExtensionContext) { 17 | if (vscode.workspace.rootPath) { 18 | process.env.projectRoot = vscode.workspace.rootPath; 19 | } 20 | 21 | if (!isServerRunning) { 22 | port = await getPort({ port: 4545 }); 23 | await startApiServer(port, context); 24 | isServerRunning = true; 25 | } 26 | 27 | const provider = new WaypointViewProvider(); 28 | context.subscriptions.push( 29 | vscode.window.registerWebviewViewProvider( 30 | WaypointViewProvider.viewType, 31 | provider 32 | ) 33 | ); 34 | } 35 | 36 | class WaypointViewProvider implements vscode.WebviewViewProvider { 37 | public static readonly viewType = 'paper'; 38 | 39 | private _view?: vscode.WebviewView; 40 | 41 | constructor() {} 42 | 43 | public resolveWebviewView( 44 | webviewView: vscode.WebviewView, 45 | context: vscode.WebviewViewResolveContext, 46 | _token: vscode.CancellationToken 47 | ) { 48 | this._view = webviewView; 49 | 50 | webviewView.webview.options = { 51 | // Allow scripts in the webview 52 | enableScripts: true, 53 | }; 54 | 55 | const contentProvider = new ContentProvider(); 56 | webviewView.webview.html = contentProvider.getContent(port); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/extension/typings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/ui/App.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider, Flex, GlobalStyle } from '@chakra-ui/react'; 2 | import React, { useCallback, useEffect, useState } from 'react'; 3 | import { Grid } from 'react-feather'; 4 | import Editor from './Editor'; 5 | import { ClientDoc } from './types'; 6 | import DocList from './docs-list'; 7 | import { theme } from './theme'; 8 | 9 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 10 | 11 | function App() { 12 | const [activeDoc, setActiveDoc] = useState(null); 13 | 14 | const updateContent = useCallback( 15 | async (content: string) => { 16 | if (activeDoc === null) { 17 | return; 18 | } 19 | 20 | fetch(`${API_URL}/content`, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify({ 26 | content, 27 | id: activeDoc.id, 28 | }), 29 | }); 30 | }, 31 | [activeDoc] 32 | ); 33 | 34 | async function getContent() { 35 | const document: ClientDoc = await ( 36 | await fetch(`${API_URL}/content`) 37 | ).json(); 38 | setActiveDoc(document); 39 | } 40 | 41 | useEffect(() => { 42 | getContent(); 43 | }, [setActiveDoc]); 44 | 45 | return ( 46 | 47 | 48 | 49 | 50 | 59 | 60 | {activeDoc && activeDoc.name} 61 | 62 | { 65 | getContent(); 66 | }} 67 | /> 68 | 69 | 70 | 74 | 75 | {!activeDoc && ( 76 | 82 | No notes found. Create a note to begin. 83 | 84 | )} 85 | 86 | 87 | ); 88 | } 89 | 90 | export default App; 91 | -------------------------------------------------------------------------------- /src/ui/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useEditor, EditorContent, FloatingMenu } from '@tiptap/react'; 3 | import StarterKit from '@tiptap/starter-kit'; 4 | import fileBookmark from './editor-views/file-bookmark'; 5 | import treeView from './editor-views/file-tree'; 6 | import selectionBookmark from './editor-views/selection-bookmark'; 7 | import './styles.css'; 8 | import { Flex } from '@chakra-ui/react'; 9 | import { File, MousePointer, Folder } from 'react-feather'; 10 | import { EditorFloatingButton } from './EditorFloatingButton'; 11 | 12 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 13 | 14 | interface Props { 15 | content: string; 16 | onChange: (content: string) => void; 17 | } 18 | 19 | const Editor = ({ content, onChange }: Props) => { 20 | const editor = useEditor({ 21 | extensions: [StarterKit, fileBookmark, treeView, selectionBookmark], 22 | content: '', 23 | }); 24 | 25 | useEffect(() => { 26 | const handler = ({ editor }: any) => { 27 | onChange(editor.getHTML()); 28 | }; 29 | editor?.on('update', handler); 30 | return () => { 31 | editor?.off('update', handler); 32 | }; 33 | }, [editor, onChange]); 34 | 35 | useEffect(() => { 36 | editor?.commands.setContent(content); 37 | }, [content, editor]); 38 | 39 | return ( 40 | <> 41 | {editor && ( 42 | 43 | 44 | { 47 | const focusResult = editor.chain().focus(); 48 | (focusResult as any) 49 | .toggleReactComponent() 50 | .run(); 51 | }} 52 | icon={} 53 | /> 54 | 59 | { 62 | const focusResult = editor.chain().focus(); 63 | (focusResult as any).toggleRange().run(); 64 | }} 65 | icon={ 66 | 67 | } 68 | /> 69 | 70 | { 73 | const focusResult = editor.chain().focus(); 74 | (focusResult as any).toggleTreeView().run(); 75 | }} 76 | icon={} 77 | /> 78 | 79 | 80 | )} 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default Editor; 87 | -------------------------------------------------------------------------------- /src/ui/EditorFloatingButton.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, Button } from '@chakra-ui/react'; 2 | import React from 'react'; 3 | 4 | interface Props { 5 | onClick: () => void; 6 | tooltip: string; 7 | icon: any; 8 | } 9 | 10 | export function EditorFloatingButton({ onClick, tooltip, icon }: Props) { 11 | return ( 12 | 13 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/ui/EventsListener.ts: -------------------------------------------------------------------------------- 1 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 2 | 3 | class Listener { 4 | source: EventSource; 5 | listeners: ((path: string) => void)[] = []; 6 | 7 | constructor() { 8 | this.source = new EventSource(`${API_URL}/events`); 9 | 10 | this.source.addEventListener('message', message => { 11 | const dataObj = JSON.parse(message.data); 12 | this.listeners.forEach(cb => { 13 | console.log(dataObj); 14 | cb(dataObj.activeEditor); 15 | }); 16 | }); 17 | } 18 | 19 | addListener(cb: (path: string) => void) { 20 | this.listeners.push(cb); 21 | } 22 | } 23 | 24 | export default new Listener(); 25 | -------------------------------------------------------------------------------- /src/ui/assets/JsBubblesLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/src/ui/assets/JsBubblesLogo.png -------------------------------------------------------------------------------- /src/ui/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/src/ui/assets/logo.png -------------------------------------------------------------------------------- /src/ui/docs-list/DocItem.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Input, Tooltip, useTheme } from '@chakra-ui/react'; 2 | import React, { useState } from 'react'; 3 | import { Edit, Check, Trash } from 'react-feather'; 4 | import { ClientDoc } from '../types'; 5 | 6 | interface Props { 7 | isActive: boolean; 8 | onClick: () => void; 9 | doc: ClientDoc; 10 | onRename: (id: string, newName: string) => void; 11 | onDelete: (id: string) => void; 12 | } 13 | 14 | export function DocItem({ isActive, onClick, doc, onRename, onDelete }: Props) { 15 | const [isEditMode, setIsEditMode] = useState(false); 16 | const [docName, setDocName] = useState(doc.name); 17 | 18 | return ( 19 | { 29 | onClick(); 30 | }} 31 | height="30px" 32 | marginBottom="3px" 33 | > 34 | 35 | {isEditMode && ( 36 | 41 | { 47 | if (e.key == 'Enter') { 48 | setIsEditMode(false); 49 | onRename(doc.id, docName); 50 | } 51 | }} 52 | size="small" 53 | value={docName} 54 | onChange={e => setDocName(e.target.value)} 55 | /> 56 | { 63 | setIsEditMode(false); 64 | onRename(doc.id, docName); 65 | }} 66 | > 67 | 68 | 69 | 70 | )} 71 | {!isEditMode && ( 72 | 73 | {doc.name || doc.id} 74 | 75 | setIsEditMode(true)} 77 | marginRight="10px" 78 | _hover={{ 79 | color: 'brand.400', 80 | }} 81 | > 82 | 83 | 84 | 85 | { 88 | if (!isActive) { 89 | onDelete(doc.id); 90 | } 91 | e.stopPropagation(); 92 | }} 93 | cursor={isActive ? 'not-allowed' : 'pointer'} 94 | _hover={{ 95 | color: isActive ? 'none' : 'brand.400', 96 | }} 97 | > 98 | 105 | 106 | 107 | 108 | 109 | 110 | )} 111 | 112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/ui/docs-list/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Flex, 4 | Input, 5 | Popover, 6 | PopoverArrow, 7 | PopoverBody, 8 | PopoverCloseButton, 9 | PopoverContent, 10 | PopoverFooter, 11 | PopoverHeader, 12 | PopoverTrigger, 13 | Portal, 14 | } from '@chakra-ui/react'; 15 | import React, { useEffect, useState } from 'react'; 16 | import { Menu, ArrowRight } from 'react-feather'; 17 | import { ClientDoc } from '../types'; 18 | import { DocItem } from './DocItem'; 19 | 20 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 21 | 22 | interface Props { 23 | activeDoc: ClientDoc | null; 24 | onActiveDocumentChange: () => void; 25 | } 26 | 27 | export default function CreateDoc({ 28 | activeDoc, 29 | onActiveDocumentChange, 30 | }: Props) { 31 | const [docs, setDocs] = useState([]); 32 | const [docName, setDocName] = useState(''); 33 | 34 | const getDocs = async () => { 35 | const response = await fetch(`${API_URL}/documents`); 36 | const documents = await response.json(); 37 | setDocs(documents); 38 | }; 39 | 40 | const changeActiveDocument = async (id: string) => { 41 | await fetch(`${API_URL}/changeActiveDocument`, { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | }, 46 | body: JSON.stringify({ 47 | id, 48 | }), 49 | }); 50 | 51 | onActiveDocumentChange(); 52 | }; 53 | 54 | const createDoc = async (name: string) => { 55 | const response = await fetch(`${API_URL}/create`, { 56 | method: 'POST', 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | }, 60 | body: JSON.stringify({ 61 | name, 62 | type: 'doc', 63 | }), 64 | }); 65 | const { documents, id } = await response.json(); 66 | setDocs(documents); 67 | changeActiveDocument(id); 68 | }; 69 | 70 | const renameDocument = async (id: string, newName: string) => { 71 | await fetch(`${API_URL}/renameDocument`, { 72 | method: 'POST', 73 | headers: { 74 | 'Content-Type': 'application/json', 75 | }, 76 | body: JSON.stringify({ 77 | id, 78 | newName, 79 | }), 80 | }); 81 | 82 | getDocs(); 83 | }; 84 | 85 | const deleteDoc = async (id: string) => { 86 | await fetch(`${API_URL}/deleteDocument`, { 87 | method: 'POST', 88 | headers: { 89 | 'Content-Type': 'application/json', 90 | }, 91 | body: JSON.stringify({ 92 | id, 93 | }), 94 | }); 95 | 96 | getDocs(); 97 | }; 98 | 99 | return ( 100 | getDocs()} 103 | arrowShadowColor="brand.300" 104 | > 105 | 106 | 113 | 114 | 115 | 116 | 117 | 128 | 133 | Docs 134 | 135 | 136 | {docs.map(item => ( 137 | { 144 | changeActiveDocument(item.id); 145 | }} 146 | onDelete={deleteDoc} 147 | onRename={renameDocument} 148 | /> 149 | ))} 150 | {docs.length === 0 && ( 151 | No documents found 152 | )} 153 | 154 | 155 | 156 | setDocName(e.target.value)} 171 | onKeyDown={e => { 172 | if (e.key === 'Enter') { 173 | createDoc(docName); 174 | setDocName(''); 175 | } 176 | }} 177 | /> 178 | 199 | 200 | 201 | 202 | 203 | 204 | ); 205 | } 206 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-bookmark/Component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { NodeViewWrapper } from '@tiptap/react'; 3 | import { Flex, Tooltip } from '@chakra-ui/react'; 4 | import { Trash } from 'react-feather'; 5 | import './styles.css'; 6 | 7 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 8 | 9 | export default (props: any) => { 10 | const setPath = (bookmark: string) => { 11 | props.updateAttributes({ 12 | path: bookmark, 13 | }); 14 | }; 15 | 16 | const getActivePath = async () => { 17 | if (props.node.attrs.path === '') { 18 | const response = await fetch(`${API_URL}/activeFilePath`); 19 | const data = await response.json(); 20 | if (data.activeFilePath) { 21 | setPath(data.activeFilePath); 22 | } 23 | } 24 | }; 25 | 26 | useEffect(() => { 27 | getActivePath(); 28 | }, []); 29 | 30 | const openFile = (path: string) => { 31 | fetch(`${API_URL}/open-file`, { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | }, 36 | body: JSON.stringify({ 37 | filePath: path, 38 | }), 39 | }); 40 | }; 41 | 42 | return ( 43 | 44 | openFile(props.node.attrs.path)} 55 | > 56 | 57 | {props.node.attrs.path} 58 | 59 | { 67 | e.stopPropagation(); 68 | props.deleteNode(); 69 | }} 70 | marginLeft="5px" 71 | height="100%" 72 | alignItems="center" 73 | > 74 | 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-bookmark/index.tsx: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core'; 2 | import { ReactNodeViewRenderer } from '@tiptap/react'; 3 | import Component from './Component'; 4 | 5 | export default Node.create({ 6 | name: 'bookmark', 7 | 8 | group: 'inline', 9 | 10 | inline: true, 11 | 12 | atom: true, 13 | 14 | addAttributes() { 15 | return { 16 | path: { 17 | default: '', 18 | }, 19 | }; 20 | }, 21 | 22 | parseHTML() { 23 | return [ 24 | { 25 | tag: 'bookmark', 26 | }, 27 | ]; 28 | }, 29 | 30 | renderHTML({ HTMLAttributes }) { 31 | return ['bookmark', mergeAttributes(HTMLAttributes)]; 32 | }, 33 | 34 | addNodeView() { 35 | return ReactNodeViewRenderer(Component); 36 | }, 37 | 38 | addCommands() { 39 | return { 40 | toggleReactComponent: () => ({ chain }: any) => { 41 | return chain() 42 | .insertContent('') 43 | .run(); 44 | }, 45 | } as any; 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-bookmark/styles.css: -------------------------------------------------------------------------------- 1 | .ProseMirror .bookmarkRenderer { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-tree/Component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { NodeViewWrapper } from '@tiptap/react'; 3 | import { 4 | Input, 5 | Box, 6 | IconButton, 7 | Flex, 8 | Button, 9 | Tooltip, 10 | } from '@chakra-ui/react'; 11 | import { Edit, Check, RefreshCw, Trash2 } from 'react-feather'; 12 | import { Node } from './Node'; 13 | import './styles.css'; 14 | import listener from '../../EventsListener'; 15 | 16 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 17 | 18 | export default (props: any) => { 19 | const [data, setData] = useState(null); 20 | const [selectedEditor, setSelectedEditor] = useState(null); 21 | const [collapsedNodes, setCollapsedNodes] = useState( 22 | JSON.parse(props.node.attrs.collapsedNodes) 23 | ); 24 | 25 | useEffect(() => { 26 | const cb = (path: string) => { 27 | setSelectedEditor(path); 28 | }; 29 | 30 | listener.addListener(cb); 31 | }, []); 32 | 33 | const getTree = async (path: string) => { 34 | const response = await fetch(`${API_URL}/tree`, { 35 | method: 'POST', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify({ 40 | directoryPath: path, 41 | }), 42 | }); 43 | 44 | const tree = await response.json(); 45 | 46 | setData(tree); 47 | }; 48 | 49 | const openFile = (path: string) => { 50 | fetch(`${API_URL}/open-file`, { 51 | method: 'POST', 52 | headers: { 53 | 'Content-Type': 'application/json', 54 | }, 55 | body: JSON.stringify({ 56 | filePath: path, 57 | }), 58 | }); 59 | }; 60 | 61 | const setCollapsedNode = (path: string, value: boolean) => { 62 | setCollapsedNodes({ 63 | ...collapsedNodes, 64 | [path]: value, 65 | }); 66 | props.updateAttributes({ 67 | collapsedNodes: JSON.stringify({ 68 | ...collapsedNodes, 69 | [path]: value, 70 | }), 71 | }); 72 | }; 73 | 74 | useEffect(() => { 75 | if (props.node.attrs.path !== '') { 76 | getTree(props.node.attrs.path); 77 | } 78 | }, []); 79 | 80 | const setPath = (bookmark: string) => { 81 | props.updateAttributes({ 82 | path: bookmark, 83 | }); 84 | }; 85 | 86 | return ( 87 | 91 | 99 | 100 | setPath(e.target.value)} 102 | value={props.node.attrs.path} 103 | borderColor="brand.300" 104 | color="brand.600" 105 | size="sm" 106 | backgroundColor="brand.300" 107 | _hover={{ border: '1px solid brand.500' }} 108 | borderRadius="4px" 109 | placeholder="Paste a directory path here" 110 | /> 111 | 112 | 124 | 125 | 126 | 137 | 138 | 139 | 145 | {data && ( 146 | { 151 | openFile(path); 152 | }} 153 | onCollapse={setCollapsedNode} 154 | /> 155 | )} 156 | 157 | 158 | 159 | ); 160 | }; 161 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-tree/Node.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex } from '@chakra-ui/react'; 2 | import React, { useState } from 'react'; 3 | import { File, Folder, ChevronDown, ChevronRight } from 'react-feather'; 4 | 5 | interface Props { 6 | name: string; 7 | type: 'file' | 'directory'; 8 | path: string; 9 | children: Props[]; 10 | collapsedNodes: { [key: string]: boolean }; 11 | onFileClick: (path: string) => void; 12 | onCollapse: (path: string, value: boolean) => void; 13 | selectedEditor: string | null; 14 | } 15 | 16 | export function Node({ 17 | name, 18 | type, 19 | path, 20 | onFileClick, 21 | collapsedNodes, 22 | onCollapse, 23 | children = [], 24 | selectedEditor, 25 | }: Props) { 26 | const isExpanded = 27 | collapsedNodes[path] === undefined ? true : collapsedNodes[path]; 28 | 29 | const ArrowIcon = 30 | type === 'directory' ? ( 31 | isExpanded ? ( 32 | 33 | ) : ( 34 | 35 | ) 36 | ) : null; 37 | const Icon = type === 'file' ? File : Folder; 38 | 39 | const isActive = selectedEditor?.toLowerCase() === path.toLowerCase(); 40 | return ( 41 | 42 | { 54 | e.stopPropagation(); 55 | if (type === 'file') { 56 | onFileClick(path); 57 | } else { 58 | onCollapse(path, !isExpanded); 59 | } 60 | }} 61 | > 62 | {ArrowIcon} 63 | 64 | 65 | {name} 66 | 67 | 68 | {isExpanded && 69 | children.map(child => ( 70 | 77 | ))} 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-tree/index.tsx: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core'; 2 | import { ReactNodeViewRenderer } from '@tiptap/react'; 3 | import Component from './Component'; 4 | 5 | export default Node.create({ 6 | name: 'treeview', 7 | 8 | group: 'block', 9 | 10 | atom: true, 11 | 12 | addAttributes() { 13 | return { 14 | path: { 15 | default: '', 16 | }, 17 | collapsedNodes: { 18 | default: '{}', 19 | }, 20 | }; 21 | }, 22 | 23 | parseHTML() { 24 | return [ 25 | { 26 | tag: 'treeview', 27 | }, 28 | ]; 29 | }, 30 | 31 | renderHTML({ HTMLAttributes }) { 32 | return ['treeview', mergeAttributes(HTMLAttributes)]; 33 | }, 34 | 35 | addNodeView() { 36 | return ReactNodeViewRenderer(Component); 37 | }, 38 | 39 | addCommands() { 40 | return { 41 | toggleTreeView: () => ({ chain }: any) => { 42 | return chain() 43 | .insertContent('') 44 | .run(); 45 | }, 46 | } as any; 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /src/ui/editor-views/file-tree/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raathigesh/paper/4011f87dbdb815259f87708cdbb845043ad39d37/src/ui/editor-views/file-tree/styles.css -------------------------------------------------------------------------------- /src/ui/editor-views/selection-bookmark/Component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { NodeViewWrapper } from '@tiptap/react'; 3 | import { 4 | Input, 5 | Box, 6 | IconButton, 7 | Flex, 8 | Modal, 9 | ModalOverlay, 10 | ModalContent, 11 | ModalHeader, 12 | ModalBody, 13 | ModalFooter, 14 | Button, 15 | } from '@chakra-ui/react'; 16 | import { Edit, Check, Trash } from 'react-feather'; 17 | import './styles.css'; 18 | 19 | const API_URL = `http://localhost:${(window as any).port || '4545'}`; 20 | 21 | export default (props: any) => { 22 | const setPath = (bookmark: string) => { 23 | props.updateAttributes({ 24 | path: bookmark, 25 | }); 26 | }; 27 | 28 | const getActivePath = async () => { 29 | if (props.node.attrs.path === '') { 30 | const response = await fetch(`${API_URL}/getSelection`); 31 | const data = await response.json(); 32 | if (data.selection) { 33 | setPath(data.selection); 34 | } 35 | } 36 | }; 37 | 38 | useEffect(() => { 39 | getActivePath(); 40 | }, []); 41 | 42 | const [ 43 | name, 44 | path, 45 | startLine, 46 | startChar, 47 | endLine, 48 | endChar, 49 | ] = props.node.attrs.path.split('|'); 50 | 51 | const openFile = (path: string) => { 52 | fetch(`${API_URL}/open-file`, { 53 | method: 'POST', 54 | headers: { 55 | 'Content-Type': 'application/json', 56 | }, 57 | body: JSON.stringify({ 58 | filePath: path, 59 | selection: { 60 | start: { 61 | line: Number(startLine), 62 | column: Number(startChar), 63 | }, 64 | end: { 65 | line: Number(endLine), 66 | column: Number(endChar), 67 | }, 68 | }, 69 | }), 70 | }); 71 | }; 72 | 73 | return ( 74 | 75 | openFile(path)} 86 | > 87 | 88 | {name} 89 | 90 | { 98 | e.stopPropagation(); 99 | props.deleteNode(); 100 | }} 101 | marginLeft="5px" 102 | height="100%" 103 | alignItems="center" 104 | > 105 | 106 | 107 | 108 | 109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /src/ui/editor-views/selection-bookmark/index.tsx: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core'; 2 | import { ReactNodeViewRenderer } from '@tiptap/react'; 3 | import Component from './Component'; 4 | 5 | export default Node.create({ 6 | name: 'range', 7 | 8 | group: 'inline', 9 | 10 | inline: true, 11 | 12 | atom: true, 13 | 14 | addAttributes() { 15 | return { 16 | path: { 17 | default: '', 18 | }, 19 | }; 20 | }, 21 | 22 | parseHTML() { 23 | return [ 24 | { 25 | tag: 'range', 26 | }, 27 | ]; 28 | }, 29 | 30 | renderHTML({ HTMLAttributes }) { 31 | return ['range', mergeAttributes(HTMLAttributes)]; 32 | }, 33 | 34 | addNodeView() { 35 | return ReactNodeViewRenderer(Component); 36 | }, 37 | 38 | addCommands() { 39 | return { 40 | toggleRange: () => ({ chain }: any) => { 41 | return chain() 42 | .insertContent('') 43 | .run(); 44 | }, 45 | } as any; 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src/ui/editor-views/selection-bookmark/styles.css: -------------------------------------------------------------------------------- 1 | .ProseMirror .bookmarkRenderer { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App'; 4 | 5 | render(, document.getElementById('root')); 6 | 7 | if ((module as any).hot) { 8 | (module as any).hot.accept('./App', () => { 9 | const NextApp = require('./App').default; 10 | render(, document.getElementById('root')); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/styles.css: -------------------------------------------------------------------------------- 1 | .ProseMirror { 2 | height: calc(95vh); 3 | border: none; 4 | margin: 10px; 5 | padding: 5px; 6 | } 7 | 8 | .ProseMirror-focused { 9 | outline: none; 10 | } 11 | -------------------------------------------------------------------------------- /src/ui/theme.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from '@chakra-ui/react'; 2 | 3 | export const theme = extendTheme({ 4 | styles: { 5 | global: { 6 | 'html, body': { 7 | padding: '0px', 8 | backgroundColor: 'brand.500', 9 | color: 'white', 10 | caretColor: 'white', 11 | fontSize: '15px', 12 | }, 13 | h1: { 14 | fontSize: '25px', 15 | fontWeight: 300, 16 | }, 17 | h2: { 18 | fontSize: '20px', 19 | fontWeight: 300, 20 | }, 21 | h3: { 22 | fontSize: '18px', 23 | fontWeight: 300, 24 | }, 25 | h4: { 26 | fontSize: '15px', 27 | fontWeight: 300, 28 | }, 29 | ul: { 30 | marginLeft: '15px', 31 | }, 32 | li: { 33 | marginLeft: '15px', 34 | }, 35 | button: { 36 | margin: '3px', 37 | }, 38 | ':focus': { 39 | outline: 'none', 40 | }, 41 | }, 42 | }, 43 | colors: { 44 | brand: { 45 | 100: '#090909', 46 | 200: '#272727', 47 | 300: '#2F2E31', 48 | 400: '#FFB800', 49 | 500: '#1C1C1E', 50 | 600: '#f1f0ee', 51 | 700: '#25252e', 52 | 800: '#8bc34a', 53 | 900: '#e91e63', 54 | }, 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /src/ui/types.ts: -------------------------------------------------------------------------------- 1 | export interface ClientDoc { 2 | content: string; 3 | id: string; 4 | type: 'doc'; 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/ui/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-color'; 2 | declare module 'react-tippy'; 3 | declare module 'react-virtualized-auto-sizer'; 4 | declare module 'react-select/creatable'; 5 | declare module 'react-select/async'; 6 | declare module 'lodash.debounce'; 7 | declare module '*.gql' { 8 | const content: any; 9 | export default content; 10 | } 11 | declare module 'redux-persist/lib/integration/react'; 12 | declare module '*.png'; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6", "dom", "es2017.object", "esnext.asynciterable"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true, 10 | "allowSyntheticDefaultImports": true, 11 | "jsx": "react", 12 | "skipLibCheck": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "resolveJsonModule": true, 16 | "baseUrl": "./src", 17 | "paths": { 18 | "entities": ["./entities"], 19 | "indexer": ["./indexer"], 20 | "common": ["./common"] 21 | } 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | 6 | module.exports = env => ({ 7 | entry: { 8 | ui: './src/ui/index.tsx', 9 | }, 10 | mode: env.production ? 'production' : 'development', 11 | output: { 12 | path: path.resolve(__dirname, './out/ui'), 13 | filename: '[name].bundle.js', 14 | }, 15 | resolve: { 16 | extensions: ['.mjs', '.ts', '.tsx', '.js', '.jsx'], 17 | alias: { 18 | common: path.resolve(__dirname, 'src/common'), 19 | ui: path.resolve(__dirname, 'src/ui'), 20 | }, 21 | }, 22 | devServer: { 23 | contentBase: path.resolve(__dirname, '../dist/ui'), 24 | hot: true, 25 | port: 9000, 26 | disableHostCheck: true, 27 | headers: { 28 | 'Access-Control-Allow-Origin': '*', 29 | }, 30 | }, 31 | optimization: { 32 | minimizer: [ 33 | new TerserPlugin({ 34 | cache: true, 35 | parallel: true, 36 | sourceMap: true, 37 | terserOptions: { 38 | compress: { 39 | inline: false, 40 | }, 41 | }, 42 | }), 43 | ], 44 | }, 45 | devtool: 'source-map', 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.(js|jsx|ts|tsx)$/, 50 | exclude: /(node_modules)/, 51 | loader: 'babel-loader', 52 | }, 53 | { 54 | test: /\.(png|svg|jpg|gif)$/, 55 | use: [ 56 | { 57 | loader: 'url-loader', 58 | options: { 59 | limit: 100192, 60 | }, 61 | }, 62 | ], 63 | }, 64 | { 65 | test: /\.css$/, 66 | use: ['style-loader', 'css-loader'], 67 | }, 68 | { 69 | test: /\.ttf$/, 70 | use: ['file-loader'], 71 | }, 72 | { 73 | test: /\.(graphql|gql)$/, 74 | exclude: /node_modules/, 75 | loader: 'graphql-tag/loader', 76 | }, 77 | ], 78 | }, 79 | plugins: [ 80 | new HtmlWebpackPlugin({ 81 | title: 'Waypoint', 82 | template: require('html-webpack-template'), 83 | appMountId: 'root', 84 | inject: false, 85 | favicon: './icons/icon.png', 86 | }), 87 | new webpack.HotModuleReplacementPlugin(), 88 | new webpack.DefinePlugin({ 89 | PRODUCTION: env.production === true, 90 | }), 91 | new webpack.optimize.LimitChunkCountPlugin({ 92 | maxChunks: 1, 93 | }), 94 | ], 95 | }); 96 | --------------------------------------------------------------------------------