diff --git a/lib/notational-velocity-view.coffee b/lib/notational-velocity-view.coffee index 2599015..9e04b58 100644 --- a/lib/notational-velocity-view.coffee +++ b/lib/notational-velocity-view.coffee @@ -1,17 +1,22 @@ -path = require 'path' -fs = require 'fs' -fsPlus = require 'fs-plus' {$, $$, SelectListView} = require 'atom-space-pen-views' +NoteDirectory = require './note-directory' +Note = require './note' module.exports = class NotationalVelocityView extends SelectListView initialize: -> super @addClass('notational-velocity from-top overlay') - @loadData() + rootDirectory = atom.config.get('notational-velocity.directory') + @noteDirectory = new NoteDirectory(rootDirectory, null, () => @updateNotes()) + @updateNotes() @prevFilterQuery = '' @prevCursorPosition = 0 + updateNotes: () -> + @notes = @noteDirectory.getNotes() + @setItems(@notes) + selectItem: (filterQuery) -> if filterQuery.length == 0 @prevCursorPosition = 0 @@ -24,9 +29,8 @@ class NotationalVelocityView extends SelectListView titleItem = null for titlePattern in titlePatterns - titleItems = @items - .filter (x) -> x.title.match(titlePattern) != null - .sort (x, y) -> if x.modified.getTime() <= y.modified.getTime() then 1 else -1 + titleItems = @notes + .filter (x) -> x.getTitle().match(titlePattern) != null titleItem = if titleItems.length > 0 then titleItems[0] else null if titleItem != null break @@ -36,74 +40,25 @@ class NotationalVelocityView extends SelectListView editor = @filterEditorView.model currCursorPosition = editor.getCursorBufferPosition().column if titleItem != null && @prevCursorPosition < currCursorPosition - @prevFilterQuery = titleItem.title - editor.setText(titleItem.title) - editor.selectLeft(titleItem.title.length - filterQuery.length) + @prevFilterQuery = titleItem.getTitle() + editor.setText(titleItem.getTitle()) + editor.selectLeft(titleItem.getTitle().length - filterQuery.length) @prevCursorPosition = currCursorPosition return titleItem filter: (filterQuery) -> if filterQuery.length == 0 - return @items + return @notes queries = filterQuery.split(' ') .filter (x) -> x.length > 0 .map (x) -> new RegExp(x, 'gi') - contentItems = @items + return @notes .filter (x) -> queries - .map (q) -> q.test(x.filetext) || q.test(x.title) + .map (q) -> q.test(x.getText()) || q.test(x.getTitle()) .reduce (x, y) -> x && y - return contentItems - - getSubPath: (baseDir, dir)-> - ret = [] - fullDir = path.join(baseDir, dir) - try - filenameList = fs.readdirSync(fullDir) - catch e - return ret - - for filename in filenameList - filePath = path.join(dir, filename) - fullPath = path.join(baseDir, filePath) - try - fileStat = fs.statSync(fullPath) - catch e - continue - if fileStat.isDirectory() - ret = ret.concat(@getSubPath(baseDir, filePath)) - else - if !fsPlus.isMarkdownExtension(path.extname(filename)) - continue - ret.push(filePath) - return ret - - loadData: -> - @data = [] - - basedir = atom.config.get('notational-velocity.directory') - - for filepath in @getSubPath(basedir, '') - fullpath = path.join(basedir, filepath) - filename = path.basename(filepath, path.extname(filepath)) - filetext = fs.readFileSync(fullpath, 'utf8') - title = path.join(path.dirname(filepath), filename) - modified = fs.statSync(fullpath).mtime - - item = { - 'title': title, - 'modified': modified, - 'filetext': filetext, - 'filename': filename, - 'filepath': fullpath - } - @data.push(item) - - @data = @data.sort (x, y) -> if x.modified.getTime() <= y.modified.getTime() then 1 else -1 - - @setItems(@data) getFilterKey: -> 'filetext' @@ -116,14 +71,13 @@ class NotationalVelocityView extends SelectListView @show() viewForItem: (item) -> - index = item.filetext.search /\n/ - content = item.filetext.slice(index, item.filetext.length) + content = item.getText()[0...100] $$ -> @li class: 'two-lines', => @div class: 'primary-line', => - @span "#{item.title}" - @div class: 'metadata', "#{item.modified.toLocaleDateString()}" + @span "#{item.getTitle()}" + @div class: 'metadata', "#{item.getModified().toLocaleDateString()}" @div class: 'secondary-line', "#{content}" confirmSelection: -> @@ -135,7 +89,7 @@ class NotationalVelocityView extends SelectListView @cancel() confirmed: (item) -> - atom.workspace.open(item.filepath) + atom.workspace.open(item.getFilePath()) @cancel() destroy: -> @@ -155,7 +109,7 @@ class NotationalVelocityView extends SelectListView @panel?.hide() populateList: -> - return unless @items? + return unless @notes? filterQuery = @getFilterQuery() filteredItems = @filter(filterQuery) @@ -176,7 +130,7 @@ class NotationalVelocityView extends SelectListView @selectItemView(@list.find("li:nth-child(#{n})")) else - @setError(@getEmptyMessage(@items.length, filteredItems.length)) + @setError(@getEmptyMessage(@notes.length, filteredItems.length)) schedulePopulateList: -> # We can skip it when we are just moving the position of the cursor. diff --git a/lib/note-directory.coffee b/lib/note-directory.coffee new file mode 100644 index 0000000..88adab0 --- /dev/null +++ b/lib/note-directory.coffee @@ -0,0 +1,58 @@ +path = require 'path' +fs = require 'fs-plus' +pathWatcher = require 'pathwatcher' +Note = require './note' + +module.exports = +class NoteDirectory + constructor: (@filePath, @parent, @onChangeCallback) -> + @directories = [] + @notes = [] + @updateMetadata() + @watcher = pathWatcher.watch(@filePath, (event) => @onChange(event)) + + destroy: -> + @notes.map (x) -> x.destroy() + @directories.map (x) -> x.destroy() + @watcher.close() + + updateMetadata: -> + @notes.map (x) -> x.destroy() + @directories.map (x) -> x.destroy() + + @directories = [] + @notes = [] + + try + filenames = fs.readdirSync(@filePath) + catch e + return + for filename in filenames + @addChild(path.join(@filePath, filename)) + + addChild: (filePath) -> + try + fileStat = fs.statSync(filePath) + catch e + return + if fileStat.isDirectory() + @directories.push(new NoteDirectory(filePath, this, @onChangeCallback)) + else + if fs.isMarkdownExtension(path.extname(filePath)) + @notes.push(new Note(filePath, this, @onChangeCallback)) + + getNotes: -> + ret = [] + ret = ret.concat(@notes) + for directory in @directories + ret = ret.concat(directory.getNotes()) + if @parent is null + ret.sort (x, y) -> if x.getModified().getTime() <= y.getModified().getTime() then 1 else -1 + return ret + + onChange: (event) -> + # For the case of rename and change, it will be handled in its parent. + if event == 'change' + @updateMetadata() + if @onChangeCallback != null + @onChangeCallback() diff --git a/lib/note.coffee b/lib/note.coffee new file mode 100644 index 0000000..b313488 --- /dev/null +++ b/lib/note.coffee @@ -0,0 +1,37 @@ +path = require 'path' +fs = require 'fs' +pathWatcher = require 'pathwatcher' + +module.exports = +class Note + constructor: (@filePath, @parent, @onChangeCallback) -> + @updateMetadata() + @updateText() + @watcher = pathWatcher.watch(@filePath, (event) => @onChange(event)) + + destroy: -> + @watcher.close() + + updateMetadata: -> + @modified = fs.statSync(@filePath).mtime + relativePath = path.relative(atom.config.get('notational-velocity.directory'), @filePath) + @title = path.join( + path.dirname(relativePath), + path.basename(relativePath, path.extname(relativePath)) + ) + + updateText: -> + @text = fs.readFileSync(@filePath, 'utf8') + + onChange: (event) -> + # For the case of rename and change, it will be handled in its parent. + if event == 'change' && fs.existsSync(@filePath) + @updateMetadata() + @updateText() + if @onChangeCallback != null + @onChangeCallback() + + getTitle: -> @title + getText: -> @text + getModified: -> @modified + getFilePath: -> @filePath diff --git a/package.json b/package.json index 43ef273..9151511 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,13 @@ }, "dependencies": { "fs-plus": "2.x", - "atom-space-pen-views": "^2.0.3" + "atom-space-pen-views": "^2.0.3", + "pathwatcher": "^4.2" + }, + "devDependencies": { + "fs-plus": "2.x", + "atom-space-pen-views": "^2.0.3", + "pathwatcher": "^4.2", + "temp": "~0.7.0" } } diff --git a/spec/note-directory-spec.coffee b/spec/note-directory-spec.coffee new file mode 100644 index 0000000..44cfa2d --- /dev/null +++ b/spec/note-directory-spec.coffee @@ -0,0 +1,117 @@ +path = require 'path' +fs = require 'fs-plus' +temp = require 'temp' +pathWatcher = require 'pathwatcher' +NoteDirectory = require '../lib/note-directory' +Note = require '../lib/note' + +describe 'NoteDirectory.getNotes', -> + defaultDirectory = atom.config.get('notational-velocity.directory') + tempDirectory = temp.mkdirSync('node-pathwatcher-directory') + noteDirectory = null + isCallbackCalled = false + + # We don't want to let the mtimes of two consecutively create files are same. + # This function ensures the mtime of the file created before calling this function to be always + # smaller than to the mtime of the file created after calling this function. + wait = -> + timestampDirectory = temp.mkdirSync('node-pathwatcher-timestamp') + filePath = path.join(timestampDirectory, 'temp') + fs.writeFileSync(filePath, '.') + mtimeOld = fs.statSync(filePath).mtime + mtimeNew = mtimeOld + while mtimeOld >= mtimeNew + fs.writeFileSync(filePath, '.') + mtimeNew = fs.statSync(filePath).mtime + + beforeEach -> + isCallbackCalled = false + callback = => isCallbackCalled = true + + atom.config.set('notational-velocity.directory', tempDirectory) + + fs.writeFileSync(path.join(tempDirectory, 'Readme.md'), 'read me') + wait() + + fs.mkdirSync(path.join(tempDirectory, 'Car')) + fs.writeFileSync(path.join(tempDirectory, 'Car', 'Mini.md'), 'mini') + wait() + + noteDirectory = new NoteDirectory(tempDirectory, null, callback) + + afterEach -> + noteDirectory.destroy() + fs.unlinkSync(path.join(tempDirectory, 'Car', 'Mini.md')) + fs.rmdirSync(path.join(tempDirectory, 'Car')) + fs.unlinkSync(path.join(tempDirectory, 'Readme.md')) + atom.config.set('notational-velocity.directory', defaultDirectory) + + it 'gives a list of notes in the order so that the newest one comes first', -> + notes = noteDirectory.getNotes() + expect(notes.length).toEqual(2) + expect(notes[0].getText()).toBe 'mini' + expect(notes[1].getText()).toBe 'read me' + expect(notes[0].getModified().getTime()).toBeGreaterThan(notes[1].getModified().getTime()) + + it 'changes its order when a note is changed', -> + fs.writeFileSync(path.join(tempDirectory, 'Readme.md'), 'read me new') + wait() + + waitsFor -> isCallbackCalled + runs -> + notes = noteDirectory.getNotes() + expect(notes[0].getText()).toBe 'read me new' + expect(notes[1].getText()).toBe 'mini' + + it 'changes its order when a note is created', -> + fs.writeFileSync(path.join(tempDirectory, 'Car', 'Prius.md'), 'prius') + wait() + + waitsFor -> isCallbackCalled + runs -> + notes = noteDirectory.getNotes() + expect(notes.length).toEqual(3) + expect(notes[0].getText()).toBe 'prius' + expect(notes[1].getText()).toBe 'mini' + expect(notes[2].getText()).toBe 'read me' + fs.unlinkSync(path.join(tempDirectory, 'Car', 'Prius.md')) + + it 'changes its order when a note is deleted', -> + fs.unlinkSync(path.join(tempDirectory, 'Car', 'Mini.md')) + wait() + + waitsFor -> isCallbackCalled + runs -> + notes = noteDirectory.getNotes() + expect(notes.length).toEqual(1) + expect(notes[0].getText()).toBe 'read me' + # So that it won't spit an error in the teardown stage. + fs.writeFileSync(path.join(tempDirectory, 'Car', 'Mini.md'), 'mini') + + it 'changes its order when a note is renamed', -> + oldPath = path.join(tempDirectory, 'Car', 'Mini.md') + newPath = path.join(tempDirectory, 'Mini.md') + fs.renameSync(oldPath, newPath) + wait() + + waitsFor -> isCallbackCalled + runs -> + notes = noteDirectory.getNotes() + expect(notes.length).toEqual(2) + expect(notes[0].getTitle()).toBe 'Mini' + expect(notes[1].getTitle()).toBe 'Readme' + # So that it won't spit an error in the teardown stage. + fs.renameSync(newPath, oldPath) + + it 'updates properly when a directory is created and a note is created inside it', -> + fs.mkdirSync(path.join(tempDirectory, 'Food')) + fs.writeFileSync(path.join(tempDirectory, 'Food', 'Milk.md'), 'milk') + wait() + waitsFor -> isCallbackCalled + runs -> + notes = noteDirectory.getNotes() + expect(notes.length).toEqual(3) + expect(notes[0].getText()).toBe 'milk' + # So that it won't spit an error in the teardown stage. + fs.unlinkSync(path.join(tempDirectory, 'Food', 'Milk.md')) + fs.rmdirSync(path.join(tempDirectory, 'Food')) diff --git a/spec/note-spec.coffee b/spec/note-spec.coffee new file mode 100644 index 0000000..3bce89d --- /dev/null +++ b/spec/note-spec.coffee @@ -0,0 +1,36 @@ +path = require 'path' +fs = require 'fs-plus' +temp = require 'temp' +pathWatcher = require 'pathwatcher' +Note = require '../lib/note' + +describe 'Note', -> + defaultDirectory = atom.config.get('notational-velocity.directory') + tempDirectory = temp.mkdirSync('node-pathwatcher-directory') + tempFilePath = path.join(tempDirectory, 'Temp.md') + + beforeEach -> + atom.config.set('notational-velocity.directory', tempDirectory) + fs.writeFileSync(tempFilePath, 'old') + + afterEach -> + fs.unlinkSync(tempFilePath) + atom.config.set('notational-velocity.directory', defaultDirectory) + + it 'creates a note', -> + note = new Note(tempFilePath, null, null) + expect(note.getTitle()).toBe 'Temp' + expect(note.getText()).toBe 'old' + expect(note.getFilePath()).toBe tempFilePath + note.destroy() + + it 'modifies a note', -> + note = new Note(tempFilePath, null, null) + expect(note.getText()).toBe 'old' + + fs.writeFileSync(tempFilePath, 'new') + oldModified = note.getModified() + waitsFor -> oldModified != note.getModified() + runs -> + expect(note.getText()).toBe 'new' + note.destroy()