├── .all-contributorsrc ├── .circleci └── config.yml ├── .eslintrc.js ├── .gitignore ├── .nvatom-all-contributorsrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── appveyor.yml ├── lib ├── atom-notes.js ├── config.coffee ├── interlink.js ├── notes-fs.js ├── notes-store.js ├── notes-view-list.js ├── notes-view.js └── select-list-view.js ├── menus └── atom-notes.cson ├── notebook ├── How to use.md └── Welcome.md ├── package-lock.json ├── package.json ├── spec ├── .eslintrc.js ├── atom-notes-spec.js ├── interlink-spec.js ├── notes-fs-spec.js └── notes-store-spec.js └── styles └── atom-notes.less /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "atom-notes", 3 | "projectOwner": "lexicalunit", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "lexicalunit", 12 | "name": "Amy Troschinetz", 13 | "avatar_url": "https://avatars1.githubusercontent.com/u/1903876?v=4", 14 | "profile": "http://lexicalunit.com", 15 | "contributions": [ 16 | "code", 17 | "doc", 18 | "bug" 19 | ] 20 | }, 21 | { 22 | "login": "seongjaelee", 23 | "name": "Seongjae Lee", 24 | "avatar_url": "https://avatars1.githubusercontent.com/u/948301?v=4", 25 | "profile": "http://bluebrown.net", 26 | "contributions": [ 27 | "code", 28 | "doc", 29 | "bug" 30 | ] 31 | }, 32 | { 33 | "login": "jonmagic", 34 | "name": "Jonathan Hoyt", 35 | "avatar_url": "https://avatars1.githubusercontent.com/u/623?v=4", 36 | "profile": "http://theprogrammingbutler.com", 37 | "contributions": [ 38 | "bug", 39 | "code" 40 | ] 41 | }, 42 | { 43 | "login": "philip-hodder", 44 | "name": "Philip Hodder", 45 | "avatar_url": "https://avatars1.githubusercontent.com/u/6660636?v=4", 46 | "profile": "http://www.encodis.com", 47 | "contributions": [ 48 | "bug" 49 | ] 50 | }, 51 | { 52 | "login": "alflanagan", 53 | "name": "A. Lloyd Flanagan", 54 | "avatar_url": "https://avatars1.githubusercontent.com/u/1546080?v=4", 55 | "profile": "http://adrian-l-flanagan.herokuapp.com", 56 | "contributions": [ 57 | "bug" 58 | ] 59 | }, 60 | { 61 | "login": "webdev-skynet", 62 | "name": "webdev-skynet", 63 | "avatar_url": "https://avatars2.githubusercontent.com/u/31057217?v=4", 64 | "profile": "https://github.com/webdev-skynet", 65 | "contributions": [ 66 | "bug" 67 | ] 68 | }, 69 | { 70 | "login": "lakonis", 71 | "name": "lakonis", 72 | "avatar_url": "https://avatars0.githubusercontent.com/u/9479788?v=4", 73 | "profile": "https://github.com/lakonis", 74 | "contributions": [ 75 | "bug" 76 | ] 77 | }, 78 | { 79 | "login": "wassname", 80 | "name": "Mike Clark", 81 | "avatar_url": "https://avatars1.githubusercontent.com/u/1103714?v=4", 82 | "profile": "http://wassname.org", 83 | "contributions": [ 84 | "bug" 85 | ] 86 | }, 87 | { 88 | "login": "sdaza", 89 | "name": "Sebastian Daza", 90 | "avatar_url": "https://avatars1.githubusercontent.com/u/2096246?v=4", 91 | "profile": "http://sdaza.com", 92 | "contributions": [ 93 | "bug" 94 | ] 95 | }, 96 | { 97 | "login": "OmeGak", 98 | "name": "Alejandro Avilés", 99 | "avatar_url": "https://avatars3.githubusercontent.com/u/716307?v=4", 100 | "profile": "http://twitter.com/OmeGak", 101 | "contributions": [ 102 | "bug" 103 | ] 104 | }, 105 | { 106 | "login": "mshenfield", 107 | "name": "Max Shenfield", 108 | "avatar_url": "https://avatars0.githubusercontent.com/u/4812055?v=4", 109 | "profile": "https://www.eventbrite.com", 110 | "contributions": [ 111 | "bug" 112 | ] 113 | }, 114 | { 115 | "login": "rsshel", 116 | "name": "Rob", 117 | "avatar_url": "https://avatars1.githubusercontent.com/u/19962963?v=4", 118 | "profile": "https://github.com/rsshel", 119 | "contributions": [ 120 | "bug" 121 | ] 122 | }, 123 | { 124 | "login": "Cutuchiqueno", 125 | "name": "Niels-Oliver Walkowski", 126 | "avatar_url": "https://avatars2.githubusercontent.com/u/306064?v=4", 127 | "profile": "http://nowalkowski.de", 128 | "contributions": [ 129 | "bug" 130 | ] 131 | }, 132 | { 133 | "login": "phdd", 134 | "name": "Peter", 135 | "avatar_url": "https://avatars0.githubusercontent.com/u/1544436?v=4", 136 | "profile": "https://google.com/+PeterHeisig", 137 | "contributions": [ 138 | "code" 139 | ] 140 | }, 141 | { 142 | "login": "yanivdll", 143 | "name": "Yaniv Gilad", 144 | "avatar_url": "https://avatars2.githubusercontent.com/u/675472?v=4", 145 | "profile": "http://prodissues.com", 146 | "contributions": [ 147 | "bug" 148 | ] 149 | }, 150 | { 151 | "login": "jmroland", 152 | "name": "jmroland", 153 | "avatar_url": "https://avatars0.githubusercontent.com/u/10378631?v=4", 154 | "profile": "https://github.com/jmroland", 155 | "contributions": [ 156 | "bug" 157 | ] 158 | }, 159 | { 160 | "login": "jonszcz", 161 | "name": "jonszcz", 162 | "avatar_url": "https://avatars0.githubusercontent.com/u/3603408?v=4", 163 | "profile": "https://github.com/jonszcz", 164 | "contributions": [ 165 | "bug" 166 | ] 167 | }, 168 | { 169 | "login": "lodestone", 170 | "name": "Matt Petty", 171 | "avatar_url": "https://avatars3.githubusercontent.com/u/8401?v=4", 172 | "profile": "http://spacerobots.net", 173 | "contributions": [ 174 | "code" 175 | ] 176 | }, 177 | { 178 | "login": "robwalton", 179 | "name": "Rob Walton", 180 | "avatar_url": "https://avatars1.githubusercontent.com/u/1565171?v=4", 181 | "profile": "http://diamond.ac.uk", 182 | "contributions": [ 183 | "code", 184 | "bug", 185 | "doc" 186 | ] 187 | }, 188 | { 189 | "login": "tthkbw", 190 | "name": "tthkbw", 191 | "avatar_url": "https://avatars2.githubusercontent.com/u/6437525?v=4", 192 | "profile": "https://github.com/tthkbw", 193 | "contributions": [ 194 | "bug" 195 | ] 196 | }, 197 | { 198 | "login": "instalab", 199 | "name": "Samuel Boczek", 200 | "avatar_url": "https://avatars2.githubusercontent.com/u/23265116?v=4", 201 | "profile": "https://github.com/instalab", 202 | "contributions": [ 203 | "code" 204 | ] 205 | }, 206 | { 207 | "login": "rcraggs", 208 | "name": "Richard", 209 | "avatar_url": "https://avatars3.githubusercontent.com/u/3081903?v=4", 210 | "profile": "https://github.com/rcraggs", 211 | "contributions": [ 212 | "bug" 213 | ] 214 | }, 215 | { 216 | "login": "MaxPower9", 217 | "name": "MaxPower9", 218 | "avatar_url": "https://avatars0.githubusercontent.com/u/1761899?v=4", 219 | "profile": "https://github.com/MaxPower9", 220 | "contributions": [ 221 | "bug" 222 | ] 223 | }, 224 | { 225 | "login": "gbirke", 226 | "name": "Gabriel Birke", 227 | "avatar_url": "https://avatars1.githubusercontent.com/u/223326?v=4", 228 | "profile": "http://www.lebenplusplus.de/", 229 | "contributions": [ 230 | "bug" 231 | ] 232 | }, 233 | { 234 | "login": "raysewell", 235 | "name": "raysewell", 236 | "avatar_url": "https://avatars2.githubusercontent.com/u/10551088?v=4", 237 | "profile": "https://github.com/raysewell", 238 | "contributions": [ 239 | "bug" 240 | ] 241 | }, 242 | { 243 | "login": "memeplex", 244 | "name": "memeplex", 245 | "avatar_url": "https://avatars3.githubusercontent.com/u/2845433?v=4", 246 | "profile": "https://github.com/memeplex", 247 | "contributions": [ 248 | "bug" 249 | ] 250 | }, 251 | { 252 | "login": "onesentforth", 253 | "name": "Jason West", 254 | "avatar_url": "https://avatars1.githubusercontent.com/u/10100089?v=4", 255 | "profile": "http://onesentforth.com", 256 | "contributions": [ 257 | "bug" 258 | ] 259 | }, 260 | { 261 | "login": "tgrrr", 262 | "name": "Phil", 263 | "avatar_url": "https://avatars3.githubusercontent.com/u/4689707?v=4", 264 | "profile": "http://botbotdot.com", 265 | "contributions": [ 266 | "bug" 267 | ] 268 | }, 269 | { 270 | "login": "OSoG", 271 | "name": "Shin", 272 | "avatar_url": "https://avatars0.githubusercontent.com/u/40944996?v=4", 273 | "profile": "https://github.com/OSoG", 274 | "contributions": [ 275 | "bug" 276 | ] 277 | }, 278 | { 279 | "login": "mlncn", 280 | "name": "Benjamin Melançon", 281 | "avatar_url": "https://avatars3.githubusercontent.com/u/27131?v=4", 282 | "profile": "http://agaric.com", 283 | "contributions": [ 284 | "bug" 285 | ] 286 | }, 287 | { 288 | "login": "oschwery", 289 | "name": "Orlando Schwery", 290 | "avatar_url": "https://avatars2.githubusercontent.com/u/9121238?v=4", 291 | "profile": "http://oschwery.github.io", 292 | "contributions": [ 293 | "bug" 294 | ] 295 | }, 296 | { 297 | "login": "aubreyz", 298 | "name": "aubreyz", 299 | "avatar_url": "https://avatars3.githubusercontent.com/u/25100168?v=4", 300 | "profile": "https://github.com/aubreyz", 301 | "contributions": [ 302 | "question" 303 | ] 304 | }, 305 | { 306 | "login": "aglime", 307 | "name": "Ashley", 308 | "avatar_url": "https://avatars0.githubusercontent.com/u/28735338?v=4", 309 | "profile": "https://github.com/aglime", 310 | "contributions": [ 311 | "bug" 312 | ] 313 | }, 314 | { 315 | "login": "anthrolisp", 316 | "name": "anthrolisp", 317 | "avatar_url": "https://avatars0.githubusercontent.com/u/36711679?v=4", 318 | "profile": "https://github.com/anthrolisp", 319 | "contributions": [ 320 | "ideas" 321 | ] 322 | }, 323 | { 324 | "login": "ljsinclair", 325 | "name": "LJ Sinclair", 326 | "avatar_url": "https://avatars3.githubusercontent.com/u/48849797?v=4", 327 | "profile": "https://github.com/ljsinclair", 328 | "contributions": [ 329 | "ideas" 330 | ] 331 | }, 332 | { 333 | "login": "jkamenik", 334 | "name": "John Kamenik", 335 | "avatar_url": "https://avatars1.githubusercontent.com/u/165914?v=4", 336 | "profile": "http://jkamenik.github.io", 337 | "contributions": [ 338 | "ideas" 339 | ] 340 | }, 341 | { 342 | "login": "aswolf", 343 | "name": "Aaron S. Wolf", 344 | "avatar_url": "https://avatars0.githubusercontent.com/u/2340371?v=4", 345 | "profile": "http://aswolf.github.io", 346 | "contributions": [ 347 | "bug" 348 | ] 349 | } 350 | ], 351 | "repoType": "github", 352 | "commitConvention": "none" 353 | } 354 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /tmp/project 5 | environment: 6 | # Required: 7 | DISPLAY: ":99" 8 | # Configurable 9 | ATOM_LINT_WITH_BUNDLED_NODE: "true" 10 | APM_TEST_PACKAGES: "" 11 | ATOM_CHANNEL: "stable" 12 | # Machine Setup 13 | # The following configuration line tells CircleCI to use the specified 14 | # docker image as the runtime environment for your job. 15 | # For more information on choosing an image (or alternatively using a 16 | # VM instead of a container) see https://circleci.com/docs/2.0/executor-types/ 17 | # To see the list of pre-built images that CircleCI provides for most common 18 | # languages see https://circleci.com/docs/2.0/circleci-images/ 19 | docker: 20 | - image: circleci/node:latest 21 | steps: 22 | - checkout 23 | - run: 24 | name: Update system package lists 25 | command: sudo apt-get update 26 | - run: 27 | name: Install some pre-requisite packages 28 | command: sudo apt-get --assume-yes --quiet install curl xvfb 29 | - run: 30 | name: Start display server for Atom 31 | command: /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1024x768x16 +extension RANDR 32 | background: true 33 | - run: 34 | name: Download Atom build script 35 | command: curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 36 | - run: 37 | name: Make build script executable 38 | command: chmod u+x build-package.sh 39 | - run: 40 | name: Run package tests 41 | command: ./build-package.sh 42 | # On MacOS: 43 | # - run: caffeinate -s build-package.sh 44 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': 'standard', 3 | 'plugins': [ 4 | 'standard', 5 | 'promise' 6 | ], 7 | 'globals': { 8 | 'atom': true, 9 | 'IntersectionObserver': true, 10 | 'MutationObserver': true, 11 | 'performance': true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /npm-debug.log 3 | /node_modules 4 | /package-lock.json 5 | .tags* 6 | -------------------------------------------------------------------------------- /.nvatom-all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "nvatom", 3 | "projectOwner": "seongjaelee", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "lexicalunit", 12 | "name": "Amy Troschinetz", 13 | "avatar_url": "https://avatars1.githubusercontent.com/u/1903876?v=4", 14 | "profile": "http://lexicalunit.com", 15 | "contributions": [ 16 | "code", 17 | "doc", 18 | "bug" 19 | ] 20 | }, 21 | { 22 | "login": "seongjaelee", 23 | "name": "Seongjae Lee", 24 | "avatar_url": "https://avatars1.githubusercontent.com/u/948301?v=4", 25 | "profile": "http://bluebrown.net", 26 | "contributions": [ 27 | "code", 28 | "doc", 29 | "bug" 30 | ] 31 | }, 32 | { 33 | "login": "jonmagic", 34 | "name": "Jonathan Hoyt", 35 | "avatar_url": "https://avatars1.githubusercontent.com/u/623?v=4", 36 | "profile": "http://theprogrammingbutler.com", 37 | "contributions": [ 38 | "bug", 39 | "code" 40 | ] 41 | }, 42 | { 43 | "login": "ghost", 44 | "name": "Deleted user", 45 | "avatar_url": "https://avatars3.githubusercontent.com/u/10137?v=4", 46 | "profile": "https://github.com/ghost", 47 | "contributions": [ 48 | "bug", 49 | "code", 50 | "doc" 51 | ] 52 | }, 53 | { 54 | "login": "geksilla", 55 | "name": "Denys Buzhor", 56 | "avatar_url": "https://avatars3.githubusercontent.com/u/3911882?v=4", 57 | "profile": "https://github.com/geksilla", 58 | "contributions": [ 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "deltaidea", 64 | "name": "Nikita Litvin", 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/4950036?v=4", 66 | "profile": "https://github.com/deltaidea", 67 | "contributions": [ 68 | "code" 69 | ] 70 | }, 71 | { 72 | "login": "maxbrunsfeld", 73 | "name": "Max Brunsfeld", 74 | "avatar_url": "https://avatars3.githubusercontent.com/u/326587?v=4", 75 | "profile": "https://github.com/maxbrunsfeld", 76 | "contributions": [ 77 | "bug" 78 | ] 79 | }, 80 | { 81 | "login": "scrod", 82 | "name": "Zachary Schneirov", 83 | "avatar_url": "https://avatars1.githubusercontent.com/u/123837?v=4", 84 | "profile": "http://notational.net/", 85 | "contributions": [ 86 | "bug" 87 | ] 88 | }, 89 | { 90 | "login": "czchen", 91 | "name": "ChangZhuo Chen (陳昌倬)", 92 | "avatar_url": "https://avatars0.githubusercontent.com/u/98758?v=4", 93 | "profile": "http://czchen.info", 94 | "contributions": [ 95 | "bug" 96 | ] 97 | }, 98 | { 99 | "login": "MaxPower9", 100 | "name": "MaxPower9", 101 | "avatar_url": "https://avatars0.githubusercontent.com/u/1761899?v=4", 102 | "profile": "https://github.com/MaxPower9", 103 | "contributions": [ 104 | "bug" 105 | ] 106 | }, 107 | { 108 | "login": "ashcomco", 109 | "name": "ashcomco", 110 | "avatar_url": "https://avatars2.githubusercontent.com/u/27955787?v=4", 111 | "profile": "https://github.com/ashcomco", 112 | "contributions": [ 113 | "bug" 114 | ] 115 | }, 116 | { 117 | "login": "timwis", 118 | "name": "Tim Wisniewski", 119 | "avatar_url": "https://avatars3.githubusercontent.com/u/761444?v=4", 120 | "profile": "http://timwis.com", 121 | "contributions": [ 122 | "bug" 123 | ] 124 | }, 125 | { 126 | "login": "sahilseth", 127 | "name": "sseth", 128 | "avatar_url": "https://avatars1.githubusercontent.com/u/684975?v=4", 129 | "profile": "http://docs.flowr.space", 130 | "contributions": [ 131 | "bug" 132 | ] 133 | }, 134 | { 135 | "login": "johjeff", 136 | "name": "johjeff", 137 | "avatar_url": "https://avatars1.githubusercontent.com/u/17050866?v=4", 138 | "profile": "https://github.com/johjeff", 139 | "contributions": [ 140 | "bug" 141 | ] 142 | }, 143 | { 144 | "login": "kafkapre", 145 | "name": "kafkapre", 146 | "avatar_url": "https://avatars0.githubusercontent.com/u/11411308?v=4", 147 | "profile": "https://github.com/kafkapre", 148 | "contributions": [ 149 | "bug" 150 | ] 151 | }, 152 | { 153 | "login": "taw00", 154 | "name": "taw00", 155 | "avatar_url": "https://avatars0.githubusercontent.com/u/6908872?v=4", 156 | "profile": "https://keybase.io/toddwarner", 157 | "contributions": [ 158 | "bug" 159 | ] 160 | }, 161 | { 162 | "login": "lantay", 163 | "name": "Mason", 164 | "avatar_url": "https://avatars2.githubusercontent.com/u/14668027?v=4", 165 | "profile": "http://lantay.github.io/myportfolio", 166 | "contributions": [ 167 | "bug" 168 | ] 169 | }, 170 | { 171 | "login": "lakonis", 172 | "name": "lakonis", 173 | "avatar_url": "https://avatars0.githubusercontent.com/u/9479788?v=4", 174 | "profile": "https://github.com/lakonis", 175 | "contributions": [ 176 | "bug" 177 | ] 178 | }, 179 | { 180 | "login": "artyhedgehog", 181 | "name": "artyhedgehog", 182 | "avatar_url": "https://avatars0.githubusercontent.com/u/7547929?v=4", 183 | "profile": "https://github.com/artyhedgehog", 184 | "contributions": [ 185 | "bug" 186 | ] 187 | }, 188 | { 189 | "login": "bulbil", 190 | "name": "Nabil Kashyap", 191 | "avatar_url": "https://avatars0.githubusercontent.com/u/2319626?v=4", 192 | "profile": "http://www.nabilk.com", 193 | "contributions": [ 194 | "bug" 195 | ] 196 | }, 197 | { 198 | "login": "JonathanReeve", 199 | "name": "Jonathan Reeve", 200 | "avatar_url": "https://avatars2.githubusercontent.com/u/1843676?v=4", 201 | "profile": "http://jonreeve.com", 202 | "contributions": [ 203 | "bug" 204 | ] 205 | }, 206 | { 207 | "login": "DivineDominion", 208 | "name": "Christian Tietze", 209 | "avatar_url": "https://avatars3.githubusercontent.com/u/59080?v=4", 210 | "profile": "http://christiantietze.de", 211 | "contributions": [ 212 | "bug" 213 | ] 214 | }, 215 | { 216 | "login": "benoitdepaire", 217 | "name": "benoitdepaire", 218 | "avatar_url": "https://avatars2.githubusercontent.com/u/3273868?v=4", 219 | "profile": "https://github.com/benoitdepaire", 220 | "contributions": [ 221 | "bug" 222 | ] 223 | }, 224 | { 225 | "login": "mo-tom", 226 | "name": "mo-tom", 227 | "avatar_url": "https://avatars0.githubusercontent.com/u/13486049?v=4", 228 | "profile": "https://github.com/mo-tom", 229 | "contributions": [ 230 | "bug" 231 | ] 232 | }, 233 | { 234 | "login": "jessejanderson", 235 | "name": "Jesse J. Anderson", 236 | "avatar_url": "https://avatars0.githubusercontent.com/u/8367129?v=4", 237 | "profile": "https://github.com/jessejanderson", 238 | "contributions": [ 239 | "bug" 240 | ] 241 | }, 242 | { 243 | "login": "garthk", 244 | "name": "Garth Kidd", 245 | "avatar_url": "https://avatars2.githubusercontent.com/u/15906?v=4", 246 | "profile": "https://github.com/garthk", 247 | "contributions": [ 248 | "doc", 249 | "bug" 250 | ] 251 | }, 252 | { 253 | "login": "PixelT", 254 | "name": "PixelT", 255 | "avatar_url": "https://avatars2.githubusercontent.com/u/6519351?v=4", 256 | "profile": "http://psdtohtml.ninja/", 257 | "contributions": [ 258 | "bug" 259 | ] 260 | }, 261 | { 262 | "login": "kwouk", 263 | "name": "Kris", 264 | "avatar_url": "https://avatars0.githubusercontent.com/u/4380600?v=4", 265 | "profile": "https://github.com/kwouk", 266 | "contributions": [ 267 | "bug" 268 | ] 269 | }, 270 | { 271 | "login": "jkamenik", 272 | "name": "John Kamenik", 273 | "avatar_url": "https://avatars1.githubusercontent.com/u/165914?v=4", 274 | "profile": "http://jkamenik.github.io", 275 | "contributions": [ 276 | "bug" 277 | ] 278 | }, 279 | { 280 | "login": "rsshel", 281 | "name": "Rob", 282 | "avatar_url": "https://avatars1.githubusercontent.com/u/19962963?v=4", 283 | "profile": "https://github.com/rsshel", 284 | "contributions": [ 285 | "bug" 286 | ] 287 | }, 288 | { 289 | "login": "hbuschme", 290 | "name": "Hendrik Buschmeier", 291 | "avatar_url": "https://avatars2.githubusercontent.com/u/122398?v=4", 292 | "profile": "https://purl.org/net/hbuschme", 293 | "contributions": [ 294 | "bug" 295 | ] 296 | }, 297 | { 298 | "login": "aviau", 299 | "name": "Alexandre Viau", 300 | "avatar_url": "https://avatars2.githubusercontent.com/u/2706882?v=4", 301 | "profile": "http://www.alexandreviau.net/", 302 | "contributions": [ 303 | "bug" 304 | ] 305 | }, 306 | { 307 | "login": "brookshelley", 308 | "name": "brook shelley", 309 | "avatar_url": "https://avatars0.githubusercontent.com/u/6990297?v=4", 310 | "profile": "http://brookshelley.github.io", 311 | "contributions": [ 312 | "bug" 313 | ] 314 | }, 315 | { 316 | "login": "ivenhov", 317 | "name": "Daniel Iwan", 318 | "avatar_url": "https://avatars2.githubusercontent.com/u/778457?v=4", 319 | "profile": "https://github.com/ivenhov", 320 | "contributions": [ 321 | "bug" 322 | ] 323 | }, 324 | { 325 | "login": "onechrisjones", 326 | "name": "Christopher Jones", 327 | "avatar_url": "https://avatars2.githubusercontent.com/u/4848043?v=4", 328 | "profile": "http://onechrisjones.me", 329 | "contributions": [ 330 | "bug" 331 | ] 332 | }, 333 | { 334 | "login": "xiaoxinghu", 335 | "name": "Xiaoxing Hu", 336 | "avatar_url": "https://avatars1.githubusercontent.com/u/1196745?v=4", 337 | "profile": "https://github.com/xiaoxinghu", 338 | "contributions": [ 339 | "bug" 340 | ] 341 | }, 342 | { 343 | "login": "strickinato", 344 | "name": "Aaron Strick", 345 | "avatar_url": "https://avatars2.githubusercontent.com/u/7566896?v=4", 346 | "profile": "https://github.com/strickinato", 347 | "contributions": [ 348 | "bug" 349 | ] 350 | }, 351 | { 352 | "login": "OrcsBR", 353 | "name": "OrcsBR", 354 | "avatar_url": "https://avatars1.githubusercontent.com/u/16272380?v=4", 355 | "profile": "https://github.com/OrcsBR", 356 | "contributions": [ 357 | "bug" 358 | ] 359 | }, 360 | { 361 | "login": "Zettt", 362 | "name": "Zettt", 363 | "avatar_url": "https://avatars0.githubusercontent.com/u/42497?v=4", 364 | "profile": "http://www.macosxscreencasts.com", 365 | "contributions": [ 366 | "bug" 367 | ] 368 | }, 369 | { 370 | "login": "jasonrudolph", 371 | "name": "Jason Rudolph", 372 | "avatar_url": "https://avatars3.githubusercontent.com/u/2988?v=4", 373 | "profile": "http://jasonrudolph.com", 374 | "contributions": [ 375 | "bug" 376 | ] 377 | }, 378 | { 379 | "login": "benzguo", 380 | "name": "Ben Guo", 381 | "avatar_url": "https://avatars2.githubusercontent.com/u/894119?v=4", 382 | "profile": "https://soundcloud.com/pastyou", 383 | "contributions": [ 384 | "bug" 385 | ] 386 | }, 387 | { 388 | "login": "zettler", 389 | "name": "zettler", 390 | "avatar_url": "https://avatars1.githubusercontent.com/u/13602288?v=4", 391 | "profile": "https://github.com/zettler", 392 | "contributions": [ 393 | "bug" 394 | ] 395 | }, 396 | { 397 | "login": "jrs65", 398 | "name": "Richard Shaw", 399 | "avatar_url": "https://avatars2.githubusercontent.com/u/695206?v=4", 400 | "profile": "http://www.cita.utoronto.ca/~jrs65/", 401 | "contributions": [ 402 | "bug" 403 | ] 404 | }, 405 | { 406 | "login": "alex-kovac", 407 | "name": "Aleksandar Kovač", 408 | "avatar_url": "https://avatars0.githubusercontent.com/u/174563?v=4", 409 | "profile": "https://github.com/alex-kovac", 410 | "contributions": [ 411 | "bug" 412 | ] 413 | }, 414 | { 415 | "login": "benbalter", 416 | "name": "Ben Balter", 417 | "avatar_url": "https://avatars3.githubusercontent.com/u/282759?v=4", 418 | "profile": "http://ben.balter.com", 419 | "contributions": [ 420 | "bug" 421 | ] 422 | }, 423 | { 424 | "login": "marek95", 425 | "name": "marek95", 426 | "avatar_url": "https://avatars3.githubusercontent.com/u/8494040?v=4", 427 | "profile": "https://github.com/marek95", 428 | "contributions": [ 429 | "bug" 430 | ] 431 | }, 432 | { 433 | "login": "aewing", 434 | "name": "Andrew Ewing", 435 | "avatar_url": "https://avatars2.githubusercontent.com/u/5033161?v=4", 436 | "profile": "http://aewing.io", 437 | "contributions": [ 438 | "bug" 439 | ] 440 | }, 441 | { 442 | "login": "juranta", 443 | "name": "juranta", 444 | "avatar_url": "https://avatars1.githubusercontent.com/u/2896799?v=4", 445 | "profile": "https://github.com/juranta", 446 | "contributions": [ 447 | "bug" 448 | ] 449 | }, 450 | { 451 | "login": "wolfromm", 452 | "name": "wolfromm", 453 | "avatar_url": "https://avatars0.githubusercontent.com/u/910132?v=4", 454 | "profile": "https://github.com/wolfromm", 455 | "contributions": [ 456 | "bug" 457 | ] 458 | }, 459 | { 460 | "login": "brandonhorst", 461 | "name": "Brandon Horst", 462 | "avatar_url": "https://avatars0.githubusercontent.com/u/1938545?v=4", 463 | "profile": "http://brandonhorst.me", 464 | "contributions": [ 465 | "bug" 466 | ] 467 | } 468 | ] 469 | } 470 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | 4 | env: 5 | global: 6 | - APM_TEST_PACKAGES="" 7 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 8 | 9 | matrix: 10 | - ATOM_CHANNEL=stable 11 | - ATOM_CHANNEL=beta 12 | 13 | os: 14 | - linux 15 | - osx 16 | 17 | ### Generic setup follows ### 18 | script: 19 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 20 | - chmod u+x build-package.sh 21 | - ./build-package.sh 22 | 23 | notifications: 24 | email: 25 | on_success: never 26 | on_failure: change 27 | 28 | branches: 29 | only: 30 | - master 31 | 32 | git: 33 | depth: 10 34 | 35 | sudo: false 36 | 37 | dist: trusty 38 | 39 | addons: 40 | apt: 41 | packages: 42 | - build-essential 43 | - fakeroot 44 | - git 45 | - libsecret-1-dev 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 lexicalunit@lexicalunit.com 2 | Copyright (c) 2014-2015 Seongjae Lee 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atom Notes 2 | 3 | [![apm package][apm-ver-link]][releases] 4 | [![mit][mit-badge]][mit] 5 | [![code-style][code-style-badge]][code-style] 6 |
[![travis-ci][travis-ci-badge]][travis-ci] 7 | [![appveyor][appveyor-badge]][appveyor] 8 | [![circle-ci][circle-ci-badge]][circle-ci] 9 | [![david][david-badge]][david] 10 |
[![download][dl-badge]][apm-pkg-link] 11 | [![All Contributors](https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square)](#contributors) 12 | [![nvatom Contributors][nvatom-contrib]][nvatom] 13 | 14 | This package is a fork and rewrite of the now unpublished package 15 | [nvatom][nvatom]. The general idea behind this package is to provide an embedded 16 | [Notational Velocity][nv]-like note-taking feature for Atom users. This package 17 | is **NOT** affiliated with [Notational Velocity][nv]. 18 | 19 | ![screencast][screencast] 20 | 21 | ## 🗒️ Features 22 | 23 | [Notational Velocity][nv] is an application that stores and retrieves notes. 24 | This package provides some similar features embedded directly in your Atom 25 | editor. 26 | 27 | - Modeless Operation 28 | - Incremental Search 29 | - [Mouseless Interaction](#keybindings) 30 | - [Interlinks](#provided-commands) 31 | - [Triggerable from outside Atom](#triggering-from-outside-atom) 32 | 33 | > **Note:** For interlink syntax highlighting, please install 34 | > [`language-atom-notes`][language-atom-notes]. 35 | 36 | Embedding these features in Atom provides the following advantages. 37 | 38 | - **Use Atom's Features** - Such as Syntax Highlighting and Tree View. 39 | - **Use Other Packages** - Such as Markdown Preview and Minimap. 40 | - **Multi-OS** - You can use it in macOS, Linux, and Windows. 41 | 42 | With no new updates to [Notational Velocity][nv] in years, some users are 43 | searching for alternatives with more features. For example Evernote. We do not 44 | believe Evernote is a good alternative to [Notational Velocity][nv]. Advantages 45 | over Evernote are: 46 | 47 | - **Open Source** - Uses the [MIT][mit] license. 48 | - **No Rich Text** - Instead, you get to use [Markdown][md]! 49 | - **Sync Wherever You Want** - You can save notes locally, in 50 | [Dropbox][dropbox], or in [Google Drive][drive]. 51 | 52 | Other solutions such as [nvALT][nvalt] are stand-alone applications which don't 53 | have the same synergy with Atom as Atom Notes provides. 54 | 55 | ## ☁️ Synchronization / Cloud Storage 56 | 57 | Most users prefer to have access to their notes from multiple computers. This is 58 | not a feature of Atom Notes per se. Instead, please use your favorite 59 | synchronization and/or cloud storage solution in conjunction with Atom Notes. 60 | For example, if you store your notes in a directory managed by 61 | [Dropbox][dropbox], then you will have all your notes available to you on any 62 | machine you wish 🎉 63 | 64 | ## ⌨️ Keybindings 65 | 66 | This package does not by default provide any keyboard command shortcuts. There's 67 | no way to know what keyboard shortcuts are even available on *your* machine. For 68 | example, on my machine I could map the Toggle command to `shift-cmd-j`. However 69 | if *you* have the popular `atom-script` package installed on your machine, then 70 | there would be a conflict because that package also wants to use that same 71 | keyboard shortcut. However, all is not lost! 72 | 73 | Atom itself already provides you with everything you need to 74 | [create your own custom keymaps][keymaps]. For example, the following 75 | `keymap.cson` would add a shortcut for the `atom-notes` Toggle command: 76 | 77 | ```cson 78 | 'atom-text-editor': 79 | 'shift-cmd-j': 'atom-notes:toggle' 80 | ``` 81 | 82 | ### Provided Commands 83 | 84 | Map any of the following commands to your own keyboard shortcuts as described 85 | above. 86 | 87 | - `atom-notes:toggle`: Toggle the search box. 88 | - `atom-notes:toggle-preview`: Toggle the search box, and automatically open 89 | Markdown files in preview. 90 | - `atom-notes:interlink`: Jumps to referred note when the cursor is on an 91 | `[[interlink]]`. 92 | 93 | ## 💥 Triggering from outside Atom 94 | 95 | To add Atom Notes to the Apple Services menu and set a keyboard shortcut for use 96 | outside Atom use this [Apple service][apple-service]. Then use your configured 97 | shortcut — see the section on Keybindings, above, for details on 98 | configuring a shortcut inside Atom — from inside or outside Atom to toggle 99 | the notes view. 100 | 101 | Alternatively in macOS and Windows the URL `atom://atom-notes/toggle` will 102 | toggle the notes view. The command will operate in the front-most or most 103 | recently active window or open a new one. It will start Atom if necessary. There 104 | are many ways to automate this. For example, in macOS: 105 | 106 | - Call `open atom://atom-notes/toggle` from the command line or a script. 107 | - Use the [Apple service][apple-service] mentioned above. 108 | - Install [Alfred][alfred] (requires the Power Pack purchase) and the 109 | [alfred-atom-notes workflow][alfred-atom-notes]. 110 | 111 | ## ⚠️ Incompatible Package Error 112 | 113 | In versions prior to `1.16.0`, a dependency of Atom Notes used a native module 114 | that required compilation for each specific version of Atom. This would cause 115 | errors whenever Atom updated from one version to the next. You'd know this had 116 | occurred when: 117 | 118 | 1. Atom Notes stopped working! 119 | 2. You saw a small icon of a red bug in your status bar: bug-icon 120 | 121 | If you're on an old version, and see that, click on it and it will take you to 122 | the **Incompatible Packages** settings in Atom. You can also bring it up by 123 | running the command `Incompatible Packages: View` from your Atom Command 124 | Palette. You will see something like the following, depending on your current 125 | Atom themes. 126 | 127 | ![Incompatible Packages][incompatible] 128 | 129 | All you need to do is click the Rebuild Packages button. If that 130 | doesn't work, please [report the issue so I can investigate][issues]. 131 | 132 | ## 🔮 Future Work 133 | 134 | This package is in active development and I'm willing to review your pull 135 | requests and triage any issues you're having. Please 136 | [report your issues][issues]! 137 | 138 | If you'd like to take a stab at improving this package, please check out the 139 | following list of possible improvements. 140 | 141 | - [ ] Fix broken spec tests that fail because the test runner can't do async. 142 | - [ ] Build a notes server to offload processing from the Atom editor. 143 | - [ ] A better screencast animated gif. 144 | - [ ] Any improvements to package activation time are welcome. 145 | - [ ] Speed and usability improvements are also always welcome. 146 | - [ ] Write more spec tests! 147 | - [ ] Does it make sense to utilize etch somehow? 148 | - [x] Replace `chokidar` usage with [Atom's new File Watch API][file-watch-api]. 149 | - [x] Rip out `DocQuery` and [use `search-index` directly][use-search-index]. 150 | - [x] When `DocQuery` match returns nothing, fallback to `fuzzaldrin-plus`. 151 | - [x] Use async to ensure the notes directory exists in the background. 152 | - [x] Start loading documents in background at package activation time. 153 | - [x] Refactor autocomplete to be less hacky — add support to 154 | [`atom-select-list`][autocomplete]? 155 | 156 | ## 💖 Contributors 157 | 158 | Thanks goes to these wonderful people ([emoji key][emoji-key]): 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 |

Amy Troschinetz

💻 📖 🐛

Seongjae Lee

💻 📖 🐛

Jonathan Hoyt

🐛 💻

Philip Hodder

🐛

A. Lloyd Flanagan

🐛

webdev-skynet

🐛

lakonis

🐛

Mike Clark

🐛

Sebastian Daza

🐛

Alejandro Avilés

🐛

Max Shenfield

🐛

Rob

🐛

Niels-Oliver Walkowski

🐛

Peter

💻

Yaniv Gilad

🐛

jmroland

🐛

jonszcz

🐛

Matt Petty

💻

Rob Walton

💻 🐛 📖

tthkbw

🐛

Samuel Boczek

💻

Richard

🐛

MaxPower9

🐛

Gabriel Birke

🐛

raysewell

🐛

memeplex

🐛

Jason West

🐛

Phil

🐛

Shin

🐛

Benjamin Melançon

🐛

Orlando Schwery

🐛

aubreyz

💬

Ashley

🐛

anthrolisp

🤔

LJ Sinclair

🤔

John Kamenik

🤔

Aaron S. Wolf

🐛
214 | 215 | 216 | 217 | 218 | 219 | ### Contributors to nvatom 220 | 221 | | [
Seongjae Lee](http://bluebrown.net)
[💻](https://github.com/seongjaelee/nvatom/commits?author=seongjaelee "Code") [📖](https://github.com/seongjaelee/nvatom/commits?author=seongjaelee "Documentation") [🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aseongjaelee "Bug reports") | [
Jonathan Hoyt](http://theprogrammingbutler.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajonmagic "Bug reports") [💻](https://github.com/seongjaelee/nvatom/commits?author=jonmagic "Code") | [
Deleted user](https://github.com/ghost)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aghost "Bug reports") [💻](https://github.com/seongjaelee/nvatom/commits?author=ghost "Code") [📖](https://github.com/seongjaelee/nvatom/commits?author=ghost "Documentation") | [
Denys Buzhor](https://github.com/geksilla)
[💻](https://github.com/seongjaelee/nvatom/commits?author=geksilla "Code") | [
Nikita Litvin](https://github.com/deltaidea)
[💻](https://github.com/seongjaelee/nvatom/commits?author=deltaidea "Code") | [
Amy Troschinetz](http://lexicalunit.com)
[💻](https://github.com/seongjaelee/nvatom/commits?author=lexicalunit "Code") [📖](https://github.com/seongjaelee/nvatom/commits?author=lexicalunit "Documentation") [🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Alexicalunit "Bug reports") | [
Max Brunsfeld](https://github.com/maxbrunsfeld)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Amaxbrunsfeld "Bug reports") | 222 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 223 | | [
Zachary Schneirov](http://notational.net/)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ascrod "Bug reports") | [
ChangZhuo Chen (陳昌倬)](http://czchen.info)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aczchen "Bug reports") | [
MaxPower9](https://github.com/MaxPower9)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3AMaxPower9 "Bug reports") | [
ashcomco](https://github.com/ashcomco)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aashcomco "Bug reports") | [
Tim Wisniewski](http://timwis.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Atimwis "Bug reports") | [
sseth](http://docs.flowr.space)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Asahilseth "Bug reports") | [
johjeff](https://github.com/johjeff)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajohjeff "Bug reports") | 224 | | [
kafkapre](https://github.com/kafkapre)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Akafkapre "Bug reports") | [
taw00](https://keybase.io/toddwarner)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ataw00 "Bug reports") | [
Mason](http://lantay.github.io/myportfolio)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Alantay "Bug reports") | [
lakonis](https://github.com/lakonis)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Alakonis "Bug reports") | [
artyhedgehog](https://github.com/artyhedgehog)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aartyhedgehog "Bug reports") | [
Nabil Kashyap](http://www.nabilk.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Abulbil "Bug reports") | [
Jonathan Reeve](http://jonreeve.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3AJonathanReeve "Bug reports") | 225 | | [
Christian Tietze](http://christiantietze.de)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3ADivineDominion "Bug reports") | [
benoitdepaire](https://github.com/benoitdepaire)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Abenoitdepaire "Bug reports") | [
mo-tom](https://github.com/mo-tom)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Amo-tom "Bug reports") | [
Jesse J. Anderson](https://github.com/jessejanderson)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajessejanderson "Bug reports") | [
Garth Kidd](https://github.com/garthk)
[📖](https://github.com/seongjaelee/nvatom/commits?author=garthk "Documentation") [🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Agarthk "Bug reports") | [
PixelT](http://psdtohtml.ninja/)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3APixelT "Bug reports") | [
Kris](https://github.com/kwouk)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Akwouk "Bug reports") | 226 | | [
John Kamenik](http://jkamenik.github.io)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajkamenik "Bug reports") | [
Rob](https://github.com/rsshel)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Arsshel "Bug reports") | [
Hendrik Buschmeier](https://purl.org/net/hbuschme)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ahbuschme "Bug reports") | [
Alexandre Viau](http://www.alexandreviau.net/)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aaviau "Bug reports") | [
brook shelley](http://brookshelley.github.io)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Abrookshelley "Bug reports") | [
Daniel Iwan](https://github.com/ivenhov)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aivenhov "Bug reports") | [
Christopher Jones](http://onechrisjones.me)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aonechrisjones "Bug reports") | 227 | | [
Xiaoxing Hu](https://github.com/xiaoxinghu)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Axiaoxinghu "Bug reports") | [
Aaron Strick](https://github.com/strickinato)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Astrickinato "Bug reports") | [
OrcsBR](https://github.com/OrcsBR)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3AOrcsBR "Bug reports") | [
Zettt](http://www.macosxscreencasts.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3AZettt "Bug reports") | [
Jason Rudolph](http://jasonrudolph.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajasonrudolph "Bug reports") | [
Ben Guo](https://soundcloud.com/pastyou)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Abenzguo "Bug reports") | [
zettler](https://github.com/zettler)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Azettler "Bug reports") | 228 | | [
Richard Shaw](http://www.cita.utoronto.ca/~jrs65/)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajrs65 "Bug reports") | [
Aleksandar Kovač](https://github.com/alex-kovac)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aalex-kovac "Bug reports") | [
Ben Balter](http://ben.balter.com)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Abenbalter "Bug reports") | [
marek95](https://github.com/marek95)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Amarek95 "Bug reports") | [
Andrew Ewing](http://aewing.io)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Aaewing "Bug reports") | [
juranta](https://github.com/juranta)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Ajuranta "Bug reports") | [
wolfromm](https://github.com/wolfromm)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Awolfromm "Bug reports") | 229 | | [
Brandon Horst](http://brandonhorst.me)
[🐛](https://github.com/seongjaelee/nvatom/issues?q=author%3Abrandonhorst "Bug reports") | 230 | 231 | This project follows the [all-contributors][all-contributors] 232 | specification. Contributions of any kind welcome! 233 | 234 | --- 235 | 236 | [MIT][mit] © [lexicalunit][lexicalunit], [seongjaelee][seongjaelee] et [al][contributors] 237 | 238 | [lexicalunit]: http://github.com/lexicalunit 239 | [seongjaelee]: http://github.com/seongjaelee 240 | 241 | [apm-pkg-link]: https://atom.io/packages/atom-notes 242 | [apm-ver-link]: https://img.shields.io/apm/v/atom-notes.svg?style=shield 243 | [appveyor-badge]: https://ci.appveyor.com/api/projects/status/a4fcn60mhewef9r0/branch/master?svg=true 244 | [appveyor]: https://ci.appveyor.com/project/lexicalunit/atom-notes?branch=master 245 | [circle-ci-badge]: https://circleci.com/gh/lexicalunit/atom-notes/tree/master.svg?style=shield 246 | [circle-ci]: https://circleci.com/gh/lexicalunit/atom-notes/tree/master 247 | [code-style-badge]: https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=shield 248 | [code-style]: https://standardjs.com/ 249 | [contributors]: https://github.com/lexicalunit/atom-notes/graphs/contributors 250 | [david-badge]: https://david-dm.org/lexicalunit/atom-notes.svg?style=shield 251 | [david]: https://david-dm.org/lexicalunit/atom-notes 252 | [dl-badge]: http://img.shields.io/apm/dm/atom-notes.svg?style=flat-square 253 | [issues]: https://github.com/lexicalunit/atom-notes/issues 254 | [mit-badge]: https://img.shields.io/apm/l/atom-notes.svg?style=shield 255 | [mit]: http://opensource.org/licenses/MIT 256 | [releases]: https://github.com/lexicalunit/atom-notes/releases 257 | [travis-ci-badge]: https://travis-ci.org/lexicalunit/atom-notes.svg?branch=master 258 | [travis-ci]: https://travis-ci.org/lexicalunit/atom-notes 259 | 260 | [all-contributors]: https://github.com/kentcdodds/all-contributors 261 | [emoji-key]: https://github.com/kentcdodds/all-contributors#emoji-key 262 | [nvatom-contrib]: https://img.shields.io/badge/nvatom_contributors-50-orange.svg?style=flat-square 263 | 264 | [autocomplete]: https://github.com/atom/atom-select-list/issues/12 265 | [bug-icon]: https://user-images.githubusercontent.com/1903876/28800778-e8023f22-7613-11e7-9843-bf7b4b1be17a.png 266 | [drive]: https://www.google.com/drive/ 267 | [dropbox]: https://www.dropbox.com 268 | [incompatible]: https://user-images.githubusercontent.com/1903876/28801648-1f0d8018-7618-11e7-8b0a-f3f93b2fca7b.png 269 | [keymaps]: http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings 270 | [language-atom-notes]: https://github.com/lexicalunit/language-atom-notes 271 | [md]: http://daringfireball.net/projects/markdown/ 272 | [file-watch-api]: https://github.com/atom/atom/pull/14853 273 | [nv]: http://notational.net/ 274 | [nvalt]: http://brettterpstra.com/projects/nvalt/ 275 | [nvatom]: https://github.com/seongjaelee/nvatom 276 | [alfred]: http://www.alfredapp.com 277 | [alfred-atom-notes]: https://github.com/robwalton/alfred-atom-notes 278 | [apple-service]: https://github.com/robwalton/apple-service-atom-notes 279 | [screencast]: https://user-images.githubusercontent.com/1903876/28757512-67bb005c-754a-11e7-99bd-5babb98ac056.gif 280 | [use-search-index]: https://github.com/seongjaelee/nvatom/issues/35#issuecomment-143653832 281 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | environment: 3 | APM_TEST_PACKAGES: 4 | ATOM_LINT_WITH_BUNDLED_NODE: "true" 5 | 6 | matrix: 7 | - ATOM_CHANNEL: stable 8 | - ATOM_CHANNEL: beta 9 | 10 | ### Generic setup follows ### 11 | build_script: 12 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) 13 | 14 | branches: 15 | only: 16 | - master 17 | 18 | version: "{build}" 19 | platform: x64 20 | clone_depth: 10 21 | skip_tags: true 22 | test: off 23 | deploy: off 24 | -------------------------------------------------------------------------------- /lib/atom-notes.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import path from 'path' 5 | 6 | import * as NotesFs from './notes-fs' 7 | 8 | /** 9 | * @typedef SerializedIndex 10 | * @type {Object} 11 | */ 12 | 13 | export default { 14 | config: require('./config.coffee').config, 15 | 16 | /** 17 | * Activate our package. Final activation and notes storage will be done async. 18 | * @param {SerializedIndex} state A previously serialized document storage index to load. 19 | */ 20 | activate (state) { 21 | console.info('Activating atom-notes') 22 | this.store = null 23 | this.ready = undefined 24 | this.doSerialize = true 25 | 26 | if (state) { 27 | makeReady(this, state) 28 | } else { 29 | makeReady(this) 30 | } 31 | }, 32 | 33 | /** 34 | * Handle URI calls to `atom://atom-notes`. See: 35 | * 36 | * 37 | * Specifically `atom://atom-notes/toggle` will toggle the atom-notes view in 38 | * the front-most, most recently active or a new, Atom window; will start 39 | * atom if necessary. 40 | * 41 | * @param {urlObject} parsedUri URI parsed with Node's url.parse(uri, true) 42 | */ 43 | handleURI (parsedUri) { 44 | const OPTS = { 45 | '/toggle': {}, 46 | '/toggle/preview': { preview: true } 47 | } 48 | const pathname = parsedUri.pathname.replace(/\/$/, '') 49 | if (Object.prototype.hasOwnProperty.call(OPTS, pathname)) { 50 | this.getNotesView().toggle(OPTS[pathname]) 51 | } else { 52 | const uris = Object.keys(OPTS).map(p => `atom://atom-notes${p}`) 53 | atom.notifications.addError(`Supported URIs: ${uris.join(', ')}`) 54 | } 55 | }, 56 | 57 | /** 58 | * Return serialized document storage index for faster package activation. 59 | * @return {SerializedIndex} 60 | */ 61 | serialize () { 62 | const rvalue = { 63 | deserializer: 'SearchIndex', 64 | data: null 65 | } 66 | if (this.store && this.doSerialize) { 67 | let data = null 68 | try { 69 | data = JSON.stringify(this.store.index) 70 | } catch (e) { 71 | console.error('atom-notes: serialization failure', e) 72 | } 73 | const maxSerializedDataLength = 133169152 74 | if (data && data.length < maxSerializedDataLength) { 75 | rvalue.data = data 76 | } else { 77 | this.doSerialize = false 78 | } 79 | } 80 | return rvalue 81 | }, 82 | 83 | /** 84 | * Returns deserialized search index. 85 | * @param {SerializedIndex} data 86 | * @return {Object} Search index if we could deserialize it, otherwise null. 87 | */ 88 | deserializeSearchIndex ({ data }) { 89 | try { 90 | const store = JSON.parse(data) 91 | 92 | // Older versions use DocQuery's lunr searchIndex v0.6.0. 93 | // We can not deserialize this properly for elasticlunr. 94 | if (Object.prototype.hasOwnProperty.call(store, 'corpusTokens')) return null 95 | 96 | return store 97 | } catch (_) { 98 | return null 99 | } 100 | }, 101 | 102 | async deactivate () { 103 | __guard__(this.subs, x => x.dispose()) 104 | this.subs = null 105 | }, 106 | 107 | /** 108 | * Returns our package's NotesView controller. 109 | * @return {NotesView} 110 | */ 111 | getNotesView () { 112 | if (!_notesView) { 113 | const { Disposable } = require('atom') 114 | const NotesView = require('./notes-view') 115 | 116 | _notesView = new NotesView(this.storePromise) 117 | this.subs.add(new Disposable(() => { 118 | _notesView.destroy() 119 | _notesView = null 120 | })) 121 | } 122 | 123 | return _notesView 124 | }, 125 | 126 | /** 127 | * Returns true iff the given file path is a note. 128 | * @param {String} filePath Any valid file path. 129 | * @return {Boolean} 130 | */ 131 | isNote (filePath) { 132 | const NotesFs = require('./notes-fs') 133 | return NotesFs.isNote(filePath) 134 | } 135 | } 136 | 137 | let _notesView 138 | 139 | /** Begin loading our document store in the background when event queue is empty. 140 | * 141 | * Note: We will signal that our module is ready when we set this.ready to true. 142 | */ 143 | 144 | /** 145 | * Begin loading our document store in the background when event queue is empty. 146 | * Note: We will signal that our module is ready when we set this.ready to true. 147 | * @param {Object} self Alias for this. 148 | * @param {SerializedIndex} [state=null] 149 | */ 150 | function makeReady (self, state = null) { 151 | setTimeout(() => { 152 | if (!ensureNotesDirectory()) { 153 | // If we don't have a suitable notes directory, we can't finish activation. 154 | __guard__(atom.packages.getActivePackage('atom-notes'), x => x.deactivate()) 155 | self.ready = false 156 | return 157 | } 158 | 159 | const t0 = performance.now() 160 | self.storePromise = new Promise(function (resolve, reject) { 161 | const NotesStore = require('./notes-store') 162 | const directoryPath = NotesFs.getNotesDirectory() 163 | const extensions = atom.config.get('atom-notes.extensions') 164 | const index = state ? atom.deserializers.deserialize(state) : null 165 | const store = new NotesStore(directoryPath, extensions, index) 166 | store.on('ready', () => { 167 | self.ready = true 168 | store.loaded = true 169 | const td = Math.round(performance.now() - t0) 170 | console.log(`atom-notes: ready in ${td}ms`) 171 | }) 172 | store.initialize() 173 | resolve(store) 174 | }) 175 | self.storePromise.then(store => (self.store = store)) 176 | 177 | handleEvents(self) 178 | }, 0) 179 | } 180 | 181 | /** 182 | * Install event handlers. 183 | * @param {Object} self Alias for this. 184 | */ 185 | function handleEvents (self) { 186 | const { openInterlink } = require('./interlink') 187 | const { CompositeDisposable, Disposable } = require('atom') 188 | self.subs = new CompositeDisposable() 189 | 190 | // user commands 191 | self.subs.add( 192 | atom.commands.add('atom-workspace', 'atom-notes:toggle', () => { 193 | self.getNotesView().toggle() 194 | }), 195 | atom.commands.add('atom-workspace', 'atom-notes:toggle-preview', () => { 196 | self.getNotesView().toggle({ preview: true }) 197 | }), 198 | atom.commands.add('atom-workspace', 'atom-notes:interlink', () => { 199 | openInterlink() 200 | }) 201 | ) 202 | 203 | // window::beforeunload 204 | window.addEventListener('beforeunload', autosaveAll, true) 205 | self.subs.add(new Disposable(() => { 206 | window.removeEventListener('beforeunload', autosaveAll, true) 207 | })) 208 | 209 | // window::blur 210 | const handleBlur = event => { 211 | if (event.target === window) { 212 | autosaveAll() 213 | } else if ( 214 | event.target.matches('atom-text-editor:not([mini])') && 215 | !event.target.contains(event.relatedTarget) 216 | ) { 217 | autosave(event.target.getModel()) 218 | } 219 | } 220 | window.addEventListener('blur', handleBlur, true) 221 | self.subs.add(new Disposable(() => { 222 | window.removeEventListener('blur', handleBlur, true) 223 | })) 224 | 225 | // atom events 226 | self.subs.add( 227 | atom.workspace.onWillDestroyPaneItem(paneItem => { 228 | autodelete(paneItem.item).then(deleted => { 229 | if (!deleted) { 230 | autosave(paneItem.item) 231 | } 232 | }) 233 | }) 234 | ) 235 | } 236 | 237 | /** 238 | * Ensures the configured notes directory exists. 239 | */ 240 | function ensureNotesDirectory () { 241 | const notesDirectory = NotesFs.getNotesDirectory() 242 | const packagesDirectory = fs.normalize(path.join(process.env.ATOM_HOME, 'packages')) 243 | const defaultNotesDirectory = path.join(packagesDirectory, 'atom-notes', 'notebook') 244 | 245 | if (notesDirectory.startsWith(packagesDirectory)) { 246 | const msg = `Notes Directory ${notesDirectory} cannot reside within your atom packages directory.` 247 | atom.notifications.addError(msg, { dismissable: true }) 248 | return false 249 | } 250 | 251 | try { 252 | if (!fs.existsSync(notesDirectory)) { 253 | fs.makeTreeSync(notesDirectory) 254 | fs.copySync(defaultNotesDirectory, notesDirectory) 255 | } 256 | } catch (err) { 257 | atom.notifications.addError( 258 | `Failed to create notes directory ${notesDirectory}: ${err}`, 259 | { dismissable: true } 260 | ) 261 | return false 262 | } 263 | 264 | return true 265 | } 266 | 267 | /** 268 | * Automatically saves the the note found in the given pane item. 269 | * @param {TextEditor} paneItem 270 | */ 271 | function autosave (paneItem) { 272 | if (!atom.config.get('atom-notes.enableAutosave')) return 273 | if (!__guard__(paneItem, x => x)) return 274 | const uri = __guard__(paneItem.getURI, f => f.call(paneItem)) 275 | if (!uri) return 276 | const modified = __guard__(paneItem.isModified, f => f.call(paneItem)) 277 | if (!modified) return 278 | if (!NotesFs.isNote(uri)) return 279 | __guard__(paneItem.save, f => f.call(paneItem)) 280 | } 281 | 282 | /** 283 | * Automatically deletes empty notes found in the given pane item. 284 | * @param {TextEditor} paneItem 285 | */ 286 | async function autodelete (paneItem) { 287 | if (!__guard__(paneItem, x => x)) return false 288 | const filePath = __guard__(paneItem.getURI, f => f.call(paneItem)) 289 | if (!filePath) return false 290 | if (!NotesFs.isNote(filePath)) return false 291 | const empty = __guard__(paneItem.isEmpty, f => f.call(paneItem)) 292 | if (!empty) return false 293 | const noteTitle = filePath.substr(filePath.lastIndexOf('/') + 1) 294 | atom.notifications.addInfo(`Deleting empty note "${noteTitle}"...`, { 295 | dismissable: true 296 | }) 297 | 298 | return new Promise(resolve => { 299 | setTimeout(() => { 300 | try { 301 | fs.unlinkSync(filePath) 302 | resolve(true) 303 | } catch (e) { 304 | if (e.code === 'ENOENT') { 305 | // The path already doesn't exist, this is fine. 306 | resolve(true) 307 | } else { 308 | atom.notifications.addError(`Failed to delete empty note "${noteTitle}"`, { 309 | detail: e.message, 310 | dismissable: true 311 | }) 312 | resolve(false) 313 | } 314 | } 315 | }) 316 | }) 317 | } 318 | 319 | /** 320 | * Go through text editors and save all notes. 321 | */ 322 | function autosaveAll () { 323 | if (!atom.config.get('atom-notes.enableAutosave')) return 324 | __guard__(atom.workspace.getPaneItems(), items => { 325 | items.forEach(i => autosave(i)) 326 | }) 327 | } 328 | 329 | function __guard__ (value, transform) { 330 | return (typeof value !== 'undefined' && value !== null) 331 | ? transform(value) 332 | : undefined 333 | } 334 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | 3 | module.exports = 4 | config: 5 | directory: 6 | order: 1 7 | title: 'Notes Directory' 8 | description: 'The directory to archive notes.' 9 | type: 'string' 10 | default: path.join(process.env.ATOM_HOME, 'atom-notes') 11 | ignorePaths: 12 | order: 2 13 | title: 'Ingore Paths' 14 | description: 'Ignore any files or directories with these names.' 15 | type: 'array' 16 | default: ['.git', '.DS_Store'] 17 | items: { type: 'string' } 18 | extensions: 19 | order: 3 20 | title: 'Extensions' 21 | description: 'The first extension will be used for newly created notes.' 22 | type: 'array' 23 | default: ['.md', '.markdown', '.adoc', '.txt'] 24 | items: { type: 'string' } 25 | enableAutosave: 26 | order: 4 27 | title: 'Enable Autosave' 28 | description: ''' 29 | Enable saving the document automatically whenever the user leaves the 30 | window or change the tab. 31 | ''' 32 | type: 'boolean' 33 | default: true 34 | useLunrPipeline: 35 | order: 5 36 | title: 'Use Lunr Pipeline' 37 | description: ''' 38 | Lunr pipeline preprocesses query to make searching faster. However, 39 | it will skip searching common stop words such as "an" or "be". 40 | ''' 41 | type: 'boolean', 42 | default: true 43 | orderByFuzzyFileNameMatch: 44 | order: 6 45 | title: 'Order by Fuzzy File Name Match' 46 | description: ''' 47 | After search results are found from the document index, use 48 | fuzzaldrin-plus to order them. 49 | ''' 50 | type: 'boolean' 51 | default: false 52 | -------------------------------------------------------------------------------- /lib/interlink.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import * as NotesFs from './notes-fs' 4 | 5 | /** 6 | * Opens the interlink under the user's current cursor position in the active editor. 7 | * @return {Promise} The `workspace.open()` if there is an interlink, otherwise undefined. 8 | */ 9 | export function openInterlink () { 10 | const editor = atom.workspace.getActiveTextEditor() 11 | // We used to only support interlinks between notes files, but that seems needlessly restrictive. 12 | // if (!__guard__(editor, x => NotesFs.isNote(editor.getPath()))) return undefined 13 | 14 | const noteTitle = getInterlinkTitle(editor) 15 | if (!__guard__(noteTitle, x => noteTitle.length > 0)) return undefined 16 | 17 | const notePath = NotesFs.notePathForTitle(noteTitle) 18 | if (!__guard__(notePath, x => x)) return undefined 19 | 20 | try { 21 | const fs = require('fs-plus') 22 | if (!fs.existsSync(notePath)) { 23 | fs.writeFileSync(notePath, '') 24 | } 25 | return atom.workspace.open(notePath) 26 | } catch (e) { 27 | atom.notifications.addError(`Failed to create new note "${noteTitle}"`, { 28 | detail: e.message, 29 | dismissable: true 30 | }) 31 | return undefined 32 | } 33 | } 34 | 35 | /** 36 | * Gets the title of the interlink under the user's current cursor position. 37 | * @param {TextEditor} editor Within the given editor. 38 | * @return {String} The title of the interlink sans wrapping braces. 39 | */ 40 | export function getInterlinkTitle (editor) { 41 | if (!__guard__(editor, x => x)) return null 42 | 43 | const pos = editor.getCursorBufferPosition() 44 | 45 | let lhs 46 | editor.buffer.backwardsScanInRange(/\[\[[^[]*/, [pos, 0], (match) => { 47 | lhs = match.range.start.column 48 | match.stop() 49 | }) 50 | if (lhs === undefined) return null 51 | 52 | let rhs 53 | const rowRange = editor.buffer.rangeForRow(pos.row) 54 | editor.buffer.scanInRange(/[^]]*]]/, [pos, rowRange.end], 55 | (match) => { 56 | rhs = match.range.end.column 57 | match.stop() 58 | }) 59 | if (rhs === undefined) return null 60 | 61 | let title 62 | editor.buffer.scanInRange(/\[\[[^|]+?]]/, [[pos.row, lhs], [pos.row, rhs]], 63 | (match) => { 64 | title = match.matchText.replace(/^\[*/, '').replace(/]*$/, '').trim() 65 | }) 66 | if (title === undefined || title === '') return null 67 | 68 | return title 69 | } 70 | 71 | function __guard__ (value, transform) { 72 | return (typeof value !== 'undefined' && value !== null) 73 | ? transform(value) 74 | : undefined 75 | } 76 | -------------------------------------------------------------------------------- /lib/notes-fs.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import path from 'path' 5 | 6 | /** Default file extension for notes. */ 7 | const defaultNoteExtension = '.md' 8 | 9 | export default { 10 | /** 11 | * Get the root directory for notes archival. 12 | * @return {String} Normalized path. 13 | */ 14 | getNotesDirectory () { 15 | return fs.normalize(atom.config.get('atom-notes.directory')) 16 | }, 17 | 18 | /** 19 | * Returns the default file extension for newly created notes. 20 | * @return {String} For example ".md". 21 | */ 22 | getPrimaryNoteExtension () { 23 | const extensions = atom.config.get('atom-notes.extensions') 24 | if (extensions.length > 0) return extensions[0] 25 | return defaultNoteExtension 26 | }, 27 | 28 | /** 29 | * Returns the intended path on your filesystem for a note with the given title. 30 | * @param {String} title The note's title sans file-extension. 31 | * @return {String} Full normalized path within your notes directory. 32 | */ 33 | notePathForTitle (title) { 34 | if (!__guard__(title, x => x)) return null 35 | return path.join( 36 | this.getNotesDirectory(), 37 | title.trim() + this.getPrimaryNoteExtension() 38 | ) 39 | }, 40 | 41 | /** 42 | * Opens a note for the given title in Atom; creates one if it doesn't exist already. 43 | * @param {String} title The note's title sans file-extension. 44 | */ 45 | openNote (title) { 46 | const destination = this.notePathForTitle(title) 47 | if (!__guard__(title, x => x)) return 48 | try { 49 | if (!fs.existsSync(destination)) { 50 | fs.writeFileSync(destination, '') 51 | } 52 | atom.workspace.open(destination) 53 | } catch (e) { 54 | atom.notifications.addError(`Failed to open note "${title}"`, { 55 | detail: e.message, 56 | dismissable: true 57 | }) 58 | } 59 | }, 60 | 61 | /** 62 | * Returns true iff the given file path is a note. 63 | * @param {String} filePath Any valid file path. 64 | * @return {Boolean} 65 | */ 66 | isNote (filePath) { 67 | if (!filePath) return false 68 | const normalPath = fs.normalize(filePath) 69 | // if (!fs.existsSync(normalPath)) return false // NOTE: Not necessary! 70 | 71 | const ignored = atom.config.get('atom-notes.ignorePaths') || [] 72 | if (normalPath.split(path.sep).some(part => ignored.includes(part))) return false 73 | 74 | const extensions = atom.config.get('atom-notes.extensions') 75 | const regex = RegExp(extensions.join('|'), 'i') 76 | const ext = path.extname(filePath.toString()) 77 | if (!regex.test(ext)) return false 78 | 79 | const notesDirectory = this.getNotesDirectory() 80 | if (normalPath.startsWith(notesDirectory)) return true 81 | 82 | // support symlinks 83 | try { 84 | const realNotesDirectory = fs.realpathSync(notesDirectory) 85 | if (normalPath.startsWith(realNotesDirectory)) return true 86 | 87 | const syncPath = fs.realpathSync(normalPath) 88 | if (syncPath.startsWith(notesDirectory)) return true 89 | if (syncPath.startsWith(realNotesDirectory)) return true 90 | } catch (e) { 91 | if (e.code === 'ENOENT') return false 92 | throw e 93 | } 94 | 95 | return false 96 | } 97 | } 98 | 99 | function __guard__ (value, transform) { 100 | return (typeof value !== 'undefined' && value !== null) 101 | ? transform(value) 102 | : undefined 103 | } 104 | -------------------------------------------------------------------------------- /lib/notes-store.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import elasticlunr from 'elasticlunr' 4 | import fs from 'fs-plus' 5 | import path from 'path' 6 | import matter from 'gray-matter' 7 | import * as NotesFs from './notes-fs' 8 | 9 | const { EventEmitter } = require('events') 10 | const { watchPath } = require('atom') 11 | 12 | function isEmpty (obj) { 13 | return Object.keys(obj).length === 0 && obj.constructor === Object 14 | } 15 | 16 | /** 17 | * @typedef DocumentItem 18 | * @type {Object} 19 | * @property {string} title - Title of the note. 20 | * @property {string} fileName - Name of the file where note is stored. 21 | * @property {string} filePath - Path to the file where note is stored. 22 | * @property {string} body - Body of the note. 23 | * @property {Array} keywords - Keywords of the note. 24 | * @property {Array} abstract - Summary of the note. 25 | * @property {Date} modifiedAt - Date and time of last modification to the note. 26 | */ 27 | 28 | /** 29 | * Builds a document object to hold state about a note document. 30 | * @param {String} filePath Full path to the document. 31 | * @return {DocumentItem} 32 | */ 33 | function createDocument (filePath) { 34 | if (!fs.existsSync(filePath)) { return false } 35 | 36 | // we can't just check isFile() b/c we want to support symlinks to files 37 | const fileStats = fs.statSync(filePath) 38 | if (fileStats.isDirectory() || 39 | fileStats.isBlockDevice() || 40 | fileStats.isCharacterDevice() || 41 | fileStats.isFIFO() || 42 | fileStats.isSocket()) { return false } 43 | 44 | const fileName = path.basename(filePath) 45 | const title = path.basename(fileName, path.extname(fileName)) 46 | let keywords = [] 47 | let abstract = null 48 | 49 | let body 50 | try { 51 | body = fs.readFileSync(filePath, { encoding: 'utf8' }) 52 | } catch (_) { 53 | // if we fail to read the file for whatever reason, let's ignore the file 54 | return false 55 | } 56 | 57 | let meta 58 | try { 59 | meta = matter(body).data 60 | } catch (_) { 61 | // If YAML parsing fails, that's fine. We just won't have meta data. 62 | } 63 | 64 | if (meta !== undefined && meta.keywords !== undefined) { 65 | keywords = meta.keywords 66 | } 67 | 68 | if (meta !== undefined && meta.abstract !== undefined) { 69 | abstract = meta.abstract 70 | } 71 | 72 | try { 73 | return { 74 | filePath: filePath, 75 | fileName: fileName, 76 | title: title, 77 | modifiedAt: fileStats.mtime, 78 | body: body, 79 | keywords: keywords, 80 | abstract: abstract 81 | } 82 | } catch (_) { 83 | return {} 84 | } 85 | } 86 | 87 | async function setupFileWatcher (directoryPath) { 88 | const watchedDirectory = fs.normalize(directoryPath) 89 | fs.listTreeSync(watchedDirectory).forEach(path => { 90 | if (!fs.isDirectorySync(path)) { 91 | if (NotesFs.isNote(path)) { 92 | this.addDocument(createDocument(path)) 93 | } 94 | } 95 | }) 96 | return watchPath(watchedDirectory, {}, events => { 97 | for (const event of events) { 98 | if (!NotesFs.isNote(event.path)) continue 99 | 100 | switch (event.action) { 101 | case 'created': 102 | this.addDocument(createDocument(event.path)) 103 | break 104 | case 'modified': 105 | this.updateDocument(createDocument(event.path)) 106 | break 107 | case 'deleted': 108 | this.removeDocument(this._documents[event.path]) 109 | break 110 | case 'renamed': 111 | this.removeDocument(this._documents[event.oldPath]) 112 | this.addDocument(createDocument(event.path)) 113 | break 114 | default: 115 | console.info('atom-notes: unhandled document event', event) 116 | } 117 | } 118 | }) 119 | } 120 | 121 | class NotesStore extends EventEmitter { 122 | /** 123 | * Document storage for notes. 124 | * @param {String} directoryPath Full path to where notes are stored. 125 | * @param {String[]} extensions File extensions of notes. 126 | * @param {Object} [index=null] Serialized elasticlunr index to load. 127 | */ 128 | constructor (directoryPath, extensions, index = null) { 129 | super() 130 | this.directoryPath = directoryPath 131 | this.extensions = extensions 132 | this.index = index 133 | this._documents = {} 134 | } 135 | 136 | /** 137 | * Initialize document storage by creating the document index. 138 | * Emits "ready" when index is ready. 139 | */ 140 | initialize () { 141 | if (this.index) { 142 | console.log('atom-notes: loading...') 143 | this.loaded = true 144 | this.index = elasticlunr.Index.load(this.index) 145 | } else { 146 | console.log('atom-notes: indexing...') 147 | this.loaded = false 148 | this.index = elasticlunr(function () { 149 | this.addField('title') 150 | this.addField('body') 151 | this.addField('keywords') 152 | this.addField('abstract') 153 | }) 154 | } 155 | 156 | this.watcher = setupFileWatcher.bind(this)(this.directoryPath) 157 | this.emit('ready') 158 | } 159 | 160 | /** 161 | * Adds the given document to our index. 162 | * @param {DocumentItem} doc 163 | */ 164 | addDocument (doc) { 165 | if (!doc || isEmpty(doc)) return 166 | try { 167 | const key = fs.realpathSync(doc.filePath) 168 | this._documents[key] = doc 169 | const data = { 170 | id: key, 171 | title: doc.title, 172 | body: doc.body, 173 | abstract: doc.abstract 174 | } 175 | if (doc.keywords && Array.isArray(doc.keywords)) { 176 | data.keywords = doc.keywords.join(', ') 177 | } 178 | this.index.addDoc(data) 179 | this.emit('added', doc) 180 | } catch (e) { 181 | console.error('atom-notes: add document failure', e) 182 | } 183 | } 184 | 185 | /** 186 | * Updates the given document, identified by filePath, in our index. 187 | * @param {DocumentItem} doc 188 | */ 189 | updateDocument (doc) { 190 | if (!doc || isEmpty(doc)) return 191 | try { 192 | const key = fs.realpathSync(doc.filePath) 193 | this._documents[key] = doc 194 | const data = { 195 | id: key, 196 | title: doc.title, 197 | body: doc.body, 198 | abstract: doc.abstract 199 | } 200 | if (doc.keywords && Array.isArray(doc.keywords)) { 201 | data.keywords = doc.keywords.join(', ') 202 | } 203 | this.index.updateDoc(data) 204 | this.emit('updated', doc) 205 | } catch (e) { 206 | console.error('atom-notes: update document failure', e) 207 | } 208 | } 209 | 210 | /** 211 | * Removes the given document, identified by filePath, from our index. 212 | * @param {DocumentItem} doc 213 | */ 214 | removeDocument (doc) { 215 | if (!doc || isEmpty(doc)) return false 216 | try { 217 | const key = doc.filePath 218 | delete this._documents[key] 219 | const data = { 220 | id: key, 221 | title: doc.title, 222 | body: doc.body, 223 | abstract: doc.abstract 224 | } 225 | if (doc.keywords && Array.isArray(doc.keywords)) { 226 | data.keywords = doc.keywords.join(', ') 227 | } 228 | this.index.removeDoc(data) 229 | this.emit('removed', doc) 230 | } catch (e) { 231 | console.error('atom-notes: remove document failure', e) 232 | } 233 | } 234 | 235 | /** 236 | * Search for notes in our document store. 237 | * @param {String} query Some text to use for matching indexed documents. 238 | * @return {DocumentItem[]} 239 | */ 240 | search (query) { 241 | const config = { 242 | fields: { 243 | keywords: { boost: 20, bool: 'AND' }, 244 | title: { boost: 10, bool: 'AND' }, 245 | abstract: { boost: 5, bool: 'AND' }, 246 | body: { boost: 1 } 247 | }, 248 | bool: 'OR', 249 | expand: true 250 | } 251 | return this.index.search(query, config).map(result => { 252 | return this._documents[result.ref] 253 | }) 254 | } 255 | 256 | /** 257 | * Fetch all documents in our index. 258 | * @return {DocumentItem[]} 259 | */ 260 | get documents () { 261 | var documents = [] 262 | for (const key in this._documents) { 263 | documents.push(this._documents[key]) 264 | } 265 | return documents.sort((a, b) => { 266 | if (a.modifiedAt < b.modifiedAt) return 1 267 | if (a.modifiedAt > b.modifiedAt) return -1 268 | return 0 269 | }) 270 | } 271 | } 272 | 273 | module.exports = NotesStore 274 | -------------------------------------------------------------------------------- /lib/notes-view-list.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import * as fp from 'fuzzaldrin-plus' 4 | import * as TimSort from 'timsort' 5 | import path from 'path' 6 | 7 | import * as NotesFs from './notes-fs' 8 | import SelectListView from './select-list-view' 9 | 10 | import matter from 'gray-matter' 11 | 12 | let autocompleteTimeout 13 | 14 | /** 15 | * @typedef DocumentItem 16 | * @type {Object} 17 | * @property {string} title - Title of the note. 18 | * @property {string} fileName - Name of the file where note is stored. 19 | * @property {string} filePath - Path to the file where note is stored. 20 | * @property {string} body - Body of the note. 21 | * @property {Date} modifiedAt - Date and time of last modification to the note. 22 | */ 23 | 24 | /** 25 | * @typedef Options 26 | * @type {Object} 27 | * @property {function()} [didHide] - Callback to call whenever this view is hidden. 28 | * @property {Boolean} [preview] - Set to true if document should open in markdown preview 29 | */ 30 | 31 | function getMarkdownPreviewUriPrefix () { 32 | if (atom.packages.getActivePackage('markdown-preview-plus')) return 'markdown-preview-plus://file' 33 | if (atom.packages.getActivePackage('markdown-preview')) return 'markdown-preview://' 34 | return null 35 | } 36 | 37 | export default class NotesViewList { 38 | /** Builds a new notes query and list interface element. 39 | * 40 | * @param {Promise.} storePromise - The document storage for notes. 41 | * @param {Options} [options] - Behaviour and configuration settings. 42 | */ 43 | constructor (storePromise, options = {}) { 44 | storePromise.then(store => { 45 | this.store = store 46 | const reload = () => { 47 | if (!this.isLoaded()) return 48 | this.selectListView.update({ items: this.store.documents }) 49 | } 50 | const makeReady = () => { 51 | this.store.loaded = true 52 | this.selectListView.update({ loadingMessage: null }) 53 | reload() 54 | this.store.on('added', _ => reload()) 55 | this.store.on('updated', _ => reload()) 56 | this.store.on('removed', _ => reload()) 57 | } 58 | if (this.store.loaded) makeReady() 59 | else this.store.on('ready', () => makeReady()) 60 | }) 61 | if (Object.prototype.hasOwnProperty.call(options, 'didHide')) { 62 | this.didHide = options.didHide 63 | } 64 | this.filteredItems = [] 65 | this.selectListView = new SelectListView({ 66 | items: [], 67 | loadingMessage: 'Loading notes...', 68 | emptyMessage: 'No matching notes', 69 | initialSelectionIndex: undefined, 70 | elementForItem: item => this.elementForItem(item), 71 | filter: (items, query) => this.filter(items, query), 72 | filterQuery: query => this.filterQuery(query), 73 | didChangeQuery: query => this.didChangeQuery(query), 74 | didConfirmSelection: item => this.didConfirmSelection(item), 75 | didConfirmEmptySelection: () => this.didConfirmEmptySelection(), 76 | didCancelSelection: () => this.didCancelSelection() 77 | }) 78 | this.selectListView.element.classList.add('atom-notes') 79 | } 80 | 81 | /** 82 | * Returns true iff the document storage has finished loading notes. 83 | * @return {Boolean} 84 | */ 85 | isLoaded () { 86 | return this.store !== undefined && this.store.loaded 87 | } 88 | 89 | /** 90 | * Returns HTMLElement object that should represent a single document item in this view. 91 | * @param {DocumentItem} item The document item to display in our list. 92 | * @return {HTMLElement} 93 | */ 94 | elementForItem (item) { 95 | const query = this.selectListView.getFilterQuery() 96 | const matches = fp.match(item.title, query) 97 | 98 | const primary = document.createElement('div') 99 | primary.classList.add('primary-line') 100 | primary.appendChild(highlight(item.title, matches)) 101 | 102 | const metadata = document.createElement('div') 103 | metadata.classList.add('metadata') 104 | metadata.textContent = item.modifiedAt.toLocaleDateString() 105 | primary.appendChild(metadata) 106 | 107 | const secondary = document.createElement('div') 108 | secondary.classList.add('secondary-line') 109 | 110 | if (item.abstract != null) { 111 | secondary.textContent = item.abstract.slice(0, 100) 112 | } else { 113 | secondary.textContent = matter(item.body).content.slice(0, 100) 114 | } 115 | 116 | if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { 117 | const keywords = document.createElement('div') 118 | keywords.classList.add('keywords') 119 | keywords.innerHTML = '' + 120 | item.keywords.join('') + 121 | '' 122 | secondary.appendChild(keywords) 123 | } 124 | 125 | const element = document.createElement('li') 126 | element.classList.add('two-lines') 127 | element.appendChild(primary) 128 | element.appendChild(secondary) 129 | return element 130 | } 131 | 132 | /** 133 | * Renders completion for query; if not supplied, re-render existing completion. 134 | * @param {String} [query=undefined] The current search query to build autocomplete off of. 135 | */ 136 | autocomplete (query = undefined) { 137 | if (!this.lastAutocompleteQuery) this.lastAutocompleteQuery = '' 138 | if (!this.isLoaded()) { 139 | this.lastAutocompleteQuery = '' 140 | return 141 | } 142 | __guard__(autocompleteTimeout, x => { 143 | clearTimeout(x) 144 | autocompleteTimeout = null 145 | }) 146 | if (query) { 147 | this.filteredItems = fp.filter(this.store.documents, query, { key: 'title' }) 148 | } else { 149 | this.lastAutocompleteQuery = '' 150 | this.selectListView.refs.queryEditor.selectAll() 151 | return 152 | } 153 | if (this.filteredItems.length <= 0) { 154 | this.lastAutocompleteQuery = '' 155 | return 156 | } 157 | autocompleteTimeout = setTimeout(() => { 158 | if (this.filteredItems.length <= 0) { 159 | this.lastAutocompleteQuery = '' 160 | return 161 | } 162 | const best = this.filteredItems[0] 163 | const q = this.selectListView.getFilterQuery() 164 | const complete = (q !== undefined && 165 | q.length > 0 && 166 | best.title.toLowerCase().startsWith(q.toLowerCase())) 167 | if (complete) { 168 | const newQuery = q + best.title.slice(q.length) 169 | this.selectListView.update({ query: newQuery, doDidChangeQuery: false }) 170 | this.selectListView.refs.queryEditor.selectLeft(newQuery.length - q.length) 171 | this.lastAutocompleteQuery = q 172 | } else { 173 | this.lastAutocompleteQuery = '' 174 | } 175 | }, 300) 176 | } 177 | 178 | /** 179 | * Our override for our SelectListView's filter() callback. 180 | * @param {String} query The users current search query. 181 | * @return {DocumentItem[]} 182 | */ 183 | filter (_, query) { 184 | let items = [] 185 | if (!this.isLoaded()) return items 186 | if (query === undefined || query.length <= 0) { 187 | items = this.store.documents 188 | TimSort.sort(items, (lhs, rhs) => { 189 | if (lhs.modifiedAt > rhs.modifiedAt) return -1 190 | else if (lhs.modifiedAt < rhs.modifiedAt) return 1 191 | return 0 192 | }) 193 | } else { 194 | items = this.store.search(query).filter(item => item !== undefined) 195 | 196 | if (atom.config.get('atom-notes.orderByFuzzyFileNameMatch')) { 197 | TimSort.sort(items, (lhs, rhs) => { 198 | if (query.length > 0) { 199 | const li = fp.score(lhs.title, query) 200 | const ri = fp.score(rhs.title, query) 201 | if (li > ri) return -1 202 | else if (li < ri) return 1 203 | return 0 204 | } 205 | }) 206 | } 207 | } 208 | // docsearch returned nothing, fallback to fuzzy finder 209 | if (items.length <= 0) { 210 | return this.filteredItems 211 | } 212 | return items 213 | } 214 | 215 | /** 216 | * Our override for our SelectListView's filterQuery() callback. 217 | * @param {String} query The users current search query. 218 | * @return {String} Sanitized version of user's query. 219 | */ 220 | filterQuery (query) { 221 | return __guard__(query, x => { 222 | const t = x.trim() 223 | return x.length > 0 && x.endsWith(' ') ? t + ' ' : t 224 | }) 225 | } 226 | 227 | /** 228 | * Our override for our SelectListView's didChangeQuery() callback. 229 | * @param {String} query The users current search query. 230 | */ 231 | didChangeQuery (query) { 232 | if (query.toLowerCase() !== this.lastAutocompleteQuery.toLowerCase()) { 233 | this.autocomplete(query) 234 | } 235 | } 236 | 237 | /** 238 | * Our override for our SelectListView's didConfirmSelection() callback. 239 | * @param {DocumentItem} item The user's selected document item. 240 | */ 241 | didConfirmSelection (item) { 242 | this.didHide() 243 | const markdownPreviewUriPrefix = getMarkdownPreviewUriPrefix() 244 | const ext = path.extname(item.filePath) 245 | const isMarkdown = ['.markdown', '.md'].includes(ext) 246 | if (this.openInPreview && isMarkdown && markdownPreviewUriPrefix) { 247 | atom.workspace.open(`${markdownPreviewUriPrefix}${encodeURI(item.filePath)}`) 248 | } else { 249 | atom.workspace.open(item.filePath) 250 | } 251 | } 252 | 253 | /** 254 | * Our override for our SelectListView's didConfirmEmptySelection() callback. 255 | */ 256 | didConfirmEmptySelection () { 257 | const title = this.selectListView.getFilterQuery() 258 | if (title.length <= 0) return 259 | this.didHide() 260 | NotesFs.openNote(title) 261 | } 262 | 263 | /** 264 | * Our override for our SelectListView's didCancelSelection() callback. 265 | */ 266 | didCancelSelection () { 267 | __guard__(autocompleteTimeout, x => { 268 | clearTimeout(x) 269 | autocompleteTimeout = null 270 | }) 271 | 272 | // focus leaving atom-notes unselects any selected item and dismisses modal 273 | if (!this.selectListView.element.contains(document.activeElement)) { 274 | this.selectListView.selectNone() 275 | this.didHide() 276 | return 277 | } 278 | 279 | // cancel with a selected item unselects the item 280 | if (this.selectListView.getSelectedItem()) { 281 | this.selectListView.selectNone() 282 | return 283 | } 284 | 285 | // cancel with selected text unselects the text 286 | const selectedText = this.selectListView.refs.queryEditor.getSelectedText() 287 | if (selectedText !== '') { 288 | let query = this.selectListView.getQuery() 289 | query = query.slice(0, query.length - selectedText.length) 290 | this.selectListView.update({ query, doDidChangeQuery: false }) 291 | return 292 | } 293 | 294 | // cancel with no selected item and no selected text dismisses the modal 295 | __guard__(this.previousFocus, x => x.focus()) 296 | this.didHide() 297 | } 298 | 299 | destroy () { 300 | __guard__(autocompleteTimeout, x => { 301 | clearTimeout(x) 302 | autocompleteTimeout = null 303 | }) 304 | } 305 | } 306 | 307 | /** 308 | * Highlights the fuzzy-finder matching text in our search results. 309 | * @see {@link https://github.com/atom/fuzzy-finder/blob/master/lib/fuzzy-finder-view.js#L304} 310 | * @param {string} path The text to be highlighted. 311 | * @param {Number[]} matches Positions of matching characters. 312 | * @param {Number} [offsetIndex=0] Offset into path to begin highlighting. 313 | * @return {HTMLElement} Returns a span element with highlight annotations. 314 | */ 315 | function highlight (path, matches, offsetIndex = 0) { 316 | // guard against case where there's nothing to highlight 317 | if (!path || path.length <= 0) return document.createTextNode(path) 318 | if (!matches || matches.length <= 0) return document.createTextNode(path) 319 | 320 | let lastIndex = 0 321 | let matchedChars = [] 322 | const fragment = document.createDocumentFragment() 323 | for (let matchIndex of matches) { 324 | matchIndex -= offsetIndex 325 | // If marking up the basename, omit path matches 326 | if (matchIndex < 0) { 327 | continue 328 | } 329 | const unmatched = path.substring(lastIndex, matchIndex) 330 | if (unmatched) { 331 | if (matchedChars.length > 0) { 332 | const span = document.createElement('span') 333 | span.classList.add('character-match') 334 | span.textContent = matchedChars.join('') 335 | fragment.appendChild(span) 336 | matchedChars = [] 337 | } 338 | 339 | fragment.appendChild(document.createTextNode(unmatched)) 340 | } 341 | 342 | matchedChars.push(path[matchIndex]) 343 | lastIndex = matchIndex + 1 344 | } 345 | 346 | if (matchedChars.length > 0) { 347 | const span = document.createElement('span') 348 | span.classList.add('character-match') 349 | span.textContent = matchedChars.join('') 350 | fragment.appendChild(span) 351 | } 352 | 353 | // Remaining characters are plain text 354 | fragment.appendChild(document.createTextNode(path.substring(lastIndex))) 355 | return fragment 356 | } 357 | 358 | function __guard__ (value, transform) { 359 | return (typeof value !== 'undefined' && value !== null) 360 | ? transform(value) 361 | : undefined 362 | } 363 | -------------------------------------------------------------------------------- /lib/notes-view.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import NotesViewList from './notes-view-list' 4 | 5 | export default class NotesView { 6 | /** 7 | * Provides querying for and a list of notes in our document store. 8 | * @param {Promise.} storePromise Document storage for notes. 9 | */ 10 | constructor (storePromise) { 11 | this.list = new NotesViewList(storePromise, { didHide: () => this.hide() }) 12 | this.panel = atom.workspace.addModalPanel({ 13 | item: this.list.selectListView.element, 14 | visible: false 15 | }) 16 | 17 | // Add a back-reference in the DOM to help with debugging. 18 | this.list.selectListView.element.notes = this 19 | } 20 | 21 | /** 22 | * Returns true iff the notes interface is visible to the user. 23 | * @return {Boolean} 24 | */ 25 | isVisible () { 26 | return this.panel.isVisible() 27 | } 28 | 29 | /** 30 | * Brings up the notes view so the user can see it. 31 | * @param {Object} options: 32 | * - preview {Boolean}: Open file in editor or preview iff true. 33 | */ 34 | show (options) { 35 | this.previousFocus = document.activeElement 36 | this.list.openInPreview = (options && options.preview === true) 37 | this.list.selectListView.selectNone() 38 | this.list.autocomplete() 39 | this.panel.show() 40 | this.list.selectListView.focus() 41 | } 42 | 43 | /** 44 | * Hides the notes view from the user's sight. 45 | */ 46 | hide () { 47 | this.panel.hide() 48 | } 49 | 50 | /** 51 | * Toggles between hidden and shown. 52 | * @param {Object} options: 53 | * - preview {Boolean}: Open file in editor or preview iff true. 54 | */ 55 | toggle (options = { preview: false }) { 56 | this.isVisible() ? this.hide() : this.show(options) 57 | } 58 | 59 | destroy () { 60 | __guard__(this.list, x => x.destroy()) 61 | this.list = null 62 | __guard__(this.panel, x => x.destroy()) 63 | this.panel = null 64 | } 65 | } 66 | 67 | function __guard__ (value, transform) { 68 | return (typeof value !== 'undefined' && value !== null) 69 | ? transform(value) 70 | : undefined 71 | } 72 | -------------------------------------------------------------------------------- /lib/select-list-view.js: -------------------------------------------------------------------------------- 1 | // NOTE: This is a copy of select-list-view.js from https://github.com/atom/atom-select-list/pull/27 2 | // (but with fuzzaldrin replaced by fuzzaldrin-plus). This is to support autocompletion; this 3 | // file can be replaced with a dependency on atom-select-list when the PR has been merged. 4 | 5 | const { Disposable, CompositeDisposable, TextEditor } = require('atom') 6 | const etch = require('etch') 7 | const $ = etch.dom 8 | const fuzzaldrin = require('fuzzaldrin-plus') 9 | 10 | module.exports = class SelectListView { 11 | static setScheduler (scheduler) { 12 | etch.setScheduler(scheduler) 13 | } 14 | 15 | static getScheduler (scheduler) { 16 | return etch.getScheduler() 17 | } 18 | 19 | constructor (props) { 20 | this.props = props 21 | if (!Object.prototype.hasOwnProperty.call(this.props, 'initialSelectionIndex')) { 22 | this.props.initialSelectionIndex = 0 23 | } 24 | if (props.initiallyVisibleItemCount) { 25 | this.initializeVisibilityObserver() 26 | } 27 | this.computeItems(false) 28 | this.disposables = new CompositeDisposable() 29 | etch.initialize(this) 30 | this.element.classList.add('select-list') 31 | this.didChangeQueryDisposable = this.refs.queryEditor.onDidChange(this.didChangeQuery.bind(this)) 32 | if (!props.skipCommandsRegistration) { 33 | this.disposables.add(this.registerAtomCommands()) 34 | } 35 | const editorElement = this.refs.queryEditor.element 36 | const didLoseFocus = this.didLoseFocus.bind(this) 37 | editorElement.addEventListener('blur', didLoseFocus) 38 | this.disposables.add(new Disposable(() => { editorElement.removeEventListener('blur', didLoseFocus) })) 39 | } 40 | 41 | initializeVisibilityObserver () { 42 | this.visibilityObserver = new IntersectionObserver(changes => { 43 | for (const change of changes) { 44 | if (change.intersectionRatio > 0) { 45 | const element = change.target 46 | this.visibilityObserver.unobserve(element) 47 | const index = Array.from(this.refs.items.children).indexOf(element) 48 | if (index >= 0) { 49 | this.renderItemAtIndex(index) 50 | } 51 | } 52 | } 53 | }) 54 | } 55 | 56 | focus () { 57 | this.refs.queryEditor.element.focus() 58 | } 59 | 60 | didLoseFocus (event) { 61 | if (this.element.contains(event.relatedTarget)) { 62 | this.refs.queryEditor.element.focus() 63 | } else if (document.hasFocus()) { 64 | this.cancelSelection() 65 | } 66 | } 67 | 68 | reset () { 69 | this.refs.queryEditor.setText('') 70 | } 71 | 72 | destroy () { 73 | this.disposables.dispose() 74 | this.didChangeQueryDisposable.dispose() 75 | if (this.visibilityObserver) this.visibilityObserver.disconnect() 76 | return etch.destroy(this) 77 | } 78 | 79 | registerAtomCommands () { 80 | return global.atom.commands.add(this.element, { 81 | 'core:move-up': (event) => { 82 | this.selectPrevious() 83 | event.stopPropagation() 84 | }, 85 | 'core:move-down': (event) => { 86 | this.selectNext() 87 | event.stopPropagation() 88 | }, 89 | 'core:move-to-top': (event) => { 90 | this.selectFirst() 91 | event.stopPropagation() 92 | }, 93 | 'core:move-to-bottom': (event) => { 94 | this.selectLast() 95 | event.stopPropagation() 96 | }, 97 | 'core:confirm': (event) => { 98 | this.confirmSelection() 99 | event.stopPropagation() 100 | }, 101 | 'core:cancel': (event) => { 102 | this.cancelSelection() 103 | event.stopPropagation() 104 | } 105 | }) 106 | } 107 | 108 | update (props = {}) { 109 | let shouldComputeItems = false 110 | 111 | if (Object.prototype.hasOwnProperty.call(props, 'items')) { 112 | this.props.items = props.items 113 | shouldComputeItems = true 114 | } 115 | 116 | if (Object.prototype.hasOwnProperty.call(props, 'maxResults')) { 117 | this.props.maxResults = props.maxResults 118 | shouldComputeItems = true 119 | } 120 | 121 | if (Object.prototype.hasOwnProperty.call(props, 'filter')) { 122 | this.props.filter = props.filter 123 | shouldComputeItems = true 124 | } 125 | 126 | if (Object.prototype.hasOwnProperty.call(props, 'filterQuery')) { 127 | this.props.filterQuery = props.filterQuery 128 | shouldComputeItems = true 129 | } 130 | 131 | if (Object.prototype.hasOwnProperty.call(props, 'query')) { 132 | const doDidChangeQuery = 133 | !Object.prototype.hasOwnProperty.call(props, 'doDidChangeQuery') || props.doDidChangeQuery 134 | if (!doDidChangeQuery) { 135 | this.didChangeQueryDisposable.dispose() 136 | } 137 | 138 | // Items will be recomputed as part of the change event handler, so we 139 | // don't need to recompute them again at the end of this function. 140 | this.refs.queryEditor.setText(props.query) 141 | shouldComputeItems = false 142 | 143 | if (!doDidChangeQuery) { 144 | this.didChangeQueryDisposable = this.refs.queryEditor.onDidChange(this.didChangeQuery.bind(this)) 145 | } 146 | } 147 | 148 | if (Object.prototype.hasOwnProperty.call(props, 'selectQuery')) { 149 | if (props.selectQuery) { 150 | this.refs.queryEditor.selectAll() 151 | } else { 152 | this.refs.queryEditor.clearSelections() 153 | } 154 | } 155 | 156 | if (Object.prototype.hasOwnProperty.call(props, 'order')) { 157 | this.props.order = props.order 158 | } 159 | 160 | if (Object.prototype.hasOwnProperty.call(props, 'emptyMessage')) { 161 | this.props.emptyMessage = props.emptyMessage 162 | } 163 | 164 | if (Object.prototype.hasOwnProperty.call(props, 'errorMessage')) { 165 | this.props.errorMessage = props.errorMessage 166 | } 167 | 168 | if (Object.prototype.hasOwnProperty.call(props, 'infoMessage')) { 169 | this.props.infoMessage = props.infoMessage 170 | } 171 | 172 | if (Object.prototype.hasOwnProperty.call(props, 'loadingMessage')) { 173 | this.props.loadingMessage = props.loadingMessage 174 | } 175 | 176 | if (Object.prototype.hasOwnProperty.call(props, 'loadingBadge')) { 177 | this.props.loadingBadge = props.loadingBadge 178 | } 179 | 180 | if (Object.prototype.hasOwnProperty.call(props, 'itemsClassList')) { 181 | this.props.itemsClassList = props.itemsClassList 182 | } 183 | 184 | if (Object.prototype.hasOwnProperty.call(props, 'initialSelectionIndex')) { 185 | this.props.initialSelectionIndex = props.initialSelectionIndex 186 | } 187 | 188 | if (shouldComputeItems) { 189 | this.computeItems() 190 | } 191 | 192 | return etch.update(this) 193 | } 194 | 195 | render () { 196 | return $.div( 197 | {}, 198 | $(TextEditor, { ref: 'queryEditor', mini: true }), 199 | this.renderLoadingMessage(), 200 | this.renderInfoMessage(), 201 | this.renderErrorMessage(), 202 | this.renderItems() 203 | ) 204 | } 205 | 206 | renderItems () { 207 | if (this.items.length > 0) { 208 | const className = ['list-group'].concat(this.props.itemsClassList || []).join(' ') 209 | 210 | if (this.visibilityObserver) { 211 | etch.getScheduler().updateDocument(() => { 212 | Array.from(this.refs.items.children).slice(this.props.initiallyVisibleItemCount).forEach(element => { 213 | this.visibilityObserver.observe(element) 214 | }) 215 | }) 216 | } 217 | 218 | this.listItems = this.items.map((item, index) => { 219 | const selected = this.getSelectedItem() === item 220 | const visible = !this.props.initiallyVisibleItemCount || index < this.props.initiallyVisibleItemCount 221 | return $(ListItemView, { 222 | element: this.props.elementForItem(item, { selected, index, visible }), 223 | selected: selected, 224 | onclick: () => this.didClickItem(index) 225 | }) 226 | }) 227 | 228 | return $.ol( 229 | { className, ref: 'items' }, 230 | ...this.listItems 231 | ) 232 | } else if (!this.props.loadingMessage && this.props.emptyMessage) { 233 | return $.span({ ref: 'emptyMessage' }, this.props.emptyMessage) 234 | } else { 235 | return '' 236 | } 237 | } 238 | 239 | renderErrorMessage () { 240 | if (this.props.errorMessage) { 241 | return $.span({ ref: 'errorMessage' }, this.props.errorMessage) 242 | } else { 243 | return '' 244 | } 245 | } 246 | 247 | renderInfoMessage () { 248 | if (this.props.infoMessage) { 249 | return $.span({ ref: 'infoMessage' }, this.props.infoMessage) 250 | } else { 251 | return '' 252 | } 253 | } 254 | 255 | renderLoadingMessage () { 256 | if (this.props.loadingMessage) { 257 | return $.div( 258 | { className: 'loading' }, 259 | $.span({ ref: 'loadingMessage', className: 'loading-message' }, this.props.loadingMessage), 260 | this.props.loadingBadge ? $.span({ ref: 'loadingBadge', className: 'badge' }, this.props.loadingBadge) : '' 261 | ) 262 | } else { 263 | return '' 264 | } 265 | } 266 | 267 | getQuery () { 268 | if (this.refs && this.refs.queryEditor) { 269 | return this.refs.queryEditor.getText() 270 | } else { 271 | return '' 272 | } 273 | } 274 | 275 | getFilterQuery () { 276 | return this.props.filterQuery ? this.props.filterQuery(this.getQuery()) : this.getQuery() 277 | } 278 | 279 | didChangeQuery () { 280 | if (this.props.didChangeQuery) { 281 | this.props.didChangeQuery(this.getFilterQuery()) 282 | } 283 | 284 | this.computeItems() 285 | } 286 | 287 | didClickItem (itemIndex) { 288 | this.selectIndex(itemIndex) 289 | this.confirmSelection() 290 | } 291 | 292 | computeItems (updateComponent) { 293 | this.listItems = null 294 | if (this.visibilityObserver) this.visibilityObserver.disconnect() 295 | const filterFn = this.props.filter || this.fuzzyFilter.bind(this) 296 | this.items = filterFn(this.props.items.slice(), this.getFilterQuery()) 297 | if (this.props.order) { 298 | this.items.sort(this.props.order) 299 | } 300 | if (this.props.maxResults) { 301 | this.items = this.items.slice(0, this.props.maxResults) 302 | } 303 | 304 | this.selectIndex(this.props.initialSelectionIndex, updateComponent) 305 | } 306 | 307 | fuzzyFilter (items, query) { 308 | if (query.length === 0) { 309 | return items 310 | } else { 311 | const scoredItems = [] 312 | for (const item of items) { 313 | const string = this.props.filterKeyForItem ? this.props.filterKeyForItem(item) : item 314 | const score = fuzzaldrin.score(string, query) 315 | if (score > 0) { 316 | scoredItems.push({ item, score }) 317 | } 318 | } 319 | scoredItems.sort((a, b) => b.score - a.score) 320 | return scoredItems.map((i) => i.item) 321 | } 322 | } 323 | 324 | getSelectedItem () { 325 | if (this.selectionIndex === undefined) return null 326 | return this.items[this.selectionIndex] 327 | } 328 | 329 | renderItemAtIndex (index) { 330 | const item = this.items[index] 331 | const selected = this.getSelectedItem() === item 332 | const component = this.listItems[index].component 333 | if (this.visibilityObserver) this.visibilityObserver.unobserve(component.element) 334 | component.update({ 335 | element: this.props.elementForItem(item, { selected, index, visible: true }), 336 | selected: selected, 337 | onclick: () => this.didClickItem(index) 338 | }) 339 | } 340 | 341 | selectPrevious () { 342 | if (this.selectionIndex === undefined) return this.selectLast() 343 | return this.selectIndex(this.selectionIndex - 1) 344 | } 345 | 346 | selectNext () { 347 | if (this.selectionIndex === undefined) return this.selectFirst() 348 | return this.selectIndex(this.selectionIndex + 1) 349 | } 350 | 351 | selectFirst () { 352 | return this.selectIndex(0) 353 | } 354 | 355 | selectLast () { 356 | return this.selectIndex(this.items.length - 1) 357 | } 358 | 359 | selectNone () { 360 | return this.selectIndex(undefined) 361 | } 362 | 363 | selectIndex (index, updateComponent = true) { 364 | if (index >= this.items.length) { 365 | index = 0 366 | } else if (index < 0) { 367 | index = this.items.length - 1 368 | } 369 | 370 | const oldIndex = this.selectionIndex 371 | 372 | this.selectionIndex = index 373 | if (index !== undefined && this.props.didChangeSelection) { 374 | this.props.didChangeSelection(this.getSelectedItem()) 375 | } 376 | 377 | if (updateComponent) { 378 | if (this.listItems) { 379 | if (oldIndex >= 0) this.renderItemAtIndex(oldIndex) 380 | if (index >= 0) this.renderItemAtIndex(index) 381 | return etch.getScheduler().getNextUpdatePromise() 382 | } else { 383 | return etch.update(this) 384 | } 385 | } else { 386 | return Promise.resolve() 387 | } 388 | } 389 | 390 | selectItem (item) { 391 | const index = this.items.indexOf(item) 392 | if (index === -1) { 393 | throw new Error('Cannot select the specified item because it does not exist.') 394 | } else { 395 | return this.selectIndex(index) 396 | } 397 | } 398 | 399 | confirmSelection () { 400 | const selectedItem = this.getSelectedItem() 401 | if (selectedItem != null) { 402 | if (this.props.didConfirmSelection) { 403 | this.props.didConfirmSelection(selectedItem) 404 | } 405 | } else { 406 | if (this.props.didConfirmEmptySelection) { 407 | this.props.didConfirmEmptySelection() 408 | } 409 | } 410 | } 411 | 412 | cancelSelection () { 413 | if (this.props.didCancelSelection) { 414 | this.props.didCancelSelection() 415 | } 416 | } 417 | } 418 | 419 | class ListItemView { 420 | constructor (props) { 421 | this.mouseDown = this.mouseDown.bind(this) 422 | this.mouseUp = this.mouseUp.bind(this) 423 | this.didClick = this.didClick.bind(this) 424 | this.selected = props.selected 425 | this.onclick = props.onclick 426 | this.element = props.element 427 | this.element.addEventListener('mousedown', this.mouseDown) 428 | this.element.addEventListener('mouseup', this.mouseUp) 429 | this.element.addEventListener('click', this.didClick) 430 | if (this.selected) { 431 | this.element.classList.add('selected') 432 | } 433 | this.domEventsDisposable = new Disposable(() => { 434 | this.element.removeEventListener('mousedown', this.mouseDown) 435 | this.element.removeEventListener('mouseup', this.mouseUp) 436 | this.element.removeEventListener('click', this.didClick) 437 | }) 438 | etch.getScheduler().updateDocument(this.scrollIntoViewIfNeeded.bind(this)) 439 | } 440 | 441 | mouseDown (event) { 442 | event.preventDefault() 443 | } 444 | 445 | mouseUp (event) { 446 | event.preventDefault() 447 | } 448 | 449 | didClick (event) { 450 | event.preventDefault() 451 | this.onclick() 452 | } 453 | 454 | destroy () { 455 | this.element.remove() 456 | this.domEventsDisposable.dispose() 457 | } 458 | 459 | update (props) { 460 | this.element.removeEventListener('mousedown', this.mouseDown) 461 | this.element.removeEventListener('mouseup', this.mouseUp) 462 | this.element.removeEventListener('click', this.didClick) 463 | 464 | this.element.parentNode.replaceChild(props.element, this.element) 465 | this.element = props.element 466 | this.element.addEventListener('mousedown', this.mouseDown) 467 | this.element.addEventListener('mouseup', this.mouseUp) 468 | this.element.addEventListener('click', this.didClick) 469 | if (props.selected) { 470 | this.element.classList.add('selected') 471 | } 472 | 473 | this.selected = props.selected 474 | this.onclick = props.onclick 475 | etch.getScheduler().updateDocument(this.scrollIntoViewIfNeeded.bind(this)) 476 | } 477 | 478 | scrollIntoViewIfNeeded () { 479 | if (this.selected) { 480 | this.element.scrollIntoViewIfNeeded(false) 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /menus/atom-notes.cson: -------------------------------------------------------------------------------- 1 | menu: [ 2 | { 3 | label: 'Packages' 4 | submenu: [ 5 | label: 'Atom Notes' 6 | submenu: [ 7 | { 8 | label: 'Toggle' 9 | command: 'atom-notes:toggle' 10 | }, 11 | { 12 | label: 'Toggle Preview' 13 | command: 'atom-notes:toggle-preview' 14 | }, 15 | { 16 | label: 'Open Interlink' 17 | command: 'atom-notes:interlink' 18 | } 19 | ] 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /notebook/How to use.md: -------------------------------------------------------------------------------- 1 | # 🤔 How to use Atom Notes 2 | 3 | Begin typing the query in the search panel. Press Enter to create a 4 | new note with your query as its title. This package searches for notes whose 5 | body or title contain your query. Naming a new note and searching for existing 6 | notes intentionally occur simultaneously! 7 | 8 | While typing a query, Atom Notes automatically filters the presented list by 9 | relevance and also autocompletes note titles by prefix. When an autocompletion 10 | is showing, press Enter to open the note. 11 | 12 | > **Tip:** To _unselect_ a note from your list or to clear an autocompletion, 13 | > press Esc! Press it again to close the pane. 14 | 15 | When the note you want to find is in the list, you can choose it by using the 16 | and arrow keys. When you press 17 | Enter, the selected note will appear in your editor. 18 | 19 | This package constantly writes your changes to disk as you create and edit notes 20 | when you have the Autosave feature enabled. 21 | 22 | > **Tip:** To remove a note entirely: Select any text in the note, delete it 23 | > all, and then save the note. When you close the tab Atom Notes will 24 | > automatically delete the file associated with your now empty note. 25 | -------------------------------------------------------------------------------- /notebook/Welcome.md: -------------------------------------------------------------------------------- 1 | # 🌈 Welcome to Atom Notes! 2 | 3 | This package is a fork and rewrite of the now unpublished package 4 | [nvatom][nvatom]. The general idea behind this package is to provide an embedded 5 | [Notational Velocity][nv]-like note-taking feature for Atom users. This package 6 | is **NOT** affiliated with [Notational Velocity][nv]. 7 | 8 | This is the body of a note. Your notes can be as long or as short as you want. 9 | 10 | Notes use [Markdown][md] syntax, so you can freely use features such as 11 | **embolden** or *italicize* in them. Atom's [GFM][gfm] syntax highlighting will 12 | automatically beautify these notes: Use the `Markdown Preview: Toggle` command 13 | to see them! 14 | 15 | Hopefully these initial notes give you a sense what you can do with Atom Notes. 16 | Please feel free to delete them at any time. 17 | 18 | For more usage tips, see the note [[How to use]]. 19 | 20 | [nvatom]: https://github.com/seongjaelee/nvatom 21 | [nv]: http://notational.net/ 22 | [md]: http://daringfireball.net/projects/markdown/ 23 | [gfm]: https://help.github.com/articles/github-flavored-markdown/ 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-notes", 3 | "main": "./lib/atom-notes", 4 | "version": "1.23.0", 5 | "contributors": [ 6 | "lexicalunit ", 7 | "Seongjae Lee ", 8 | "Nikita Litvin ", 9 | "Jonathan Hoyt " 10 | ], 11 | "description": "Embedded Notational Velocity-like features for Atom", 12 | "keywords": [ 13 | "wiki", 14 | "notational velocity", 15 | "notes", 16 | "notetaking" 17 | ], 18 | "repository": "https://github.com/lexicalunit/atom-notes", 19 | "license": "MIT", 20 | "engines": { 21 | "atom": ">=1.24.0 <2.0.0" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/lexicalunit/atom-notes/issues" 25 | }, 26 | "homepage": "https://github.com/lexicalunit/atom-notes", 27 | "scripts": { 28 | "add": "all-contributors add", 29 | "generate": "all-contributors generate", 30 | "lint": "eslint ." 31 | }, 32 | "deserializers": { 33 | "SearchIndex": "deserializeSearchIndex" 34 | }, 35 | "uriHandler": { 36 | "method": "handleURI", 37 | "deferActivation": false 38 | }, 39 | "dependencies": { 40 | "elasticlunr": "^0.9.5", 41 | "etch": "^0.14.0", 42 | "fs-plus": "^3.1.1", 43 | "fuzzaldrin-plus": "^0.6.0", 44 | "gray-matter": "^4.0.2", 45 | "timsort": "^0.3.0" 46 | }, 47 | "devDependencies": { 48 | "all-contributors-cli": "^6.14.1", 49 | "eslint": "^6.8.0", 50 | "eslint-config-standard": "^14.1.1", 51 | "eslint-plugin-import": "^2.20.2", 52 | "eslint-plugin-node": "^11.1.0", 53 | "eslint-plugin-promise": "^4.2.1", 54 | "eslint-plugin-standard": "^4.0.1", 55 | "temp": "^0.9.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'jasmine': true, 4 | 'atomtest': true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/atom-notes-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // Writing tests for Atom is awful. For whatever reason, these tests 4 | // that used to pass just fine are now failing with: 5 | // timeout: timed out after 5000 msec waiting for module to be ready 6 | // 7 | // import path from 'path' 8 | // import temp from 'temp' 9 | // 10 | // temp.track() 11 | // 12 | // describe('atom-notes', () => { 13 | // const dir = atom.config.get('atom-notes.directory') 14 | // let wsview = null 15 | // 16 | // beforeEach(() => { 17 | // wsview = atom.views.getView(atom.workspace) 18 | // }) 19 | // 20 | // afterEach(() => { 21 | // atom.config.set('atom-notes.directory', dir) 22 | // }) 23 | // 24 | // describe('when the toggle event is triggered', () => { 25 | // it('attaches and then detaches the view', () => { 26 | // let done = jasmine.createSpy('done') 27 | // 28 | // runs(() => { 29 | // const noteDirectory = path.join(temp.mkdirSync()) 30 | // atom.config.set('atom-notes.directory', noteDirectory) 31 | // atom.packages.activatePackage('atom-notes').then(pack => { 32 | // let module = pack.mainModule 33 | // setInterval(() => { 34 | // window.advanceClock(1) 35 | // if (module && module.ready) done() 36 | // }, 5) 37 | // }) 38 | // }) 39 | // 40 | // waitsFor(() => { 41 | // return done.callCount > 0 42 | // }, 'module to be ready') 43 | // 44 | // runs(() => { 45 | // expect(wsview.querySelector('.atom-notes')).not.toExist() 46 | // 47 | // atom.commands.dispatch(wsview, 'atom-notes:toggle') 48 | // expect(wsview.querySelector('.atom-notes')).toExist() 49 | // expect(wsview.querySelector('.atom-notes').parentNode.style.display).not.toBe('none') 50 | // 51 | // atom.commands.dispatch(wsview, 'atom-notes:toggle') 52 | // expect(wsview.querySelector('.atom-notes').parentNode.style.display).toBe('none') 53 | // }) 54 | // }) 55 | // }) 56 | // 57 | // describe('when the notes directory is invalid', () => { 58 | // it('automatically deactivate the package', () => { 59 | // atom.notifications.addError = jasmine.createSpy('atom.notifications.addError') 60 | // let done = jasmine.createSpy('done') 61 | // 62 | // runs(() => { 63 | // const noteDirectory = path.join(process.env.ATOM_HOME, 'packages', 'atom-notes', 'notebook') 64 | // atom.config.set('atom-notes.directory', noteDirectory) 65 | // atom.packages.activatePackage('atom-notes').then(pack => { 66 | // let module = pack.mainModule 67 | // setInterval(() => { 68 | // window.advanceClock(1) 69 | // if (module && module.ready !== undefined && !module.ready) done() 70 | // }, 5) 71 | // }) 72 | // }) 73 | // 74 | // waitsFor(() => { 75 | // return done.callCount > 0 76 | // }, 'module to abort activation') 77 | // 78 | // runs(() => { 79 | // expect(wsview.querySelector('.atom-notes')).not.toExist() 80 | // expect(atom.notifications.addError.callCount).toBe(1) 81 | // }) 82 | // }) 83 | // }) 84 | // }) 85 | -------------------------------------------------------------------------------- /spec/interlink-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import path from 'path' 4 | import temp from 'temp' 5 | 6 | import * as Interlink from '../lib/interlink' 7 | 8 | temp.track() 9 | 10 | describe('Interlink', () => { 11 | let editor = null 12 | 13 | describe('when the editor is opened to a note', () => { 14 | beforeEach(() => { 15 | const notesDirectory = temp.mkdirSync() 16 | const notePath = path.join(notesDirectory, 'Interlink.md') 17 | atom.config.set('atom-notes.directory', notesDirectory) 18 | waitsForPromise(() => atom.packages.activatePackage('atom-notes')) 19 | waitsForPromise(() => atom.workspace.open(notePath)) 20 | 21 | runs(() => { 22 | editor = atom.workspace.getActiveTextEditor() 23 | waitsFor(done => { 24 | editor.getBuffer().onDidSave(() => done()) 25 | editor.save() 26 | }) 27 | }) 28 | }) 29 | 30 | it('can get interlink text', () => { 31 | editor.setText('[[Car]]') 32 | editor.setCursorBufferPosition([0, 2]) 33 | expect(Interlink.getInterlinkTitle(editor)).toBe('Car') 34 | 35 | editor.setText('[[Notational Velocity]]') 36 | editor.setCursorBufferPosition([0, 2]) 37 | expect(Interlink.getInterlinkTitle(editor)).toBe('Notational Velocity') 38 | 39 | editor.setText('[[ Car ]]') 40 | editor.setCursorBufferPosition([0, 2]) 41 | expect(Interlink.getInterlinkTitle(editor)).toBe('Car') 42 | 43 | editor.setText('[[Car/Mini]]') 44 | editor.setCursorBufferPosition([0, 2]) 45 | expect(Interlink.getInterlinkTitle(editor)).toBe('Car/Mini') 46 | 47 | editor.setText('[[[Car]]]') 48 | editor.setCursorBufferPosition([0, 3]) 49 | expect(Interlink.getInterlinkTitle(editor)).toBe('Car') 50 | 51 | editor.setText('[[Car]]]') 52 | editor.setCursorBufferPosition([0, 2]) 53 | expect(Interlink.getInterlinkTitle(editor)).toBe('Car') 54 | }) 55 | 56 | it('recognizes invalid interlink text', () => { 57 | editor.setText('[[]]') 58 | editor.setCursorBufferPosition([0, 2]) 59 | expect(Interlink.getInterlinkTitle(editor)).toBeNull() 60 | 61 | editor.setText('[[]]') 62 | editor.setCursorBufferPosition([0, 3]) 63 | expect(Interlink.getInterlinkTitle(editor)).toBeNull() 64 | 65 | editor.setText('[[ ]]') 66 | editor.setCursorBufferPosition([0, 2]) 67 | expect(Interlink.getInterlinkTitle(editor)).toBeNull() 68 | 69 | editor.setText('[Car]') 70 | editor.setCursorBufferPosition([0, 1]) 71 | expect(Interlink.getInterlinkTitle(editor)).toBeNull() 72 | 73 | editor.setText('[[Car]') 74 | editor.setCursorBufferPosition([0, 2]) 75 | expect(Interlink.getInterlinkTitle(editor)).toBeNull() 76 | 77 | editor.setText('Car') 78 | editor.setCursorBufferPosition([0, 1]) 79 | expect(Interlink.getInterlinkTitle(editor)).toBeNull() 80 | }) 81 | 82 | it('can open valid interlinks', () => { 83 | editor.setText('[[Car]]') 84 | editor.setCursorBufferPosition([0, 2]) 85 | 86 | const openPromise = Interlink.openInterlink() 87 | expect(openPromise).not.toBe(undefined) 88 | waitsForPromise(() => openPromise) 89 | 90 | runs(() => { 91 | const activeEditor = atom.workspace.getActiveTextEditor() 92 | expect(activeEditor.getPath().endsWith('Car.md')).toBe(true) 93 | }) 94 | }) 95 | 96 | // Writing tests for Atom is awful. For whatever reason, this test 97 | // that used to pass just fine is now failing with: 98 | // timeout: timed out after 5000 msec waiting for module to be ready 99 | // it('can dispatch open interlink command', () => { 100 | // editor.setText('[[Car]]') 101 | // editor.setCursorBufferPosition([0, 2]) 102 | // 103 | // waitsFor(done => { 104 | // atom.commands.dispatch(atom.views.getView(atom.workspace), 'atom-notes:interlink') 105 | // atom.workspace.observeActiveTextEditor(observedEditor => { 106 | // if (observedEditor.getPath() !== editor.getPath()) done() 107 | // }) 108 | // }) 109 | // 110 | // runs(() => { 111 | // const activeEditor = atom.workspace.getActiveTextEditor() 112 | // expect(activeEditor.getPath().endsWith('Car.md')).toBe(true) 113 | // }) 114 | // }) 115 | }) 116 | 117 | describe('when the editor is NOT opened to a note', () => { 118 | beforeEach(() => { 119 | const notesDirectory = temp.mkdirSync() 120 | atom.config.set('atom-notes.directory', notesDirectory) 121 | const randomDirectory = temp.mkdirSync() 122 | const filePath = path.join(randomDirectory, 'Interlink.md') 123 | waitsForPromise(() => atom.packages.activatePackage('atom-notes')) 124 | waitsForPromise(() => atom.workspace.open(filePath)) 125 | 126 | runs(() => { 127 | editor = atom.workspace.getActiveTextEditor() 128 | waitsFor(done => { 129 | editor.getBuffer().onDidSave(() => done()) 130 | editor.save() 131 | }) 132 | }) 133 | }) 134 | 135 | // I see no reason not to just support interlinks everywhere. 136 | it('still does open valid interlinks', () => { 137 | editor.setText('[[Car]]') 138 | editor.setCursorBufferPosition([0, 2]) 139 | 140 | const openPromise = Interlink.openInterlink() 141 | expect(openPromise).not.toBe(undefined) 142 | waitsForPromise(() => openPromise) 143 | 144 | runs(() => { 145 | const activeEditor = atom.workspace.getActiveTextEditor() 146 | expect(activeEditor.getPath().endsWith('Car.md')).toBe(true) 147 | }) 148 | }) 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /spec/notes-fs-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import fs from 'fs-plus' 4 | import path from 'path' 5 | import temp from 'temp' 6 | 7 | import * as NotesFs from '../lib/notes-fs' 8 | 9 | temp.track() 10 | 11 | describe('NotesFs', () => { 12 | const defaultDirectory = atom.config.get('atom-notes.directory') 13 | const defaultNoteExtensions = atom.config.get('atom-notes.extensions') 14 | 15 | afterEach(() => { 16 | atom.config.set('atom-notes.directory', defaultDirectory) 17 | atom.config.set('atom-notes.extensions', defaultNoteExtensions) 18 | }) 19 | 20 | describe('getPrimaryNoteExtension', () => { 21 | it('test suite', () => { 22 | atom.config.set('atom-notes.extensions', ['.md', '.markdown']) 23 | expect(NotesFs.getPrimaryNoteExtension()).toBe('.md') 24 | atom.config.set('atom-notes.extensions', ['.markdown']) 25 | expect(NotesFs.getPrimaryNoteExtension()).toBe('.markdown') 26 | atom.config.set('atom-notes.extensions', []) 27 | expect(NotesFs.getPrimaryNoteExtension()).toBe('.md') 28 | }) 29 | }) 30 | 31 | describe('isNote', () => { 32 | it('handles symlinks correctly', () => { 33 | atom.config.set('atom-notes.extensions', ['.md', '.markdown']) 34 | 35 | const tempDirectoryPath = path.join(temp.mkdirSync()) 36 | const notesDirectoryPath = path.join(temp.mkdirSync()) 37 | const notesDirectoryPathSymlink = path.join(tempDirectoryPath, 'note book') 38 | const notePath = path.join(notesDirectoryPath, 'note.mD') 39 | const notePathSymlink = path.join(notesDirectoryPathSymlink, 'note symlink.md') 40 | 41 | fs.writeFileSync(notePath, 'dummy') 42 | fs.symlinkSync(notesDirectoryPath, notesDirectoryPathSymlink) 43 | fs.symlinkSync(notePath, notePathSymlink) 44 | 45 | expect(fs.existsSync(notePath)).toBe(true) 46 | expect(fs.existsSync(fs.normalize(notePath))).toBe(true) 47 | 48 | atom.config.set('atom-notes.directory', notesDirectoryPath) 49 | expect(NotesFs.isNote(notePath)).toBe(true) 50 | expect(NotesFs.isNote(notePathSymlink)).toBe(true) 51 | 52 | atom.config.set('atom-notes.directory', notesDirectoryPathSymlink) 53 | expect(NotesFs.isNote(notePath)).toBe(true) 54 | expect(NotesFs.isNote(notePathSymlink)).toBe(true) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /spec/notes-store-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | // Writing tests for Atom is awful. For whatever reason, this test 4 | // that used to pass just fine is now failing with: 5 | // timeout: timed out after 5000 msec waiting for module to be ready 6 | // 7 | // import fs from 'fs-plus' 8 | // import path from 'path' 9 | // import temp from 'temp' 10 | // 11 | // import NotesStore from '../lib/notes-store' 12 | // 13 | // temp.track() 14 | // 15 | // describe('NotesStore', () => { 16 | // it('search', () => { 17 | // let dir = path.join(temp.mkdirSync()) 18 | // 19 | // let doc1 = { 20 | // fileName: 'What are the best animals in the world.md', 21 | // body: '---\n' + 22 | // 'keywords:\n' + 23 | // ' - Animals\n' + 24 | // ' - Question\n' + 25 | // '---\n' + 26 | // '\n' + 27 | // 'Dasypodidae are the best animals!' 28 | // } 29 | // let doc2 = { 30 | // fileName: 'Question is Welcome to Atom Notes.markdown', 31 | // body: `The general idea behind this package is to provide an embedded 32 | // Notational Velocity-like note-taking feature for Atom users.` 33 | // } 34 | // 35 | // fs.writeFileSync(path.join(dir, doc1.fileName), doc1.body) 36 | // fs.writeFileSync(path.join(dir, doc2.fileName), doc2.body) 37 | // 38 | // let storePromise = new Promise(function (resolve, reject) { 39 | // let store = new NotesStore(dir, ['.md', '.markdown']) 40 | // store.on('ready', () => resolve(store)) 41 | // }) 42 | // waitsForPromise(() => storePromise) 43 | // storePromise.then(store => { 44 | // let results = store.search('Dasyp') 45 | // expect(results.length).toBe(1) 46 | // expect(results[0].fileName).toBe(doc1.fileName) 47 | // expect(results[0].title).toBe(path.parse(doc1.fileName).name) 48 | // expect(results[0].body).toBe(doc1.body) 49 | // expect(results[0].keywords).toEqual(['Animals', 'Question']) 50 | // 51 | // results = store.search('Question') 52 | // expect(results.length).toBe(2) 53 | // expect(results[0].fileName).toBe(doc1.fileName) 54 | // expect(results[1].fileName).toBe(doc2.fileName) 55 | // 56 | // results = store.search('notational velocity') 57 | // expect(results.length).toBe(1) 58 | // expect(results[0].fileName).toBe(doc2.fileName) 59 | // expect(results[0].title).toBe(path.parse(doc2.fileName).name) 60 | // expect(results[0].body).toBe(doc2.body) 61 | // expect(results[0].keywords).toEqual([]) 62 | // 63 | // results = store.search('welcome to') 64 | // expect(results.length).toBe(1) 65 | // expect(results[0].fileName).toBe(doc2.fileName) 66 | // expect(results[0].title).toBe(path.parse(doc2.fileName).name) 67 | // expect(results[0].body).toBe(doc2.body) 68 | // expect(results[0].keywords).toEqual([]) 69 | // 70 | // results = store.search('nothing') 71 | // expect(results.length).toBe(0) 72 | // }) 73 | // }) 74 | // }) 75 | -------------------------------------------------------------------------------- /styles/atom-notes.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | .atom-notes { 4 | li { 5 | text-overflow: ellipsis; 6 | overflow-x: hidden; 7 | } 8 | 9 | .list-group { 10 | overflow-y: scroll !important; 11 | } 12 | 13 | .metadata { 14 | float: right; 15 | color: @text-color-info; 16 | } 17 | 18 | .keywords span { 19 | margin-right: 3px; 20 | } 21 | 22 | .atom-notes-autocomplete { 23 | font-style: italic; 24 | color: @text-color-info; 25 | } 26 | } 27 | --------------------------------------------------------------------------------