├── .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 | [](#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:
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 |
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 |
--------------------------------------------------------------------------------