mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge pull request #11828 from atom/as-file-recovery-service
File Recovery Service
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
"event-kit": "^1.5.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"first-mate": "^5.1.1",
|
||||
"fs-plus": "^2.8.0",
|
||||
"fs-plus": "2.9.1",
|
||||
"fstream": "0.1.24",
|
||||
"fuzzaldrin": "^2.1",
|
||||
"git-utils": "^4.1.2",
|
||||
@@ -55,6 +55,7 @@
|
||||
"season": "^5.3",
|
||||
"semver": "^4.3.3",
|
||||
"service-hub": "^0.7.0",
|
||||
"sinon": "1.17.4",
|
||||
"source-map-support": "^0.3.2",
|
||||
"temp": "0.8.1",
|
||||
"text-buffer": "9.1.0",
|
||||
|
||||
@@ -583,7 +583,7 @@ describe('GitRepositoryAsync', () => {
|
||||
it('subscribes to all the serialized buffers in the project', async () => {
|
||||
await atom.workspace.open('file.txt')
|
||||
|
||||
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate})
|
||||
project2.deserialize(atom.project.serialize({isUnloading: true}))
|
||||
|
||||
const repo = project2.getRepositories()[0].async
|
||||
|
||||
@@ -356,7 +356,7 @@ describe "GitRepository", ->
|
||||
atom.workspace.open('file.txt')
|
||||
|
||||
runs ->
|
||||
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate})
|
||||
project2.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
buffer = project2.getBuffers()[0]
|
||||
|
||||
|
||||
127
spec/main-process/file-recovery-service.spec.js
Normal file
127
spec/main-process/file-recovery-service.spec.js
Normal file
@@ -0,0 +1,127 @@
|
||||
'use babel'
|
||||
|
||||
import {dialog} from 'electron'
|
||||
import FileRecoveryService from '../../src/main-process/file-recovery-service'
|
||||
import temp from 'temp'
|
||||
import fs from 'fs-plus'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe("FileRecoveryService", () => {
|
||||
let recoveryService, recoveryDirectory
|
||||
|
||||
beforeEach(() => {
|
||||
recoveryDirectory = temp.mkdirSync()
|
||||
recoveryService = new FileRecoveryService(recoveryDirectory)
|
||||
})
|
||||
|
||||
describe("when no crash happens during a save", () => {
|
||||
it("creates a recovery file and deletes it after saving", () => {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "some content")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "changed")
|
||||
recoveryService.didSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
|
||||
})
|
||||
|
||||
it("creates only one recovery file when many windows attempt to save the same file, deleting it when the last one finishes saving it", () => {
|
||||
const mockWindow = {}
|
||||
const anotherMockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "some content")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "changed")
|
||||
recoveryService.didSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
|
||||
|
||||
recoveryService.didSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
|
||||
})
|
||||
})
|
||||
|
||||
describe("when a crash happens during a save", () => {
|
||||
it("restores the created recovery file and deletes it", () => {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "some content")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "changed")
|
||||
recoveryService.didCrashWindow(mockWindow)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "some content")
|
||||
})
|
||||
|
||||
it("restores the created recovery file when many windows attempt to save the same file and one of them crashes", () => {
|
||||
const mockWindow = {}
|
||||
const anotherMockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "A")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
fs.writeFileSync(filePath, "B")
|
||||
recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "C")
|
||||
|
||||
recoveryService.didCrashWindow(mockWindow)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "A")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
|
||||
fs.writeFileSync(filePath, "D")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
fs.writeFileSync(filePath, "E")
|
||||
recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "F")
|
||||
|
||||
recoveryService.didCrashWindow(anotherMockWindow)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "D")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
})
|
||||
|
||||
it("emits a warning when a file can't be recovered", sinon.test(function () {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
fs.writeFileSync(filePath, "content")
|
||||
fs.chmodSync(filePath, 0444)
|
||||
|
||||
let logs = []
|
||||
this.stub(console, 'log', (message) => logs.push(message))
|
||||
this.stub(dialog, 'showMessageBox')
|
||||
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
recoveryService.didCrashWindow(mockWindow)
|
||||
let recoveryFiles = fs.listTreeSync(recoveryDirectory)
|
||||
assert.equal(recoveryFiles.length, 1)
|
||||
assert.equal(logs.length, 1)
|
||||
assert.match(logs[0], new RegExp(filePath))
|
||||
assert.match(logs[0], new RegExp(recoveryFiles[0]))
|
||||
}))
|
||||
})
|
||||
|
||||
it("doesn't create a recovery file when the file that's being saved doesn't exist yet", () => {
|
||||
const mockWindow = {}
|
||||
|
||||
recoveryService.willSavePath(mockWindow, "a-file-that-doesnt-exist")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
|
||||
recoveryService.didSavePath(mockWindow, "a-file-that-doesnt-exist")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
})
|
||||
})
|
||||
@@ -112,6 +112,28 @@ describe "Project", ->
|
||||
editor.saveAs(tempFile)
|
||||
expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile)
|
||||
|
||||
describe "before and after saving a buffer", ->
|
||||
[buffer] = []
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) ->
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
|
||||
afterEach ->
|
||||
buffer.release()
|
||||
|
||||
it "emits save events on the main process", ->
|
||||
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
|
||||
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
|
||||
|
||||
buffer.save()
|
||||
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
|
||||
describe "when a watch error is thrown from the TextBuffer", ->
|
||||
editor = null
|
||||
beforeEach ->
|
||||
|
||||
@@ -25,7 +25,7 @@ describe "Workspace", ->
|
||||
projectState = atom.project.serialize({isUnloading: true})
|
||||
atom.workspace.destroy()
|
||||
atom.project.destroy()
|
||||
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)})
|
||||
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate})
|
||||
atom.project.deserialize(projectState)
|
||||
atom.workspace = new Workspace({
|
||||
config: atom.config, project: atom.project, packageManager: atom.packages,
|
||||
|
||||
@@ -266,3 +266,9 @@ class ApplicationDelegate
|
||||
|
||||
getAutoUpdateManagerErrorMessage: ->
|
||||
ipcRenderer.sendSync('get-auto-update-manager-error')
|
||||
|
||||
emitWillSavePath: (path) ->
|
||||
ipcRenderer.sendSync('will-save-path', path)
|
||||
|
||||
emitDidSavePath: (path) ->
|
||||
ipcRenderer.sendSync('did-save-path', path)
|
||||
|
||||
@@ -185,7 +185,7 @@ class AtomEnvironment extends Model
|
||||
|
||||
@clipboard = new Clipboard()
|
||||
|
||||
@project = new Project({notificationManager: @notifications, packageManager: @packages, @config})
|
||||
@project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate})
|
||||
|
||||
@commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ AtomProtocolHandler = require './atom-protocol-handler'
|
||||
AutoUpdateManager = require './auto-update-manager'
|
||||
StorageFolder = require '../storage-folder'
|
||||
Config = require '../config'
|
||||
FileRecoveryService = require './file-recovery-service'
|
||||
ipcHelpers = require '../ipc-helpers'
|
||||
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
|
||||
fs = require 'fs-plus'
|
||||
@@ -78,6 +79,7 @@ class AtomApplication
|
||||
@autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath, @config)
|
||||
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
|
||||
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
|
||||
@fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery"))
|
||||
|
||||
@listenForArgumentsFromNewProcess()
|
||||
@setupJavaScriptArguments()
|
||||
@@ -242,7 +244,7 @@ class AtomApplication
|
||||
options.window = window
|
||||
@openPaths(options)
|
||||
else
|
||||
new AtomWindow(options)
|
||||
new AtomWindow(@fileRecoveryService, options)
|
||||
else
|
||||
@promptForPathToOpen('all', {window})
|
||||
|
||||
@@ -325,6 +327,14 @@ class AtomApplication
|
||||
ipcMain.on 'get-auto-update-manager-error', (event) =>
|
||||
event.returnValue = @autoUpdateManager.getErrorMessage()
|
||||
|
||||
ipcMain.on 'will-save-path', (event, path) =>
|
||||
@fileRecoveryService.willSavePath(@windowForEvent(event), path)
|
||||
event.returnValue = true
|
||||
|
||||
ipcMain.on 'did-save-path', (event, path) =>
|
||||
@fileRecoveryService.didSavePath(@windowForEvent(event), path)
|
||||
event.returnValue = true
|
||||
|
||||
setupDockMenu: ->
|
||||
if process.platform is 'darwin'
|
||||
dockMenu = Menu.buildFromTemplate [
|
||||
@@ -485,7 +495,7 @@ class AtomApplication
|
||||
windowInitializationScript ?= require.resolve('../initialize-application-window')
|
||||
resourcePath ?= @resourcePath
|
||||
windowDimensions ?= @getDimensionsForNewWindow()
|
||||
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
|
||||
openedWindow = new AtomWindow(@fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
|
||||
|
||||
if pidToKillWhenClosed?
|
||||
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
|
||||
@@ -564,7 +574,7 @@ class AtomApplication
|
||||
packagePath = @packages.resolvePackagePath(packageName)
|
||||
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
|
||||
new AtomWindow(@fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
|
||||
else
|
||||
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
|
||||
else
|
||||
@@ -609,7 +619,7 @@ class AtomApplication
|
||||
devMode = true
|
||||
isSpec = true
|
||||
safeMode ?= false
|
||||
new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
|
||||
new AtomWindow(@fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
|
||||
|
||||
resolveTestRunnerPath: (testPath) ->
|
||||
FindParentDir ?= require 'find-parent-dir'
|
||||
|
||||
@@ -15,7 +15,7 @@ class AtomWindow
|
||||
loaded: null
|
||||
isSpec: null
|
||||
|
||||
constructor: (settings={}) ->
|
||||
constructor: (@fileRecoveryService, settings={}) ->
|
||||
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
|
||||
locationsToOpen ?= [{pathToOpen}] if pathToOpen
|
||||
locationsToOpen ?= []
|
||||
@@ -125,6 +125,7 @@ class AtomWindow
|
||||
global.atomApplication.saveState(false)
|
||||
|
||||
@browserWindow.on 'closed', =>
|
||||
@fileRecoveryService.didCloseWindow(this)
|
||||
global.atomApplication.removeWindow(this)
|
||||
|
||||
@browserWindow.on 'unresponsive', =>
|
||||
@@ -140,6 +141,7 @@ class AtomWindow
|
||||
@browserWindow.webContents.on 'crashed', =>
|
||||
global.atomApplication.exit(100) if @headless
|
||||
|
||||
@fileRecoveryService.didCrashWindow(this)
|
||||
chosen = dialog.showMessageBox @browserWindow,
|
||||
type: 'warning'
|
||||
buttons: ['Close Window', 'Reload', 'Keep It Open']
|
||||
|
||||
129
src/main-process/file-recovery-service.js
Normal file
129
src/main-process/file-recovery-service.js
Normal file
@@ -0,0 +1,129 @@
|
||||
'use babel'
|
||||
|
||||
import {dialog} from 'electron'
|
||||
import crypto from 'crypto'
|
||||
import Path from 'path'
|
||||
import fs from 'fs-plus'
|
||||
|
||||
export default class FileRecoveryService {
|
||||
constructor (recoveryDirectory) {
|
||||
this.recoveryDirectory = recoveryDirectory
|
||||
this.recoveryFilesByFilePath = new Map()
|
||||
this.recoveryFilesByWindow = new WeakMap()
|
||||
this.windowsByRecoveryFile = new Map()
|
||||
}
|
||||
|
||||
willSavePath (window, path) {
|
||||
if (!fs.existsSync(path)) return
|
||||
|
||||
const recoveryPath = Path.join(this.recoveryDirectory, RecoveryFile.fileNameForPath(path))
|
||||
const recoveryFile =
|
||||
this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, recoveryPath)
|
||||
|
||||
try {
|
||||
recoveryFile.retain()
|
||||
} catch (err) {
|
||||
console.log(`Couldn't retain ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.recoveryFilesByWindow.has(window)) {
|
||||
this.recoveryFilesByWindow.set(window, new Set())
|
||||
}
|
||||
if (!this.windowsByRecoveryFile.has(recoveryFile)) {
|
||||
this.windowsByRecoveryFile.set(recoveryFile, new Set())
|
||||
}
|
||||
|
||||
this.recoveryFilesByWindow.get(window).add(recoveryFile)
|
||||
this.windowsByRecoveryFile.get(recoveryFile).add(window)
|
||||
this.recoveryFilesByFilePath.set(path, recoveryFile)
|
||||
}
|
||||
|
||||
didSavePath (window, path) {
|
||||
const recoveryFile = this.recoveryFilesByFilePath.get(path)
|
||||
if (recoveryFile != null) {
|
||||
try {
|
||||
recoveryFile.release()
|
||||
} catch (err) {
|
||||
console.log(`Couldn't release ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
|
||||
}
|
||||
if (recoveryFile.isReleased()) this.recoveryFilesByFilePath.delete(path)
|
||||
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
|
||||
this.windowsByRecoveryFile.get(recoveryFile).delete(window)
|
||||
}
|
||||
}
|
||||
|
||||
didCrashWindow (window) {
|
||||
if (!this.recoveryFilesByWindow.has(window)) return
|
||||
|
||||
for (const recoveryFile of this.recoveryFilesByWindow.get(window)) {
|
||||
try {
|
||||
recoveryFile.recoverSync()
|
||||
} catch (error) {
|
||||
const message = 'A file that Atom was saving could be corrupted'
|
||||
const detail =
|
||||
`Error ${error.code}. There was a crash while saving "${recoveryFile.originalPath}", so this file might be blank or corrupted.\n` +
|
||||
`Atom couldn't recover it automatically, but a recovery file has been saved at: "${recoveryFile.recoveryPath}".`
|
||||
console.log(detail)
|
||||
dialog.showMessageBox(window.browserWindow, {type: 'info', buttons: ['OK'], message, detail})
|
||||
} finally {
|
||||
for (let window of this.windowsByRecoveryFile.get(recoveryFile)) {
|
||||
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
|
||||
}
|
||||
this.windowsByRecoveryFile.delete(recoveryFile)
|
||||
this.recoveryFilesByFilePath.delete(recoveryFile.originalPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
didCloseWindow (window) {
|
||||
if (!this.recoveryFilesByWindow.has(window)) return
|
||||
|
||||
for (let recoveryFile of this.recoveryFilesByWindow.get(window)) {
|
||||
this.windowsByRecoveryFile.get(recoveryFile).delete(window)
|
||||
}
|
||||
this.recoveryFilesByWindow.delete(window)
|
||||
}
|
||||
}
|
||||
|
||||
class RecoveryFile {
|
||||
static fileNameForPath (path) {
|
||||
const extension = Path.extname(path)
|
||||
const basename = Path.basename(path, extension).substring(0, 34)
|
||||
const randomSuffix = crypto.randomBytes(3).toString('hex')
|
||||
return `${basename}-${randomSuffix}${extension}`
|
||||
}
|
||||
|
||||
constructor (originalPath, recoveryPath) {
|
||||
this.originalPath = originalPath
|
||||
this.recoveryPath = recoveryPath
|
||||
this.refCount = 0
|
||||
}
|
||||
|
||||
storeSync () {
|
||||
fs.copyFileSync(this.originalPath, this.recoveryPath)
|
||||
}
|
||||
|
||||
recoverSync () {
|
||||
fs.copyFileSync(this.recoveryPath, this.originalPath)
|
||||
this.removeSync()
|
||||
}
|
||||
|
||||
removeSync () {
|
||||
fs.unlinkSync(this.recoveryPath)
|
||||
}
|
||||
|
||||
retain () {
|
||||
if (this.isReleased()) this.storeSync()
|
||||
this.refCount++
|
||||
}
|
||||
|
||||
release () {
|
||||
this.refCount--
|
||||
if (this.isReleased()) this.removeSync()
|
||||
}
|
||||
|
||||
isReleased () {
|
||||
return this.refCount === 0
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ class Project extends Model
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
constructor: ({@notificationManager, packageManager, config}) ->
|
||||
constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) ->
|
||||
@emitter = new Emitter
|
||||
@buffers = []
|
||||
@paths = []
|
||||
@@ -360,7 +360,6 @@ class Project extends Model
|
||||
|
||||
addBuffer: (buffer, options={}) ->
|
||||
@addBufferAtIndex(buffer, @buffers.length, options)
|
||||
@subscribeToBuffer(buffer)
|
||||
|
||||
addBufferAtIndex: (buffer, index, options={}) ->
|
||||
@buffers.splice(index, 0, buffer)
|
||||
@@ -390,6 +389,8 @@ class Project extends Model
|
||||
@on 'buffer-created', (buffer) -> callback(buffer)
|
||||
|
||||
subscribeToBuffer: (buffer) ->
|
||||
buffer.onWillSave ({path}) => @applicationDelegate.emitWillSavePath(path)
|
||||
buffer.onDidSave ({path}) => @applicationDelegate.emitDidSavePath(path)
|
||||
buffer.onDidDestroy => @removeBuffer(buffer)
|
||||
buffer.onDidChangePath =>
|
||||
unless @getPaths().length > 0
|
||||
|
||||
@@ -124,7 +124,7 @@ class TextEditor extends Model
|
||||
@softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, @tabLength,
|
||||
@softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
|
||||
@mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @config, @clipboard, @grammarRegistry,
|
||||
@assert, @applicationDelegate, grammar, @showInvisibles, @autoHeight, @scrollPastEnd, @editorWidthInChars,
|
||||
@assert, grammar, @showInvisibles, @autoHeight, @scrollPastEnd, @editorWidthInChars,
|
||||
@tokenizedBuffer, @ignoreInvisibles, @displayLayer
|
||||
} = params
|
||||
|
||||
|
||||
Reference in New Issue
Block a user