mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-20 07:28:08 +03:00
Merge pull request #1553 from atom/ks-extract-text-buffer
Extract TextBuffer
This commit is contained in:
commit
d87393f21e
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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", ->
|
||||||
|
@ -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
@ -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", ->
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
|
@ -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 }
|
||||||
|
172
src/file.coffee
172
src/file.coffee
@ -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
|
|
@ -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']
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user