8 Commits
Author SHA1 Message Date
Seongjae Lee 7e12c57f0b Rename startWatcher to initWatcher to match initSearchIndex 2016-07-05 01:07:03 -07:00
Seongjae Lee 81c36a7521 Upgrade lunr to 0.7.1 2016-07-05 01:05:56 -07:00
Seongjae Lee a73b308639 Fix a bug that throwing an error when enableLunrPipeline is off
Also, enableLunrPipeline option should match on restoring lunr.
2016-07-05 01:05:42 -07:00
Seongjae Lee 4eb6ada48a Fix 54, uncaught error: cannot find module 'docquery' 2016-07-03 22:15:07 -07:00
Seongjae Lee 31a2b6a66e Remove unnecessary code from note-cache.coffee 2016-07-03 11:06:53 -07:00
Seongjae Lee 68c8b47ffb Fix a note-watcher error during deactivation 2016-07-03 10:44:32 -07:00
Seongjae Lee b22d94450c Use note watcher's save/load functionality 2016-07-03 01:31:40 -07:00
Seongjae Lee f0b1fcd075 Replace docquery to note-watcher, which supports lunr search index loading
NoteWatcher holds NoteCache, which caches recent notes and file modified time to help search index recovery.
2016-07-02 23:20:41 -07:00
7 changed files with 599 additions and 22 deletions
+10 -18
View File
@@ -2,15 +2,15 @@ path = require 'path'
fs = require 'fs-plus'
_ = require 'underscore-plus'
{$, $$, SelectListView} = require 'atom-space-pen-views'
DocQuery = require 'docquery'
NoteWatcher = require './note-watcher'
Utility = require './utility'
module.exports =
class NotationalVelocityView extends SelectListView
initialize: (state) ->
@initializedAt = new Date()
super
@addClass('nvatom')
@maxItems = 100
@rootDirectory = Utility.getNoteDirectory()
unless fs.existsSync(@rootDirectory)
throw new Error("The given directory #{@rootDirectory} does not exist. "
@@ -18,19 +18,13 @@ class NotationalVelocityView extends SelectListView
@skipPopulateList = false
@prevCursorPosition = 0
@documentsLoaded = false
@docQuery = new DocQuery(@rootDirectory, {recursive: true, extensions: atom.config.get('nvatom.extensions')})
@docQuery.on "ready", () =>
@noteWatcher = new NoteWatcher(@rootDirectory, atom.config.get('nvatom.extensions'), @maxItems, atom.config.get('nvatom.enableLunrPipeline'))
@noteWatcher.on "ready", () =>
@documentsLoaded = true
@setLoading()
@populateList()
@docQuery.on "added", (fileDetails) =>
@noteWatcher.on "update", () =>
@populateList() if @documentsLoaded
@docQuery.on "updated", (fileDetails) =>
@populateList() if @documentsLoaded
@docQuery.on "removed", (fileDetails) =>
@populateList() if @documentsLoaded
unless atom.config.get('nvatom.enableLunrPipeline')
@docQuery.searchIndex.pipeline.reset()
isCursorProceeded: ->
editor = @filterEditorView.model
@@ -62,9 +56,7 @@ class NotationalVelocityView extends SelectListView
@selectItemView(@list.find("li:nth-child(#{n})"))
filter: (filterQuery) ->
if (filterQuery is "") or (filterQuery is undefined)
return @docQuery.documents
return @docQuery.search(filterQuery)
@noteWatcher.search(filterQuery)
getFilterKey: ->
'filetext'
@@ -80,14 +72,12 @@ class NotationalVelocityView extends SelectListView
@show()
viewForItem: (item) ->
content = item.body[0...100]
$$ ->
@li class: 'two-lines', =>
@div class: 'primary-line', =>
@span "#{item.title}"
@div class: 'metadata', "#{item.modifiedAt.toLocaleDateString()}"
@div class: 'secondary-line', "#{content}"
@div class: 'secondary-line', "#{item.body}"
confirmSelection: ->
item = @getSelectedItem()
@@ -120,6 +110,8 @@ class NotationalVelocityView extends SelectListView
destroy: ->
@cancel()
@panel?.destroy()
@noteWatcher.save()
@noteWatcher.close()
show: ->
@storeFocusedElement()
@@ -156,7 +148,7 @@ class NotationalVelocityView extends SelectListView
@selectItem(filteredItems, filterQuery)
else
@setError(@getEmptyMessage(@docQuery.documents.length, filteredItems.length))
@setError(@getEmptyMessage(@noteWatcher.length, filteredItems.length))
schedulePopulateList: ->
unless @skipPopulateList
+118
View File
@@ -0,0 +1,118 @@
fs = require 'fs-plus'
path = require 'path'
# Keeps a track of notes and its modified time.
#
# It caches recent notes. Note that a note may partially contain its file content. A file system watcher should call
# `upsert` and `remove` accordingly once the note cache is created.
#
# TODO: Is it a good design? On creation, it deals with file system directly. Once it is created, it relies on outer
# feedback, instead of monitoring the file system by itself.
#
module.exports =
class NoteCache
constructor: (@_baseDirectory, @_maxItem, @_maxNoteLength) ->
@_maxItem = @_maxItem ? 100
@_maxNoteLength = @_maxNoteLength ? 100
@_noteStats = {}
@_state = 'init'
load: (noteStats) ->
@_noteStats = noteStats
@_state = 'cache'
this
ready: ->
@_buildNoteSortedList()
@_buildNoteCache()
@_assert()
@_state = 'ready'
this
toJSON: ->
@_assertReady()
JSON.stringify(@_noteStats)
upsert: (noteId, mtime) ->
@_noteStats[noteId] = mtime.getTime()
return this unless @_state == 'ready'
if !(noteId in @_noteSortedList)
@_noteSortedList.push(noteId)
# TODO: Can this be improved?
@_noteSortedList.sort(@_noteIdCompare)
if @_noteSortedList.indexOf(noteId) < @_maxItem
@_noteCache[noteId] = @_buildNote(noteId)
if @_noteSortedList.length > @_maxItem and @_noteSortedList[@_maxItem] in Object.keys(@_noteCache)
delete @_noteCache[@_noteSortedList[@_maxItem]]
@_assert()
this
remove: (noteId) ->
delete @_noteStats[noteId]
return this unless @_state == 'ready'
if noteId in Object.keys(@_noteCache)
delete @_noteCache[noteId]
if @_noteSortedList.length > @_maxItem and !(@_noteSortedList[@_maxItem] in Object.keys(@_noteCache))
@_noteCache[@_noteSortedList[@_maxItem]] = @_buildNote(@_noteSortedList[@_maxItem])
@_noteSortedList.splice(@_noteSortedList.indexOf(noteId), 1)
@_assert()
this
getNote: (noteId) ->
@_assertReady()
if noteId in @_noteCache then @_noteCache[noteId] else @_buildNote(noteId)
getRecentNotes: ->
@_assertReady()
@_noteSortedList
.slice(0, @_maxItem)
.map((noteId) => @_noteCache[noteId])
hasNoteId: (noteId) ->
@_noteStats.hasOwnProperty(noteId)
getNoteIds: ->
@_noteSortedList ? Object.keys(@_noteStats)
getModifiedAt: (noteId) ->
@_noteStats[noteId]
length: ->
@_assertReady()
@_noteSortedList.length
_assertReady: ->
throw new Error "state is not ready; #{@_state}" unless @_state == 'ready'
_assert: ->
throw new Error "cache length is wrong; #{Object.keys(@_noteCache).length} #{@_noteSortedList} #{@_maxItem}" unless Object.keys(@_noteCache).length == Math.min(@_maxItem, @_noteSortedList.length)
throw new Error 'list length is wrong' unless @_noteSortedList.length == Object.keys(@_noteStats).length
_buildNoteSortedList: ->
@_noteSortedList = Object.keys(@_noteStats)
@_noteSortedList.sort(@_noteIdCompare)
_buildNoteCache: ->
@_noteCache = {}
for noteId in @_noteSortedList.slice(0, @_maxItem)
@_noteCache[noteId] = @_buildNote(noteId)
_buildNote: (noteId) ->
filePath = path.join(@_baseDirectory, noteId)
fileName = path.basename(filePath)
# TODO: Verify it is right
title = path.basename(fileName, path.extname(fileName))
body = ''
if fs.existsSync(filePath)
# TODO: Read just the first n letters.
body = fs.readFileSync(filePath, 'utf8').slice(0, @_maxNoteLength)
return { title: title, body: body, filePath: filePath, modifiedAt: fs.statSync(filePath).mtime }
_noteIdCompare: (a, b) =>
if @_noteStats[a] > @_noteStats[b]
return -1
if @_noteStats[a] < @_noteStats[b]
return 1
return 0
+136
View File
@@ -0,0 +1,136 @@
chokidar = require 'chokidar'
fs = require 'fs'
lunr = require 'lunr'
path = require 'path'
_ = require 'underscore-plus'
zlib = require 'zlib'
{EventEmitter} = require 'events'
NoteCache = require './note-cache'
module.exports =
class NoteWatcher extends EventEmitter
@cacheVersion = 0
constructor: (@_baseDirectory, @_extensions, @_maxItems, @_enableLunrPipeline) ->
@_maxItems = @_maxItems ? 100
@_extensions = @_extensions ? ['.txt', '.md']
@_enableLunrPipeline = @_enableLunrPipeline ? false
@_noteCache = new NoteCache(@_baseDirectory, @_maxItems)
@_restoreSearchIndex() || @_initSearchIndex()
@_initWatcher()
save: ->
return unless @_state == 'ready'
cache = {
baseDirectory: @_baseDirectory,
extensions: @_extensions,
enableLunrPipeline: @_enableLunrPipeline,
version: @_cacheVersion,
searchIndex: JSON.stringify(@_searchIndex),
noteCache: @_noteCache.toJSON(),
}
fs.writeFileSync(
path.join(@_baseDirectory, 'nvatom.cache'),
zlib.deflateSync(JSON.stringify(cache)))
close: ->
@_watcher?.close()
search: (query) ->
if query? and query.length > 0
return @_searchIndex.search(query)
.slice(0, @_maxItems)
.map((x) => @_noteCache.getNote(x.ref))
else
return @_noteCache.getRecentNotes()
length: ->
@_noteCache.length()
_initSearchIndex: ->
@_state = 'initializing'
@_searchIndex = lunr(() ->
@field('title', { boost: 10 })
@field('body')
)
if !@_enableLunrPipeline
@_searchIndex.pipeline.reset()
_restoreSearchIndex: ->
cacheFilePath = path.join(@_baseDirectory, 'nvatom.cache')
return false unless fs.existsSync(cacheFilePath)
cache = JSON.parse(zlib.inflateSync(fs.readFileSync(cacheFilePath)))
return false if cache == null
return false unless _.isEqual(cache.baseDirectory, @_baseDirectory)
return false unless _.isEqual(cache.extensions, @_extensions)
return false unless _.isEqual(cache.enableLunrPipeline, @_enableLunrPipeline)
return false unless _.isEqual(cache.version, @_cacheVersion)
@_searchIndex = lunr.Index.load(JSON.parse(cache.searchIndex))
# TODO: Make this as an interface. Old note cache does not have to know upsert, remove, ready and so on.
# It only needs to know getNoteIds, hasNoteId, and getModifiedAt.
@_oldNoteCache = new NoteCache(@_baseDirectory, @_maxItems).load(JSON.parse(cache.noteCache))
@_state = 'recovering'
return true
_initWatcher: ->
options = {
ignored: (filePath, fileStat) =>
if fileStat?.isFile() then @_extensions.indexOf(path.extname(filePath)) < 0 else false
}
@_watcher = chokidar
.watch @_baseDirectory, options
.on 'add', (args) => @_add(args)
.on 'change', (args) => @_change(args)
.on 'unlink', (args) => @_unlink(args)
.on 'ready', () => @_ready()
_add: (filePath) ->
@_noteCache.upsert(@_toNoteId(filePath), fs.statSync(filePath).mtime)
if @_state != 'recovering'
@_searchIndex.add(@_toFileDetails(filePath))
@emit 'update', filePath
_change: (filePath) ->
@_noteCache.upsert(@_toNoteId(filePath), fs.statSync(filePath).mtime)
if @_state != 'recovering'
@_searchIndex.update(@_toFileDetails(filePath))
@emit 'update', filePath
_unlink: (filePath) ->
@_noteCache.remove(@_toNoteId(filePath))
if @_state != 'recovering'
@_searchIndex.remove(@_toFileDetails(filePath))
@emit 'update', filePath
_ready: () ->
@_noteCache.ready()
if @_state == 'recovering'
@_updateSearchIndex(@_oldNoteCache, @_noteCache)
delete @_oldNoteCache
@_state = 'ready'
@emit 'ready'
_updateSearchIndex: (oldNoteCache, newNoteCache) ->
for noteId in oldNoteCache.getNoteIds()
if !newNoteCache.hasNoteId(noteId)
@_searchIndex.remove(@_toFileDetails(@_toFilePath(noteId)))
else if oldNoteCache.getModifiedAt(noteId) <= newNoteCache.getModifiedAt(noteId)
@_searchIndex.update(@_toFileDetails(@_toFilePath(noteId)))
for noteId in newNoteCache.getNoteIds()
if !oldNoteCache.hasNoteId(noteId)
@_searchIndex.add(@_toFileDetails(@_toFilePath(noteId)))
_toNoteId: (filePath) ->
path.relative(@_baseDirectory, filePath)
_toFilePath: (noteId) ->
path.join(@_baseDirectory, noteId)
_toFileDetails: (filePath) ->
fileName = path.basename(filePath)
return {
id: @_toNoteId(filePath),
title: path.basename(fileName, path.extname(fileName)),
body: if fs.existsSync(filePath) then fs.readFileSync(filePath, { encoding: 'utf8' }) else ''
}
+1 -1
View File
@@ -28,8 +28,8 @@
"dependencies": {
"atom-space-pen-views": "^2.0.3",
"chokidar": "^1.0.5",
"docquery": "^1.1.1",
"fs-plus": "2.x",
"lunr": "^0.7.1",
"underscore-plus": "^1.6.6"
},
"devDependencies": {
+3 -3
View File
@@ -35,14 +35,14 @@ describe "nvAtom", ->
it "checks if we banned the default directory under packages directory", ->
noteDirectory = path.join(process.env.ATOM_HOME, 'packages', 'nvatom', 'notebook')
atom.config.set('nvatom.directory', noteDirectory)
waitsForPromise ->
atom.packages.activatePackage('nvatom')
runs ->
waitsForPromise ->
atom.packages.activatePackage('notifications')
runs ->
notificationContainer = workspaceElement.querySelector('atom-notifications')
notification = notificationContainer.querySelector('atom-notification.fatal')
+188
View File
@@ -0,0 +1,188 @@
fs = require 'fs'
path = require 'path'
temp = require 'temp'
NoteCache = require '../lib/note-cache'
temp.track()
String::repeat = (n) -> Array(n+1).join(this)
describe 'note cache', ->
cache = null
directoryPath = null
beforeEach ->
directoryPath = temp.mkdirSync()
fs.writeFileSync(path.join(directoryPath, 'foo.md'), 'foo'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'foo.md'), 0, 1)
fs.writeFileSync(path.join(directoryPath, 'bar.md'), 'bar'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'bar.md'), 0, 2)
fs.mkdirSync(path.join(directoryPath, 'baz'))
fs.writeFileSync(path.join(directoryPath, 'baz', 'baz.md'), 'baz'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'baz', 'baz.md'), 0, 3)
cache = new NoteCache(directoryPath, 2, 100)
.upsert('foo.md', fs.statSync(path.join(directoryPath, 'foo.md')).mtime)
.upsert(path.join('baz', 'baz.md'), fs.statSync(path.join(directoryPath, 'baz/baz.md')).mtime)
.upsert('bar.md', fs.statSync(path.join(directoryPath, 'bar.md')).mtime)
.ready()
it 'getNoteIds', ->
noteIds = cache.getNoteIds()
for noteId in ['foo.md', 'bar.md', 'baz/baz.md']
expect(noteIds).toContain(noteId)
expect(cache.length()).toBe(3)
it 'getRecentNotes', ->
notes = cache.getRecentNotes()
expect(notes.length).toBe(2)
expect(notes[0].title).toBe('baz')
expect(notes[1].title).toBe('bar')
it 'getNote', ->
expect(cache.getNote('baz/baz.md').title).toBe('baz')
expect(cache.getNote('bar.md').title).toBe('bar')
expect(cache.getNote('foo.md').title).toBe('foo')
it 'hasNoteId', ->
expect(cache.hasNoteId('baz/baz.md')).toBe(true)
expect(cache.hasNoteId('bar.md')).toBe(true)
expect(cache.hasNoteId('foo.md')).toBe(true)
expect(cache.hasNoteId('baz.md')).toBe(false)
it 'getModifiedAt', ->
expect(cache.getModifiedAt('baz/baz.md')).toBe(3000)
expect(cache.getModifiedAt('bar.md')).toBe(2000)
expect(cache.getModifiedAt('foo.md')).toBe(1000)
it 'adds a new note', ->
fs.writeFileSync(path.join(directoryPath, 'moo.md'), 'moo'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'moo.md'), 0, 4)
cache.upsert('moo.md', fs.statSync(path.join(directoryPath, 'moo.md')).mtime)
expect(cache.length()).toBe(4)
expect(cache.hasNoteId('moo.md')).toBe(true)
expect(cache.getModifiedAt('moo.md')).toBe(4000)
expect(cache.getRecentNotes().length).toBe(2)
expect(cache.getRecentNotes()[0].title).toBe('moo')
expect(cache.getNote('moo.md').title).toBe('moo')
it 'updates a note not in the recent notes', ->
fs.writeFileSync(path.join(directoryPath, 'foo.md'), 'foo'.repeat(2))
fs.utimesSync(path.join(directoryPath, 'foo.md'), 0, 4)
cache.upsert('foo.md', fs.statSync(path.join(directoryPath, 'foo.md')).mtime)
expect(cache.length()).toBe(3)
expect(cache.hasNoteId('foo.md')).toBe(true)
expect(cache.getModifiedAt('foo.md')).toBe(4000)
expect(cache.getRecentNotes().length).toBe(2)
expect(cache.getRecentNotes()[0].title).toBe('foo')
expect(cache.getNote('foo.md').title).toBe('foo')
it 'updates a note in the recent notes', ->
fs.writeFileSync(path.join(directoryPath, 'bar.md'), 'bar'.repeat(2))
fs.utimesSync(path.join(directoryPath, 'bar.md'), 0, 4)
cache.upsert('bar.md', fs.statSync(path.join(directoryPath, 'bar.md')).mtime)
expect(cache.length()).toBe(3)
expect(cache.hasNoteId('bar.md')).toBe(true)
expect(cache.getModifiedAt('bar.md')).toBe(4000)
expect(cache.getRecentNotes().length).toBe(2)
expect(cache.getRecentNotes()[0].title).toBe('bar')
expect(cache.getNote('bar.md').title).toBe('bar')
it 'deletes a note not in the recent notes', ->
fs.unlinkSync(path.join(directoryPath, 'foo.md'))
cache.remove('foo.md')
expect(cache.length()).toBe(2)
expect(cache.hasNoteId('foo.md')).toBe(false)
expect(cache.getRecentNotes().length).toBe(2)
expect(cache.getRecentNotes()[0].title).toBe('baz')
expect(cache.getRecentNotes()[1].title).toBe('bar')
it 'deletes a note in the recent notes', ->
cache.remove('bar.md')
expect(cache.length()).toBe(2)
expect(cache.hasNoteId('bar.md')).toBe(false)
expect(cache.getRecentNotes().length).toBe(2)
expect(cache.getRecentNotes()[0].title).toBe('baz')
expect(cache.getRecentNotes()[1].title).toBe('foo')
it 'deletes two notes', ->
cache.remove('bar.md')
cache.remove('foo.md')
expect(cache.length()).toBe(1)
expect(cache.hasNoteId('foo.md')).toBe(false)
expect(cache.hasNoteId('bar.md')).toBe(false)
expect(cache.getRecentNotes().length).toBe(1)
expect(cache.getRecentNotes()[0].title).toBe('baz')
# describe 'note cache', ->
# cache = null
# directoryPath = null
#
# beforeEach ->
# directoryPath = temp.mkdirSync()
#
# it 'handles cyclic symlink directories', ->
# fs.symlinkSync(directoryPath, path.join(directoryPath, 'foo'))
# fs.writeFileSync(path.join(directoryPath, 'foo.md'), 'foo'.repeat(100))
# cache = new NoteCache(directoryPath, ['.md'], 2, 100)
# expect(cache.length()).toBe(1)
#
# it 'handles symlink notes', ->
# # TODO: this behavior should be same for chokidar.
# anotherDirectoryPath = temp.mkdirSync()
# fs.writeFileSync(path.join(anotherDirectoryPath, 'foo.md'), 'foo'.repeat(100))
# fs.symlinkSync(path.join(anotherDirectoryPath, 'foo.md'), path.join(directoryPath, 'foo.md'))
# fs.symlinkSync(path.join(anotherDirectoryPath, 'foo.md'), path.join(directoryPath, 'bar.md'))
# fs.symlinkSync(anotherDirectoryPath, path.join(directoryPath, 'bar'))
# cache = new NoteCache(directoryPath, ['.md'], 2, 100)
# # foo.md, bar.md, bar/foo.md
# expect(cache.length()).toBe(3)
describe 'note cache', ->
cache = null
directoryPath = null
beforeEach ->
directoryPath = temp.mkdirSync()
cache = new NoteCache(directoryPath, 2, 100)
.ready()
fs.writeFileSync(path.join(directoryPath, 'foo.md'), 'foo'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'foo.md'), 0, 1)
fs.writeFileSync(path.join(directoryPath, 'bar.md'), 'bar'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'bar.md'), 0, 2)
fs.mkdirSync(path.join(directoryPath, 'baz'))
fs.writeFileSync(path.join(directoryPath, 'baz', 'baz.md'), 'baz'.repeat(100))
fs.utimesSync(path.join(directoryPath, 'baz', 'baz.md'), 0, 3)
cache
.upsert('foo.md', fs.statSync(path.join(directoryPath, 'foo.md')).mtime)
.upsert('bar.md', fs.statSync(path.join(directoryPath, 'bar.md')).mtime)
.upsert(path.join('baz', 'baz.md'), fs.statSync(path.join(directoryPath, 'baz', 'baz.md')).mtime)
it 'has all note ids', ->
noteIds = cache.getNoteIds()
for noteId in ['foo.md', 'bar.md', 'baz/baz.md']
expect(noteIds).toContain(noteId)
expect(cache.length()).toBe(3)
it 'getRecentNotes', ->
notes = cache.getRecentNotes()
expect(notes.length).toBe(2)
expect(notes[0].title).toBe('baz')
expect(notes[1].title).toBe('bar')
it 'getNote', ->
expect(cache.getNote('baz/baz.md').title).toBe('baz')
expect(cache.getNote('bar.md').title).toBe('bar')
expect(cache.getNote('foo.md').title).toBe('foo')
it 'getModifiedAt', ->
expect(cache.getModifiedAt('baz/baz.md')).toBe(3000)
expect(cache.getModifiedAt('bar.md')).toBe(2000)
expect(cache.getModifiedAt('foo.md')).toBe(1000)
+143
View File
@@ -0,0 +1,143 @@
fs = require 'fs'
path = require 'path'
temp = require 'temp'
NoteWatcher = require '../lib/note-watcher'
temp.track()
describe 'note-watcher', ->
watcher = null
directoryPath = null
beforeEach ->
directoryPath = temp.mkdirSync()
fs.writeFileSync(
path.join(directoryPath, 'foo.md'),
'The use of foo in a programming context is generally credited to the Tech Model Railroad Club (TMRC) of MIT.')
fs.writeFileSync(
path.join(directoryPath, 'bar.md'),
'When used in connection with bar it is generally traced to the World War II military slang FUBAR.')
spy = jasmine.createSpy()
watcher = new NoteWatcher(directoryPath)
watcher.on 'ready', spy
waitsFor -> spy.wasCalled
afterEach ->
watcher.close()
it 'handles a symbolic link note properly', ->
spy = jasmine.createSpy()
watcher.on 'update', spy
anotherDirectoryPath = temp.mkdirSync()
fs.writeFileSync(path.join(anotherDirectoryPath, 'baz.md'), 'A common name for the foobar, also foobaz.')
fs.symlinkSync(path.join(anotherDirectoryPath, 'baz.md'), path.join(directoryPath, 'baz.md'))
waitsFor -> spy.wasCalled
runs ->
result = watcher.search('baz')
expect(result.length).toBe(1)
it 'handles a symbolic link directory properly', ->
spy = jasmine.createSpy()
watcher.on 'update', spy
anotherDirectoryPath = temp.mkdirSync()
fs.writeFileSync(path.join(anotherDirectoryPath, 'baz.md'), 'A common name for the foobar, also foobaz.')
fs.symlinkSync(anotherDirectoryPath, path.join(directoryPath, 'baz'))
waitsFor -> spy.wasCalled
runs ->
result = watcher.search('baz')
expect(result.length).toBe(1)
expect(result[0].filePath).toBe(path.join(directoryPath, 'baz', 'baz.md'))
it 'handles paired symbolic links properly without falling into an infinite loop', ->
spy = jasmine.createSpy()
watcher.on 'update', spy
anotherDirectoryPath = temp.mkdirSync()
fs.writeFileSync(path.join(anotherDirectoryPath, 'baz.md'), 'A common name for the foobar, also foobaz.')
fs.symlinkSync(anotherDirectoryPath, path.join(directoryPath, 'bazDir'))
fs.symlinkSync(directoryPath, path.join(anotherDirectoryPath, 'fooDir'))
waitsFor -> spy.wasCalled
runs ->
result = watcher.search('baz')
expect(result.length).toBe(1)
expect(result[0].filePath).toBe(path.join(directoryPath, 'bazDir', 'baz.md'))
it 'watches adding a file', ->
spy = jasmine.createSpy()
watcher.on 'update', spy
fs.writeFileSync(path.join(directoryPath, 'note.md'), 'hello world')
waitsFor -> spy.wasCalled
it 'watches changing a file', ->
spy = jasmine.createSpy()
watcher.on 'update', spy
filePath = path.join(directoryPath, 'note.md')
fs.writeFileSync(filePath, 'hello world')
waitsFor -> spy.callCount == 1
runs -> fs.writeFileSync(filePath, 'hello world 2')
waitsFor -> spy.callCount == 2
it 'watches removing a file', ->
spy = jasmine.createSpy()
watcher.on('update', spy)
filePath = path.join(directoryPath, 'note.md')
fs.writeFileSync(filePath, 'hello world')
waitsFor -> spy.callCount == 1
runs -> fs.unlinkSync(filePath)
waitsFor -> spy.callCount == 2
it 'searches', ->
result = watcher.search('programming')
expect(result.length).toBe(1)
expect(result[0].title).toBe('foo')
it 'saves and loads', ->
watcher.save()
watcher.close()
watcher = new NoteWatcher(directoryPath)
spy = jasmine.createSpy()
watcher.on 'ready', spy
waitsFor -> spy.wasCalled
runs ->
result = watcher.search('programming')
expect(result.length).toBe(1)
expect(result[0].title).toBe('foo')
it 'loads the modified contents correctly', ->
watcher.save()
watcher.close()
# create, change, and delete
fs.writeFileSync(path.join(directoryPath, 'baz.md'), 'A common name for the foobar, also foobaz.')
fs.writeFileSync(path.join(directoryPath, 'foo.md'), 'The etymology of foo is obscure.')
fs.unlinkSync(path.join(directoryPath, 'bar.md'))
watcher = new NoteWatcher(directoryPath)
spy = jasmine.createSpy()
watcher.on('ready', spy)
waitsFor -> spy.wasCalled
runs ->
# in deleted foo.md
expect(watcher.search('programming').length).toBe(0)
# in replaced foo.md
expect(watcher.search('obscure').length).toBe(1)
# in bar.md
expect(watcher.search('connection').length).toBe(0)
# in baz.md
expect(watcher.search('baz').length).toBe(1)