mirror of
https://github.com/wassname/nvatom.git
synced 2026-09-10 12:21:11 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e12c57f0b | ||
|
|
81c36a7521 | ||
|
|
a73b308639 | ||
|
|
4eb6ada48a | ||
|
|
31a2b6a66e | ||
|
|
68c8b47ffb | ||
|
|
b22d94450c | ||
|
|
f0b1fcd075 |
@@ -2,15 +2,15 @@ path = require 'path'
|
|||||||
fs = require 'fs-plus'
|
fs = require 'fs-plus'
|
||||||
_ = require 'underscore-plus'
|
_ = require 'underscore-plus'
|
||||||
{$, $$, SelectListView} = require 'atom-space-pen-views'
|
{$, $$, SelectListView} = require 'atom-space-pen-views'
|
||||||
DocQuery = require 'docquery'
|
NoteWatcher = require './note-watcher'
|
||||||
Utility = require './utility'
|
Utility = require './utility'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class NotationalVelocityView extends SelectListView
|
class NotationalVelocityView extends SelectListView
|
||||||
initialize: (state) ->
|
initialize: (state) ->
|
||||||
@initializedAt = new Date()
|
|
||||||
super
|
super
|
||||||
@addClass('nvatom')
|
@addClass('nvatom')
|
||||||
|
@maxItems = 100
|
||||||
@rootDirectory = Utility.getNoteDirectory()
|
@rootDirectory = Utility.getNoteDirectory()
|
||||||
unless fs.existsSync(@rootDirectory)
|
unless fs.existsSync(@rootDirectory)
|
||||||
throw new Error("The given directory #{@rootDirectory} does not exist. "
|
throw new Error("The given directory #{@rootDirectory} does not exist. "
|
||||||
@@ -18,19 +18,13 @@ class NotationalVelocityView extends SelectListView
|
|||||||
@skipPopulateList = false
|
@skipPopulateList = false
|
||||||
@prevCursorPosition = 0
|
@prevCursorPosition = 0
|
||||||
@documentsLoaded = false
|
@documentsLoaded = false
|
||||||
@docQuery = new DocQuery(@rootDirectory, {recursive: true, extensions: atom.config.get('nvatom.extensions')})
|
@noteWatcher = new NoteWatcher(@rootDirectory, atom.config.get('nvatom.extensions'), @maxItems, atom.config.get('nvatom.enableLunrPipeline'))
|
||||||
@docQuery.on "ready", () =>
|
@noteWatcher.on "ready", () =>
|
||||||
@documentsLoaded = true
|
@documentsLoaded = true
|
||||||
@setLoading()
|
@setLoading()
|
||||||
@populateList()
|
@populateList()
|
||||||
@docQuery.on "added", (fileDetails) =>
|
@noteWatcher.on "update", () =>
|
||||||
@populateList() if @documentsLoaded
|
@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: ->
|
isCursorProceeded: ->
|
||||||
editor = @filterEditorView.model
|
editor = @filterEditorView.model
|
||||||
@@ -62,9 +56,7 @@ class NotationalVelocityView extends SelectListView
|
|||||||
@selectItemView(@list.find("li:nth-child(#{n})"))
|
@selectItemView(@list.find("li:nth-child(#{n})"))
|
||||||
|
|
||||||
filter: (filterQuery) ->
|
filter: (filterQuery) ->
|
||||||
if (filterQuery is "") or (filterQuery is undefined)
|
@noteWatcher.search(filterQuery)
|
||||||
return @docQuery.documents
|
|
||||||
return @docQuery.search(filterQuery)
|
|
||||||
|
|
||||||
getFilterKey: ->
|
getFilterKey: ->
|
||||||
'filetext'
|
'filetext'
|
||||||
@@ -80,14 +72,12 @@ class NotationalVelocityView extends SelectListView
|
|||||||
@show()
|
@show()
|
||||||
|
|
||||||
viewForItem: (item) ->
|
viewForItem: (item) ->
|
||||||
content = item.body[0...100]
|
|
||||||
|
|
||||||
$$ ->
|
$$ ->
|
||||||
@li class: 'two-lines', =>
|
@li class: 'two-lines', =>
|
||||||
@div class: 'primary-line', =>
|
@div class: 'primary-line', =>
|
||||||
@span "#{item.title}"
|
@span "#{item.title}"
|
||||||
@div class: 'metadata', "#{item.modifiedAt.toLocaleDateString()}"
|
@div class: 'metadata', "#{item.modifiedAt.toLocaleDateString()}"
|
||||||
@div class: 'secondary-line', "#{content}"
|
@div class: 'secondary-line', "#{item.body}"
|
||||||
|
|
||||||
confirmSelection: ->
|
confirmSelection: ->
|
||||||
item = @getSelectedItem()
|
item = @getSelectedItem()
|
||||||
@@ -120,6 +110,8 @@ class NotationalVelocityView extends SelectListView
|
|||||||
destroy: ->
|
destroy: ->
|
||||||
@cancel()
|
@cancel()
|
||||||
@panel?.destroy()
|
@panel?.destroy()
|
||||||
|
@noteWatcher.save()
|
||||||
|
@noteWatcher.close()
|
||||||
|
|
||||||
show: ->
|
show: ->
|
||||||
@storeFocusedElement()
|
@storeFocusedElement()
|
||||||
@@ -156,7 +148,7 @@ class NotationalVelocityView extends SelectListView
|
|||||||
@selectItem(filteredItems, filterQuery)
|
@selectItem(filteredItems, filterQuery)
|
||||||
|
|
||||||
else
|
else
|
||||||
@setError(@getEmptyMessage(@docQuery.documents.length, filteredItems.length))
|
@setError(@getEmptyMessage(@noteWatcher.length, filteredItems.length))
|
||||||
|
|
||||||
schedulePopulateList: ->
|
schedulePopulateList: ->
|
||||||
unless @skipPopulateList
|
unless @skipPopulateList
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -28,8 +28,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"atom-space-pen-views": "^2.0.3",
|
"atom-space-pen-views": "^2.0.3",
|
||||||
"chokidar": "^1.0.5",
|
"chokidar": "^1.0.5",
|
||||||
"docquery": "^1.1.1",
|
|
||||||
"fs-plus": "2.x",
|
"fs-plus": "2.x",
|
||||||
|
"lunr": "^0.7.1",
|
||||||
"underscore-plus": "^1.6.6"
|
"underscore-plus": "^1.6.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ describe "nvAtom", ->
|
|||||||
it "checks if we banned the default directory under packages directory", ->
|
it "checks if we banned the default directory under packages directory", ->
|
||||||
noteDirectory = path.join(process.env.ATOM_HOME, 'packages', 'nvatom', 'notebook')
|
noteDirectory = path.join(process.env.ATOM_HOME, 'packages', 'nvatom', 'notebook')
|
||||||
atom.config.set('nvatom.directory', noteDirectory)
|
atom.config.set('nvatom.directory', noteDirectory)
|
||||||
|
|
||||||
waitsForPromise ->
|
waitsForPromise ->
|
||||||
atom.packages.activatePackage('nvatom')
|
atom.packages.activatePackage('nvatom')
|
||||||
|
|
||||||
runs ->
|
runs ->
|
||||||
waitsForPromise ->
|
waitsForPromise ->
|
||||||
atom.packages.activatePackage('notifications')
|
atom.packages.activatePackage('notifications')
|
||||||
|
|
||||||
runs ->
|
runs ->
|
||||||
notificationContainer = workspaceElement.querySelector('atom-notifications')
|
notificationContainer = workspaceElement.querySelector('atom-notifications')
|
||||||
notification = notificationContainer.querySelector('atom-notification.fatal')
|
notification = notificationContainer.querySelector('atom-notification.fatal')
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user