From f0b1fcd075afeb11977855d49678f2dfa5deecba Mon Sep 17 00:00:00 2001 From: Seongjae Lee Date: Sat, 2 Jul 2016 23:20:41 -0700 Subject: [PATCH] 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. --- lib/notational-velocity-view.coffee | 21 ++-- lib/note-cache.coffee | 136 ++++++++++++++++++++ lib/note-watcher.coffee | 130 +++++++++++++++++++ package.json | 2 +- spec/note-cache-spec.coffee | 188 ++++++++++++++++++++++++++++ spec/note-watcher-spec.coffee | 143 +++++++++++++++++++++ 6 files changed, 606 insertions(+), 14 deletions(-) create mode 100644 lib/note-cache.coffee create mode 100644 lib/note-watcher.coffee create mode 100644 spec/note-cache-spec.coffee create mode 100644 spec/note-watcher-spec.coffee diff --git a/lib/notational-velocity-view.coffee b/lib/notational-velocity-view.coffee index a91fc28..1ba38dd 100644 --- a/lib/notational-velocity-view.coffee +++ b/lib/notational-velocity-view.coffee @@ -3,14 +3,15 @@ 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 +19,15 @@ 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) + @noteWatcher.on "ready", () => @documentsLoaded = true @setLoading() @populateList() - @docQuery.on "added", (fileDetails) => - @populateList() if @documentsLoaded - @docQuery.on "updated", (fileDetails) => - @populateList() if @documentsLoaded - @docQuery.on "removed", (fileDetails) => + @noteWatcher.on "update", () => @populateList() if @documentsLoaded unless atom.config.get('nvatom.enableLunrPipeline') - @docQuery.searchIndex.pipeline.reset() + @noteWatcher.searchIndex.pipeline.reset() isCursorProceeded: -> editor = @filterEditorView.model @@ -62,9 +59,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' @@ -156,7 +151,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 diff --git a/lib/note-cache.coffee b/lib/note-cache.coffee new file mode 100644 index 0000000..968e3b1 --- /dev/null +++ b/lib/note-cache.coffee @@ -0,0 +1,136 @@ +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 + + # _buildNoteStats: -> + # ret = {} + # visited = [fs.absolute(@_baseDirectory)] + # fs.traverseTreeSync( + # @_baseDirectory, + # (filePath) => + # fileName = path.basename(filePath) + # if @_extensions.indexOf(path.extname(fileName)) >= 0 + # noteId = path.relative(@_baseDirectory, filePath) + # ret[noteId] = fs.statSync(filePath).mtime.getTime() + # , + # (directoryPath) => + # return false if fs.absolute(directoryPath) in visited + # visited.push(fs.absolute(directoryPath)) + # return true + # ) + # ret + + _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 \ No newline at end of file diff --git a/lib/note-watcher.coffee b/lib/note-watcher.coffee new file mode 100644 index 0000000..a4bf000 --- /dev/null +++ b/lib/note-watcher.coffee @@ -0,0 +1,130 @@ +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) -> + @_maxItems = @_maxItems ? 100 + @_extensions = @_extensions ? ['.txt', '.md'] + @_noteCache = new NoteCache(@_baseDirectory, @_maxItems) + @_restoreSearchIndex() || @_initSearchIndex() + @_startWatcher() + + save: -> + cache = { + baseDirectory: @_baseDirectory, + extensions: @_extensions, + 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') + ) + + _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.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 + + _startWatcher: -> + 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 '' + } \ No newline at end of file diff --git a/package.json b/package.json index ddb7c34..ba809b0 100644 --- a/package.json +++ b/package.json @@ -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.6.0", "underscore-plus": "^1.6.6" }, "devDependencies": { diff --git a/spec/note-cache-spec.coffee b/spec/note-cache-spec.coffee new file mode 100644 index 0000000..884c3f7 --- /dev/null +++ b/spec/note-cache-spec.coffee @@ -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) diff --git a/spec/note-watcher-spec.coffee b/spec/note-watcher-spec.coffee new file mode 100644 index 0000000..895c310 --- /dev/null +++ b/spec/note-watcher-spec.coffee @@ -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) \ No newline at end of file