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 | ||
|
|
1f005a3025 | ||
|
|
2cbf6a63d7 | ||
|
|
3c36f51fe6 | ||
|
|
956c0017e4 | ||
|
|
0dd99d805b | ||
|
|
aa641ba695 | ||
|
|
6a7e04d177 | ||
|
|
5aab86660f | ||
|
|
6ec1c4c2e8 | ||
|
|
f582f741ad | ||
|
|
452c34ba36 |
@@ -1,3 +1,7 @@
|
||||
## 0.11.0
|
||||
- Make autosave a selectable feature
|
||||
- Fix the bug that shows an error message on save when whitespace package is disabled
|
||||
|
||||
## 0.9.0
|
||||
- Add an interlink feature
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ You can also override `cmd-l` if you want to keep your muscle memory from Notati
|
||||
'cmd-l': 'nvatom:toggle'
|
||||
```
|
||||
|
||||
To set Windows and Linux key binds you can add the following lines to your keymap:
|
||||
|
||||
```cson
|
||||
'atom-workspace':
|
||||
'alt-ctrl-l': 'nvatom:toggle'
|
||||
'atom-workspace atom-text-editor':
|
||||
'alt-ctrl-o': 'nvatom:openInterlink'
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
v0.1.0 under published package name `notational-velocity` had a fatal bug that sets the default value of its note directory under package directory. In that case, all notes are deleted once the user updates the package, since package directory is overwritten when the user. For more information, refer [#25][6].
|
||||
|
||||
@@ -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()
|
||||
@@ -105,9 +95,12 @@ class NotationalVelocityView extends SelectListView
|
||||
if filePath
|
||||
atom.workspace.open(filePath).then (editor) ->
|
||||
save = ->
|
||||
atom.packages.deactivatePackage 'whitespace'
|
||||
isWhiteSpaceActive = atom.packages.isPackageActive 'whitespace'
|
||||
if isWhiteSpaceActive
|
||||
atom.packages.deactivatePackage 'whitespace'
|
||||
editor.save()
|
||||
atom.packages.activatePackage 'whitespace'
|
||||
if isWhiteSpaceActive
|
||||
atom.packages.activatePackage 'whitespace'
|
||||
debouncedSave = _.debounce save, 1000
|
||||
editor.onDidStopChanging () ->
|
||||
debouncedSave() if editor.isModified()
|
||||
@@ -117,6 +110,8 @@ class NotationalVelocityView extends SelectListView
|
||||
destroy: ->
|
||||
@cancel()
|
||||
@panel?.destroy()
|
||||
@noteWatcher.save()
|
||||
@noteWatcher.close()
|
||||
|
||||
show: ->
|
||||
@storeFocusedElement()
|
||||
@@ -153,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
|
||||
|
||||
@@ -23,7 +23,11 @@ module.exports =
|
||||
description: 'Lunr pipeline preprocesses query to make search faster. However, it will skip searching some of stop words such as "an" or "be".'
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
enableAutosave:
|
||||
title: 'Enable Autosave'
|
||||
description: 'Enable saving the document automatically whenever the user leaves the window or change the tab.'
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
activate: (state) ->
|
||||
@rootDirectory = @ensureNoteDirectory()
|
||||
@@ -64,6 +68,7 @@ module.exports =
|
||||
@notationalVelocityView
|
||||
|
||||
autosave: (paneItem) ->
|
||||
return unless atom.config.get('nvatom.enableAutosave')
|
||||
return unless paneItem?.getURI?()?
|
||||
return unless paneItem?.isModified?()
|
||||
uri = paneItem.getURI()
|
||||
@@ -81,6 +86,7 @@ module.exports =
|
||||
return true
|
||||
|
||||
autosaveAll: ->
|
||||
return unless atom.config.get('nvatom.enableAutosave')
|
||||
@autosave(paneItem) for paneItem in atom.workspace.getPaneItems()
|
||||
|
||||
ensureNoteDirectory: ->
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nvatom",
|
||||
"main": "./lib/notational-velocity",
|
||||
"version": "0.9.2",
|
||||
"version": "0.11.0",
|
||||
"private": true,
|
||||
"contributors": [
|
||||
"Seongjae Lee <seongjae@gmail.com>",
|
||||
@@ -28,8 +28,8 @@
|
||||
"dependencies": {
|
||||
"atom-space-pen-views": "^2.0.3",
|
||||
"chokidar": "^1.0.5",
|
||||
"docquery": "^1.1.0",
|
||||
"fs-plus": "2.x",
|
||||
"lunr": "^0.7.1",
|
||||
"underscore-plus": "^1.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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