Merge pull request #1553 from atom/ks-extract-text-buffer

Extract TextBuffer
This commit is contained in:
Kevin Sawicki 2014-02-19 09:26:47 -08:00
commit d87393f21e
17 changed files with 32 additions and 2078 deletions

View File

@ -1,11 +1,12 @@
{Point, Range} = require 'text-buffer' {Point, Range} = require 'text-buffer'
{File, Directory} = require 'pathwatcher'
module.exports = module.exports =
_: require 'underscore-plus' _: require 'underscore-plus'
BufferedNodeProcess: require '../src/buffered-node-process' BufferedNodeProcess: require '../src/buffered-node-process'
BufferedProcess: require '../src/buffered-process' BufferedProcess: require '../src/buffered-process'
Directory: require '../src/directory' Directory: Directory
File: require '../src/file' File: File
fs: require 'fs-plus' fs: require 'fs-plus'
Git: require '../src/git' Git: require '../src/git'
Point: Point Point: Point

View File

@ -39,7 +39,7 @@
"nslog": "0.5.0", "nslog": "0.5.0",
"oniguruma": "1.x", "oniguruma": "1.x",
"optimist": "0.4.0", "optimist": "0.4.0",
"pathwatcher": "0.14.2", "pathwatcher": "0.16.0",
"pegjs": "0.8.0", "pegjs": "0.8.0",
"property-accessors": "1.x", "property-accessors": "1.x",
"q": "1.0.x", "q": "1.0.x",
@ -51,7 +51,7 @@
"serializable": "1.x", "serializable": "1.x",
"space-pen": "3.1.1", "space-pen": "3.1.1",
"temp": "0.5.0", "temp": "0.5.0",
"text-buffer": "1.x", "text-buffer": ">=1.1.1 <2.0",
"theorist": "1.x", "theorist": "1.x",
"underscore-plus": "1.x", "underscore-plus": "1.x",
"vm-compatibility-layer": "0.1.0" "vm-compatibility-layer": "0.1.0"
@ -97,7 +97,7 @@
"spell-check": "0.25.0", "spell-check": "0.25.0",
"status-bar": "0.32.0", "status-bar": "0.32.0",
"styleguide": "0.24.0", "styleguide": "0.24.0",
"symbols-view": "0.35.0", "symbols-view": "0.36.0",
"tabs": "0.19.0", "tabs": "0.19.0",
"terminal": "0.27.0", "terminal": "0.27.0",
"timecop": "0.13.0", "timecop": "0.13.0",

View File

@ -1,138 +0,0 @@
Directory = require '../src/directory'
{fs} = require 'atom'
path = require 'path'
describe "Directory", ->
directory = null
beforeEach ->
directory = new Directory(path.join(__dirname, 'fixtures'))
afterEach ->
directory.off()
describe "when the contents of the directory change on disk", ->
temporaryFilePath = null
beforeEach ->
temporaryFilePath = path.join(__dirname, 'fixtures', 'temporary')
fs.removeSync(temporaryFilePath) if fs.existsSync(temporaryFilePath)
afterEach ->
fs.removeSync(temporaryFilePath) if fs.existsSync(temporaryFilePath)
it "triggers 'contents-changed' event handlers", ->
changeHandler = null
runs ->
changeHandler = jasmine.createSpy('changeHandler')
directory.on 'contents-changed', changeHandler
fs.writeFileSync(temporaryFilePath, '')
waitsFor "first change", -> changeHandler.callCount > 0
runs ->
changeHandler.reset()
fs.removeSync(temporaryFilePath)
waitsFor "second change", -> changeHandler.callCount > 0
describe "when the directory unsubscribes from events", ->
temporaryFilePath = null
beforeEach ->
temporaryFilePath = path.join(directory.path, 'temporary')
fs.removeSync(temporaryFilePath) if fs.existsSync(temporaryFilePath)
afterEach ->
fs.removeSync(temporaryFilePath) if fs.existsSync(temporaryFilePath)
it "no longer triggers events", ->
changeHandler = null
runs ->
changeHandler = jasmine.createSpy('changeHandler')
directory.on 'contents-changed', changeHandler
fs.writeFileSync(temporaryFilePath, '')
waitsFor "change event", -> changeHandler.callCount > 0
runs ->
changeHandler.reset()
directory.off()
waits 20
runs -> fs.removeSync(temporaryFilePath)
waits 20
runs -> expect(changeHandler.callCount).toBe 0
describe "on #darwin or #linux", ->
it "includes symlink information about entries", ->
entries = directory.getEntriesSync()
for entry in entries
name = entry.getBaseName()
if name is 'symlink-to-dir' or name is 'symlink-to-file'
expect(entry.symlink).toBeTruthy()
else
expect(entry.symlink).toBeFalsy()
callback = jasmine.createSpy('getEntries')
directory.getEntries(callback)
waitsFor -> callback.callCount is 1
runs ->
entries = callback.mostRecentCall.args[1]
for entry in entries
name = entry.getBaseName()
if name is 'symlink-to-dir' or name is 'symlink-to-file'
expect(entry.symlink).toBeTruthy()
else
expect(entry.symlink).toBeFalsy()
describe ".relativize(path)", ->
describe "on #darwin or #linux", ->
it "returns a relative path based on the directory's path", ->
absolutePath = directory.getPath()
expect(directory.relativize(absolutePath)).toBe ''
expect(directory.relativize(path.join(absolutePath, "b"))).toBe "b"
expect(directory.relativize(path.join(absolutePath, "b/file.coffee"))).toBe "b/file.coffee"
expect(directory.relativize(path.join(absolutePath, "file.coffee"))).toBe "file.coffee"
it "returns a relative path based on the directory's symlinked source path", ->
symlinkPath = path.join(__dirname, 'fixtures', 'symlink-to-dir')
symlinkDirectory = new Directory(symlinkPath)
realFilePath = require.resolve('./fixtures/dir/a')
expect(symlinkDirectory.relativize(symlinkPath)).toBe ''
expect(symlinkDirectory.relativize(realFilePath)).toBe 'a'
it "returns the full path if the directory's path is not a prefix of the path", ->
expect(directory.relativize('/not/relative')).toBe '/not/relative'
describe "on #win32", ->
it "returns a relative path based on the directory's path", ->
absolutePath = directory.getPath()
expect(directory.relativize(absolutePath)).toBe ''
expect(directory.relativize(path.join(absolutePath, "b"))).toBe "b"
expect(directory.relativize(path.join(absolutePath, "b/file.coffee"))).toBe "b\\file.coffee"
expect(directory.relativize(path.join(absolutePath, "file.coffee"))).toBe "file.coffee"
it "returns the full path if the directory's path is not a prefix of the path", ->
expect(directory.relativize('/not/relative')).toBe "\\not\\relative"
describe ".contains(path)", ->
it "returns true if the path is a child of the directory's path", ->
absolutePath = directory.getPath()
expect(directory.contains(path.join(absolutePath, "b"))).toBe true
expect(directory.contains(path.join(absolutePath, "b", "file.coffee"))).toBe true
expect(directory.contains(path.join(absolutePath, "file.coffee"))).toBe true
it "returns false if the directory's path is not a prefix of the path", ->
expect(directory.contains('/not/relative')).toBe false
describe "on #darwin or #linux", ->
it "returns true if the path is a child of the directory's symlinked source path", ->
symlinkPath = path.join(__dirname, 'fixtures', 'symlink-to-dir')
symlinkDirectory = new Directory(symlinkPath)
realFilePath = require.resolve('./fixtures/dir/a')
expect(symlinkDirectory.contains(realFilePath)).toBe true

View File

@ -1,132 +0,0 @@
{File, fs} = require 'atom'
path = require 'path'
describe 'File', ->
[filePath, file] = []
beforeEach ->
filePath = path.join(__dirname, 'fixtures', 'atom-file-test.txt') # Don't put in /tmp because /tmp symlinks to /private/tmp and screws up the rename test
fs.removeSync(filePath) if fs.existsSync(filePath)
fs.writeFileSync(filePath, "this is old!")
file = new File(filePath)
afterEach ->
file.off()
fs.removeSync(filePath) if fs.existsSync(filePath)
describe "when the file has not been read", ->
describe "when the contents of the file change", ->
it "triggers 'contents-changed' event handlers", ->
file.on 'contents-changed', changeHandler = jasmine.createSpy('changeHandler')
fs.writeFileSync(file.getPath(), "this is new!")
waitsFor "change event", ->
changeHandler.callCount > 0
describe "when the file has already been read", ->
beforeEach ->
file.readSync()
describe "when the contents of the file change", ->
it "triggers 'contents-changed' event handlers", ->
changeHandler = null
changeHandler = jasmine.createSpy('changeHandler')
file.on 'contents-changed', changeHandler
fs.writeFileSync(file.getPath(), "this is new!")
waitsFor "change event", ->
changeHandler.callCount > 0
runs ->
changeHandler.reset()
fs.writeFileSync(file.getPath(), "this is newer!")
waitsFor "second change event", ->
changeHandler.callCount > 0
describe "when the file is removed", ->
it "triggers 'remove' event handlers", ->
removeHandler = null
removeHandler = jasmine.createSpy('removeHandler')
file.on 'removed', removeHandler
fs.removeSync(file.getPath())
waitsFor "remove event", ->
removeHandler.callCount > 0
describe "when a file is moved (via the filesystem)", ->
newPath = null
beforeEach ->
newPath = path.join(path.dirname(filePath), "atom-file-was-moved-test.txt")
afterEach ->
if fs.existsSync(newPath)
fs.removeSync(newPath)
waitsFor "remove event", 30000, (done) -> file.on 'removed', done
it "it updates its path", ->
jasmine.unspy(window, "setTimeout")
moveHandler = null
moveHandler = jasmine.createSpy('moveHandler')
file.on 'moved', moveHandler
fs.moveSync(filePath, newPath)
waitsFor "move event", 30000, ->
moveHandler.callCount > 0
runs ->
expect(file.getPath()).toBe newPath
it "maintains 'contents-changed' events set on previous path", ->
jasmine.unspy(window, "setTimeout")
moveHandler = null
moveHandler = jasmine.createSpy('moveHandler')
file.on 'moved', moveHandler
changeHandler = null
changeHandler = jasmine.createSpy('changeHandler')
file.on 'contents-changed', changeHandler
fs.moveSync(filePath, newPath)
waitsFor "move event", ->
moveHandler.callCount > 0
runs ->
expect(changeHandler).not.toHaveBeenCalled()
fs.writeFileSync(file.getPath(), "this is new!")
waitsFor "change event", ->
changeHandler.callCount > 0
describe "when a file is deleted and the recreated within a small amount of time (git sometimes does this)", ->
it "triggers a contents change event if the contents change", ->
jasmine.unspy(File.prototype, 'detectResurrectionAfterDelay')
jasmine.unspy(window, "setTimeout")
changeHandler = jasmine.createSpy("file changed")
removeHandler = jasmine.createSpy("file removed")
file.on 'contents-changed', changeHandler
file.on 'removed', removeHandler
expect(changeHandler).not.toHaveBeenCalled()
fs.removeSync(filePath)
expect(changeHandler).not.toHaveBeenCalled()
waits 20
runs ->
fs.writeFileSync(filePath, "HE HAS RISEN!")
expect(changeHandler).not.toHaveBeenCalled()
waitsFor "resurrection change event", ->
changeHandler.callCount == 1
runs ->
expect(removeHandler).not.toHaveBeenCalled()
fs.writeFileSync(filePath, "Hallelujah!")
changeHandler.reset()
waitsFor "post-resurrection change event", ->
changeHandler.callCount > 0

View File

@ -164,7 +164,7 @@ describe "PaneView", ->
expect(pane.items).toHaveLength(5) expect(pane.items).toHaveLength(5)
fs.removeSync(filePath) fs.removeSync(filePath)
waitsFor -> waitsFor 30000, ->
pane.items.length == 4 pane.items.length == 4
describe "when a pane is destroyed", -> describe "when a pane is destroyed", ->

View File

@ -1,7 +1,7 @@
{times, random} = require 'underscore-plus' {times, random} = require 'underscore-plus'
randomWords = require 'random-words' randomWords = require 'random-words'
TextBuffer = require 'text-buffer'
Editor = require '../src/editor' Editor = require '../src/editor'
TextBuffer = require '../src/text-buffer'
describe "Editor", -> describe "Editor", ->
[editor, tokenizedBuffer, buffer, steps, previousSteps] = [] [editor, tokenizedBuffer, buffer, steps, previousSteps] = []

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ describe "Workspace", ->
workspace.open('a').then (o) -> editor = o workspace.open('a').then (o) -> editor = o
runs -> runs ->
expect(editor.getUri()).toBe 'a' expect(editor.getUri()).toBe atom.project.resolve('a')
expect(workspace.activePaneItem).toBe editor expect(workspace.activePaneItem).toBe editor
expect(workspace.activePane.items).toEqual [editor] expect(workspace.activePane.items).toEqual [editor]
expect(workspace.activePane.activate).toHaveBeenCalled() expect(workspace.activePane.activate).toHaveBeenCalled()
@ -199,23 +199,23 @@ describe "Workspace", ->
expect(workspace.activePaneItem.getUri()).not.toBeUndefined() expect(workspace.activePaneItem.getUri()).not.toBeUndefined()
# destroy all items # destroy all items
expect(workspace.activePaneItem.getUri()).toBe 'file1' expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('file1')
pane.destroyActiveItem() pane.destroyActiveItem()
expect(workspace.activePaneItem.getUri()).toBe 'b' expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('b')
pane.destroyActiveItem() pane.destroyActiveItem()
expect(workspace.activePaneItem.getUri()).toBe 'a' expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('a')
pane.destroyActiveItem() pane.destroyActiveItem()
# reopens items with uris # reopens items with uris
expect(workspace.activePaneItem).toBeUndefined() expect(workspace.activePaneItem).toBeUndefined()
workspace.reopenItemSync() workspace.reopenItemSync()
expect(workspace.activePaneItem.getUri()).toBe 'a' expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('a')
# does not reopen items that are already open # does not reopen items that are already open
workspace.openSync('b') workspace.openSync('b')
expect(workspace.activePaneItem.getUri()).toBe 'b' expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('b')
workspace.reopenItemSync() workspace.reopenItemSync()
expect(workspace.activePaneItem.getUri()).toBe 'file1' expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('file1')
describe "::increase/decreaseFontSize()", -> describe "::increase/decreaseFontSize()", ->
it "increases/decreases the font size without going below 1", -> it "increases/decreases the font size without going below 1", ->

View File

@ -161,7 +161,8 @@ class Atom extends Model
@subscribe @packages, 'activated', => @watchThemes() @subscribe @packages, 'activated', => @watchThemes()
Project = require './project' Project = require './project'
TextBuffer = require './text-buffer' TextBuffer = require 'text-buffer'
@deserializers.add(TextBuffer)
TokenizedBuffer = require './tokenized-buffer' TokenizedBuffer = require './tokenized-buffer'
DisplayBuffer = require './display-buffer' DisplayBuffer = require './display-buffer'
Editor = require './editor' Editor = require './editor'

View File

@ -1,146 +0,0 @@
path = require 'path'
async = require 'async'
{Emitter} = require 'emissary'
fs = require 'fs-plus'
pathWatcher = require 'pathwatcher'
File = require './file'
# Public: Represents a directory on disk.
#
# ## Requiring in packages
#
# ```coffee
# {Directory} = require 'atom'
# ```
module.exports =
class Directory
Emitter.includeInto(this)
realPath: null
# Public: Configures a new Directory instance, no files are accessed.
#
# path - A {String} containing the absolute path to the directory.
# symlink - A {Boolean} indicating if the path is a symlink (default: false).
constructor: (@path, @symlink=false) ->
@on 'first-contents-changed-subscription-will-be-added', =>
# Triggered by emissary, when a new contents-changed listener attaches
@subscribeToNativeChangeEvents()
@on 'last-contents-changed-subscription-removed', =>
# Triggered by emissary, when the last contents-changed listener detaches
@unsubscribeFromNativeChangeEvents()
# Public: Returns the {String} basename of the directory.
getBaseName: ->
path.basename(@path)
# Public: Returns the directory's symbolic path.
#
# This may include unfollowed symlinks or relative directory entries. Or it
# may be fully resolved, it depends on what you give it.
getPath: -> @path
# Public: Returns this directory's completely resolved path.
#
# All relative directory entries are removed and symlinks are resolved to
# their final destination.
getRealPathSync: ->
unless @realPath?
try
@realPath = fs.realpathSync(@path)
catch e
@realPath = @path
@realPath
# Public: Returns whether the given path (real or symbolic) is inside this
# directory.
contains: (pathToCheck) ->
return false unless pathToCheck
if pathToCheck.indexOf(path.join(@getPath(), path.sep)) is 0
true
else if pathToCheck.indexOf(path.join(@getRealPathSync(), path.sep)) is 0
true
else
false
# Public: Returns the relative path to the given path from this directory.
relativize: (fullPath) ->
return fullPath unless fullPath
# Normalize forward slashes to back slashes on windows
fullPath = fullPath.replace(/\//g, '\\') if process.platform is 'win32'
if fullPath is @getPath()
''
else if @isPathPrefixOf(@getPath(), fullPath)
fullPath.substring(@getPath().length + 1)
else if fullPath is @getRealPathSync()
''
else if @isPathPrefixOf(@getRealPathSync(), fullPath)
fullPath.substring(@getRealPathSync().length + 1)
else
fullPath
# Public: Reads file entries in this directory from disk synchronously.
#
# Returns an {Array} of {File} and {Directory} objects.
getEntriesSync: ->
directories = []
files = []
for entryPath in fs.listSync(@path)
if stat = fs.lstatSyncNoException(entryPath)
symlink = stat.isSymbolicLink()
stat = fs.statSyncNoException(entryPath) if symlink
continue unless stat
if stat.isDirectory()
directories.push(new Directory(entryPath, symlink))
else if stat.isFile()
files.push(new File(entryPath, symlink))
directories.concat(files)
# Public: Reads file entries in this directory from disk asynchronously.
#
# callback - A {Function} to call with an {Error} as the 1st argument and
# an {Array} of {File} and {Directory} objects as the 2nd argument.
getEntries: (callback) ->
fs.list @path, (error, entries) ->
return callback(error) if error?
directories = []
files = []
addEntry = (entryPath, stat, symlink, callback) ->
if stat?.isDirectory()
directories.push(new Directory(entryPath, symlink))
else if stat?.isFile()
files.push(new File(entryPath, symlink))
callback()
statEntry = (entryPath, callback) ->
fs.lstat entryPath, (error, stat) ->
if stat?.isSymbolicLink()
fs.stat entryPath, (error, stat) ->
addEntry(entryPath, stat, true, callback)
else
addEntry(entryPath, stat, false, callback)
async.eachLimit entries, 1, statEntry, ->
callback(null, directories.concat(files))
subscribeToNativeChangeEvents: ->
unless @watchSubscription?
@watchSubscription = pathWatcher.watch @path, (eventType) =>
@emit "contents-changed" if eventType is "change"
unsubscribeFromNativeChangeEvents: ->
if @watchSubscription?
@watchSubscription.close()
@watchSubscription = null
# Does given full path start with the given prefix?
isPathPrefixOf: (prefix, fullPath) ->
fullPath.indexOf(prefix) is 0 and fullPath[prefix.length] is path.sep

View File

@ -1,5 +1,4 @@
{View, $, $$$} = require './space-pen-extensions' {View, $, $$$} = require './space-pen-extensions'
TextBuffer = require './text-buffer'
GutterView = require './gutter-view' GutterView = require './gutter-view'
{Point, Range} = require 'text-buffer' {Point, Range} = require 'text-buffer'
Editor = require './editor' Editor = require './editor'
@ -7,6 +6,7 @@ CursorView = require './cursor-view'
SelectionView = require './selection-view' SelectionView = require './selection-view'
fs = require 'fs-plus' fs = require 'fs-plus'
_ = require 'underscore-plus' _ = require 'underscore-plus'
TextBuffer = require 'text-buffer'
MeasureRange = document.createRange() MeasureRange = document.createRange()
TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }

View File

@ -1,172 +0,0 @@
crypto = require 'crypto'
path = require 'path'
pathWatcher = require 'pathwatcher'
Q = require 'q'
{Emitter} = require 'emissary'
_ = require 'underscore-plus'
fs = require 'fs-plus'
runas = require 'runas'
# Public: Represents an individual file.
#
# You should probably create a {Directory} and access the {File} objects that
# it creates, rather than instantiating the {File} class directly.
#
# ## Requiring in packages
#
# ```coffee
# {File} = require 'atom'
# ```
module.exports =
class File
Emitter.includeInto(this)
path: null
cachedContents: null
# Public: Creates a new file.
#
# path - A {String} containing the absolute path to the file
# symlink - A {Boolean} indicating if the path is a symlink (default: false).
constructor: (@path, @symlink=false) ->
throw new Error("#{@path} is a directory") if fs.isDirectorySync(@path)
@handleEventSubscriptions()
# Subscribes to file system notifications when necessary.
handleEventSubscriptions: ->
eventNames = ['contents-changed', 'moved', 'removed']
subscriptionsAdded = eventNames.map (eventName) -> "first-#{eventName}-subscription-will-be-added"
@on subscriptionsAdded.join(' '), =>
# Only subscribe when a listener of eventName attaches (triggered by emissary)
@subscribeToNativeChangeEvents() if @exists()
subscriptionsRemoved = eventNames.map (eventName) -> "last-#{eventName}-subscription-removed"
@on subscriptionsRemoved.join(' '), =>
# Detach when the last listener of eventName detaches (triggered by emissary)
subscriptionsEmpty = _.every eventNames, (eventName) => @getSubscriptionCount(eventName) is 0
@unsubscribeFromNativeChangeEvents() if subscriptionsEmpty
# Sets the path for the file.
setPath: (@path) ->
# Public: Returns the {String} path for the file.
getPath: -> @path
# Public: Return the {String} filename without any directory information.
getBaseName: ->
path.basename(@path)
# Public: Overwrites the file with the given String.
write: (text) ->
previouslyExisted = @exists()
@writeFileWithPrivilegeEscalationSync(@getPath(), text)
@cachedContents = text
@subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions()
# Deprecated
readSync: (flushCache) ->
if not @exists()
@cachedContents = null
else if not @cachedContents? or flushCache
@cachedContents = fs.readFileSync(@getPath(), 'utf8')
else
@cachedContents
@setDigest(@cachedContents)
@cachedContents
# Public: Reads the contents of the file.
#
# flushCache - A {Boolean} indicating whether to require a direct read or if
# a cached copy is acceptable.
#
# Returns a promise that resovles to a String.
read: (flushCache) ->
if not @exists()
promise = Q(null)
else if not @cachedContents? or flushCache
if fs.getSizeSync(@getPath()) >= 1048576 # 1MB
throw new Error("Atom can only handle files < 1MB, for now.")
deferred = Q.defer()
promise = deferred.promise
content = []
bytesRead = 0
readStream = fs.createReadStream @getPath(), encoding: 'utf8'
readStream.on 'data', (chunk) ->
content.push(chunk)
bytesRead += chunk.length
deferred.notify(bytesRead)
readStream.on 'end', ->
deferred.resolve(content.join(''))
readStream.on 'error', (error) ->
deferred.reject(error)
else
promise = Q(@cachedContents)
promise.then (contents) =>
@setDigest(contents)
@cachedContents = contents
# Public: Returns whether the file exists.
exists: ->
fs.existsSync(@getPath())
setDigest: (contents) ->
@digest = crypto.createHash('sha1').update(contents ? '').digest('hex')
# Public: Get the SHA-1 digest of this file
getDigest: ->
@digest ? @setDigest(@readSync())
# Writes the text to specified path.
#
# Privilege escalation would be asked when current user doesn't have
# permission to the path.
writeFileWithPrivilegeEscalationSync: (path, text) ->
try
fs.writeFileSync(path, text)
catch error
if error.code is 'EACCES' and process.platform is 'darwin'
authopen = '/usr/libexec/authopen' # man 1 authopen
unless runas(authopen, ['-w', '-c', path], stdin: text) is 0
throw error
else
throw error
handleNativeChangeEvent: (eventType, path) ->
if eventType is "delete"
@unsubscribeFromNativeChangeEvents()
@detectResurrectionAfterDelay()
else if eventType is "rename"
@setPath(path)
@emit "moved"
else if eventType is "change"
oldContents = @cachedContents
@read(true).done (newContents) =>
@emit 'contents-changed' unless oldContents == newContents
detectResurrectionAfterDelay: ->
_.delay (=> @detectResurrection()), 50
detectResurrection: ->
if @exists()
@subscribeToNativeChangeEvents()
@handleNativeChangeEvent("change", @getPath())
else
@cachedContents = null
@emit "removed"
subscribeToNativeChangeEvents: ->
unless @watchSubscription?
@watchSubscription = pathWatcher.watch @path, (eventType, path) =>
@handleNativeChangeEvent(eventType, path)
unsubscribeFromNativeChangeEvents: ->
if @watchSubscription?
@watchSubscription.close()
@watchSubscription = null

View File

@ -4,7 +4,7 @@ fs = require 'fs-plus'
path = require 'path' path = require 'path'
CSON = require 'season' CSON = require 'season'
KeyBinding = require './key-binding' KeyBinding = require './key-binding'
File = require './file' {File} = require 'pathwatcher'
{Emitter} = require 'emissary' {Emitter} = require 'emissary'
Modifiers = ['alt', 'control', 'ctrl', 'shift', 'cmd'] Modifiers = ['alt', 'control', 'ctrl', 'shift', 'cmd']

View File

@ -7,10 +7,10 @@ Q = require 'q'
{Model} = require 'theorist' {Model} = require 'theorist'
{Emitter, Subscriber} = require 'emissary' {Emitter, Subscriber} = require 'emissary'
Serializable = require 'serializable' Serializable = require 'serializable'
TextBuffer = require 'text-buffer'
{Directory} = require 'pathwatcher'
TextBuffer = require './text-buffer'
Editor = require './editor' Editor = require './editor'
Directory = require './directory'
Task = require './task' Task = require './task'
Git = require './git' Git = require './git'
@ -94,7 +94,7 @@ class Project extends Model
# #
# uri - The {String} name of the path to convert. # uri - The {String} name of the path to convert.
# #
# Returns a String. # Returns a {String} or undefined if the uri is not missing or empty.
resolve: (uri) -> resolve: (uri) ->
return unless uri return unless uri
@ -102,7 +102,7 @@ class Project extends Model
uri uri
else else
uri = path.join(@getPath(), uri) unless fs.isAbsolute(uri) uri = path.join(@getPath(), uri) unless fs.isAbsolute(uri)
fs.absolute uri fs.absolute(uri)
# Public: Make the given path relative to the project directory. # Public: Make the given path relative to the project directory.
relativize: (fullPath) -> relativize: (fullPath) ->
@ -189,6 +189,9 @@ class Project extends Model
# #
# Returns a promise that resolves to the {TextBuffer}. # Returns a promise that resolves to the {TextBuffer}.
buildBuffer: (absoluteFilePath) -> buildBuffer: (absoluteFilePath) ->
if fs.getSizeSync(absoluteFilePath) >= 1048576 # 1MB
throw new Error("Atom can only handle files < 1MB, for now.")
buffer = new TextBuffer({filePath: absoluteFilePath}) buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer) @addBuffer(buffer)
buffer.load() buffer.load()

View File

@ -1,405 +0,0 @@
_ = require 'underscore-plus'
Q = require 'q'
{P} = require 'scandal'
Serializable = require 'serializable'
TextBufferCore = require 'text-buffer'
{Point, Range} = TextBufferCore
{Subscriber, Emitter} = require 'emissary'
File = require './file'
# Represents the contents of a file.
#
# The `TextBuffer` is often associated with a {File}. However, this is not always
# the case, as a `TextBuffer` could be an unsaved chunk of text.
module.exports =
class TextBuffer extends TextBufferCore
atom.deserializers.add(this)
Serializable.includeInto(this)
Subscriber.includeInto(this)
Emitter.includeInto(this)
stoppedChangingDelay: 300
stoppedChangingTimeout: null
cachedDiskContents: null
conflict: false
file: null
refcount: 0
constructor: ({filePath, @modifiedWhenLastPersisted, @digestWhenLastPersisted, loadWhenAttached}={}) ->
super
@loaded = false
@modifiedWhenLastPersisted ?= false
@useSerializedText = @modifiedWhenLastPersisted != false
@subscribe this, 'changed', @handleTextChange
@setPath(filePath)
@load() if loadWhenAttached
serializeParams: ->
params = super
_.extend params,
filePath: @getPath()
modifiedWhenLastPersisted: @isModified()
digestWhenLastPersisted: @file?.getDigest()
deserializeParams: (params) ->
params = super(params)
params.loadWhenAttached = true
params
loadSync: ->
@updateCachedDiskContentsSync()
@finishLoading()
load: ->
@updateCachedDiskContents().then => @finishLoading()
finishLoading: ->
if @isAlive()
@loaded = true
if @useSerializedText and @digestWhenLastPersisted is @file?.getDigest()
@emitModifiedStatusChanged(true)
else
@reload()
@clearUndoStack()
this
handleTextChange: (event) =>
@conflict = false if @conflict and !@isModified()
@scheduleModifiedEvents()
destroy: ->
unless @destroyed
@cancelStoppedChangingTimeout()
@file?.off()
@unsubscribe()
@destroyed = true
@emit 'destroyed'
isAlive: -> not @destroyed
isDestroyed: -> @destroyed
isRetained: -> @refcount > 0
retain: ->
@refcount++
this
release: ->
@refcount--
@destroy() unless @isRetained()
this
subscribeToFile: ->
@file.on "contents-changed", =>
@conflict = true if @isModified()
previousContents = @cachedDiskContents
# Synchrounously update the disk contents because the {File} has already cached them. If the
# contents updated asynchrounously multiple `conlict` events could trigger for the same disk
# contents.
@updateCachedDiskContentsSync()
return if previousContents == @cachedDiskContents
if @conflict
@emit "contents-conflicted"
else
@reload()
@file.on "removed", =>
modified = @getText() != @cachedDiskContents
@wasModifiedBeforeRemove = modified
if modified
@updateCachedDiskContents()
else
@destroy()
@file.on "moved", =>
@emit "path-changed", this
# Identifies if the buffer belongs to multiple editors.
#
# For example, if the {EditorView} was split.
#
# Returns a {Boolean}.
hasMultipleEditors: -> @refcount > 1
# Reloads a file in the {Editor}.
#
# Sets the buffer's content to the cached disk contents
reload: ->
@emit 'will-reload'
@setTextViaDiff(@cachedDiskContents)
@emitModifiedStatusChanged(false)
@emit 'reloaded'
# Rereads the contents of the file, and stores them in the cache.
updateCachedDiskContentsSync: ->
@cachedDiskContents = @file?.readSync() ? ""
# Rereads the contents of the file, and stores them in the cache.
updateCachedDiskContents: ->
Q(@file?.read() ? "").then (contents) =>
@cachedDiskContents = contents
# Gets the file's basename--that is, the file without any directory information.
#
# Returns a {String}.
getBaseName: ->
@file?.getBaseName()
# Retrieves the path for the file.
#
# Returns a {String}.
getPath: ->
@file?.getPath()
getUri: ->
atom.project.relativize(@getPath())
# Sets the path for the file.
#
# filePath - A {String} representing the new file path
setPath: (filePath) ->
return if filePath == @getPath()
@file?.off()
if filePath
@file = new File(filePath)
@subscribeToFile()
else
@file = null
@emit "path-changed", this
# Deprecated: Use ::getEndPosition instead
getEofPosition: -> @getEndPosition()
# Saves the buffer.
save: ->
@saveAs(@getPath()) if @isModified()
# Saves the buffer at a specific path.
#
# filePath - The path to save at.
saveAs: (filePath) ->
unless filePath then throw new Error("Can't save buffer with no file path")
@emit 'will-be-saved', this
@setPath(filePath)
@file.write(@getText())
@cachedDiskContents = @getText()
@conflict = false
@emitModifiedStatusChanged(false)
@emit 'saved', this
# Identifies if the buffer was modified.
#
# Returns a {Boolean}.
isModified: ->
return false unless @loaded
if @file
if @file.exists()
@getText() != @cachedDiskContents
else
@wasModifiedBeforeRemove ? not @isEmpty()
else
not @isEmpty()
# Is the buffer's text in conflict with the text on disk?
#
# This occurs when the buffer's file changes on disk while the buffer has
# unsaved changes.
#
# Returns a {Boolean}.
isInConflict: -> @conflict
destroyMarker: (id) ->
@getMarker(id)?.destroy()
# Identifies if a character sequence is within a certain range.
#
# regex - The {RegExp} to check
# startIndex - The starting row {Number}
# endIndex - The ending row {Number}
#
# Returns an {Array} of {RegExp}s, representing the matches.
matchesInCharacterRange: (regex, startIndex, endIndex) ->
text = @getText()
matches = []
regex.lastIndex = startIndex
while match = regex.exec(text)
matchLength = match[0].length
matchStartIndex = match.index
matchEndIndex = matchStartIndex + matchLength
if matchEndIndex > endIndex
regex.lastIndex = 0
if matchStartIndex < endIndex and submatch = regex.exec(text[matchStartIndex...endIndex])
submatch.index = matchStartIndex
matches.push submatch
break
matchEndIndex++ if matchLength is 0
regex.lastIndex = matchEndIndex
matches.push match
matches
# Scans for text in the buffer, calling a function on each match.
#
# regex - A {RegExp} representing the text to find
# iterator - A {Function} that's called on each match
scan: (regex, iterator) ->
@scanInRange regex, @getRange(), (result) =>
result.lineText = @lineForRow(result.range.start.row)
result.lineTextOffset = 0
iterator(result)
# Replace all matches of regex with replacementText
#
# regex: A {RegExp} representing the text to find
# replacementText: A {String} representing the text to replace
#
# Returns the number of replacements made
replace: (regex, replacementText) ->
doSave = !@isModified()
replacements = 0
@transact =>
@scan regex, ({matchText, replace}) ->
replace(matchText.replace(regex, replacementText))
replacements++
@save() if doSave
replacements
# Scans for text in a given range, calling a function on each match.
#
# regex - A {RegExp} representing the text to find
# range - A {Range} in the buffer to search within
# iterator - A {Function} that's called on each match
# reverse - A {Boolean} indicating if the search should be backwards (default: `false`)
scanInRange: (regex, range, iterator, reverse=false) ->
range = @clipRange(range)
global = regex.global
flags = "gm"
flags += "i" if regex.ignoreCase
regex = new RegExp(regex.source, flags)
startIndex = @characterIndexForPosition(range.start)
endIndex = @characterIndexForPosition(range.end)
matches = @matchesInCharacterRange(regex, startIndex, endIndex)
lengthDelta = 0
keepLooping = null
replacementText = null
stop = -> keepLooping = false
replace = (text) -> replacementText = text
matches.reverse() if reverse
for match in matches
matchLength = match[0].length
matchStartIndex = match.index
matchEndIndex = matchStartIndex + matchLength
startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
range = new Range(startPosition, endPosition)
keepLooping = true
replacementText = null
matchText = match[0]
iterator({ match, matchText, range, stop, replace })
if replacementText?
@change(range, replacementText)
lengthDelta += replacementText.length - matchLength unless reverse
break unless global and keepLooping
# Scans for text in a given range _backwards_, calling a function on each match.
#
# regex - A {RegExp} representing the text to find
# range - A {Range} in the buffer to search within
# iterator - A {Function} that's called on each match
backwardsScanInRange: (regex, range, iterator) ->
@scanInRange regex, range, iterator, true
# Given a row, identifies if it is blank.
#
# row - A row {Number} to check
#
# Returns a {Boolean}.
isRowBlank: (row) ->
not /\S/.test @lineForRow(row)
# Given a row, this finds the next row above it that's empty.
#
# startRow - A {Number} identifying the row to start checking at
#
# Returns the row {Number} of the first blank row.
# Returns `null` if there's no other blank row.
previousNonBlankRow: (startRow) ->
return null if startRow == 0
startRow = Math.min(startRow, @getLastRow())
for row in [(startRow - 1)..0]
return row unless @isRowBlank(row)
null
# Given a row, this finds the next row that's blank.
#
# startRow - A row {Number} to check
#
# Returns the row {Number} of the next blank row.
# Returns `null` if there's no other blank row.
nextNonBlankRow: (startRow) ->
lastRow = @getLastRow()
if startRow < lastRow
for row in [(startRow + 1)..lastRow]
return row unless @isRowBlank(row)
null
# Identifies if the buffer has soft tabs anywhere.
#
# Returns a {Boolean},
usesSoftTabs: ->
for row in [0..@getLastRow()]
if match = @lineForRow(row).match(/^\s/)
return match[0][0] != '\t'
undefined
change: (oldRange, newText, options={}) ->
@setTextInRange(oldRange, newText, options.normalizeLineEndings)
cancelStoppedChangingTimeout: ->
clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout
scheduleModifiedEvents: ->
@cancelStoppedChangingTimeout()
stoppedChangingCallback = =>
@stoppedChangingTimeout = null
modifiedStatus = @isModified()
@emit 'contents-modified', modifiedStatus
@emitModifiedStatusChanged(modifiedStatus)
@stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay)
emitModifiedStatusChanged: (modifiedStatus) ->
return if modifiedStatus is @previousModifiedStatus
@previousModifiedStatus = modifiedStatus
@emit 'modified-status-changed', modifiedStatus
logLines: (start=0, end=@getLastRow())->
for row in [start..end]
line = @lineForRow(row)
console.log row, line, line.length

View File

@ -7,7 +7,7 @@ Q = require 'q'
{$} = require './space-pen-extensions' {$} = require './space-pen-extensions'
Package = require './package' Package = require './package'
File = require './file' {File} = require 'pathwatcher'
# Public: Handles loading and activating available themes. # Public: Handles loading and activating available themes.
# #

View File

@ -72,10 +72,10 @@ class Workspace extends Model
# if the uri is already open (default: false) # if the uri is already open (default: false)
# #
# Returns a promise that resolves to the {Editor} for the file URI. # Returns a promise that resolves to the {Editor} for the file URI.
open: (uri, options={}) -> open: (uri='', options={}) ->
searchAllPanes = options.searchAllPanes searchAllPanes = options.searchAllPanes
split = options.split split = options.split
uri = atom.project.relativize(uri) ? '' uri = atom.project.resolve(uri)
pane = switch split pane = switch split
when 'left' when 'left'
@ -91,15 +91,15 @@ class Workspace extends Model
@openUriInPane(uri, pane, options) @openUriInPane(uri, pane, options)
# Only used in specs # Only used in specs
openSync: (uri, options={}) -> openSync: (uri='', options={}) ->
{initialLine} = options {initialLine} = options
# TODO: Remove deprecated changeFocus option # TODO: Remove deprecated changeFocus option
activatePane = options.activatePane ? options.changeFocus ? true activatePane = options.activatePane ? options.changeFocus ? true
uri = atom.project.relativize(uri) ? '' uri = atom.project.resolve(uri)
item = @activePane.itemForUri(uri) item = @activePane.itemForUri(uri)
if uri if uri
item ?= opener(atom.project.resolve(uri), options) for opener in @getOpeners() when !item item ?= opener(uri, options) for opener in @getOpeners() when !item
item ?= atom.project.openSync(uri, {initialLine}) item ?= atom.project.openSync(uri, {initialLine})
@activePane.activateItem(item) @activePane.activateItem(item)