From 54c7924f7e48340f27a08f5a41fcf44b1eb5510c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 4 Oct 2022 11:28:37 -0300 Subject: [PATCH 01/64] Internalizing pathwatcher --- package.json | 3 +- src/file.coffee | 405 +++++++++++++++++++++++++++++++++++++++++++++ src/text-buffer.js | 2 +- 3 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 src/file.coffee diff --git a/package.json b/package.json index fddec0944a..6a16ddfafd 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,8 @@ "fs-plus": "^3.0.0", "grim": "^2.0.2", "mkdirp": "^0.5.1", - "pathwatcher": "^8.1.0", "serializable": "^1.0.3", - "superstring": "^2.4.4", + "superstring": "https://github.com/pulsar-edit/superstring#deprecate-old-api", "underscore-plus": "^1.0.0", "winattr": "^3.0.0" }, diff --git a/src/file.coffee b/src/file.coffee new file mode 100644 index 0000000000..95aaed2016 --- /dev/null +++ b/src/file.coffee @@ -0,0 +1,405 @@ +crypto = require 'crypto' +path = require 'path' + +_ = require 'underscore-plus' +{Emitter, Disposable} = require 'event-kit' +fs = require 'fs-plus' +Grim = require 'grim' + +iconv = null # Defer until used + +Directory = null + +# Extended: Represents an individual file that can be watched, read from, and +# written to. +module.exports = +class File + encoding: 'utf8' + realPath: null + subscriptionCount: 0 + + ### + Section: Construction + ### + + # Public: Configures a new File instance, no files are accessed. + # + # * `filePath` A {String} containing the absolute path to the file + # * `symlink` (optional) A {Boolean} indicating if the path is a symlink (default: false). + constructor: (filePath, @symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) -> + filePath = path.normalize(filePath) if filePath + @path = filePath + @emitter = new Emitter + + if includeDeprecatedAPIs + @on 'contents-changed-subscription-will-be-added', @willAddSubscription + @on 'moved-subscription-will-be-added', @willAddSubscription + @on 'removed-subscription-will-be-added', @willAddSubscription + @on 'contents-changed-subscription-removed', @didRemoveSubscription + @on 'moved-subscription-removed', @didRemoveSubscription + @on 'removed-subscription-removed', @didRemoveSubscription + + @cachedContents = null + @reportOnDeprecations = true + + # Public: Creates the file on disk that corresponds to `::getPath()` if no + # such file already exists. + # + # Returns a {Promise} that resolves once the file is created on disk. It + # resolves to a boolean value that is true if the file was created or false if + # it already existed. + create: -> + @exists().then (isExistingFile) => + unless isExistingFile + parent = @getParent() + parent.create().then => + @write('').then -> true + else + false + + ### + Section: Event Subscription + ### + + # Public: Invoke the given callback when the file's contents change. + # + # * `callback` {Function} to be called when the file's contents change. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange: (callback) -> + @willAddSubscription() + @trackUnsubscription(@emitter.on('did-change', callback)) + + # Public: Invoke the given callback when the file's path changes. + # + # * `callback` {Function} to be called when the file's path changes. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRename: (callback) -> + @willAddSubscription() + @trackUnsubscription(@emitter.on('did-rename', callback)) + + # Public: Invoke the given callback when the file is deleted. + # + # * `callback` {Function} to be called when the file is deleted. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDelete: (callback) -> + @willAddSubscription() + @trackUnsubscription(@emitter.on('did-delete', callback)) + + # Public: Invoke the given callback when there is an error with the watch. + # When your callback has been invoked, the file will have unsubscribed from + # the file watches. + # + # * `callback` {Function} callback + # * `errorObject` {Object} + # * `error` {Object} the error object + # * `handle` {Function} call this to indicate you have handled the error. + # The error will not be thrown if this function is called. + onWillThrowWatchError: (callback) -> + @emitter.on('will-throw-watch-error', callback) + + willAddSubscription: => + @subscriptionCount++ + try + @subscribeToNativeChangeEvents() + + didRemoveSubscription: => + @subscriptionCount-- + @unsubscribeFromNativeChangeEvents() if @subscriptionCount is 0 + + trackUnsubscription: (subscription) -> + new Disposable => + subscription.dispose() + @didRemoveSubscription() + + ### + Section: File Metadata + ### + + # Public: Returns a {Boolean}, always true. + isFile: -> true + + # Public: Returns a {Boolean}, always false. + isDirectory: -> false + + # Public: Returns a {Boolean} indicating whether or not this is a symbolic link + isSymbolicLink: -> + @symlink + + # Public: Returns a promise that resolves to a {Boolean}, true if the file + # exists, false otherwise. + exists: -> + new Promise (resolve) => + fs.exists @getPath(), resolve + + # Public: Returns a {Boolean}, true if the file exists, false otherwise. + existsSync: -> + fs.existsSync(@getPath()) + + # Public: Get the SHA-1 digest of this file + # + # Returns a promise that resolves to a {String}. + getDigest: -> + if @digest? + Promise.resolve(@digest) + else + @read().then => @digest # read assigns digest as a side-effect + + # Public: Get the SHA-1 digest of this file + # + # Returns a {String}. + getDigestSync: -> + @readSync() unless @digest + @digest + + setDigest: (contents) -> + @digest = crypto.createHash('sha1').update(contents ? '').digest('hex') + + # Public: Sets the file's character set encoding name. + # + # * `encoding` The {String} encoding to use (default: 'utf8') + setEncoding: (encoding='utf8') -> + # Throws if encoding doesn't exist. Better to throw an exception early + # instead of waiting until the file is saved. + + if encoding isnt 'utf8' + iconv ?= require 'iconv-lite' + iconv.getCodec(encoding) + + @encoding = encoding + + # Public: Returns the {String} encoding name for this file (default: 'utf8'). + getEncoding: -> @encoding + + ### + Section: Managing Paths + ### + + # Public: Returns the {String} path for the file. + getPath: -> @path + + # Sets the path for the file. + setPath: (@path) -> + @realPath = null + + # Public: Returns this file's completely resolved {String} path. + getRealPathSync: -> + unless @realPath? + try + @realPath = fs.realpathSync(@path) + catch error + @realPath = @path + @realPath + + # Public: Returns a promise that resolves to the file's completely resolved {String} path. + getRealPath: -> + if @realPath? + Promise.resolve(@realPath) + else + new Promise (resolve, reject) => + fs.realpath @path, (err, result) => + if err? + reject(err) + else + resolve(@realPath = result) + + # Public: Return the {String} filename without any directory information. + getBaseName: -> + path.basename(@path) + + ### + Section: Traversing + ### + + # Public: Return the {Directory} that contains this file. + getParent: -> + Directory ?= require './directory' + new Directory(path.dirname @path) + + ### + Section: Reading and Writing + ### + + readSync: (flushCache) -> + if not @existsSync() + @cachedContents = null + else if not @cachedContents? or flushCache + encoding = @getEncoding() + if encoding is 'utf8' + @cachedContents = fs.readFileSync(@getPath(), encoding) + else + iconv ?= require 'iconv-lite' + @cachedContents = iconv.decode(fs.readFileSync(@getPath()), encoding) + + @setDigest(@cachedContents) + @cachedContents + + writeFileSync: (filePath, contents) -> + encoding = @getEncoding() + if encoding is 'utf8' + fs.writeFileSync(filePath, contents, {encoding}) + else + iconv ?= require 'iconv-lite' + fs.writeFileSync(filePath, iconv.encode(contents, encoding)) + + # 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 resolves to either a {String}, or null if the file does not exist. + read: (flushCache) -> + if @cachedContents? and not flushCache + promise = Promise.resolve(@cachedContents) + else + promise = new Promise (resolve, reject) => + content = [] + readStream = @createReadStream() + + readStream.on 'data', (chunk) -> + content.push(chunk) + + readStream.on 'end', -> + resolve(content.join('')) + + readStream.on 'error', (error) -> + if error.code == 'ENOENT' + resolve(null) + else + reject(error) + + promise.then (contents) => + @setDigest(contents) + @cachedContents = contents + + # Public: Returns a stream to read the content of the file. + # + # Returns a {ReadStream} object. + createReadStream: -> + encoding = @getEncoding() + if encoding is 'utf8' + fs.createReadStream(@getPath(), {encoding}) + else + iconv ?= require 'iconv-lite' + fs.createReadStream(@getPath()).pipe(iconv.decodeStream(encoding)) + + # Public: Overwrites the file with the given text. + # + # * `text` The {String} text to write to the underlying file. + # + # Returns a {Promise} that resolves when the file has been written. + write: (text) -> + @exists().then (previouslyExisted) => + @writeFile(@getPath(), text).then => + @cachedContents = text + @setDigest(text) + @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() + undefined + + # Public: Returns a stream to write content to the file. + # + # Returns a {WriteStream} object. + createWriteStream: -> + encoding = @getEncoding() + if encoding is 'utf8' + fs.createWriteStream(@getPath(), {encoding}) + else + iconv ?= require 'iconv-lite' + stream = iconv.encodeStream(encoding) + stream.pipe(fs.createWriteStream(@getPath())) + stream + + # Public: Overwrites the file with the given text. + # + # * `text` The {String} text to write to the underlying file. + # + # Returns undefined. + writeSync: (text) -> + previouslyExisted = @existsSync() + @writeFileSync(@getPath(), text) + @cachedContents = text + @setDigest(text) + @emit 'contents-changed' if Grim.includeDeprecatedAPIs + @emitter.emit 'did-change' + @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() + undefined + + writeFile: (filePath, contents) -> + encoding = @getEncoding() + if encoding is 'utf8' + new Promise (resolve, reject) -> + fs.writeFile filePath, contents, {encoding}, (err, result) -> + if err? + reject(err) + else + resolve(result) + else + iconv ?= require 'iconv-lite' + new Promise (resolve, reject) -> + fs.writeFile filePath, iconv.encode(contents, encoding), (err, result) -> + if err? + reject(err) + else + resolve(result) + + subscribeToNativeChangeEvents: -> + # @watchSubscription ?= PathWatcher.watch @path, (args...) => + # @handleNativeChangeEvent(args...) + + unsubscribeFromNativeChangeEvents: -> + # if @watchSubscription? + # @watchSubscription.close() + # @watchSubscription = null + + ### + Section: Private + ### + + handleNativeChangeEvent: (eventType, eventPath) -> + switch eventType + when 'delete' + @unsubscribeFromNativeChangeEvents() + @detectResurrectionAfterDelay() + when 'rename' + @setPath(eventPath) + @emit 'moved' if Grim.includeDeprecatedAPIs + @emitter.emit 'did-rename' + when 'change', 'resurrect' + @cachedContents = null + @emitter.emit 'did-change' + + detectResurrectionAfterDelay: -> + _.delay (=> @detectResurrection()), 50 + + detectResurrection: -> + @exists().then (exists) => + if exists + @subscribeToNativeChangeEvents() + @handleNativeChangeEvent('resurrect') + else + @cachedContents = null + @emit 'removed' if Grim.includeDeprecatedAPIs + @emitter.emit 'did-delete' + +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(File) + + File::on = (eventName) -> + switch eventName + when 'contents-changed' + Grim.deprecate("Use File::onDidChange instead") + when 'moved' + Grim.deprecate("Use File::onDidRename instead") + when 'removed' + Grim.deprecate("Use File::onDidDelete instead") + else + if @reportOnDeprecations + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) +else + File::hasSubscriptions = -> + @subscriptionCount > 0 diff --git a/src/text-buffer.js b/src/text-buffer.js index c6b39ccd9d..c307714f08 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -1,5 +1,5 @@ const {Emitter, CompositeDisposable} = require('event-kit') -const {File} = require('pathwatcher') +const File = require('./file') const diff = require('diff') const _ = require('underscore-plus') const path = require('path') From 4ab75b0565336f3769e2b619a0a7abb1996715fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 13 Oct 2022 19:11:24 -0300 Subject: [PATCH 02/64] Bumping superstring to WASM version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a16ddfafd..dbbd9e546f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring#deprecate-old-api", + "superstring": "https://github.com/pulsar-edit/superstring.git#0d0929b2286a25c81ee74a511707c015594a697a", "underscore-plus": "^1.0.0", "winattr": "^3.0.0" }, From 723f303429292315a9c8e448080cb9ad90113be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 13 Oct 2022 19:11:42 -0300 Subject: [PATCH 03/64] Making all requires to superstring async --- spec/helpers/test-language-mode.js | 4 +++- spec/text-buffer-io-spec.js | 3 ++- src/default-history-provider.coffee | 3 ++- src/display-layer.js | 3 ++- src/helpers.js | 3 ++- src/marker-layer.coffee | 3 ++- src/text-buffer.js | 14 +++++++++----- 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/spec/helpers/test-language-mode.js b/spec/helpers/test-language-mode.js index c229524054..6dbc885b77 100644 --- a/spec/helpers/test-language-mode.js +++ b/spec/helpers/test-language-mode.js @@ -1,4 +1,6 @@ -const {MarkerIndex} = require('superstring') +let MarkerIndex; +console.log(require('superstring')) +require('superstring').superstring.then(s => MarkerIndex = s.MarkerIndex) const {Emitter} = require('event-kit') const Point = require('../../src/point') const Range = require('../../src/range') diff --git a/spec/text-buffer-io-spec.js b/spec/text-buffer-io-spec.js index 8129692c4e..b5fa34ec86 100644 --- a/spec/text-buffer-io-spec.js +++ b/spec/text-buffer-io-spec.js @@ -6,7 +6,8 @@ const {Disposable} = require('event-kit') const Point = require('../src/point') const Range = require('../src/range') const TextBuffer = require('../src/text-buffer') -const {TextBuffer: NativeTextBuffer} = require('superstring') +let NativeTextBuffer; +require('superstring').superstring.then(s => NativeTextBuffer = s.TextBuffer); const fsAdmin = require('fs-admin') const pathwatcher = require('pathwatcher') const winattr = require('winattr') diff --git a/src/default-history-provider.coffee b/src/default-history-provider.coffee index 8679d9066b..40a1a194f4 100644 --- a/src/default-history-provider.coffee +++ b/src/default-history-provider.coffee @@ -1,4 +1,5 @@ -{Patch} = require 'superstring' +Patch = null; +require('superstring').superstring.then (r) => Patch = r.Patch; MarkerLayer = require './marker-layer' {traversal} = require './point-helpers' {patchFromChanges} = require './helpers' diff --git a/src/display-layer.js b/src/display-layer.js index 87bc774f69..d3d1eab6cd 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -1,4 +1,5 @@ -const {Patch} = require('superstring') +let Patch; +require('superstring').superstring.then(r => Patch = r.Patch); const {Emitter} = require('event-kit') const Point = require('./point') const Range = require('./range') diff --git a/src/helpers.js b/src/helpers.js index 094685521d..27dc9096e4 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,4 +1,5 @@ -const {Patch} = require('superstring') +let Patch; +require('superstring').superstring.then(r => Patch = r.Patch); const Range = require('./range') const {traversal} = require('./point-helpers') diff --git a/src/marker-layer.coffee b/src/marker-layer.coffee index 64d17a4bc3..5ec81cd1ba 100644 --- a/src/marker-layer.coffee +++ b/src/marker-layer.coffee @@ -3,7 +3,8 @@ Point = require "./point" Range = require "./range" Marker = require "./marker" -{MarkerIndex} = require "superstring" +MarkerIndex = null; +require('superstring').superstring.then (r) => MarkerIndex = r.MarkerIndex; {intersectSet} = require "./set-helpers" SerializationVersion = 2 diff --git a/src/text-buffer.js b/src/text-buffer.js index c307714f08..8a5beafb3d 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -5,8 +5,11 @@ const _ = require('underscore-plus') const path = require('path') const crypto = require('crypto') const mkdirp = require('mkdirp') -const superstring = require('superstring') -const NativeTextBuffer = superstring.TextBuffer +let NativeTextBuffer; +require('superstring').superstring.then(s => { + console.log("Required", s) + NativeTextBuffer = s.TextBuffer +}); const Point = require('./point') const Range = require('./range') const DefaultHistoryProvider = require('./default-history-provider') @@ -83,7 +86,7 @@ class TextBuffer { this.changesSinceLastStoppedChangingEvent = [] this.changesSinceLastDidChangeTextEvent = [] this.id = crypto.randomBytes(16).toString('hex') - this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text) + this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text || "") this.debouncedEmitDidStopChangingEvent = debounce(this.emitDidStopChangingEvent.bind(this), this.stoppedChangingDelay) this.maxUndoEntries = params.maxUndoEntries != null ? params.maxUndoEntries : this.defaultMaxUndoEntries this.setHistoryProvider(new DefaultHistoryProvider(this)) @@ -2491,7 +2494,8 @@ Object.assign(TextBuffer, { spliceArray: spliceArray }) -TextBuffer.Patch = superstring.Patch +let Patch; +require('superstring').superstring.then(r => Patch = r.Patch); Object.assign(TextBuffer.prototype, { stoppedChangingDelay: 300, @@ -2518,7 +2522,7 @@ class ChangeEvent { enumerable: false, get () { if (oldText == null) { - const oldBuffer = new NativeTextBuffer(this.newText) + const oldBuffer = new NativeTextBuffer(this.newText || "") for (let i = changes.length - 1; i >= 0; i--) { const change = changes[i] oldBuffer.setTextInRange( From 56fcfc6c75ca2f1371a63852c4f4caeefdd98777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 13 Oct 2022 19:17:36 -0300 Subject: [PATCH 04/64] Source? --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbbd9e546f..4c96cc83d2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "text-buffer", "version": "13.18.6", "description": "A container for large mutable strings with annotated regions", - "main": "./lib/text-buffer", + "main": "./src/text-buffer", "scripts": { "prepublish": "npm run clean && npm run compile && npm run lint && npm run docs", "docs": "node script/generate-docs", From abb4bba05c59d89335eef70d4d8244bae90a67b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 13 Oct 2022 20:10:41 -0300 Subject: [PATCH 05/64] Removed log --- src/text-buffer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-buffer.js b/src/text-buffer.js index 8a5beafb3d..c10e3a1c59 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -7,7 +7,6 @@ const crypto = require('crypto') const mkdirp = require('mkdirp') let NativeTextBuffer; require('superstring').superstring.then(s => { - console.log("Required", s) NativeTextBuffer = s.TextBuffer }); const Point = require('./point') From 5a96a8cd5b9a6e4afb5aab90615efe1d4c502c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 13 Oct 2022 20:11:25 -0300 Subject: [PATCH 06/64] Another debug info --- spec/helpers/test-language-mode.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/helpers/test-language-mode.js b/spec/helpers/test-language-mode.js index 6dbc885b77..a0cf464e9f 100644 --- a/spec/helpers/test-language-mode.js +++ b/spec/helpers/test-language-mode.js @@ -1,5 +1,4 @@ let MarkerIndex; -console.log(require('superstring')) require('superstring').superstring.then(s => MarkerIndex = s.MarkerIndex) const {Emitter} = require('event-kit') const Point = require('../../src/point') From 29e948113c2ae81dfdb975ffd7920e64f161758e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 14 Oct 2022 16:33:17 -0300 Subject: [PATCH 07/64] Trying to fix another issue with async require --- src/text-buffer.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/text-buffer.js b/src/text-buffer.js index c10e3a1c59..30a9457590 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -5,8 +5,9 @@ const _ = require('underscore-plus') const path = require('path') const crypto = require('crypto') const mkdirp = require('mkdirp') +const {superstring} = require('superstring'); let NativeTextBuffer; -require('superstring').superstring.then(s => { +superstring.then(s => { NativeTextBuffer = s.TextBuffer }); const Point = require('./point') @@ -85,7 +86,9 @@ class TextBuffer { this.changesSinceLastStoppedChangingEvent = [] this.changesSinceLastDidChangeTextEvent = [] this.id = crypto.randomBytes(16).toString('hex') - this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text || "") + superstring.then(() => { + this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text || "") + }) this.debouncedEmitDidStopChangingEvent = debounce(this.emitDidStopChangingEvent.bind(this), this.stoppedChangingDelay) this.maxUndoEntries = params.maxUndoEntries != null ? params.maxUndoEntries : this.defaultMaxUndoEntries this.setHistoryProvider(new DefaultHistoryProvider(this)) From 34976dd052eba07f8eca10bf64d809b7ea919c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 14 Oct 2022 18:20:56 -0300 Subject: [PATCH 08/64] More async instantiations, and bump superstring --- package.json | 2 +- src/display-layer.js | 7 +++++-- src/marker-layer.coffee | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4c96cc83d2..0cf5b8347c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring.git#0d0929b2286a25c81ee74a511707c015594a697a", + "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0" }, diff --git a/src/display-layer.js b/src/display-layer.js index d3d1eab6cd..be7a405e3e 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -1,5 +1,6 @@ let Patch; -require('superstring').superstring.then(r => Patch = r.Patch); +const {superstring} = require('superstring') +superstring.then(r => Patch = r.Patch); const {Emitter} = require('event-kit') const Point = require('./point') const Range = require('./range') @@ -23,7 +24,9 @@ class DisplayLayer { this.nextBuiltInScopeId = 1 this.displayMarkerLayersById = new Map() this.destroyed = false - this.changesSinceLastEvent = new Patch() + superstring.then(() => { + this.changesSinceLastEvent = new Patch() + }) this.invisibles = params.invisibles != null ? params.invisibles : {} this.tabLength = params.tabLength != null ? params.tabLength : 4 diff --git a/src/marker-layer.coffee b/src/marker-layer.coffee index 5ec81cd1ba..fddedeb700 100644 --- a/src/marker-layer.coffee +++ b/src/marker-layer.coffee @@ -3,8 +3,9 @@ Point = require "./point" Range = require "./range" Marker = require "./marker" +{superstring} = require 'superstring' MarkerIndex = null; -require('superstring').superstring.then (r) => MarkerIndex = r.MarkerIndex; +superstring.then (r) => MarkerIndex = r.MarkerIndex; {intersectSet} = require "./set-helpers" SerializationVersion = 2 @@ -39,7 +40,8 @@ class MarkerLayer @delegate.registerSelectionsMarkerLayer(this) if @role is "selections" @persistent = options?.persistent ? false @emitter = new Emitter - @index = new MarkerIndex + superstring.then () => + @index = new MarkerIndex @markersById = {} @markersWithChangeListeners = new Set @markersWithDestroyListeners = new Set From b011a9f9e5d772a9cdbd67a4a9781bbc05717f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 14 Oct 2022 20:30:48 -0300 Subject: [PATCH 09/64] Moving back to sync things --- src/display-layer.js | 4 +--- src/text-buffer.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/display-layer.js b/src/display-layer.js index be7a405e3e..ed8d7097f8 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -24,9 +24,7 @@ class DisplayLayer { this.nextBuiltInScopeId = 1 this.displayMarkerLayersById = new Map() this.destroyed = false - superstring.then(() => { - this.changesSinceLastEvent = new Patch() - }) + this.changesSinceLastEvent = new Patch() this.invisibles = params.invisibles != null ? params.invisibles : {} this.tabLength = params.tabLength != null ? params.tabLength : 4 diff --git a/src/text-buffer.js b/src/text-buffer.js index 30a9457590..eae9a830a1 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -86,9 +86,7 @@ class TextBuffer { this.changesSinceLastStoppedChangingEvent = [] this.changesSinceLastDidChangeTextEvent = [] this.id = crypto.randomBytes(16).toString('hex') - superstring.then(() => { - this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text || "") - }) + this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text || "") this.debouncedEmitDidStopChangingEvent = debounce(this.emitDidStopChangingEvent.bind(this), this.stoppedChangingDelay) this.maxUndoEntries = params.maxUndoEntries != null ? params.maxUndoEntries : this.defaultMaxUndoEntries this.setHistoryProvider(new DefaultHistoryProvider(this)) From 738c797a5aab45ca78c383e29fa545984e0a2977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Fri, 14 Oct 2022 20:32:12 -0300 Subject: [PATCH 10/64] Migrated marker-layer to JS --- src/marker-layer.coffee | 428 ------------------------------ src/marker-layer.js | 564 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 564 insertions(+), 428 deletions(-) delete mode 100644 src/marker-layer.coffee create mode 100644 src/marker-layer.js diff --git a/src/marker-layer.coffee b/src/marker-layer.coffee deleted file mode 100644 index fddedeb700..0000000000 --- a/src/marker-layer.coffee +++ /dev/null @@ -1,428 +0,0 @@ -{clone} = require "underscore-plus" -{Emitter} = require 'event-kit' -Point = require "./point" -Range = require "./range" -Marker = require "./marker" -{superstring} = require 'superstring' -MarkerIndex = null; -superstring.then (r) => MarkerIndex = r.MarkerIndex; -{intersectSet} = require "./set-helpers" - -SerializationVersion = 2 - -# Public: *Experimental:* A container for a related set of markers. -# -# This API is experimental and subject to change on any release. -module.exports = -class MarkerLayer - @deserialize: (delegate, state) -> - store = new MarkerLayer(delegate, 0) - store.deserialize(state) - store - - @deserializeSnapshot: (snapshot) -> - result = {} - for layerId, markerSnapshots of snapshot - result[layerId] = {} - for markerId, markerSnapshot of markerSnapshots - result[layerId][markerId] = clone(markerSnapshot) - result[layerId][markerId].range = Range.fromObject(markerSnapshot.range) - result - - ### - Section: Lifecycle - ### - - constructor: (@delegate, @id, options) -> - @maintainHistory = options?.maintainHistory ? false - @destroyInvalidatedMarkers = options?.destroyInvalidatedMarkers ? false - @role = options?.role - @delegate.registerSelectionsMarkerLayer(this) if @role is "selections" - @persistent = options?.persistent ? false - @emitter = new Emitter - superstring.then () => - @index = new MarkerIndex - @markersById = {} - @markersWithChangeListeners = new Set - @markersWithDestroyListeners = new Set - @displayMarkerLayers = new Set - @destroyed = false - @emitCreateMarkerEvents = false - - # Public: Create a copy of this layer with markers in the same state and - # locations. - copy: -> - copy = @delegate.addMarkerLayer({@maintainHistory, @role}) - for markerId, marker of @markersById - snapshot = marker.getSnapshot(null) - copy.createMarker(marker.getRange(), marker.getSnapshot()) - copy - - # Public: Destroy this layer. - destroy: -> - return if @destroyed - @clear() - @delegate.markerLayerDestroyed(this) - @displayMarkerLayers.forEach (displayMarkerLayer) -> displayMarkerLayer.destroy() - @displayMarkerLayers.clear() - @destroyed = true - @emitter.emit 'did-destroy' - @emitter.clear() - - # Public: Remove all markers from this layer. - clear: -> - @markersWithDestroyListeners.forEach (marker) -> marker.destroy() - @markersWithDestroyListeners.clear() - @markersById = {} - @index = new MarkerIndex - @displayMarkerLayers.forEach (layer) -> layer.didClearBufferMarkerLayer() - @delegate.markersUpdated(this) - - # Public: Determine whether this layer has been destroyed. - isDestroyed: -> - @destroyed - - isAlive: -> - not @destroyed - - ### - Section: Querying - ### - - # Public: Get an existing marker by its id. - # - # Returns a {Marker}. - getMarker: (id) -> - @markersById[id] - - # Public: Get all existing markers on the marker layer. - # - # Returns an {Array} of {Marker}s. - getMarkers: -> - marker for id, marker of @markersById - - # Public: Get the number of markers in the marker layer. - # - # Returns a {Number}. - getMarkerCount: -> - Object.keys(@markersById).length - - # Public: Find markers in the layer conforming to the given parameters. - # - # See the documentation for {TextBuffer::findMarkers}. - findMarkers: (params) -> - markerIds = null - - for key in Object.keys(params) - value = params[key] - switch key - when 'startPosition' - markerIds = filterSet(markerIds, @index.findStartingAt(Point.fromObject(value))) - when 'endPosition' - markerIds = filterSet(markerIds, @index.findEndingAt(Point.fromObject(value))) - when 'startsInRange' - {start, end} = Range.fromObject(value) - markerIds = filterSet(markerIds, @index.findStartingIn(start, end)) - when 'endsInRange' - {start, end} = Range.fromObject(value) - markerIds = filterSet(markerIds, @index.findEndingIn(start, end)) - when 'containsPoint', 'containsPosition' - position = Point.fromObject(value) - markerIds = filterSet(markerIds, @index.findContaining(position, position)) - when 'containsRange' - {start, end} = Range.fromObject(value) - markerIds = filterSet(markerIds, @index.findContaining(start, end)) - when 'intersectsRange' - {start, end} = Range.fromObject(value) - markerIds = filterSet(markerIds, @index.findIntersecting(start, end)) - when 'startRow' - markerIds = filterSet(markerIds, @index.findStartingIn(Point(value, 0), Point(value, Infinity))) - when 'endRow' - markerIds = filterSet(markerIds, @index.findEndingIn(Point(value, 0), Point(value, Infinity))) - when 'intersectsRow' - markerIds = filterSet(markerIds, @index.findIntersecting(Point(value, 0), Point(value, Infinity))) - when 'intersectsRowRange' - markerIds = filterSet(markerIds, @index.findIntersecting(Point(value[0], 0), Point(value[1], Infinity))) - when 'containedInRange' - {start, end} = Range.fromObject(value) - markerIds = filterSet(markerIds, @index.findContainedIn(start, end)) - else - continue - delete params[key] - - markerIds ?= new Set(Object.keys(@markersById)) - - result = [] - markerIds.forEach (markerId) => - marker = @markersById[markerId] - return unless marker.matchesParams(params) - result.push(marker) - result.sort (a, b) -> a.compare(b) - - # Public: Get the role of the marker layer e.g. `atom.selection`. - # - # Returns a {String}. - getRole: -> - @role - - ### - Section: Marker creation - ### - - # Public: Create a marker with the given range. - # - # * `range` A {Range} or range-compatible {Array} - # * `options` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `exclusive` {Boolean} indicating whether insertions at the start or end - # of the marked range should be interpreted as happening *outside* the - # marker. Defaults to `false`, except when using the `inside` - # invalidation strategy or when when the marker has no tail, in which - # case it defaults to true. Explicitly assigning this option overrides - # behavior in all circumstances. - # - # Returns a {Marker}. - markRange: (range, options={}) -> - @createMarker(@delegate.clipRange(range), Marker.extractParams(options)) - - # Public: Create a marker at with its head at the given position with no tail. - # - # * `position` {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `exclusive` {Boolean} indicating whether insertions at the start or end - # of the marked range should be interpreted as happening *outside* the - # marker. Defaults to `false`, except when using the `inside` - # invalidation strategy or when when the marker has no tail, in which - # case it defaults to true. Explicitly assigning this option overrides - # behavior in all circumstances. - # - # Returns a {Marker}. - markPosition: (position, options={}) -> - position = @delegate.clipPosition(position) - options = Marker.extractParams(options) - options.tailed = false - @createMarker(@delegate.clipRange(new Range(position, position)), options) - - ### - Section: Event subscription - ### - - # Public: Subscribe to be notified asynchronously whenever markers are - # created, updated, or destroyed on this layer. *Prefer this method for - # optimal performance when interacting with layers that could contain large - # numbers of markers.* - # - # * `callback` A {Function} that will be called with no arguments when changes - # occur on this layer. - # - # Subscribers are notified once, asynchronously when any number of changes - # occur in a given tick of the event loop. You should re-query the layer - # to determine the state of markers in which you're interested in. It may - # be counter-intuitive, but this is much more efficient than subscribing to - # events on individual markers, which are expensive to deliver. - # - # Returns a {Disposable}. - onDidUpdate: (callback) -> - @emitter.on 'did-update', callback - - # Public: Subscribe to be notified synchronously whenever markers are created - # on this layer. *Avoid this method for optimal performance when interacting - # with layers that could contain large numbers of markers.* - # - # * `callback` A {Function} that will be called with a {Marker} whenever a - # new marker is created. - # - # You should prefer {::onDidUpdate} when synchronous notifications aren't - # absolutely necessary. - # - # Returns a {Disposable}. - onDidCreateMarker: (callback) -> - @emitCreateMarkerEvents = true - @emitter.on 'did-create-marker', callback - - # Public: Subscribe to be notified synchronously when this layer is destroyed. - # - # Returns a {Disposable}. - onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback - - ### - Section: Private - TextBuffer interface - ### - - splice: (start, oldExtent, newExtent) -> - invalidated = @index.splice(start, oldExtent, newExtent) - invalidated.touch.forEach (id) => - marker = @markersById[id] - if invalidated[marker.getInvalidationStrategy()]?.has(id) - if @destroyInvalidatedMarkers - marker.destroy() - else - marker.valid = false - - restoreFromSnapshot: (snapshots, alwaysCreate) -> - return unless snapshots? - - snapshotIds = Object.keys(snapshots) - existingMarkerIds = Object.keys(@markersById) - - for id in snapshotIds - snapshot = snapshots[id] - if alwaysCreate - @createMarker(snapshot.range, snapshot, true) - continue - - if marker = @markersById[id] - marker.update(marker.getRange(), snapshot, true, true) - else - {marker} = snapshot - if marker - @markersById[marker.id] = marker - {range} = snapshot - @index.insert(marker.id, range.start, range.end) - marker.update(marker.getRange(), snapshot, true, true) - @emitter.emit 'did-create-marker', marker if @emitCreateMarkerEvents - else - newMarker = @createMarker(snapshot.range, snapshot, true) - - for id in existingMarkerIds - if (marker = @markersById[id]) and (not snapshots[id]?) - marker.destroy(true) - - createSnapshot: -> - result = {} - ranges = @index.dump() - for id in Object.keys(@markersById) - marker = @markersById[id] - result[id] = marker.getSnapshot(Range.fromObject(ranges[id])) - result - - emitChangeEvents: (snapshot) -> - @markersWithChangeListeners.forEach (marker) -> - unless marker.isDestroyed() # event handlers could destroy markers - marker.emitChangeEvent(snapshot?[marker.id]?.range, true, false) - - serialize: -> - ranges = @index.dump() - markersById = {} - for id in Object.keys(@markersById) - marker = @markersById[id] - snapshot = marker.getSnapshot(Range.fromObject(ranges[id]), false) - markersById[id] = snapshot - - {@id, @maintainHistory, @role, @persistent, markersById, version: SerializationVersion} - - deserialize: (state) -> - return unless state.version is SerializationVersion - @id = state.id - @maintainHistory = state.maintainHistory - @role = state.role - @delegate.registerSelectionsMarkerLayer(this) if @role is "selections" - @persistent = state.persistent - for id, markerState of state.markersById - range = Range.fromObject(markerState.range) - delete markerState.range - @addMarker(id, range, markerState) - return - - ### - Section: Private - Marker interface - ### - - markerUpdated: -> - @delegate.markersUpdated(this) - - destroyMarker: (marker, suppressMarkerLayerUpdateEvents=false) -> - if @markersById.hasOwnProperty(marker.id) - delete @markersById[marker.id] - @index.remove(marker.id) - @markersWithChangeListeners.delete(marker) - @markersWithDestroyListeners.delete(marker) - @displayMarkerLayers.forEach (displayMarkerLayer) -> displayMarkerLayer.destroyMarker(marker.id) - @delegate.markersUpdated(this) unless suppressMarkerLayerUpdateEvents - - hasMarker: (id) -> - not @destroyed and @index.has(id) - - getMarkerRange: (id) -> - Range.fromObject(@index.getRange(id)) - - getMarkerStartPosition: (id) -> - Point.fromObject(@index.getStart(id)) - - getMarkerEndPosition: (id) -> - Point.fromObject(@index.getEnd(id)) - - compareMarkers: (id1, id2) -> - @index.compare(id1, id2) - - setMarkerRange: (id, range) -> - {start, end} = Range.fromObject(range) - start = @delegate.clipPosition(start) - end = @delegate.clipPosition(end) - @index.remove(id) - @index.insert(id, start, end) - - setMarkerIsExclusive: (id, exclusive) -> - @index.setExclusive(id, exclusive) - - createMarker: (range, params, suppressMarkerLayerUpdateEvents=false) -> - id = @delegate.getNextMarkerId() - marker = @addMarker(id, range, params) - @delegate.markerCreated(this, marker) - @delegate.markersUpdated(this) unless suppressMarkerLayerUpdateEvents - marker.trackDestruction = @trackDestructionInOnDidCreateMarkerCallbacks ? false - @emitter.emit 'did-create-marker', marker if @emitCreateMarkerEvents - marker.trackDestruction = false - marker - - ### - Section: Internal - ### - - addMarker: (id, range, params) -> - range = Range.fromObject(range) - Point.assertValid(range.start) - Point.assertValid(range.end) - @index.insert(id, range.start, range.end) - @markersById[id] = new Marker(id, this, range, params) - - emitUpdateEvent: -> - @emitter.emit('did-update') - -filterSet = (set1, set2) -> - if set1 - intersectSet(set1, set2) - set1 - else - set2 diff --git a/src/marker-layer.js b/src/marker-layer.js new file mode 100644 index 0000000000..7f378a68f4 --- /dev/null +++ b/src/marker-layer.js @@ -0,0 +1,564 @@ +const {clone} = require("underscore-plus"); +const {Emitter} = require('event-kit'); +const Point = require("./point"); +const Range = require("./range"); +const Marker = require("./marker"); +const {superstring} = require('superstring'); +let MarkerIndex = null; +superstring.then((r) => { return MarkerIndex = r.MarkerIndex; }); +const {intersectSet} = require("./set-helpers"); +const SerializationVersion = 2; + +// Public: *Experimental:* A container for a related set of markers. + +// This API is experimental and subject to change on any release. +module.exports = class MarkerLayer { + static deserialize(delegate, state) { + var store; + store = new MarkerLayer(delegate, 0); + store.deserialize(state); + return store; + } + + static deserializeSnapshot(snapshot) { + var layerId, markerId, markerSnapshot, markerSnapshots, result; + result = {}; + for (layerId in snapshot) { + markerSnapshots = snapshot[layerId]; + result[layerId] = {}; + for (markerId in markerSnapshots) { + markerSnapshot = markerSnapshots[markerId]; + result[layerId][markerId] = clone(markerSnapshot); + result[layerId][markerId].range = Range.fromObject(markerSnapshot.range); + } + } + return result; + } + + /* + Section: Lifecycle + */ + constructor(delegate1, id3, options) { + var ref, ref1, ref2; + this.delegate = delegate1; + this.id = id3; + this.maintainHistory = (ref = options != null ? options.maintainHistory : void 0) != null ? ref : false; + this.destroyInvalidatedMarkers = (ref1 = options != null ? options.destroyInvalidatedMarkers : void 0) != null ? ref1 : false; + this.role = options != null ? options.role : void 0; + if (this.role === "selections") { + this.delegate.registerSelectionsMarkerLayer(this); + } + this.persistent = (ref2 = options != null ? options.persistent : void 0) != null ? ref2 : false; + this.emitter = new Emitter(); + this.index = new MarkerIndex(); + this.markersById = {}; + this.markersWithChangeListeners = new Set(); + this.markersWithDestroyListeners = new Set(); + this.displayMarkerLayers = new Set(); + this.destroyed = false; + this.emitCreateMarkerEvents = false; + } + + // Public: Create a copy of this layer with markers in the same state and + // locations. + copy() { + var copy, marker, markerId, ref, snapshot; + copy = this.delegate.addMarkerLayer({maintainHistory: this.maintainHistory, role: this.role}); + ref = this.markersById; + for (markerId in ref) { + marker = ref[markerId]; + snapshot = marker.getSnapshot(null); + copy.createMarker(marker.getRange(), marker.getSnapshot()); + } + return copy; + } + + // Public: Destroy this layer. + destroy() { + if (this.destroyed) { + return; + } + this.clear(); + this.delegate.markerLayerDestroyed(this); + this.displayMarkerLayers.forEach(function(displayMarkerLayer) { + return displayMarkerLayer.destroy(); + }); + this.displayMarkerLayers.clear(); + this.destroyed = true; + this.emitter.emit('did-destroy'); + return this.emitter.clear(); + } + + // Public: Remove all markers from this layer. + clear() { + this.markersWithDestroyListeners.forEach(function(marker) { + return marker.destroy(); + }); + this.markersWithDestroyListeners.clear(); + this.markersById = {}; + this.index = new MarkerIndex(); + this.displayMarkerLayers.forEach(function(layer) { + return layer.didClearBufferMarkerLayer(); + }); + return this.delegate.markersUpdated(this); + } + + // Public: Determine whether this layer has been destroyed. + isDestroyed() { + return this.destroyed; + } + + isAlive() { + return !this.destroyed; + } + + /* + Section: Querying + */ + // Public: Get an existing marker by its id. + + // Returns a {Marker}. + getMarker(id) { + return this.markersById[id]; + } + + // Public: Get all existing markers on the marker layer. + + // Returns an {Array} of {Marker}s. + getMarkers() { + var id, marker, ref, results; + ref = this.markersById; + results = []; + for (id in ref) { + marker = ref[id]; + results.push(marker); + } + return results; + } + + // Public: Get the number of markers in the marker layer. + + // Returns a {Number}. + getMarkerCount() { + return Object.keys(this.markersById).length; + } + + // Public: Find markers in the layer conforming to the given parameters. + + // See the documentation for {TextBuffer::findMarkers}. + findMarkers(params) { + var end, i, key, len, markerIds, position, ref, result, start, value; + markerIds = null; + ref = Object.keys(params); + for (i = 0, len = ref.length; i < len; i++) { + key = ref[i]; + value = params[key]; + switch (key) { + case 'startPosition': + markerIds = filterSet(markerIds, this.index.findStartingAt(Point.fromObject(value))); + break; + case 'endPosition': + markerIds = filterSet(markerIds, this.index.findEndingAt(Point.fromObject(value))); + break; + case 'startsInRange': + ({start, end} = Range.fromObject(value)); + markerIds = filterSet(markerIds, this.index.findStartingIn(start, end)); + break; + case 'endsInRange': + ({start, end} = Range.fromObject(value)); + markerIds = filterSet(markerIds, this.index.findEndingIn(start, end)); + break; + case 'containsPoint': + case 'containsPosition': + position = Point.fromObject(value); + markerIds = filterSet(markerIds, this.index.findContaining(position, position)); + break; + case 'containsRange': + ({start, end} = Range.fromObject(value)); + markerIds = filterSet(markerIds, this.index.findContaining(start, end)); + break; + case 'intersectsRange': + ({start, end} = Range.fromObject(value)); + markerIds = filterSet(markerIds, this.index.findIntersecting(start, end)); + break; + case 'startRow': + markerIds = filterSet(markerIds, this.index.findStartingIn(Point(value, 0), Point(value, 2e308))); + break; + case 'endRow': + markerIds = filterSet(markerIds, this.index.findEndingIn(Point(value, 0), Point(value, 2e308))); + break; + case 'intersectsRow': + markerIds = filterSet(markerIds, this.index.findIntersecting(Point(value, 0), Point(value, 2e308))); + break; + case 'intersectsRowRange': + markerIds = filterSet(markerIds, this.index.findIntersecting(Point(value[0], 0), Point(value[1], 2e308))); + break; + case 'containedInRange': + ({start, end} = Range.fromObject(value)); + markerIds = filterSet(markerIds, this.index.findContainedIn(start, end)); + break; + default: + continue; + } + delete params[key]; + } + if (markerIds == null) { + markerIds = new Set(Object.keys(this.markersById)); + } + result = []; + markerIds.forEach((markerId) => { + var marker; + marker = this.markersById[markerId]; + if (!marker.matchesParams(params)) { + return; + } + return result.push(marker); + }); + return result.sort(function(a, b) { + return a.compare(b); + }); + } + + // Public: Get the role of the marker layer e.g. `atom.selection`. + + // Returns a {String}. + getRole() { + return this.role; + } + + /* + Section: Marker creation + */ + // Public: Create a marker with the given range. + + // * `range` A {Range} or range-compatible {Array} + // * `options` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `exclusive` {Boolean} indicating whether insertions at the start or end + // of the marked range should be interpreted as happening *outside* the + // marker. Defaults to `false`, except when using the `inside` + // invalidation strategy or when when the marker has no tail, in which + // case it defaults to true. Explicitly assigning this option overrides + // behavior in all circumstances. + + // Returns a {Marker}. + markRange(range, options = {}) { + return this.createMarker(this.delegate.clipRange(range), Marker.extractParams(options)); + } + + // Public: Create a marker at with its head at the given position with no tail. + + // * `position` {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `exclusive` {Boolean} indicating whether insertions at the start or end + // of the marked range should be interpreted as happening *outside* the + // marker. Defaults to `false`, except when using the `inside` + // invalidation strategy or when when the marker has no tail, in which + // case it defaults to true. Explicitly assigning this option overrides + // behavior in all circumstances. + + // Returns a {Marker}. + markPosition(position, options = {}) { + position = this.delegate.clipPosition(position); + options = Marker.extractParams(options); + options.tailed = false; + return this.createMarker(this.delegate.clipRange(new Range(position, position)), options); + } + + /* + Section: Event subscription + */ + // Public: Subscribe to be notified asynchronously whenever markers are + // created, updated, or destroyed on this layer. *Prefer this method for + // optimal performance when interacting with layers that could contain large + // numbers of markers.* + + // * `callback` A {Function} that will be called with no arguments when changes + // occur on this layer. + + // Subscribers are notified once, asynchronously when any number of changes + // occur in a given tick of the event loop. You should re-query the layer + // to determine the state of markers in which you're interested in. It may + // be counter-intuitive, but this is much more efficient than subscribing to + // events on individual markers, which are expensive to deliver. + + // Returns a {Disposable}. + onDidUpdate(callback) { + return this.emitter.on('did-update', callback); + } + + // Public: Subscribe to be notified synchronously whenever markers are created + // on this layer. *Avoid this method for optimal performance when interacting + // with layers that could contain large numbers of markers.* + + // * `callback` A {Function} that will be called with a {Marker} whenever a + // new marker is created. + + // You should prefer {::onDidUpdate} when synchronous notifications aren't + // absolutely necessary. + + // Returns a {Disposable}. + onDidCreateMarker(callback) { + this.emitCreateMarkerEvents = true; + return this.emitter.on('did-create-marker', callback); + } + + // Public: Subscribe to be notified synchronously when this layer is destroyed. + + // Returns a {Disposable}. + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback); + } + + /* + Section: Private - TextBuffer interface + */ + splice(start, oldExtent, newExtent) { + var invalidated; + invalidated = this.index.splice(start, oldExtent, newExtent); + return invalidated.touch.forEach((id) => { + var marker, ref; + marker = this.markersById[id]; + if ((ref = invalidated[marker.getInvalidationStrategy()]) != null ? ref.has(id) : void 0) { + if (this.destroyInvalidatedMarkers) { + return marker.destroy(); + } else { + return marker.valid = false; + } + } + }); + } + + restoreFromSnapshot(snapshots, alwaysCreate) { + var existingMarkerIds, i, id, j, len, len1, marker, newMarker, range, results, snapshot, snapshotIds; + if (snapshots == null) { + return; + } + snapshotIds = Object.keys(snapshots); + existingMarkerIds = Object.keys(this.markersById); + for (i = 0, len = snapshotIds.length; i < len; i++) { + id = snapshotIds[i]; + snapshot = snapshots[id]; + if (alwaysCreate) { + this.createMarker(snapshot.range, snapshot, true); + continue; + } + if (marker = this.markersById[id]) { + marker.update(marker.getRange(), snapshot, true, true); + } else { + ({marker} = snapshot); + if (marker) { + this.markersById[marker.id] = marker; + ({range} = snapshot); + this.index.insert(marker.id, range.start, range.end); + marker.update(marker.getRange(), snapshot, true, true); + if (this.emitCreateMarkerEvents) { + this.emitter.emit('did-create-marker', marker); + } + } else { + newMarker = this.createMarker(snapshot.range, snapshot, true); + } + } + } + results = []; + for (j = 0, len1 = existingMarkerIds.length; j < len1; j++) { + id = existingMarkerIds[j]; + if ((marker = this.markersById[id]) && (snapshots[id] == null)) { + results.push(marker.destroy(true)); + } else { + results.push(void 0); + } + } + return results; + } + + createSnapshot() { + var i, id, len, marker, ranges, ref, result; + result = {}; + ranges = this.index.dump(); + ref = Object.keys(this.markersById); + for (i = 0, len = ref.length; i < len; i++) { + id = ref[i]; + marker = this.markersById[id]; + result[id] = marker.getSnapshot(Range.fromObject(ranges[id])); + } + return result; + } + + emitChangeEvents(snapshot) { + return this.markersWithChangeListeners.forEach(function(marker) { + var ref; + if (!marker.isDestroyed()) { // event handlers could destroy markers + return marker.emitChangeEvent(snapshot != null ? (ref = snapshot[marker.id]) != null ? ref.range : void 0 : void 0, true, false); + } + }); + } + + serialize() { + var i, id, len, marker, markersById, ranges, ref, snapshot; + ranges = this.index.dump(); + markersById = {}; + ref = Object.keys(this.markersById); + for (i = 0, len = ref.length; i < len; i++) { + id = ref[i]; + marker = this.markersById[id]; + snapshot = marker.getSnapshot(Range.fromObject(ranges[id]), false); + markersById[id] = snapshot; + } + return { + id: this.id, + maintainHistory: this.maintainHistory, + role: this.role, + persistent: this.persistent, + markersById, + version: SerializationVersion + }; + } + + deserialize(state) { + var id, markerState, range, ref; + if (state.version !== SerializationVersion) { + return; + } + this.id = state.id; + this.maintainHistory = state.maintainHistory; + this.role = state.role; + if (this.role === "selections") { + this.delegate.registerSelectionsMarkerLayer(this); + } + this.persistent = state.persistent; + ref = state.markersById; + for (id in ref) { + markerState = ref[id]; + range = Range.fromObject(markerState.range); + delete markerState.range; + this.addMarker(id, range, markerState); + } + } + + /* + Section: Private - Marker interface + */ + markerUpdated() { + return this.delegate.markersUpdated(this); + } + + destroyMarker(marker, suppressMarkerLayerUpdateEvents = false) { + if (this.markersById.hasOwnProperty(marker.id)) { + delete this.markersById[marker.id]; + this.index.remove(marker.id); + this.markersWithChangeListeners.delete(marker); + this.markersWithDestroyListeners.delete(marker); + this.displayMarkerLayers.forEach(function(displayMarkerLayer) { + return displayMarkerLayer.destroyMarker(marker.id); + }); + if (!suppressMarkerLayerUpdateEvents) { + return this.delegate.markersUpdated(this); + } + } + } + + hasMarker(id) { + return !this.destroyed && this.index.has(id); + } + + getMarkerRange(id) { + return Range.fromObject(this.index.getRange(id)); + } + + getMarkerStartPosition(id) { + return Point.fromObject(this.index.getStart(id)); + } + + getMarkerEndPosition(id) { + return Point.fromObject(this.index.getEnd(id)); + } + + compareMarkers(id1, id2) { + return this.index.compare(id1, id2); + } + + setMarkerRange(id, range) { + var end, start; + ({start, end} = Range.fromObject(range)); + start = this.delegate.clipPosition(start); + end = this.delegate.clipPosition(end); + this.index.remove(id); + return this.index.insert(id, start, end); + } + + setMarkerIsExclusive(id, exclusive) { + return this.index.setExclusive(id, exclusive); + } + + createMarker(range, params, suppressMarkerLayerUpdateEvents = false) { + var id, marker, ref; + id = this.delegate.getNextMarkerId(); + marker = this.addMarker(id, range, params); + this.delegate.markerCreated(this, marker); + if (!suppressMarkerLayerUpdateEvents) { + this.delegate.markersUpdated(this); + } + marker.trackDestruction = (ref = this.trackDestructionInOnDidCreateMarkerCallbacks) != null ? ref : false; + if (this.emitCreateMarkerEvents) { + this.emitter.emit('did-create-marker', marker); + } + marker.trackDestruction = false; + return marker; + } + + /* + Section: Internal + */ + addMarker(id, range, params) { + range = Range.fromObject(range); + Point.assertValid(range.start); + Point.assertValid(range.end); + this.index.insert(id, range.start, range.end); + return this.markersById[id] = new Marker(id, this, range, params); + } + + emitUpdateEvent() { + return this.emitter.emit('did-update'); + } + +}; + +const filterSet = function(set1, set2) { + if (set1) { + intersectSet(set1, set2); + return set1; + } else { + return set2; + } +}; From ceddf2811ada777f6624602ab9d11ad8b12007d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Mon, 17 Oct 2022 11:34:50 -0300 Subject: [PATCH 11/64] Using different pathwatcher --- package.json | 3 ++- src/text-buffer.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0cf5b8347c..b56eae663b 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "serializable": "^1.0.3", "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", "underscore-plus": "^1.0.0", - "winattr": "^3.0.0" + "winattr": "^3.0.0", + "pathwatcher": "file:../pulsar-pathwatcher" }, "standard": { "env": { diff --git a/src/text-buffer.js b/src/text-buffer.js index eae9a830a1..f72d66c998 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -1,5 +1,5 @@ const {Emitter, CompositeDisposable} = require('event-kit') -const File = require('./file') +const {File} = require('pathwatcher') const diff = require('diff') const _ = require('underscore-plus') const path = require('path') From c7e6b80feb44db449180e919e61ffe00f190a8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Sun, 23 Oct 2022 13:00:40 -0300 Subject: [PATCH 12/64] Updating pathwatcher dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b56eae663b..a0d1cccf24 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "file:../pulsar-pathwatcher" + "pathwatcher": "https://github.com/pulsar-edit/pulsar-pathwatcher.git" }, "standard": { "env": { From 18fb410751032c5ed312ce224db0fdc7b07969dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Mon, 31 Oct 2022 10:35:55 -0300 Subject: [PATCH 13/64] Bump pathwatcher --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0d1cccf24..14e4870d9a 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "https://github.com/pulsar-edit/pulsar-pathwatcher.git" + "pathwatcher": "https://github.com/pulsar-edit/pulsar-pathwatcher.git#de91ac16e20f12cc04779cc256c6f55087b91a92" }, "standard": { "env": { From 60dc5e7965f5dafef72f4d81541f7a80b70281a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Wed, 2 Nov 2022 11:19:10 -0300 Subject: [PATCH 14/64] Back to pathwatcher... --- package.json | 2 +- src/file.coffee | 405 ------------------------------------------------ 2 files changed, 1 insertion(+), 406 deletions(-) delete mode 100644 src/file.coffee diff --git a/package.json b/package.json index 14e4870d9a..e8cc6eb1ef 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "https://github.com/pulsar-edit/pulsar-pathwatcher.git#de91ac16e20f12cc04779cc256c6f55087b91a92" + "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#2bf9921" }, "standard": { "env": { diff --git a/src/file.coffee b/src/file.coffee deleted file mode 100644 index 95aaed2016..0000000000 --- a/src/file.coffee +++ /dev/null @@ -1,405 +0,0 @@ -crypto = require 'crypto' -path = require 'path' - -_ = require 'underscore-plus' -{Emitter, Disposable} = require 'event-kit' -fs = require 'fs-plus' -Grim = require 'grim' - -iconv = null # Defer until used - -Directory = null - -# Extended: Represents an individual file that can be watched, read from, and -# written to. -module.exports = -class File - encoding: 'utf8' - realPath: null - subscriptionCount: 0 - - ### - Section: Construction - ### - - # Public: Configures a new File instance, no files are accessed. - # - # * `filePath` A {String} containing the absolute path to the file - # * `symlink` (optional) A {Boolean} indicating if the path is a symlink (default: false). - constructor: (filePath, @symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) -> - filePath = path.normalize(filePath) if filePath - @path = filePath - @emitter = new Emitter - - if includeDeprecatedAPIs - @on 'contents-changed-subscription-will-be-added', @willAddSubscription - @on 'moved-subscription-will-be-added', @willAddSubscription - @on 'removed-subscription-will-be-added', @willAddSubscription - @on 'contents-changed-subscription-removed', @didRemoveSubscription - @on 'moved-subscription-removed', @didRemoveSubscription - @on 'removed-subscription-removed', @didRemoveSubscription - - @cachedContents = null - @reportOnDeprecations = true - - # Public: Creates the file on disk that corresponds to `::getPath()` if no - # such file already exists. - # - # Returns a {Promise} that resolves once the file is created on disk. It - # resolves to a boolean value that is true if the file was created or false if - # it already existed. - create: -> - @exists().then (isExistingFile) => - unless isExistingFile - parent = @getParent() - parent.create().then => - @write('').then -> true - else - false - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the file's contents change. - # - # * `callback` {Function} to be called when the file's contents change. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-change', callback)) - - # Public: Invoke the given callback when the file's path changes. - # - # * `callback` {Function} to be called when the file's path changes. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRename: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-rename', callback)) - - # Public: Invoke the given callback when the file is deleted. - # - # * `callback` {Function} to be called when the file is deleted. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDelete: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-delete', callback)) - - # Public: Invoke the given callback when there is an error with the watch. - # When your callback has been invoked, the file will have unsubscribed from - # the file watches. - # - # * `callback` {Function} callback - # * `errorObject` {Object} - # * `error` {Object} the error object - # * `handle` {Function} call this to indicate you have handled the error. - # The error will not be thrown if this function is called. - onWillThrowWatchError: (callback) -> - @emitter.on('will-throw-watch-error', callback) - - willAddSubscription: => - @subscriptionCount++ - try - @subscribeToNativeChangeEvents() - - didRemoveSubscription: => - @subscriptionCount-- - @unsubscribeFromNativeChangeEvents() if @subscriptionCount is 0 - - trackUnsubscription: (subscription) -> - new Disposable => - subscription.dispose() - @didRemoveSubscription() - - ### - Section: File Metadata - ### - - # Public: Returns a {Boolean}, always true. - isFile: -> true - - # Public: Returns a {Boolean}, always false. - isDirectory: -> false - - # Public: Returns a {Boolean} indicating whether or not this is a symbolic link - isSymbolicLink: -> - @symlink - - # Public: Returns a promise that resolves to a {Boolean}, true if the file - # exists, false otherwise. - exists: -> - new Promise (resolve) => - fs.exists @getPath(), resolve - - # Public: Returns a {Boolean}, true if the file exists, false otherwise. - existsSync: -> - fs.existsSync(@getPath()) - - # Public: Get the SHA-1 digest of this file - # - # Returns a promise that resolves to a {String}. - getDigest: -> - if @digest? - Promise.resolve(@digest) - else - @read().then => @digest # read assigns digest as a side-effect - - # Public: Get the SHA-1 digest of this file - # - # Returns a {String}. - getDigestSync: -> - @readSync() unless @digest - @digest - - setDigest: (contents) -> - @digest = crypto.createHash('sha1').update(contents ? '').digest('hex') - - # Public: Sets the file's character set encoding name. - # - # * `encoding` The {String} encoding to use (default: 'utf8') - setEncoding: (encoding='utf8') -> - # Throws if encoding doesn't exist. Better to throw an exception early - # instead of waiting until the file is saved. - - if encoding isnt 'utf8' - iconv ?= require 'iconv-lite' - iconv.getCodec(encoding) - - @encoding = encoding - - # Public: Returns the {String} encoding name for this file (default: 'utf8'). - getEncoding: -> @encoding - - ### - Section: Managing Paths - ### - - # Public: Returns the {String} path for the file. - getPath: -> @path - - # Sets the path for the file. - setPath: (@path) -> - @realPath = null - - # Public: Returns this file's completely resolved {String} path. - getRealPathSync: -> - unless @realPath? - try - @realPath = fs.realpathSync(@path) - catch error - @realPath = @path - @realPath - - # Public: Returns a promise that resolves to the file's completely resolved {String} path. - getRealPath: -> - if @realPath? - Promise.resolve(@realPath) - else - new Promise (resolve, reject) => - fs.realpath @path, (err, result) => - if err? - reject(err) - else - resolve(@realPath = result) - - # Public: Return the {String} filename without any directory information. - getBaseName: -> - path.basename(@path) - - ### - Section: Traversing - ### - - # Public: Return the {Directory} that contains this file. - getParent: -> - Directory ?= require './directory' - new Directory(path.dirname @path) - - ### - Section: Reading and Writing - ### - - readSync: (flushCache) -> - if not @existsSync() - @cachedContents = null - else if not @cachedContents? or flushCache - encoding = @getEncoding() - if encoding is 'utf8' - @cachedContents = fs.readFileSync(@getPath(), encoding) - else - iconv ?= require 'iconv-lite' - @cachedContents = iconv.decode(fs.readFileSync(@getPath()), encoding) - - @setDigest(@cachedContents) - @cachedContents - - writeFileSync: (filePath, contents) -> - encoding = @getEncoding() - if encoding is 'utf8' - fs.writeFileSync(filePath, contents, {encoding}) - else - iconv ?= require 'iconv-lite' - fs.writeFileSync(filePath, iconv.encode(contents, encoding)) - - # 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 resolves to either a {String}, or null if the file does not exist. - read: (flushCache) -> - if @cachedContents? and not flushCache - promise = Promise.resolve(@cachedContents) - else - promise = new Promise (resolve, reject) => - content = [] - readStream = @createReadStream() - - readStream.on 'data', (chunk) -> - content.push(chunk) - - readStream.on 'end', -> - resolve(content.join('')) - - readStream.on 'error', (error) -> - if error.code == 'ENOENT' - resolve(null) - else - reject(error) - - promise.then (contents) => - @setDigest(contents) - @cachedContents = contents - - # Public: Returns a stream to read the content of the file. - # - # Returns a {ReadStream} object. - createReadStream: -> - encoding = @getEncoding() - if encoding is 'utf8' - fs.createReadStream(@getPath(), {encoding}) - else - iconv ?= require 'iconv-lite' - fs.createReadStream(@getPath()).pipe(iconv.decodeStream(encoding)) - - # Public: Overwrites the file with the given text. - # - # * `text` The {String} text to write to the underlying file. - # - # Returns a {Promise} that resolves when the file has been written. - write: (text) -> - @exists().then (previouslyExisted) => - @writeFile(@getPath(), text).then => - @cachedContents = text - @setDigest(text) - @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() - undefined - - # Public: Returns a stream to write content to the file. - # - # Returns a {WriteStream} object. - createWriteStream: -> - encoding = @getEncoding() - if encoding is 'utf8' - fs.createWriteStream(@getPath(), {encoding}) - else - iconv ?= require 'iconv-lite' - stream = iconv.encodeStream(encoding) - stream.pipe(fs.createWriteStream(@getPath())) - stream - - # Public: Overwrites the file with the given text. - # - # * `text` The {String} text to write to the underlying file. - # - # Returns undefined. - writeSync: (text) -> - previouslyExisted = @existsSync() - @writeFileSync(@getPath(), text) - @cachedContents = text - @setDigest(text) - @emit 'contents-changed' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-change' - @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() - undefined - - writeFile: (filePath, contents) -> - encoding = @getEncoding() - if encoding is 'utf8' - new Promise (resolve, reject) -> - fs.writeFile filePath, contents, {encoding}, (err, result) -> - if err? - reject(err) - else - resolve(result) - else - iconv ?= require 'iconv-lite' - new Promise (resolve, reject) -> - fs.writeFile filePath, iconv.encode(contents, encoding), (err, result) -> - if err? - reject(err) - else - resolve(result) - - subscribeToNativeChangeEvents: -> - # @watchSubscription ?= PathWatcher.watch @path, (args...) => - # @handleNativeChangeEvent(args...) - - unsubscribeFromNativeChangeEvents: -> - # if @watchSubscription? - # @watchSubscription.close() - # @watchSubscription = null - - ### - Section: Private - ### - - handleNativeChangeEvent: (eventType, eventPath) -> - switch eventType - when 'delete' - @unsubscribeFromNativeChangeEvents() - @detectResurrectionAfterDelay() - when 'rename' - @setPath(eventPath) - @emit 'moved' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-rename' - when 'change', 'resurrect' - @cachedContents = null - @emitter.emit 'did-change' - - detectResurrectionAfterDelay: -> - _.delay (=> @detectResurrection()), 50 - - detectResurrection: -> - @exists().then (exists) => - if exists - @subscribeToNativeChangeEvents() - @handleNativeChangeEvent('resurrect') - else - @cachedContents = null - @emit 'removed' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-delete' - -if Grim.includeDeprecatedAPIs - EmitterMixin = require('emissary').Emitter - EmitterMixin.includeInto(File) - - File::on = (eventName) -> - switch eventName - when 'contents-changed' - Grim.deprecate("Use File::onDidChange instead") - when 'moved' - Grim.deprecate("Use File::onDidRename instead") - when 'removed' - Grim.deprecate("Use File::onDidDelete instead") - else - if @reportOnDeprecations - Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") - - EmitterMixin::on.apply(this, arguments) -else - File::hasSubscriptions = -> - @subscriptionCount > 0 From c62430d7ea0ba03d0f81ad9e0de8fc04f0caeb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Wed, 2 Nov 2022 11:43:22 -0300 Subject: [PATCH 15/64] Back to pathwatcher... --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8cc6eb1ef..bb68ab2b3f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#2bf9921" + "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" }, "standard": { "env": { From 275c296a19ee8a4cb7c43f0a5fd5db2f3b55e0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Mon, 21 Nov 2022 11:59:03 -0300 Subject: [PATCH 16/64] Updated superstring --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb68ab2b3f..c2027081b6 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring.git#708924046f8db9d05901a5372337f249ff5c47bb", + "superstring": "https://github.com/pulsar-edit/superstring-wasm.git", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" From f8702c877d7399936405e2f9a18ec71c7149000f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Sat, 10 Dec 2022 00:37:18 -0300 Subject: [PATCH 17/64] Bump Superstring --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2027081b6..492b797bc3 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring-wasm.git", + "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#536739fef8f8cab757970b5aa56397e2f9493a02", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" From 219a14ef919cf87c8d838097cdb572f8111f3424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Sun, 11 Dec 2022 16:33:44 -0300 Subject: [PATCH 18/64] Superstring bump again --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 492b797bc3..46b67608b7 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#536739fef8f8cab757970b5aa56397e2f9493a02", + "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#e320512", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" From d19015124294a0cd88f51c4caac6467567aeb547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 13 Dec 2022 14:25:48 -0300 Subject: [PATCH 19/64] Fixing Superstring's TextBuffer#load. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46b67608b7..afba795296 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#e320512", + "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#2624f63", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" From edcf6d4ec9b1ddf99419fefc1a11415b67e0bcf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 13 Dec 2022 17:43:29 -0300 Subject: [PATCH 20/64] Some fixes on Superstring's WASM differences --- package.json | 2 +- src/text-buffer.js | 25 ++++++++++--------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index afba795296..a4a553753a 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#2624f63", + "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#3fa4588", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" diff --git a/src/text-buffer.js b/src/text-buffer.js index f72d66c998..9e2c1473fa 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -2116,20 +2116,17 @@ class TextBuffer { encoding: this.getEncoding(), force: options && options.discardChanges, patch: this.loaded - }, - (percentDone, patch) => { - if (this.loadCount > loadCount) return false - if (patch) { - if (patch.getChangeCount() > 0) { - checkpoint = this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(), isBarrier: true}) - this.emitter.emit('will-reload') - return this.emitWillChangeEvent() - } else if (options && options.discardChanges) { - return this.emitter.emit('will-reload') - } - } } ) + if (patch) { + if (patch.getChangeCount() > 0) { + checkpoint = this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(), isBarrier: true}) + this.emitter.emit('will-reload') + return this.emitWillChangeEvent() + } else if (options && options.discardChanges) { + return this.emitter.emit('will-reload') + } + } this.finishLoading(checkpoint, patch, options) } catch (error) { if ((!options || !options.mustExist) && error.code === 'ENOENT') { @@ -2263,9 +2260,7 @@ class TextBuffer { this.fileHasChangedSinceLastLoad = true if (this.isModified()) { - const source = this.file instanceof File - ? this.file.getPath() - : this.file.createReadStream() + const source = this.file.getPath() if (!(await this.buffer.baseTextMatchesFile(source, this.getEncoding()))) { this.emitter.emit('did-conflict') } From 5cd684c07e3712766393b17bbd67d7af8850ed8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 13 Dec 2022 17:49:43 -0300 Subject: [PATCH 21/64] Fixes returns of old callbacks --- src/text-buffer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-buffer.js b/src/text-buffer.js index 9e2c1473fa..c268f7f549 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -2122,9 +2122,9 @@ class TextBuffer { if (patch.getChangeCount() > 0) { checkpoint = this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(), isBarrier: true}) this.emitter.emit('will-reload') - return this.emitWillChangeEvent() + this.emitWillChangeEvent() } else if (options && options.discardChanges) { - return this.emitter.emit('will-reload') + this.emitter.emit('will-reload') } } this.finishLoading(checkpoint, patch, options) From c0c7cf3e369dc9924488712d31bf08881eacf032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Wed, 14 Dec 2022 11:53:06 -0300 Subject: [PATCH 22/64] Bump superstring --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4a553753a..c8b03bf078 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#3fa4588", + "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#312d1fb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" From eed7259cd80165d90341c3543228270439e1321b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 12:57:25 -0300 Subject: [PATCH 23/64] Atom => Pulsar on README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 32c381198b..c14ed01fef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -# Atom TextBuffer Core -[![CI](https://github.com/atom/text-buffer/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/text-buffer/actions/workflows/ci.yml) +# Pulsar TextBuffer Core -This is the core of the Atom text buffer, separated into its own module so its tests can be run headless. It handles the storage and manipulation of text and associated regions (markers). +This is the core of the Pulsar's text buffer, separated into its own module so its tests can be run headless. It handles the storage and manipulation of text and associated regions (markers). From 12d22ab488863679c5600b751298fe5dd7234740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 13:11:09 -0300 Subject: [PATCH 24/64] Decaff DefaultHistoryProvider --- src/default-history-provider.coffee | 434 -------------------- src/default-history-provider.js | 591 ++++++++++++++++++++++++++++ 2 files changed, 591 insertions(+), 434 deletions(-) delete mode 100644 src/default-history-provider.coffee create mode 100644 src/default-history-provider.js diff --git a/src/default-history-provider.coffee b/src/default-history-provider.coffee deleted file mode 100644 index 40a1a194f4..0000000000 --- a/src/default-history-provider.coffee +++ /dev/null @@ -1,434 +0,0 @@ -Patch = null; -require('superstring').superstring.then (r) => Patch = r.Patch; -MarkerLayer = require './marker-layer' -{traversal} = require './point-helpers' -{patchFromChanges} = require './helpers' - -SerializationVersion = 6 - -class Checkpoint - constructor: (@id, @snapshot, @isBarrier) -> - unless @snapshot? - global.atom?.assert(false, "Checkpoint created without snapshot") - @snapshot = {} - -class Transaction - constructor: (@markerSnapshotBefore, @patch, @markerSnapshotAfter, @groupingInterval=0) -> - @timestamp = Date.now() - - shouldGroupWith: (previousTransaction) -> - timeBetweenTransactions = @timestamp - previousTransaction.timestamp - timeBetweenTransactions < Math.min(@groupingInterval, previousTransaction.groupingInterval) - - groupWith: (previousTransaction) -> - new Transaction( - previousTransaction.markerSnapshotBefore, - Patch.compose([previousTransaction.patch, @patch]), - @markerSnapshotAfter, - @groupingInterval - ) - -# Manages undo/redo for {TextBuffer} -module.exports = -class DefaultHistoryProvider - constructor: (@buffer) -> - @maxUndoEntries = @buffer.maxUndoEntries - @nextCheckpointId = 1 - @undoStack = [] - @redoStack = [] - - createCheckpoint: (options) -> - checkpoint = new Checkpoint(@nextCheckpointId++, options?.markers, options?.isBarrier) - @undoStack.push(checkpoint) - checkpoint.id - - groupChangesSinceCheckpoint: (checkpointId, options) -> - deleteCheckpoint = options?.deleteCheckpoint ? false - markerSnapshotAfter = options?.markers - checkpointIndex = null - markerSnapshotBefore = null - patchesSinceCheckpoint = [] - - for entry, i in @undoStack by -1 - break if checkpointIndex? - - switch entry.constructor - when Checkpoint - if entry.id is checkpointId - checkpointIndex = i - markerSnapshotBefore = entry.snapshot - else if entry.isBarrier - return false - when Transaction - patchesSinceCheckpoint.unshift(entry.patch) - when Patch - patchesSinceCheckpoint.unshift(entry) - else - throw new Error("Unexpected undo stack entry type: #{entry.constructor.name}") - - if checkpointIndex? - composedPatches = Patch.compose(patchesSinceCheckpoint) - if patchesSinceCheckpoint.length > 0 - @undoStack.splice(checkpointIndex + 1) - @undoStack.push(new Transaction(markerSnapshotBefore, composedPatches, markerSnapshotAfter)) - if deleteCheckpoint - @undoStack.splice(checkpointIndex, 1) - composedPatches.getChanges() - else - false - - getChangesSinceCheckpoint: (checkpointId) -> - checkpointIndex = null - patchesSinceCheckpoint = [] - - for entry, i in @undoStack by -1 - break if checkpointIndex? - - switch entry.constructor - when Checkpoint - if entry.id is checkpointId - checkpointIndex = i - when Transaction - patchesSinceCheckpoint.unshift(entry.patch) - when Patch - patchesSinceCheckpoint.unshift(entry) - else - throw new Error("Unexpected undo stack entry type: #{entry.constructor.name}") - - if checkpointIndex? - Patch.compose(patchesSinceCheckpoint).getChanges() - else - null - - groupLastChanges: -> - markerSnapshotAfter = null - markerSnapshotBefore = null - patchesSinceCheckpoint = [] - - for entry, i in @undoStack by -1 - switch entry.constructor - when Checkpoint - return false if entry.isBarrier - when Transaction - if patchesSinceCheckpoint.length is 0 - markerSnapshotAfter = entry.markerSnapshotAfter - else if patchesSinceCheckpoint.length is 1 - markerSnapshotBefore = entry.markerSnapshotBefore - patchesSinceCheckpoint.unshift(entry.patch) - when Patch - patchesSinceCheckpoint.unshift(entry) - else - throw new Error("Unexpected undo stack entry type: #{entry.constructor.name}") - - if patchesSinceCheckpoint.length is 2 - composedPatch = Patch.compose(patchesSinceCheckpoint) - @undoStack.splice(i) - @undoStack.push(new Transaction(markerSnapshotBefore, composedPatch, markerSnapshotAfter)) - return true - return - - enforceUndoStackSizeLimit: -> - if @undoStack.length > @maxUndoEntries - @undoStack.splice(0, @undoStack.length - @maxUndoEntries) - - applyGroupingInterval: (groupingInterval) -> - topEntry = @undoStack[@undoStack.length - 1] - previousEntry = @undoStack[@undoStack.length - 2] - - if topEntry instanceof Transaction - topEntry.groupingInterval = groupingInterval - else - return - - return if groupingInterval is 0 - - if previousEntry instanceof Transaction and topEntry.shouldGroupWith(previousEntry) - @undoStack.splice(@undoStack.length - 2, 2, topEntry.groupWith(previousEntry)) - - pushChange: ({newStart, oldExtent, newExtent, oldText, newText}) -> - patch = new Patch - patch.splice(newStart, oldExtent, newExtent, oldText, newText) - @pushPatch(patch) - - pushPatch: (patch) -> - @undoStack.push(patch) - @clearRedoStack() - - undo: -> - snapshotBelow = null - patch = null - spliceIndex = null - - for entry, i in @undoStack by -1 - break if spliceIndex? - - switch entry.constructor - when Checkpoint - if entry.isBarrier - return false - when Transaction - snapshotBelow = entry.markerSnapshotBefore - patch = entry.patch.invert() - spliceIndex = i - when Patch - patch = entry.invert() - spliceIndex = i - else - throw new Error("Unexpected entry type when popping undoStack: #{entry.constructor.name}") - - if spliceIndex? - @redoStack.push(@undoStack.splice(spliceIndex).reverse()...) - { - textUpdates: patch.getChanges() - markers: snapshotBelow - } - else - false - - redo: -> - snapshotBelow = null - patch = null - spliceIndex = null - - for entry, i in @redoStack by -1 - break if spliceIndex? - - switch entry.constructor - when Checkpoint - if entry.isBarrier - throw new Error("Invalid redo stack state") - when Transaction - snapshotBelow = entry.markerSnapshotAfter - patch = entry.patch - spliceIndex = i - when Patch - patch = entry - spliceIndex = i - else - throw new Error("Unexpected entry type when popping redoStack: #{entry.constructor.name}") - - while @redoStack[spliceIndex - 1] instanceof Checkpoint - spliceIndex-- - - if spliceIndex? - @undoStack.push(@redoStack.splice(spliceIndex).reverse()...) - { - textUpdates: patch.getChanges() - markers: snapshotBelow - } - else - false - - revertToCheckpoint: (checkpointId) -> - snapshotBelow = null - spliceIndex = null - patchesSinceCheckpoint = [] - - for entry, i in @undoStack by -1 - break if spliceIndex? - - switch entry.constructor - when Checkpoint - if entry.id is checkpointId - snapshotBelow = entry.snapshot - spliceIndex = i - else if entry.isBarrier - return false - when Transaction - patchesSinceCheckpoint.push(entry.patch.invert()) - else - patchesSinceCheckpoint.push(entry.invert()) - - if spliceIndex? - @undoStack.splice(spliceIndex) - { - textUpdates: Patch.compose(patchesSinceCheckpoint).getChanges() - markers: snapshotBelow - } - else - false - - clear: -> - @clearUndoStack() - @clearRedoStack() - - clearUndoStack: -> - @undoStack.length = 0 - - clearRedoStack: -> - @redoStack.length = 0 - - toString: -> - output = '' - for entry in @undoStack - switch entry.constructor - when Checkpoint - output += "Checkpoint, " - when Transaction - output += "Transaction, " - when Patch - output += "Patch, " - else - output += "Unknown {#{JSON.stringify(entry)}}, " - '[' + output.slice(0, -2) + ']' - - serialize: (options) -> - version: SerializationVersion - nextCheckpointId: @nextCheckpointId - undoStack: @serializeStack(@undoStack, options) - redoStack: @serializeStack(@redoStack, options) - maxUndoEntries: @maxUndoEntries - - deserialize: (state) -> - return unless state.version is SerializationVersion - @nextCheckpointId = state.nextCheckpointId - @maxUndoEntries = state.maxUndoEntries - @undoStack = @deserializeStack(state.undoStack) - @redoStack = @deserializeStack(state.redoStack) - - getSnapshot: (maxEntries) -> - undoStackPatches = [] - undoStack = [] - for entry in @undoStack by -1 - switch entry.constructor - when Checkpoint - undoStack.unshift(snapshotFromCheckpoint(entry)) - when Transaction - undoStack.unshift(snapshotFromTransaction(entry)) - undoStackPatches.unshift(entry.patch) - - break if undoStack.length is maxEntries - - redoStack = [] - for entry in @redoStack by -1 - switch entry.constructor - when Checkpoint - redoStack.unshift(snapshotFromCheckpoint(entry)) - when Transaction - redoStack.unshift(snapshotFromTransaction(entry)) - - break if redoStack.length is maxEntries - - { - nextCheckpointId: @nextCheckpointId, - undoStackChanges: Patch.compose(undoStackPatches).getChanges(), - undoStack, - redoStack - } - - restoreFromSnapshot: ({@nextCheckpointId, undoStack, redoStack}) -> - @undoStack = undoStack.map (entry) -> - switch entry.type - when 'transaction' - transactionFromSnapshot(entry) - when 'checkpoint' - checkpointFromSnapshot(entry) - - @redoStack = redoStack.map (entry) -> - switch entry.type - when 'transaction' - transactionFromSnapshot(entry) - when 'checkpoint' - checkpointFromSnapshot(entry) - - ### - Section: Private - ### - - getCheckpointIndex: (checkpointId) -> - for entry, i in @undoStack by -1 - if entry instanceof Checkpoint and entry.id is checkpointId - return i - return null - - serializeStack: (stack, options) -> - for entry in stack - switch entry.constructor - when Checkpoint - { - type: 'checkpoint' - id: entry.id - snapshot: @serializeSnapshot(entry.snapshot, options) - isBarrier: entry.isBarrier - } - when Transaction - { - type: 'transaction' - markerSnapshotBefore: @serializeSnapshot(entry.markerSnapshotBefore, options) - markerSnapshotAfter: @serializeSnapshot(entry.markerSnapshotAfter, options) - patch: entry.patch.serialize().toString('base64') - } - when Patch - { - type: 'patch' - data: entry.serialize().toString('base64') - } - else - throw new Error("Unexpected undoStack entry type during serialization: #{entry.constructor.name}") - - deserializeStack: (stack) -> - for entry in stack - switch entry.type - when 'checkpoint' - new Checkpoint( - entry.id - MarkerLayer.deserializeSnapshot(entry.snapshot) - entry.isBarrier - ) - when 'transaction' - new Transaction( - MarkerLayer.deserializeSnapshot(entry.markerSnapshotBefore) - Patch.deserialize(Buffer.from(entry.patch, 'base64')) - MarkerLayer.deserializeSnapshot(entry.markerSnapshotAfter) - ) - when 'patch' - Patch.deserialize(Buffer.from(entry.data, 'base64')) - else - throw new Error("Unexpected undoStack entry type during deserialization: #{entry.type}") - - serializeSnapshot: (snapshot, options) -> - return unless options.markerLayers - - serializedLayerSnapshots = {} - for layerId, layerSnapshot of snapshot - continue unless @buffer.getMarkerLayer(layerId)?.persistent - serializedMarkerSnapshots = {} - for markerId, markerSnapshot of layerSnapshot - serializedMarkerSnapshot = Object.assign({}, markerSnapshot) - delete serializedMarkerSnapshot.marker - serializedMarkerSnapshots[markerId] = serializedMarkerSnapshot - serializedLayerSnapshots[layerId] = serializedMarkerSnapshots - serializedLayerSnapshots - -snapshotFromCheckpoint = (checkpoint) -> - { - type: 'checkpoint', - id: checkpoint.id, - markers: checkpoint.snapshot - } - -checkpointFromSnapshot = ({id, markers}) -> - new Checkpoint(id, markers, false) - -snapshotFromTransaction = (transaction) -> - changes = [] - for change in transaction.patch.getChanges() by 1 - changes.push({ - oldStart: change.oldStart, - oldEnd: change.oldEnd, - newStart: change.newStart, - newEnd: change.newEnd, - oldText: change.oldText, - newText: change.newText - }) - - { - type: 'transaction', - changes, - markersBefore: transaction.markerSnapshotBefore - markersAfter: transaction.markerSnapshotAfter - } - -transactionFromSnapshot = ({changes, markersBefore, markersAfter}) -> - # TODO: Return raw patch if there's no markersBefore && markersAfter - new Transaction(markersBefore, patchFromChanges(changes), markersAfter) diff --git a/src/default-history-provider.js b/src/default-history-provider.js new file mode 100644 index 0000000000..65c9022344 --- /dev/null +++ b/src/default-history-provider.js @@ -0,0 +1,591 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +let DefaultHistoryProvider; +let Patch = null; +require('superstring').superstring.then(r => { return Patch = r.Patch; }); +const MarkerLayer = require('./marker-layer'); +const {traversal} = require('./point-helpers'); +const {patchFromChanges} = require('./helpers'); + +const SerializationVersion = 6; + +class Checkpoint { + constructor(id, snapshot, isBarrier) { + this.id = id; + this.snapshot = snapshot; + this.isBarrier = isBarrier; + if (this.snapshot == null) { + global.atom?.assert(false, "Checkpoint created without snapshot"); + this.snapshot = {}; + } + } +} + +class Transaction { + constructor(markerSnapshotBefore, patch, markerSnapshotAfter, groupingInterval=0) { + this.markerSnapshotBefore = markerSnapshotBefore; + this.patch = patch; + this.markerSnapshotAfter = markerSnapshotAfter; + this.groupingInterval = groupingInterval; + this.timestamp = Date.now(); + } + + shouldGroupWith(previousTransaction) { + const timeBetweenTransactions = this.timestamp - previousTransaction.timestamp; + return timeBetweenTransactions < Math.min(this.groupingInterval, previousTransaction.groupingInterval); + } + + groupWith(previousTransaction) { + return new Transaction( + previousTransaction.markerSnapshotBefore, + Patch.compose([previousTransaction.patch, this.patch]), + this.markerSnapshotAfter, + this.groupingInterval + ); + } +} + +// Manages undo/redo for {TextBuffer} +module.exports = +(DefaultHistoryProvider = class DefaultHistoryProvider { + constructor(buffer) { + this.buffer = buffer; + this.maxUndoEntries = this.buffer.maxUndoEntries; + this.nextCheckpointId = 1; + this.undoStack = []; + this.redoStack = []; + } + + createCheckpoint(options) { + const checkpoint = new Checkpoint(this.nextCheckpointId++, options?.markers, options?.isBarrier); + this.undoStack.push(checkpoint); + return checkpoint.id; + } + + groupChangesSinceCheckpoint(checkpointId, options) { + const deleteCheckpoint = options?.deleteCheckpoint != null ? options?.deleteCheckpoint : false; + const markerSnapshotAfter = options?.markers; + let checkpointIndex = null; + let markerSnapshotBefore = null; + const patchesSinceCheckpoint = []; + + for (let i = this.undoStack.length - 1; i >= 0; i--) { + const entry = this.undoStack[i]; + if (checkpointIndex != null) { break; } + + switch (entry.constructor) { + case Checkpoint: + if (entry.id === checkpointId) { + checkpointIndex = i; + markerSnapshotBefore = entry.snapshot; + } else if (entry.isBarrier) { + return false; + } + break; + case Transaction: + patchesSinceCheckpoint.unshift(entry.patch); + break; + case Patch: + patchesSinceCheckpoint.unshift(entry); + break; + default: + throw new Error(`Unexpected undo stack entry type: ${entry.constructor.name}`); + } + } + + if (checkpointIndex != null) { + const composedPatches = Patch.compose(patchesSinceCheckpoint); + if (patchesSinceCheckpoint.length > 0) { + this.undoStack.splice(checkpointIndex + 1); + this.undoStack.push(new Transaction(markerSnapshotBefore, composedPatches, markerSnapshotAfter)); + } + if (deleteCheckpoint) { + this.undoStack.splice(checkpointIndex, 1); + } + return composedPatches.getChanges(); + } else { + return false; + } + } + + getChangesSinceCheckpoint(checkpointId) { + let checkpointIndex = null; + const patchesSinceCheckpoint = []; + + for (let i = this.undoStack.length - 1; i >= 0; i--) { + const entry = this.undoStack[i]; + if (checkpointIndex != null) { break; } + + switch (entry.constructor) { + case Checkpoint: + if (entry.id === checkpointId) { + checkpointIndex = i; + } + break; + case Transaction: + patchesSinceCheckpoint.unshift(entry.patch); + break; + case Patch: + patchesSinceCheckpoint.unshift(entry); + break; + default: + throw new Error(`Unexpected undo stack entry type: ${entry.constructor.name}`); + } + } + + if (checkpointIndex != null) { + return Patch.compose(patchesSinceCheckpoint).getChanges(); + } else { + return null; + } + } + + groupLastChanges() { + let markerSnapshotAfter = null; + let markerSnapshotBefore = null; + const patchesSinceCheckpoint = []; + + for (let i = this.undoStack.length - 1; i >= 0; i--) { + const entry = this.undoStack[i]; + switch (entry.constructor) { + case Checkpoint: + if (entry.isBarrier) { return false; } + break; + case Transaction: + if (patchesSinceCheckpoint.length === 0) { + ({ + markerSnapshotAfter + } = entry); + } else if (patchesSinceCheckpoint.length === 1) { + ({ + markerSnapshotBefore + } = entry); + } + patchesSinceCheckpoint.unshift(entry.patch); + break; + case Patch: + patchesSinceCheckpoint.unshift(entry); + break; + default: + throw new Error(`Unexpected undo stack entry type: ${entry.constructor.name}`); + } + + if (patchesSinceCheckpoint.length === 2) { + const composedPatch = Patch.compose(patchesSinceCheckpoint); + this.undoStack.splice(i); + this.undoStack.push(new Transaction(markerSnapshotBefore, composedPatch, markerSnapshotAfter)); + return true; + } + } + } + + enforceUndoStackSizeLimit() { + if (this.undoStack.length > this.maxUndoEntries) { + return this.undoStack.splice(0, this.undoStack.length - this.maxUndoEntries); + } + } + + applyGroupingInterval(groupingInterval) { + const topEntry = this.undoStack[this.undoStack.length - 1]; + const previousEntry = this.undoStack[this.undoStack.length - 2]; + + if (topEntry instanceof Transaction) { + topEntry.groupingInterval = groupingInterval; + } else { + return; + } + + if (groupingInterval === 0) { return; } + + if (previousEntry instanceof Transaction && topEntry.shouldGroupWith(previousEntry)) { + return this.undoStack.splice(this.undoStack.length - 2, 2, topEntry.groupWith(previousEntry)); + } + } + + pushChange({newStart, oldExtent, newExtent, oldText, newText}) { + const patch = new Patch; + patch.splice(newStart, oldExtent, newExtent, oldText, newText); + return this.pushPatch(patch); + } + + pushPatch(patch) { + this.undoStack.push(patch); + return this.clearRedoStack(); + } + + undo() { + let snapshotBelow = null; + let patch = null; + let spliceIndex = null; + + for (let i = this.undoStack.length - 1; i >= 0; i--) { + const entry = this.undoStack[i]; + if (spliceIndex != null) { break; } + + switch (entry.constructor) { + case Checkpoint: + if (entry.isBarrier) { + return false; + } + break; + case Transaction: + snapshotBelow = entry.markerSnapshotBefore; + patch = entry.patch.invert(); + spliceIndex = i; + break; + case Patch: + patch = entry.invert(); + spliceIndex = i; + break; + default: + throw new Error(`Unexpected entry type when popping undoStack: ${entry.constructor.name}`); + } + } + + if (spliceIndex != null) { + this.redoStack.push(...Array.from(this.undoStack.splice(spliceIndex).reverse() || [])); + return { + textUpdates: patch.getChanges(), + markers: snapshotBelow + }; + } else { + return false; + } + } + + redo() { + let snapshotBelow = null; + let patch = null; + let spliceIndex = null; + + for (let i = this.redoStack.length - 1; i >= 0; i--) { + const entry = this.redoStack[i]; + if (spliceIndex != null) { break; } + + switch (entry.constructor) { + case Checkpoint: + if (entry.isBarrier) { + throw new Error("Invalid redo stack state"); + } + break; + case Transaction: + snapshotBelow = entry.markerSnapshotAfter; + ({ + patch + } = entry); + spliceIndex = i; + break; + case Patch: + patch = entry; + spliceIndex = i; + break; + default: + throw new Error(`Unexpected entry type when popping redoStack: ${entry.constructor.name}`); + } + } + + while (this.redoStack[spliceIndex - 1] instanceof Checkpoint) { + spliceIndex--; + } + + if (spliceIndex != null) { + this.undoStack.push(...Array.from(this.redoStack.splice(spliceIndex).reverse() || [])); + return { + textUpdates: patch.getChanges(), + markers: snapshotBelow + }; + } else { + return false; + } + } + + revertToCheckpoint(checkpointId) { + let snapshotBelow = null; + let spliceIndex = null; + const patchesSinceCheckpoint = []; + + for (let i = this.undoStack.length - 1; i >= 0; i--) { + const entry = this.undoStack[i]; + if (spliceIndex != null) { break; } + + switch (entry.constructor) { + case Checkpoint: + if (entry.id === checkpointId) { + snapshotBelow = entry.snapshot; + spliceIndex = i; + } else if (entry.isBarrier) { + return false; + } + break; + case Transaction: + patchesSinceCheckpoint.push(entry.patch.invert()); + break; + default: + patchesSinceCheckpoint.push(entry.invert()); + } + } + + if (spliceIndex != null) { + this.undoStack.splice(spliceIndex); + return { + textUpdates: Patch.compose(patchesSinceCheckpoint).getChanges(), + markers: snapshotBelow + }; + } else { + return false; + } + } + + clear() { + this.clearUndoStack(); + return this.clearRedoStack(); + } + + clearUndoStack() { + return this.undoStack.length = 0; + } + + clearRedoStack() { + return this.redoStack.length = 0; + } + + toString() { + let output = ''; + for (let entry of this.undoStack) { + switch (entry.constructor) { + case Checkpoint: + output += "Checkpoint, "; + break; + case Transaction: + output += "Transaction, "; + break; + case Patch: + output += "Patch, "; + break; + default: + output += `Unknown {${JSON.stringify(entry)}}, `; + } + } + return '[' + output.slice(0, -2) + ']'; + } + + serialize(options) { + return { + version: SerializationVersion, + nextCheckpointId: this.nextCheckpointId, + undoStack: this.serializeStack(this.undoStack, options), + redoStack: this.serializeStack(this.redoStack, options), + maxUndoEntries: this.maxUndoEntries + }; + } + + deserialize(state) { + if (state.version !== SerializationVersion) { return; } + this.nextCheckpointId = state.nextCheckpointId; + this.maxUndoEntries = state.maxUndoEntries; + this.undoStack = this.deserializeStack(state.undoStack); + return this.redoStack = this.deserializeStack(state.redoStack); + } + + getSnapshot(maxEntries) { + let entry; + const undoStackPatches = []; + const undoStack = []; + for (let i = this.undoStack.length - 1; i >= 0; i--) { + entry = this.undoStack[i]; + switch (entry.constructor) { + case Checkpoint: + undoStack.unshift(snapshotFromCheckpoint(entry)); + break; + case Transaction: + undoStack.unshift(snapshotFromTransaction(entry)); + undoStackPatches.unshift(entry.patch); + break; + } + + if (undoStack.length === maxEntries) { break; } + } + + const redoStack = []; + for (let j = this.redoStack.length - 1; j >= 0; j--) { + entry = this.redoStack[j]; + switch (entry.constructor) { + case Checkpoint: + redoStack.unshift(snapshotFromCheckpoint(entry)); + break; + case Transaction: + redoStack.unshift(snapshotFromTransaction(entry)); + break; + } + + if (redoStack.length === maxEntries) { break; } + } + + return { + nextCheckpointId: this.nextCheckpointId, + undoStackChanges: Patch.compose(undoStackPatches).getChanges(), + undoStack, + redoStack + }; + } + + restoreFromSnapshot({nextCheckpointId, undoStack, redoStack}) { + this.nextCheckpointId = nextCheckpointId; + this.undoStack = undoStack.map(function(entry) { + switch (entry.type) { + case 'transaction': + return transactionFromSnapshot(entry); + case 'checkpoint': + return checkpointFromSnapshot(entry); + } + }); + + return this.redoStack = redoStack.map(function(entry) { + switch (entry.type) { + case 'transaction': + return transactionFromSnapshot(entry); + case 'checkpoint': + return checkpointFromSnapshot(entry); + } + }); + } + + /* + Section: Private + */ + + getCheckpointIndex(checkpointId) { + for (let i = this.undoStack.length - 1; i >= 0; i--) { + const entry = this.undoStack[i]; + if (entry instanceof Checkpoint && (entry.id === checkpointId)) { + return i; + } + } + return null; + } + + serializeStack(stack, options) { + return (() => { + const result = []; + for (let entry of stack) { + switch (entry.constructor) { + case Checkpoint: + result.push({ + type: 'checkpoint', + id: entry.id, + snapshot: this.serializeSnapshot(entry.snapshot, options), + isBarrier: entry.isBarrier + }); + break; + case Transaction: + result.push({ + type: 'transaction', + markerSnapshotBefore: this.serializeSnapshot(entry.markerSnapshotBefore, options), + markerSnapshotAfter: this.serializeSnapshot(entry.markerSnapshotAfter, options), + patch: entry.patch.serialize().toString('base64') + }); + break; + case Patch: + result.push({ + type: 'patch', + data: entry.serialize().toString('base64') + }); + break; + default: + throw new Error(`Unexpected undoStack entry type during serialization: ${entry.constructor.name}`); + } + } + return result; + })(); + } + + deserializeStack(stack) { + return (() => { + const result = []; + for (let entry of stack) { + switch (entry.type) { + case 'checkpoint': + result.push(new Checkpoint( + entry.id, + MarkerLayer.deserializeSnapshot(entry.snapshot), + entry.isBarrier + )); + break; + case 'transaction': + result.push(new Transaction( + MarkerLayer.deserializeSnapshot(entry.markerSnapshotBefore), + Patch.deserialize(Buffer.from(entry.patch, 'base64')), + MarkerLayer.deserializeSnapshot(entry.markerSnapshotAfter) + )); + break; + case 'patch': + result.push(Patch.deserialize(Buffer.from(entry.data, 'base64'))); + break; + default: + throw new Error(`Unexpected undoStack entry type during deserialization: ${entry.type}`); + } + } + return result; + })(); + } + + serializeSnapshot(snapshot, options) { + if (!options.markerLayers) { return; } + + const serializedLayerSnapshots = {}; + for (let layerId in snapshot) { + const layerSnapshot = snapshot[layerId]; + if (!this.buffer.getMarkerLayer(layerId)?.persistent) { continue; } + const serializedMarkerSnapshots = {}; + for (let markerId in layerSnapshot) { + const markerSnapshot = layerSnapshot[markerId]; + const serializedMarkerSnapshot = Object.assign({}, markerSnapshot); + delete serializedMarkerSnapshot.marker; + serializedMarkerSnapshots[markerId] = serializedMarkerSnapshot; + } + serializedLayerSnapshots[layerId] = serializedMarkerSnapshots; + } + return serializedLayerSnapshots; + } +}); + +var snapshotFromCheckpoint = checkpoint => ({ + type: 'checkpoint', + id: checkpoint.id, + markers: checkpoint.snapshot +}); + +var checkpointFromSnapshot = ({id, markers}) => new Checkpoint(id, markers, false); + +var snapshotFromTransaction = function(transaction) { + const changes = []; + const iterable = transaction.patch.getChanges(); + for (let i = 0; i < iterable.length; i++) { + const change = iterable[i]; + changes.push({ + oldStart: change.oldStart, + oldEnd: change.oldEnd, + newStart: change.newStart, + newEnd: change.newEnd, + oldText: change.oldText, + newText: change.newText + }); + } + + return { + type: 'transaction', + changes, + markersBefore: transaction.markerSnapshotBefore, + markersAfter: transaction.markerSnapshotAfter + }; +}; + +var transactionFromSnapshot = ({changes, markersBefore, markersAfter}) => // TODO: Return raw patch if there's no markersBefore && markersAfter + new Transaction(markersBefore, patchFromChanges(changes), markersAfter); + From de5e3652b3c792dc1d0b7c8a13c7ac562259a275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 13:12:10 -0300 Subject: [PATCH 25/64] Decaff DisplayMarker --- src/display-marker.coffee | 414 ---------------------------------- src/display-marker.js | 464 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 464 insertions(+), 414 deletions(-) delete mode 100644 src/display-marker.coffee create mode 100644 src/display-marker.js diff --git a/src/display-marker.coffee b/src/display-marker.coffee deleted file mode 100644 index d8c92a2a02..0000000000 --- a/src/display-marker.coffee +++ /dev/null @@ -1,414 +0,0 @@ -{Emitter} = require 'event-kit' - -# Essential: Represents a buffer annotation that remains logically stationary -# even as the buffer changes. This is used to represent cursors, folds, snippet -# targets, misspelled words, and anything else that needs to track a logical -# location in the buffer over time. -# -# ### DisplayMarker Creation -# -# Use {DisplayMarkerLayer::markBufferRange} or {DisplayMarkerLayer::markScreenRange} -# rather than creating Markers directly. -# -# ### Head and Tail -# -# Markers always have a *head* and sometimes have a *tail*. If you think of a -# marker as an editor selection, the tail is the part that's stationary and the -# head is the part that moves when the mouse is moved. A marker without a tail -# always reports an empty range at the head position. A marker with a head position -# greater than the tail is in a "normal" orientation. If the head precedes the -# tail the marker is in a "reversed" orientation. -# -# ### Validity -# -# Markers are considered *valid* when they are first created. Depending on the -# invalidation strategy you choose, certain changes to the buffer can cause a -# marker to become invalid, for example if the text surrounding the marker is -# deleted. The strategies, in order of descending fragility: -# -# * __never__: The marker is never marked as invalid. This is a good choice for -# markers representing selections in an editor. -# * __surround__: The marker is invalidated by changes that completely surround it. -# * __overlap__: The marker is invalidated by changes that surround the -# start or end of the marker. This is the default. -# * __inside__: The marker is invalidated by changes that extend into the -# inside of the marker. Changes that end at the marker's start or -# start at the marker's end do not invalidate the marker. -# * __touch__: The marker is invalidated by a change that touches the marked -# region in any way, including changes that end at the marker's -# start or start at the marker's end. This is the most fragile strategy. -# -# See {TextBuffer::markRange} for usage. -module.exports = -class DisplayMarker - ### - Section: Construction and Destruction - ### - - constructor: (@layer, @bufferMarker) -> - {@id} = @bufferMarker - @hasChangeObservers = false - @emitter = new Emitter - @bufferMarkerSubscription = null - - # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once - # destroyed, a marker cannot be restored by undo/redo operations. - destroy: -> - unless @isDestroyed() - @bufferMarker.destroy() - - didDestroyBufferMarker: -> - @emitter.emit('did-destroy') - @layer.didDestroyMarker(this) - @emitter.dispose() - @emitter.clear() - @bufferMarkerSubscription?.dispose() - - # Essential: Creates and returns a new {DisplayMarker} with the same properties as - # this marker. - # - # {Selection} markers (markers with a custom property `type: "selection"`) - # should be copied with a different `type` value, for example with - # `marker.copy({type: null})`. Otherwise, the new marker's selection will - # be merged with this marker's selection, and a `null` value will be - # returned. - # - # * `properties` (optional) {Object} properties to associate with the new - # marker. The new marker's properties are computed by extending this marker's - # properties with `properties`. - # - # Returns a {DisplayMarker}. - copy: (params) -> - @layer.getMarker(@bufferMarker.copy(params).id) - - ### - Section: Event Subscription - ### - - # Essential: Invoke the given callback when the state of the marker changes. - # - # * `callback` {Function} to be called when the marker changes. - # * `event` {Object} with the following keys: - # * `oldHeadBufferPosition` {Point} representing the former head buffer position - # * `newHeadBufferPosition` {Point} representing the new head buffer position - # * `oldTailBufferPosition` {Point} representing the former tail buffer position - # * `newTailBufferPosition` {Point} representing the new tail buffer position - # * `oldHeadScreenPosition` {Point} representing the former head screen position - # * `newHeadScreenPosition` {Point} representing the new head screen position - # * `oldTailScreenPosition` {Point} representing the former tail screen position - # * `newTailScreenPosition` {Point} representing the new tail screen position - # * `wasValid` {Boolean} indicating whether the marker was valid before the change - # * `isValid` {Boolean} indicating whether the marker is now valid - # * `hadTail` {Boolean} indicating whether the marker had a tail before the change - # * `hasTail` {Boolean} indicating whether the marker now has a tail - # * `oldProperties` {Object} containing the marker's custom properties before the change. - # * `newProperties` {Object} containing the marker's custom properties after the change. - # * `textChanged` {Boolean} indicating whether this change was caused by a textual change - # to the buffer or whether the marker was manipulated directly via its public API. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - unless @hasChangeObservers - @oldHeadBufferPosition = @getHeadBufferPosition() - @oldHeadScreenPosition = @getHeadScreenPosition() - @oldTailBufferPosition = @getTailBufferPosition() - @oldTailScreenPosition = @getTailScreenPosition() - @wasValid = @isValid() - @bufferMarkerSubscription = @bufferMarker.onDidChange (event) => @notifyObservers(event.textChanged) - @hasChangeObservers = true - @emitter.on 'did-change', callback - - # Essential: Invoke the given callback when the marker is destroyed. - # - # * `callback` {Function} to be called when the marker is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @layer.markersWithDestroyListeners.add(this) - @emitter.on('did-destroy', callback) - - ### - Section: TextEditorMarker Details - ### - - # Essential: Returns a {Boolean} indicating whether the marker is valid. - # Markers can be invalidated when a region surrounding them in the buffer is - # changed. - isValid: -> - @bufferMarker.isValid() - - # Essential: Returns a {Boolean} indicating whether the marker has been - # destroyed. A marker can be invalid without being destroyed, in which case - # undoing the invalidating operation would restore the marker. Once a marker - # is destroyed by calling {DisplayMarker::destroy}, no undo/redo operation - # can ever bring it back. - isDestroyed: -> - @layer.isDestroyed() or @bufferMarker.isDestroyed() - - # Essential: Returns a {Boolean} indicating whether the head precedes the tail. - isReversed: -> - @bufferMarker.isReversed() - - # Essential: Returns a {Boolean} indicating whether changes that occur exactly - # at the marker's head or tail cause it to move. - isExclusive: -> - @bufferMarker.isExclusive() - - # Essential: Get the invalidation strategy for this marker. - # - # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. - # - # Returns a {String}. - getInvalidationStrategy: -> - @bufferMarker.getInvalidationStrategy() - - # Essential: Returns an {Object} containing any custom properties associated with - # the marker. - getProperties: -> - @bufferMarker.getProperties() - - # Essential: Merges an {Object} containing new properties into the marker's - # existing properties. - # - # * `properties` {Object} - setProperties: (properties) -> - @bufferMarker.setProperties(properties) - - # Essential: Returns whether this marker matches the given parameters. The - # parameters are the same as {DisplayMarkerLayer::findMarkers}. - matchesProperties: (attributes) -> - attributes = @layer.translateToBufferMarkerParams(attributes) - @bufferMarker.matchesParams(attributes) - - ### - Section: Comparing to other markers - ### - - # Essential: Compares this marker to another based on their ranges. - # - # * `other` {DisplayMarker} - # - # Returns a {Number} - compare: (otherMarker) -> - @bufferMarker.compare(otherMarker.bufferMarker) - - # Essential: Returns a {Boolean} indicating whether this marker is equivalent to - # another marker, meaning they have the same range and options. - # - # * `other` {DisplayMarker} other marker - isEqual: (other) -> - return false unless other instanceof @constructor - @bufferMarker.isEqual(other.bufferMarker) - - ### - Section: Managing the marker's range - ### - - # Essential: Gets the buffer range of this marker. - # - # Returns a {Range}. - getBufferRange: -> - @bufferMarker.getRange() - - # Essential: Gets the screen range of this marker. - # - # Returns a {Range}. - getScreenRange: -> - @layer.translateBufferRange(@getBufferRange()) - - # Essential: Modifies the buffer range of this marker. - # - # * `bufferRange` The new {Range} to use - # * `properties` (optional) {Object} properties to associate with the marker. - # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. - setBufferRange: (bufferRange, properties) -> - @bufferMarker.setRange(bufferRange, properties) - - # Essential: Modifies the screen range of this marker. - # - # * `screenRange` The new {Range} to use - # * `options` (optional) An {Object} with the following keys: - # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - setScreenRange: (screenRange, options) -> - @setBufferRange(@layer.translateScreenRange(screenRange, options), options) - - # Extended: Retrieves the buffer position of the marker's head. - # - # Returns a {Point}. - getHeadBufferPosition: -> - @bufferMarker.getHeadPosition() - - # Extended: Sets the buffer position of the marker's head. - # - # * `bufferPosition` The new {Point} to use - setHeadBufferPosition: (bufferPosition) -> - @bufferMarker.setHeadPosition(bufferPosition) - - # Extended: Retrieves the screen position of the marker's head. - # - # * `options` (optional) An {Object} with the following keys: - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - # - # Returns a {Point}. - getHeadScreenPosition: (options) -> - @layer.translateBufferPosition(@bufferMarker.getHeadPosition(), options) - - # Extended: Sets the screen position of the marker's head. - # - # * `screenPosition` The new {Point} to use - # * `options` (optional) An {Object} with the following keys: - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - setHeadScreenPosition: (screenPosition, options) -> - @setHeadBufferPosition(@layer.translateScreenPosition(screenPosition, options)) - - # Extended: Retrieves the buffer position of the marker's tail. - # - # Returns a {Point}. - getTailBufferPosition: -> - @bufferMarker.getTailPosition() - - # Extended: Sets the buffer position of the marker's tail. - # - # * `bufferPosition` The new {Point} to use - setTailBufferPosition: (bufferPosition) -> - @bufferMarker.setTailPosition(bufferPosition) - - # Extended: Retrieves the screen position of the marker's tail. - # - # * `options` (optional) An {Object} with the following keys: - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - # - # Returns a {Point}. - getTailScreenPosition: (options) -> - @layer.translateBufferPosition(@bufferMarker.getTailPosition(), options) - - # Extended: Sets the screen position of the marker's tail. - # - # * `screenPosition` The new {Point} to use - # * `options` (optional) An {Object} with the following keys: - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - setTailScreenPosition: (screenPosition, options) -> - @bufferMarker.setTailPosition(@layer.translateScreenPosition(screenPosition, options)) - - # Extended: Retrieves the buffer position of the marker's start. This will always be - # less than or equal to the result of {DisplayMarker::getEndBufferPosition}. - # - # Returns a {Point}. - getStartBufferPosition: -> - @bufferMarker.getStartPosition() - - # Essential: Retrieves the screen position of the marker's start. This will always be - # less than or equal to the result of {DisplayMarker::getEndScreenPosition}. - # - # * `options` (optional) An {Object} with the following keys: - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - # - # Returns a {Point}. - getStartScreenPosition: (options) -> - @layer.translateBufferPosition(@getStartBufferPosition(), options) - - # Extended: Retrieves the buffer position of the marker's end. This will always be - # greater than or equal to the result of {DisplayMarker::getStartBufferPosition}. - # - # Returns a {Point}. - getEndBufferPosition: -> - @bufferMarker.getEndPosition() - - # Essential: Retrieves the screen position of the marker's end. This will always be - # greater than or equal to the result of {DisplayMarker::getStartScreenPosition}. - # - # * `options` (optional) An {Object} with the following keys: - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - # - # Returns a {Point}. - getEndScreenPosition: (options) -> - @layer.translateBufferPosition(@getEndBufferPosition(), options) - - # Extended: Returns a {Boolean} indicating whether the marker has a tail. - hasTail: -> - @bufferMarker.hasTail() - - # Extended: Plants the marker's tail at the current head position. After calling - # the marker's tail position will be its head position at the time of the - # call, regardless of where the marker's head is moved. - plantTail: -> - @bufferMarker.plantTail() - - # Extended: Removes the marker's tail. After calling the marker's head position - # will be reported as its current tail position until the tail is planted - # again. - clearTail: -> - @bufferMarker.clearTail() - - toString: -> - "[Marker #{@id}, bufferRange: #{@getBufferRange()}, screenRange: #{@getScreenRange()}}]" - - ### - Section: Private - ### - - inspect: -> - @toString() - - notifyObservers: (textChanged) -> - return unless @hasChangeObservers - textChanged ?= false - - newHeadBufferPosition = @getHeadBufferPosition() - newHeadScreenPosition = @getHeadScreenPosition() - newTailBufferPosition = @getTailBufferPosition() - newTailScreenPosition = @getTailScreenPosition() - isValid = @isValid() - - return if isValid is @wasValid and - newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and - newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and - newTailBufferPosition.isEqual(@oldTailBufferPosition) and - newTailScreenPosition.isEqual(@oldTailScreenPosition) - - changeEvent = { - @oldHeadScreenPosition, newHeadScreenPosition, - @oldTailScreenPosition, newTailScreenPosition, - @oldHeadBufferPosition, newHeadBufferPosition, - @oldTailBufferPosition, newTailBufferPosition, - textChanged, - @wasValid, - isValid - } - - @oldHeadBufferPosition = newHeadBufferPosition - @oldHeadScreenPosition = newHeadScreenPosition - @oldTailBufferPosition = newTailBufferPosition - @oldTailScreenPosition = newTailScreenPosition - @wasValid = isValid - - @emitter.emit 'did-change', changeEvent diff --git a/src/display-marker.js b/src/display-marker.js new file mode 100644 index 0000000000..961dadc034 --- /dev/null +++ b/src/display-marker.js @@ -0,0 +1,464 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +let DisplayMarker; +const {Emitter} = require('event-kit'); + +// Essential: Represents a buffer annotation that remains logically stationary +// even as the buffer changes. This is used to represent cursors, folds, snippet +// targets, misspelled words, and anything else that needs to track a logical +// location in the buffer over time. +// +// ### DisplayMarker Creation +// +// Use {DisplayMarkerLayer::markBufferRange} or {DisplayMarkerLayer::markScreenRange} +// rather than creating Markers directly. +// +// ### Head and Tail +// +// Markers always have a *head* and sometimes have a *tail*. If you think of a +// marker as an editor selection, the tail is the part that's stationary and the +// head is the part that moves when the mouse is moved. A marker without a tail +// always reports an empty range at the head position. A marker with a head position +// greater than the tail is in a "normal" orientation. If the head precedes the +// tail the marker is in a "reversed" orientation. +// +// ### Validity +// +// Markers are considered *valid* when they are first created. Depending on the +// invalidation strategy you choose, certain changes to the buffer can cause a +// marker to become invalid, for example if the text surrounding the marker is +// deleted. The strategies, in order of descending fragility: +// +// * __never__: The marker is never marked as invalid. This is a good choice for +// markers representing selections in an editor. +// * __surround__: The marker is invalidated by changes that completely surround it. +// * __overlap__: The marker is invalidated by changes that surround the +// start or end of the marker. This is the default. +// * __inside__: The marker is invalidated by changes that extend into the +// inside of the marker. Changes that end at the marker's start or +// start at the marker's end do not invalidate the marker. +// * __touch__: The marker is invalidated by a change that touches the marked +// region in any way, including changes that end at the marker's +// start or start at the marker's end. This is the most fragile strategy. +// +// See {TextBuffer::markRange} for usage. +module.exports = +(DisplayMarker = class DisplayMarker { + /* + Section: Construction and Destruction + */ + + constructor(layer, bufferMarker) { + this.layer = layer; + this.bufferMarker = bufferMarker; + ({id: this.id} = this.bufferMarker); + this.hasChangeObservers = false; + this.emitter = new Emitter; + this.bufferMarkerSubscription = null; + } + + // Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once + // destroyed, a marker cannot be restored by undo/redo operations. + destroy() { + if (!this.isDestroyed()) { + return this.bufferMarker.destroy(); + } + } + + didDestroyBufferMarker() { + this.emitter.emit('did-destroy'); + this.layer.didDestroyMarker(this); + this.emitter.dispose(); + this.emitter.clear(); + return this.bufferMarkerSubscription?.dispose(); + } + + // Essential: Creates and returns a new {DisplayMarker} with the same properties as + // this marker. + // + // {Selection} markers (markers with a custom property `type: "selection"`) + // should be copied with a different `type` value, for example with + // `marker.copy({type: null})`. Otherwise, the new marker's selection will + // be merged with this marker's selection, and a `null` value will be + // returned. + // + // * `properties` (optional) {Object} properties to associate with the new + // marker. The new marker's properties are computed by extending this marker's + // properties with `properties`. + // + // Returns a {DisplayMarker}. + copy(params) { + return this.layer.getMarker(this.bufferMarker.copy(params).id); + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback when the state of the marker changes. + // + // * `callback` {Function} to be called when the marker changes. + // * `event` {Object} with the following keys: + // * `oldHeadBufferPosition` {Point} representing the former head buffer position + // * `newHeadBufferPosition` {Point} representing the new head buffer position + // * `oldTailBufferPosition` {Point} representing the former tail buffer position + // * `newTailBufferPosition` {Point} representing the new tail buffer position + // * `oldHeadScreenPosition` {Point} representing the former head screen position + // * `newHeadScreenPosition` {Point} representing the new head screen position + // * `oldTailScreenPosition` {Point} representing the former tail screen position + // * `newTailScreenPosition` {Point} representing the new tail screen position + // * `wasValid` {Boolean} indicating whether the marker was valid before the change + // * `isValid` {Boolean} indicating whether the marker is now valid + // * `hadTail` {Boolean} indicating whether the marker had a tail before the change + // * `hasTail` {Boolean} indicating whether the marker now has a tail + // * `oldProperties` {Object} containing the marker's custom properties before the change. + // * `newProperties` {Object} containing the marker's custom properties after the change. + // * `textChanged` {Boolean} indicating whether this change was caused by a textual change + // to the buffer or whether the marker was manipulated directly via its public API. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange(callback) { + if (!this.hasChangeObservers) { + this.oldHeadBufferPosition = this.getHeadBufferPosition(); + this.oldHeadScreenPosition = this.getHeadScreenPosition(); + this.oldTailBufferPosition = this.getTailBufferPosition(); + this.oldTailScreenPosition = this.getTailScreenPosition(); + this.wasValid = this.isValid(); + this.bufferMarkerSubscription = this.bufferMarker.onDidChange(event => this.notifyObservers(event.textChanged)); + this.hasChangeObservers = true; + } + return this.emitter.on('did-change', callback); + } + + // Essential: Invoke the given callback when the marker is destroyed. + // + // * `callback` {Function} to be called when the marker is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy(callback) { + this.layer.markersWithDestroyListeners.add(this); + return this.emitter.on('did-destroy', callback); + } + + /* + Section: TextEditorMarker Details + */ + + // Essential: Returns a {Boolean} indicating whether the marker is valid. + // Markers can be invalidated when a region surrounding them in the buffer is + // changed. + isValid() { + return this.bufferMarker.isValid(); + } + + // Essential: Returns a {Boolean} indicating whether the marker has been + // destroyed. A marker can be invalid without being destroyed, in which case + // undoing the invalidating operation would restore the marker. Once a marker + // is destroyed by calling {DisplayMarker::destroy}, no undo/redo operation + // can ever bring it back. + isDestroyed() { + return this.layer.isDestroyed() || this.bufferMarker.isDestroyed(); + } + + // Essential: Returns a {Boolean} indicating whether the head precedes the tail. + isReversed() { + return this.bufferMarker.isReversed(); + } + + // Essential: Returns a {Boolean} indicating whether changes that occur exactly + // at the marker's head or tail cause it to move. + isExclusive() { + return this.bufferMarker.isExclusive(); + } + + // Essential: Get the invalidation strategy for this marker. + // + // Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. + // + // Returns a {String}. + getInvalidationStrategy() { + return this.bufferMarker.getInvalidationStrategy(); + } + + // Essential: Returns an {Object} containing any custom properties associated with + // the marker. + getProperties() { + return this.bufferMarker.getProperties(); + } + + // Essential: Merges an {Object} containing new properties into the marker's + // existing properties. + // + // * `properties` {Object} + setProperties(properties) { + return this.bufferMarker.setProperties(properties); + } + + // Essential: Returns whether this marker matches the given parameters. The + // parameters are the same as {DisplayMarkerLayer::findMarkers}. + matchesProperties(attributes) { + attributes = this.layer.translateToBufferMarkerParams(attributes); + return this.bufferMarker.matchesParams(attributes); + } + + /* + Section: Comparing to other markers + */ + + // Essential: Compares this marker to another based on their ranges. + // + // * `other` {DisplayMarker} + // + // Returns a {Number} + compare(otherMarker) { + return this.bufferMarker.compare(otherMarker.bufferMarker); + } + + // Essential: Returns a {Boolean} indicating whether this marker is equivalent to + // another marker, meaning they have the same range and options. + // + // * `other` {DisplayMarker} other marker + isEqual(other) { + if (!(other instanceof this.constructor)) { return false; } + return this.bufferMarker.isEqual(other.bufferMarker); + } + + /* + Section: Managing the marker's range + */ + + // Essential: Gets the buffer range of this marker. + // + // Returns a {Range}. + getBufferRange() { + return this.bufferMarker.getRange(); + } + + // Essential: Gets the screen range of this marker. + // + // Returns a {Range}. + getScreenRange() { + return this.layer.translateBufferRange(this.getBufferRange()); + } + + // Essential: Modifies the buffer range of this marker. + // + // * `bufferRange` The new {Range} to use + // * `properties` (optional) {Object} properties to associate with the marker. + // * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. + setBufferRange(bufferRange, properties) { + return this.bufferMarker.setRange(bufferRange, properties); + } + + // Essential: Modifies the screen range of this marker. + // + // * `screenRange` The new {Range} to use + // * `options` (optional) An {Object} with the following keys: + // * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + setScreenRange(screenRange, options) { + return this.setBufferRange(this.layer.translateScreenRange(screenRange, options), options); + } + + // Extended: Retrieves the buffer position of the marker's head. + // + // Returns a {Point}. + getHeadBufferPosition() { + return this.bufferMarker.getHeadPosition(); + } + + // Extended: Sets the buffer position of the marker's head. + // + // * `bufferPosition` The new {Point} to use + setHeadBufferPosition(bufferPosition) { + return this.bufferMarker.setHeadPosition(bufferPosition); + } + + // Extended: Retrieves the screen position of the marker's head. + // + // * `options` (optional) An {Object} with the following keys: + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + // + // Returns a {Point}. + getHeadScreenPosition(options) { + return this.layer.translateBufferPosition(this.bufferMarker.getHeadPosition(), options); + } + + // Extended: Sets the screen position of the marker's head. + // + // * `screenPosition` The new {Point} to use + // * `options` (optional) An {Object} with the following keys: + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + setHeadScreenPosition(screenPosition, options) { + return this.setHeadBufferPosition(this.layer.translateScreenPosition(screenPosition, options)); + } + + // Extended: Retrieves the buffer position of the marker's tail. + // + // Returns a {Point}. + getTailBufferPosition() { + return this.bufferMarker.getTailPosition(); + } + + // Extended: Sets the buffer position of the marker's tail. + // + // * `bufferPosition` The new {Point} to use + setTailBufferPosition(bufferPosition) { + return this.bufferMarker.setTailPosition(bufferPosition); + } + + // Extended: Retrieves the screen position of the marker's tail. + // + // * `options` (optional) An {Object} with the following keys: + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + // + // Returns a {Point}. + getTailScreenPosition(options) { + return this.layer.translateBufferPosition(this.bufferMarker.getTailPosition(), options); + } + + // Extended: Sets the screen position of the marker's tail. + // + // * `screenPosition` The new {Point} to use + // * `options` (optional) An {Object} with the following keys: + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + setTailScreenPosition(screenPosition, options) { + return this.bufferMarker.setTailPosition(this.layer.translateScreenPosition(screenPosition, options)); + } + + // Extended: Retrieves the buffer position of the marker's start. This will always be + // less than or equal to the result of {DisplayMarker::getEndBufferPosition}. + // + // Returns a {Point}. + getStartBufferPosition() { + return this.bufferMarker.getStartPosition(); + } + + // Essential: Retrieves the screen position of the marker's start. This will always be + // less than or equal to the result of {DisplayMarker::getEndScreenPosition}. + // + // * `options` (optional) An {Object} with the following keys: + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + // + // Returns a {Point}. + getStartScreenPosition(options) { + return this.layer.translateBufferPosition(this.getStartBufferPosition(), options); + } + + // Extended: Retrieves the buffer position of the marker's end. This will always be + // greater than or equal to the result of {DisplayMarker::getStartBufferPosition}. + // + // Returns a {Point}. + getEndBufferPosition() { + return this.bufferMarker.getEndPosition(); + } + + // Essential: Retrieves the screen position of the marker's end. This will always be + // greater than or equal to the result of {DisplayMarker::getStartScreenPosition}. + // + // * `options` (optional) An {Object} with the following keys: + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + // + // Returns a {Point}. + getEndScreenPosition(options) { + return this.layer.translateBufferPosition(this.getEndBufferPosition(), options); + } + + // Extended: Returns a {Boolean} indicating whether the marker has a tail. + hasTail() { + return this.bufferMarker.hasTail(); + } + + // Extended: Plants the marker's tail at the current head position. After calling + // the marker's tail position will be its head position at the time of the + // call, regardless of where the marker's head is moved. + plantTail() { + return this.bufferMarker.plantTail(); + } + + // Extended: Removes the marker's tail. After calling the marker's head position + // will be reported as its current tail position until the tail is planted + // again. + clearTail() { + return this.bufferMarker.clearTail(); + } + + toString() { + return `[Marker ${this.id}, bufferRange: ${this.getBufferRange()}, screenRange: ${this.getScreenRange()}}]`; + } + + /* + Section: Private + */ + + inspect() { + return this.toString(); + } + + notifyObservers(textChanged) { + if (!this.hasChangeObservers) { return; } + if (textChanged == null) { textChanged = false; } + + const newHeadBufferPosition = this.getHeadBufferPosition(); + const newHeadScreenPosition = this.getHeadScreenPosition(); + const newTailBufferPosition = this.getTailBufferPosition(); + const newTailScreenPosition = this.getTailScreenPosition(); + const isValid = this.isValid(); + + if ((isValid === this.wasValid) && + newHeadBufferPosition.isEqual(this.oldHeadBufferPosition) && + newHeadScreenPosition.isEqual(this.oldHeadScreenPosition) && + newTailBufferPosition.isEqual(this.oldTailBufferPosition) && + newTailScreenPosition.isEqual(this.oldTailScreenPosition)) { return; } + + const changeEvent = { + oldHeadScreenPosition: this.oldHeadScreenPosition, newHeadScreenPosition, + oldTailScreenPosition: this.oldTailScreenPosition, newTailScreenPosition, + oldHeadBufferPosition: this.oldHeadBufferPosition, newHeadBufferPosition, + oldTailBufferPosition: this.oldTailBufferPosition, newTailBufferPosition, + textChanged, + wasValid: this.wasValid, + isValid + }; + + this.oldHeadBufferPosition = newHeadBufferPosition; + this.oldHeadScreenPosition = newHeadScreenPosition; + this.oldTailBufferPosition = newTailBufferPosition; + this.oldTailScreenPosition = newTailScreenPosition; + this.wasValid = isValid; + + return this.emitter.emit('did-change', changeEvent); + } +}); From 06f597251da4c3cf77abb4cb7da7d78f8bbb17fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 13:13:31 -0300 Subject: [PATCH 26/64] Defaff display-marker-layer --- src/display-marker-layer.coffee | 402 --------------------------- src/display-marker-layer.js | 468 ++++++++++++++++++++++++++++++++ 2 files changed, 468 insertions(+), 402 deletions(-) delete mode 100644 src/display-marker-layer.coffee create mode 100644 src/display-marker-layer.js diff --git a/src/display-marker-layer.coffee b/src/display-marker-layer.coffee deleted file mode 100644 index 056d83f4d0..0000000000 --- a/src/display-marker-layer.coffee +++ /dev/null @@ -1,402 +0,0 @@ -{Emitter, CompositeDisposable} = require 'event-kit' -DisplayMarker = require './display-marker' -Range = require './range' -Point = require './point' - -# Public: *Experimental:* A container for a related set of markers at the -# {DisplayLayer} level. Wraps an underlying {MarkerLayer} on the {TextBuffer}. -# -# This API is experimental and subject to change on any release. -module.exports = -class DisplayMarkerLayer - constructor: (@displayLayer, @bufferMarkerLayer, @ownsBufferMarkerLayer) -> - {@id} = @bufferMarkerLayer - @bufferMarkerLayer.displayMarkerLayers.add(this) - @markersById = {} - @destroyed = false - @emitter = new Emitter - @subscriptions = new CompositeDisposable - @markersWithDestroyListeners = new Set - @subscriptions.add(@bufferMarkerLayer.onDidUpdate(@emitDidUpdate.bind(this))) - - ### - Section: Lifecycle - ### - - # Essential: Destroy this layer. - destroy: -> - return if @destroyed - @destroyed = true - @clear() if @ownsBufferMarkerLayer - @subscriptions.dispose() - @bufferMarkerLayer.displayMarkerLayers.delete(this) - @bufferMarkerLayer.destroy() if @ownsBufferMarkerLayer - @displayLayer.didDestroyMarkerLayer(@id) - @emitter.emit('did-destroy') - @emitter.clear() - - # Public: Destroy all markers in this layer. - clear: -> - @bufferMarkerLayer.clear() - - didClearBufferMarkerLayer: -> - @markersWithDestroyListeners.forEach (marker) -> marker.didDestroyBufferMarker() - @markersById = {} - - # Essential: Determine whether this layer has been destroyed. - # - # Returns a {Boolean}. - isDestroyed: -> - @destroyed - - ### - Section: Event Subscription - ### - - # Public: Subscribe to be notified synchronously when this layer is destroyed. - # - # Returns a {Disposable}. - onDidDestroy: (callback) -> - @emitter.on('did-destroy', callback) - - # Public: Subscribe to be notified asynchronously whenever markers are - # created, updated, or destroyed on this layer. *Prefer this method for - # optimal performance when interacting with layers that could contain large - # numbers of markers.* - # - # * `callback` A {Function} that will be called with no arguments when changes - # occur on this layer. - # - # Subscribers are notified once, asynchronously when any number of changes - # occur in a given tick of the event loop. You should re-query the layer - # to determine the state of markers in which you're interested in. It may - # be counter-intuitive, but this is much more efficient than subscribing to - # events on individual markers, which are expensive to deliver. - # - # Returns a {Disposable}. - onDidUpdate: (callback) -> - @emitter.on('did-update', callback) - - # Public: Subscribe to be notified synchronously whenever markers are created - # on this layer. *Avoid this method for optimal performance when interacting - # with layers that could contain large numbers of markers.* - # - # * `callback` A {Function} that will be called with a {TextEditorMarker} - # whenever a new marker is created. - # - # You should prefer {::onDidUpdate} when synchronous notifications aren't - # absolutely necessary. - # - # Returns a {Disposable}. - onDidCreateMarker: (callback) -> - @bufferMarkerLayer.onDidCreateMarker (bufferMarker) => - callback(@getMarker(bufferMarker.id)) - - ### - Section: Marker creation - ### - - # Public: Create a marker with the given screen range. - # - # * `range` A {Range} or range-compatible {Array} - # * `options` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `exclusive` {Boolean} indicating whether insertions at the start or end - # of the marked range should be interpreted as happening *outside* the - # marker. Defaults to `false`, except when using the `inside` - # invalidation strategy or when when the marker has no tail, in which - # case it defaults to true. Explicitly assigning this option overrides - # behavior in all circumstances. - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. Applies to the start and end of the given range. - # - # Returns a {DisplayMarker}. - markScreenRange: (screenRange, options) -> - screenRange = Range.fromObject(screenRange) - bufferRange = @displayLayer.translateScreenRange(screenRange, options) - @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) - - # Public: Create a marker on this layer with its head at the given screen - # position and no tail. - # - # * `screenPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `exclusive` {Boolean} indicating whether insertions at the start or end - # of the marked range should be interpreted as happening *outside* the - # marker. Defaults to `false`, except when using the `inside` - # invalidation strategy or when when the marker has no tail, in which - # case it defaults to true. Explicitly assigning this option overrides - # behavior in all circumstances. - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {DisplayMarker}. - markScreenPosition: (screenPosition, options) -> - screenPosition = Point.fromObject(screenPosition) - bufferPosition = @displayLayer.translateScreenPosition(screenPosition, options) - @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id) - - # Public: Create a marker with the given buffer range. - # - # * `range` A {Range} or range-compatible {Array} - # * `options` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `exclusive` {Boolean} indicating whether insertions at the start or end - # of the marked range should be interpreted as happening *outside* the - # marker. Defaults to `false`, except when using the `inside` - # invalidation strategy or when when the marker has no tail, in which - # case it defaults to true. Explicitly assigning this option overrides - # behavior in all circumstances. - # - # Returns a {DisplayMarker}. - markBufferRange: (bufferRange, options) -> - bufferRange = Range.fromObject(bufferRange) - @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) - - # Public: Create a marker on this layer with its head at the given buffer - # position and no tail. - # - # * `bufferPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `exclusive` {Boolean} indicating whether insertions at the start or end - # of the marked range should be interpreted as happening *outside* the - # marker. Defaults to `false`, except when using the `inside` - # invalidation strategy or when when the marker has no tail, in which - # case it defaults to true. Explicitly assigning this option overrides - # behavior in all circumstances. - # - # Returns a {DisplayMarker}. - markBufferPosition: (bufferPosition, options) -> - @getMarker(@bufferMarkerLayer.markPosition(Point.fromObject(bufferPosition), options).id) - - ### - Section: Querying - ### - - # Essential: Get an existing marker by its id. - # - # Returns a {DisplayMarker}. - getMarker: (id) -> - if displayMarker = @markersById[id] - displayMarker - else if bufferMarker = @bufferMarkerLayer.getMarker(id) - @markersById[id] = new DisplayMarker(this, bufferMarker) - - # Essential: Get all markers in the layer. - # - # Returns an {Array} of {DisplayMarker}s. - getMarkers: -> - @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id) - - # Public: Get the number of markers in the marker layer. - # - # Returns a {Number}. - getMarkerCount: -> - @bufferMarkerLayer.getMarkerCount() - - # Public: Find markers in the layer conforming to the given parameters. - # - # This method finds markers based on the given properties. Markers can be - # associated with custom properties that will be compared with basic equality. - # In addition, there are several special properties that will be compared - # with the range of the markers rather than their properties. - # - # * `properties` An {Object} containing properties that each returned marker - # must satisfy. Markers can be associated with custom properties, which are - # compared with basic equality. In addition, several reserved properties - # can be used to filter markers based on their current range: - # * `startBufferPosition` Only include markers starting at this {Point} in buffer coordinates. - # * `endBufferPosition` Only include markers ending at this {Point} in buffer coordinates. - # * `startScreenPosition` Only include markers starting at this {Point} in screen coordinates. - # * `endScreenPosition` Only include markers ending at this {Point} in screen coordinates. - # * `startsInBufferRange` Only include markers starting inside this {Range} in buffer coordinates. - # * `endsInBufferRange` Only include markers ending inside this {Range} in buffer coordinates. - # * `startsInScreenRange` Only include markers starting inside this {Range} in screen coordinates. - # * `endsInScreenRange` Only include markers ending inside this {Range} in screen coordinates. - # * `startBufferRow` Only include markers starting at this row in buffer coordinates. - # * `endBufferRow` Only include markers ending at this row in buffer coordinates. - # * `startScreenRow` Only include markers starting at this row in screen coordinates. - # * `endScreenRow` Only include markers ending at this row in screen coordinates. - # * `intersectsBufferRowRange` Only include markers intersecting this {Array} - # of `[startRow, endRow]` in buffer coordinates. - # * `intersectsScreenRowRange` Only include markers intersecting this {Array} - # of `[startRow, endRow]` in screen coordinates. - # * `containsBufferRange` Only include markers containing this {Range} in buffer coordinates. - # * `containsBufferPosition` Only include markers containing this {Point} in buffer coordinates. - # * `containedInBufferRange` Only include markers contained in this {Range} in buffer coordinates. - # * `containedInScreenRange` Only include markers contained in this {Range} in screen coordinates. - # * `intersectsBufferRange` Only include markers intersecting this {Range} in buffer coordinates. - # * `intersectsScreenRange` Only include markers intersecting this {Range} in screen coordinates. - # - # Returns an {Array} of {DisplayMarker}s - findMarkers: (params) -> - params = @translateToBufferMarkerLayerFindParams(params) - @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) - - ### - Section: Private - ### - - translateBufferPosition: (bufferPosition, options) -> - @displayLayer.translateBufferPosition(bufferPosition, options) - - translateBufferRange: (bufferRange, options) -> - @displayLayer.translateBufferRange(bufferRange, options) - - translateScreenPosition: (screenPosition, options) -> - @displayLayer.translateScreenPosition(screenPosition, options) - - translateScreenRange: (screenRange, options) -> - @displayLayer.translateScreenRange(screenRange, options) - - emitDidUpdate: -> - @emitter.emit('did-update') - - notifyObserversIfMarkerScreenPositionsChanged: -> - for marker in @getMarkers() - marker.notifyObservers(false) - return - - destroyMarker: (id) -> - if marker = @markersById[id] - marker.didDestroyBufferMarker() - - didDestroyMarker: (marker) -> - @markersWithDestroyListeners.delete(marker) - delete @markersById[marker.id] - - translateToBufferMarkerLayerFindParams: (params) -> - bufferMarkerLayerFindParams = {} - for key, value of params - switch key - when 'startBufferPosition' - key = 'startPosition' - when 'endBufferPosition' - key = 'endPosition' - when 'startScreenPosition' - key = 'startPosition' - value = @displayLayer.translateScreenPosition(value) - when 'endScreenPosition' - key = 'endPosition' - value = @displayLayer.translateScreenPosition(value) - when 'startsInBufferRange' - key = 'startsInRange' - when 'endsInBufferRange' - key = 'endsInRange' - when 'startsInScreenRange' - key = 'startsInRange' - value = @displayLayer.translateScreenRange(value) - when 'endsInScreenRange' - key = 'endsInRange' - value = @displayLayer.translateScreenRange(value) - when 'startBufferRow' - key = 'startRow' - when 'endBufferRow' - key = 'endRow' - when 'startScreenRow' - key = 'startsInRange' - startBufferPosition = @displayLayer.translateScreenPosition(Point(value, 0)) - endBufferPosition = @displayLayer.translateScreenPosition(Point(value, Infinity)) - value = Range(startBufferPosition, endBufferPosition) - when 'endScreenRow' - key = 'endsInRange' - startBufferPosition = @displayLayer.translateScreenPosition(Point(value, 0)) - endBufferPosition = @displayLayer.translateScreenPosition(Point(value, Infinity)) - value = Range(startBufferPosition, endBufferPosition) - when 'intersectsBufferRowRange' - key = 'intersectsRowRange' - when 'intersectsScreenRowRange' - key = 'intersectsRange' - [startScreenRow, endScreenRow] = value - startBufferPosition = @displayLayer.translateScreenPosition(Point(startScreenRow, 0)) - endBufferPosition = @displayLayer.translateScreenPosition(Point(endScreenRow, Infinity)) - value = Range(startBufferPosition, endBufferPosition) - when 'containsBufferRange' - key = 'containsRange' - when 'containsScreenRange' - key = 'containsRange' - value = @displayLayer.translateScreenRange(value) - when 'containsBufferPosition' - key = 'containsPosition' - when 'containsScreenPosition' - key = 'containsPosition' - value = @displayLayer.translateScreenPosition(value) - when 'containedInBufferRange' - key = 'containedInRange' - when 'containedInScreenRange' - key = 'containedInRange' - value = @displayLayer.translateScreenRange(value) - when 'intersectsBufferRange' - key = 'intersectsRange' - when 'intersectsScreenRange' - key = 'intersectsRange' - value = @displayLayer.translateScreenRange(value) - bufferMarkerLayerFindParams[key] = value - - bufferMarkerLayerFindParams diff --git a/src/display-marker-layer.js b/src/display-marker-layer.js new file mode 100644 index 0000000000..7c3cb521ca --- /dev/null +++ b/src/display-marker-layer.js @@ -0,0 +1,468 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +let DisplayMarkerLayer; +const {Emitter, CompositeDisposable} = require('event-kit'); +const DisplayMarker = require('./display-marker'); +const Range = require('./range'); +const Point = require('./point'); + +// Public: *Experimental:* A container for a related set of markers at the +// {DisplayLayer} level. Wraps an underlying {MarkerLayer} on the {TextBuffer}. +// +// This API is experimental and subject to change on any release. +module.exports = +(DisplayMarkerLayer = class DisplayMarkerLayer { + constructor(displayLayer, bufferMarkerLayer, ownsBufferMarkerLayer) { + this.displayLayer = displayLayer; + this.bufferMarkerLayer = bufferMarkerLayer; + this.ownsBufferMarkerLayer = ownsBufferMarkerLayer; + ({id: this.id} = this.bufferMarkerLayer); + this.bufferMarkerLayer.displayMarkerLayers.add(this); + this.markersById = {}; + this.destroyed = false; + this.emitter = new Emitter; + this.subscriptions = new CompositeDisposable; + this.markersWithDestroyListeners = new Set; + this.subscriptions.add(this.bufferMarkerLayer.onDidUpdate(this.emitDidUpdate.bind(this))); + } + + /* + Section: Lifecycle + */ + + // Essential: Destroy this layer. + destroy() { + if (this.destroyed) { return; } + this.destroyed = true; + if (this.ownsBufferMarkerLayer) { this.clear(); } + this.subscriptions.dispose(); + this.bufferMarkerLayer.displayMarkerLayers.delete(this); + if (this.ownsBufferMarkerLayer) { this.bufferMarkerLayer.destroy(); } + this.displayLayer.didDestroyMarkerLayer(this.id); + this.emitter.emit('did-destroy'); + return this.emitter.clear(); + } + + // Public: Destroy all markers in this layer. + clear() { + return this.bufferMarkerLayer.clear(); + } + + didClearBufferMarkerLayer() { + this.markersWithDestroyListeners.forEach(marker => marker.didDestroyBufferMarker()); + return this.markersById = {}; + } + + // Essential: Determine whether this layer has been destroyed. + // + // Returns a {Boolean}. + isDestroyed() { + return this.destroyed; + } + + /* + Section: Event Subscription + */ + + // Public: Subscribe to be notified synchronously when this layer is destroyed. + // + // Returns a {Disposable}. + onDidDestroy(callback) { + return this.emitter.on('did-destroy', callback); + } + + // Public: Subscribe to be notified asynchronously whenever markers are + // created, updated, or destroyed on this layer. *Prefer this method for + // optimal performance when interacting with layers that could contain large + // numbers of markers.* + // + // * `callback` A {Function} that will be called with no arguments when changes + // occur on this layer. + // + // Subscribers are notified once, asynchronously when any number of changes + // occur in a given tick of the event loop. You should re-query the layer + // to determine the state of markers in which you're interested in. It may + // be counter-intuitive, but this is much more efficient than subscribing to + // events on individual markers, which are expensive to deliver. + // + // Returns a {Disposable}. + onDidUpdate(callback) { + return this.emitter.on('did-update', callback); + } + + // Public: Subscribe to be notified synchronously whenever markers are created + // on this layer. *Avoid this method for optimal performance when interacting + // with layers that could contain large numbers of markers.* + // + // * `callback` A {Function} that will be called with a {TextEditorMarker} + // whenever a new marker is created. + // + // You should prefer {::onDidUpdate} when synchronous notifications aren't + // absolutely necessary. + // + // Returns a {Disposable}. + onDidCreateMarker(callback) { + return this.bufferMarkerLayer.onDidCreateMarker(bufferMarker => { + return callback(this.getMarker(bufferMarker.id)); + }); + } + + /* + Section: Marker creation + */ + + // Public: Create a marker with the given screen range. + // + // * `range` A {Range} or range-compatible {Array} + // * `options` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `exclusive` {Boolean} indicating whether insertions at the start or end + // of the marked range should be interpreted as happening *outside* the + // marker. Defaults to `false`, except when using the `inside` + // invalidation strategy or when when the marker has no tail, in which + // case it defaults to true. Explicitly assigning this option overrides + // behavior in all circumstances. + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. Applies to the start and end of the given range. + // + // Returns a {DisplayMarker}. + markScreenRange(screenRange, options) { + screenRange = Range.fromObject(screenRange); + const bufferRange = this.displayLayer.translateScreenRange(screenRange, options); + return this.getMarker(this.bufferMarkerLayer.markRange(bufferRange, options).id); + } + + // Public: Create a marker on this layer with its head at the given screen + // position and no tail. + // + // * `screenPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `exclusive` {Boolean} indicating whether insertions at the start or end + // of the marked range should be interpreted as happening *outside* the + // marker. Defaults to `false`, except when using the `inside` + // invalidation strategy or when when the marker has no tail, in which + // case it defaults to true. Explicitly assigning this option overrides + // behavior in all circumstances. + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {DisplayMarker}. + markScreenPosition(screenPosition, options) { + screenPosition = Point.fromObject(screenPosition); + const bufferPosition = this.displayLayer.translateScreenPosition(screenPosition, options); + return this.getMarker(this.bufferMarkerLayer.markPosition(bufferPosition, options).id); + } + + // Public: Create a marker with the given buffer range. + // + // * `range` A {Range} or range-compatible {Array} + // * `options` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `exclusive` {Boolean} indicating whether insertions at the start or end + // of the marked range should be interpreted as happening *outside* the + // marker. Defaults to `false`, except when using the `inside` + // invalidation strategy or when when the marker has no tail, in which + // case it defaults to true. Explicitly assigning this option overrides + // behavior in all circumstances. + // + // Returns a {DisplayMarker}. + markBufferRange(bufferRange, options) { + bufferRange = Range.fromObject(bufferRange); + return this.getMarker(this.bufferMarkerLayer.markRange(bufferRange, options).id); + } + + // Public: Create a marker on this layer with its head at the given buffer + // position and no tail. + // + // * `bufferPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `exclusive` {Boolean} indicating whether insertions at the start or end + // of the marked range should be interpreted as happening *outside* the + // marker. Defaults to `false`, except when using the `inside` + // invalidation strategy or when when the marker has no tail, in which + // case it defaults to true. Explicitly assigning this option overrides + // behavior in all circumstances. + // + // Returns a {DisplayMarker}. + markBufferPosition(bufferPosition, options) { + return this.getMarker(this.bufferMarkerLayer.markPosition(Point.fromObject(bufferPosition), options).id); + } + + /* + Section: Querying + */ + + // Essential: Get an existing marker by its id. + // + // Returns a {DisplayMarker}. + getMarker(id) { + let bufferMarker, displayMarker; + if (displayMarker = this.markersById[id]) { + return displayMarker; + } else if (bufferMarker = this.bufferMarkerLayer.getMarker(id)) { + return this.markersById[id] = new DisplayMarker(this, bufferMarker); + } + } + + // Essential: Get all markers in the layer. + // + // Returns an {Array} of {DisplayMarker}s. + getMarkers() { + return this.bufferMarkerLayer.getMarkers().map(({id}) => this.getMarker(id)); + } + + // Public: Get the number of markers in the marker layer. + // + // Returns a {Number}. + getMarkerCount() { + return this.bufferMarkerLayer.getMarkerCount(); + } + + // Public: Find markers in the layer conforming to the given parameters. + // + // This method finds markers based on the given properties. Markers can be + // associated with custom properties that will be compared with basic equality. + // In addition, there are several special properties that will be compared + // with the range of the markers rather than their properties. + // + // * `properties` An {Object} containing properties that each returned marker + // must satisfy. Markers can be associated with custom properties, which are + // compared with basic equality. In addition, several reserved properties + // can be used to filter markers based on their current range: + // * `startBufferPosition` Only include markers starting at this {Point} in buffer coordinates. + // * `endBufferPosition` Only include markers ending at this {Point} in buffer coordinates. + // * `startScreenPosition` Only include markers starting at this {Point} in screen coordinates. + // * `endScreenPosition` Only include markers ending at this {Point} in screen coordinates. + // * `startsInBufferRange` Only include markers starting inside this {Range} in buffer coordinates. + // * `endsInBufferRange` Only include markers ending inside this {Range} in buffer coordinates. + // * `startsInScreenRange` Only include markers starting inside this {Range} in screen coordinates. + // * `endsInScreenRange` Only include markers ending inside this {Range} in screen coordinates. + // * `startBufferRow` Only include markers starting at this row in buffer coordinates. + // * `endBufferRow` Only include markers ending at this row in buffer coordinates. + // * `startScreenRow` Only include markers starting at this row in screen coordinates. + // * `endScreenRow` Only include markers ending at this row in screen coordinates. + // * `intersectsBufferRowRange` Only include markers intersecting this {Array} + // of `[startRow, endRow]` in buffer coordinates. + // * `intersectsScreenRowRange` Only include markers intersecting this {Array} + // of `[startRow, endRow]` in screen coordinates. + // * `containsBufferRange` Only include markers containing this {Range} in buffer coordinates. + // * `containsBufferPosition` Only include markers containing this {Point} in buffer coordinates. + // * `containedInBufferRange` Only include markers contained in this {Range} in buffer coordinates. + // * `containedInScreenRange` Only include markers contained in this {Range} in screen coordinates. + // * `intersectsBufferRange` Only include markers intersecting this {Range} in buffer coordinates. + // * `intersectsScreenRange` Only include markers intersecting this {Range} in screen coordinates. + // + // Returns an {Array} of {DisplayMarker}s + findMarkers(params) { + params = this.translateToBufferMarkerLayerFindParams(params); + return this.bufferMarkerLayer.findMarkers(params).map(stringMarker => this.getMarker(stringMarker.id)); + } + + /* + Section: Private + */ + + translateBufferPosition(bufferPosition, options) { + return this.displayLayer.translateBufferPosition(bufferPosition, options); + } + + translateBufferRange(bufferRange, options) { + return this.displayLayer.translateBufferRange(bufferRange, options); + } + + translateScreenPosition(screenPosition, options) { + return this.displayLayer.translateScreenPosition(screenPosition, options); + } + + translateScreenRange(screenRange, options) { + return this.displayLayer.translateScreenRange(screenRange, options); + } + + emitDidUpdate() { + return this.emitter.emit('did-update'); + } + + notifyObserversIfMarkerScreenPositionsChanged() { + for (let marker of this.getMarkers()) { + marker.notifyObservers(false); + } + } + + destroyMarker(id) { + let marker; + if (marker = this.markersById[id]) { + return marker.didDestroyBufferMarker(); + } + } + + didDestroyMarker(marker) { + this.markersWithDestroyListeners.delete(marker); + return delete this.markersById[marker.id]; + } + + translateToBufferMarkerLayerFindParams(params) { + const bufferMarkerLayerFindParams = {}; + for (let key in params) { + let value = params[key]; + switch (key) { + case 'startBufferPosition': + key = 'startPosition'; + break; + case 'endBufferPosition': + key = 'endPosition'; + break; + case 'startScreenPosition': + key = 'startPosition'; + value = this.displayLayer.translateScreenPosition(value); + break; + case 'endScreenPosition': + key = 'endPosition'; + value = this.displayLayer.translateScreenPosition(value); + break; + case 'startsInBufferRange': + key = 'startsInRange'; + break; + case 'endsInBufferRange': + key = 'endsInRange'; + break; + case 'startsInScreenRange': + key = 'startsInRange'; + value = this.displayLayer.translateScreenRange(value); + break; + case 'endsInScreenRange': + key = 'endsInRange'; + value = this.displayLayer.translateScreenRange(value); + break; + case 'startBufferRow': + key = 'startRow'; + break; + case 'endBufferRow': + key = 'endRow'; + break; + case 'startScreenRow': + key = 'startsInRange'; + var startBufferPosition = this.displayLayer.translateScreenPosition(Point(value, 0)); + var endBufferPosition = this.displayLayer.translateScreenPosition(Point(value, Infinity)); + value = Range(startBufferPosition, endBufferPosition); + break; + case 'endScreenRow': + key = 'endsInRange'; + startBufferPosition = this.displayLayer.translateScreenPosition(Point(value, 0)); + endBufferPosition = this.displayLayer.translateScreenPosition(Point(value, Infinity)); + value = Range(startBufferPosition, endBufferPosition); + break; + case 'intersectsBufferRowRange': + key = 'intersectsRowRange'; + break; + case 'intersectsScreenRowRange': + key = 'intersectsRange'; + var [startScreenRow, endScreenRow] = Array.from(value); + startBufferPosition = this.displayLayer.translateScreenPosition(Point(startScreenRow, 0)); + endBufferPosition = this.displayLayer.translateScreenPosition(Point(endScreenRow, Infinity)); + value = Range(startBufferPosition, endBufferPosition); + break; + case 'containsBufferRange': + key = 'containsRange'; + break; + case 'containsScreenRange': + key = 'containsRange'; + value = this.displayLayer.translateScreenRange(value); + break; + case 'containsBufferPosition': + key = 'containsPosition'; + break; + case 'containsScreenPosition': + key = 'containsPosition'; + value = this.displayLayer.translateScreenPosition(value); + break; + case 'containedInBufferRange': + key = 'containedInRange'; + break; + case 'containedInScreenRange': + key = 'containedInRange'; + value = this.displayLayer.translateScreenRange(value); + break; + case 'intersectsBufferRange': + key = 'intersectsRange'; + break; + case 'intersectsScreenRange': + key = 'intersectsRange'; + value = this.displayLayer.translateScreenRange(value); + break; + } + bufferMarkerLayerFindParams[key] = value; + } + + return bufferMarkerLayerFindParams; + } +}); From d9146222eaea81ec5ebebeee82b30fa639415925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 13:14:12 -0300 Subject: [PATCH 27/64] Decaff point --- src/point.coffee | 268 -------------------------------------- src/point.js | 325 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 325 insertions(+), 268 deletions(-) delete mode 100644 src/point.coffee create mode 100644 src/point.js diff --git a/src/point.coffee b/src/point.coffee deleted file mode 100644 index 86f9688192..0000000000 --- a/src/point.coffee +++ /dev/null @@ -1,268 +0,0 @@ -# Public: Represents a point in a buffer in row/column coordinates. -# -# Every public method that takes a point also accepts a *point-compatible* -# {Array}. This means a 2-element array containing {Number}s representing the -# row and column. So the following are equivalent: -# -# ```coffee -# new Point(1, 2) -# [1, 2] # Point compatible Array -# ``` -module.exports = -class Point - ### - Section: Properties - ### - - # Public: A zero-indexed {Number} representing the row of the {Point}. - row: null - - # Public: A zero-indexed {Number} representing the column of the {Point}. - column: null - - ### - Section: Construction - ### - - # Public: Convert any point-compatible object to a {Point}. - # - # * `object` This can be an object that's already a {Point}, in which case it's - # simply returned, or an array containing two {Number}s representing the - # row and column. - # * `copy` An optional boolean indicating whether to force the copying of objects - # that are already points. - # - # Returns: A {Point} based on the given object. - @fromObject: (object, copy) -> - if object instanceof Point - if copy then object.copy() else object - else - if Array.isArray(object) - [row, column] = object - else - {row, column} = object - - new Point(row, column) - - ### - Section: Comparison - ### - - # Public: Returns the given {Point} that is earlier in the buffer. - # - # * `point1` {Point} - # * `point2` {Point} - @min: (point1, point2) -> - point1 = @fromObject(point1) - point2 = @fromObject(point2) - if point1.isLessThanOrEqual(point2) - point1 - else - point2 - - @max: (point1, point2) -> - point1 = Point.fromObject(point1) - point2 = Point.fromObject(point2) - if point1.compare(point2) >= 0 - point1 - else - point2 - - @assertValid: (point) -> - unless isNumber(point.row) and isNumber(point.column) - throw new TypeError("Invalid Point: #{point}") - - @ZERO: Object.freeze(new Point(0, 0)) - - @INFINITY: Object.freeze(new Point(Infinity, Infinity)) - - ### - Section: Construction - ### - - # Public: Construct a {Point} object - # - # * `row` {Number} row - # * `column` {Number} column - constructor: (row=0, column=0) -> - unless this instanceof Point - return new Point(row, column) - @row = row - @column = column - - # Public: Returns a new {Point} with the same row and column. - copy: -> - new Point(@row, @column) - - # Public: Returns a new {Point} with the row and column negated. - negate: -> - new Point(-@row, -@column) - - ### - Section: Operations - ### - - # Public: Makes this point immutable and returns itself. - # - # Returns an immutable version of this {Point} - freeze: -> - Object.freeze(this) - - # Public: Build and return a new point by adding the rows and columns of - # the given point. - # - # * `other` A {Point} whose row and column will be added to this point's row - # and column to build the returned point. - # - # Returns a {Point}. - translate: (other) -> - {row, column} = Point.fromObject(other) - new Point(@row + row, @column + column) - - # Public: Build and return a new {Point} by traversing the rows and columns - # specified by the given point. - # - # * `other` A {Point} providing the rows and columns to traverse by. - # - # This method differs from the direct, vector-style addition offered by - # {::translate}. Rather than adding the rows and columns directly, it derives - # the new point from traversing in "typewriter space". At the end of every row - # traversed, a carriage return occurs that returns the columns to 0 before - # continuing the traversal. - # - # ## Examples - # - # Traversing 0 rows, 2 columns: - # `new Point(10, 5).traverse(new Point(0, 2)) # => [10, 7]` - # - # Traversing 2 rows, 2 columns. Note the columns reset from 0 before adding: - # `new Point(10, 5).traverse(new Point(2, 2)) # => [12, 2]` - # - # Returns a {Point}. - traverse: (other) -> - other = Point.fromObject(other) - row = @row + other.row - if other.row is 0 - column = @column + other.column - else - column = other.column - - new Point(row, column) - - traversalFrom: (other) -> - other = Point.fromObject(other) - if @row is other.row - if @column is Infinity and other.column is Infinity - new Point(0, 0) - else - new Point(0, @column - other.column) - else - new Point(@row - other.row, @column) - - splitAt: (column) -> - if @row is 0 - rightColumn = @column - column - else - rightColumn = @column - - [new Point(0, column), new Point(@row, rightColumn)] - - ### - Section: Comparison - ### - - # Public: - # - # * `other` A {Point} or point-compatible {Array}. - # - # Returns `-1` if this point precedes the argument. - # Returns `0` if this point is equivalent to the argument. - # Returns `1` if this point follows the argument. - compare: (other) -> - other = Point.fromObject(other) - if @row > other.row - 1 - else if @row < other.row - -1 - else - if @column > other.column - 1 - else if @column < other.column - -1 - else - 0 - - # Public: Returns a {Boolean} indicating whether this point has the same row - # and column as the given {Point} or point-compatible {Array}. - # - # * `other` A {Point} or point-compatible {Array}. - isEqual: (other) -> - return false unless other - other = Point.fromObject(other) - @row is other.row and @column is other.column - - # Public: Returns a {Boolean} indicating whether this point precedes the given - # {Point} or point-compatible {Array}. - # - # * `other` A {Point} or point-compatible {Array}. - isLessThan: (other) -> - @compare(other) < 0 - - # Public: Returns a {Boolean} indicating whether this point precedes or is - # equal to the given {Point} or point-compatible {Array}. - # - # * `other` A {Point} or point-compatible {Array}. - isLessThanOrEqual: (other) -> - @compare(other) <= 0 - - # Public: Returns a {Boolean} indicating whether this point follows the given - # {Point} or point-compatible {Array}. - # - # * `other` A {Point} or point-compatible {Array}. - isGreaterThan: (other) -> - @compare(other) > 0 - - # Public: Returns a {Boolean} indicating whether this point follows or is - # equal to the given {Point} or point-compatible {Array}. - # - # * `other` A {Point} or point-compatible {Array}. - isGreaterThanOrEqual: (other) -> - @compare(other) >= 0 - - isZero: -> - @row is 0 and @column is 0 - - isPositive: -> - if @row > 0 - true - else if @row < 0 - false - else - @column > 0 - - isNegative: -> - if @row < 0 - true - else if @row > 0 - false - else - @column < 0 - - ### - Section: Conversion - ### - - # Public: Returns an array of this point's row and column. - toArray: -> - [@row, @column] - - # Public: Returns an array of this point's row and column. - serialize: -> - @toArray() - - # Public: Returns a string representation of the point. - toString: -> - "(#{@row}, #{@column})" - -isNumber = (value) -> - (typeof value is 'number') and (not Number.isNaN(value)) diff --git a/src/point.js b/src/point.js new file mode 100644 index 0000000000..c3e6c8238c --- /dev/null +++ b/src/point.js @@ -0,0 +1,325 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +// Public: Represents a point in a buffer in row/column coordinates. +// +// Every public method that takes a point also accepts a *point-compatible* +// {Array}. This means a 2-element array containing {Number}s representing the +// row and column. So the following are equivalent: +// +// ```coffee +// new Point(1, 2) +// [1, 2] # Point compatible Array +// ``` +let Point; +module.exports = +(Point = (function() { + Point = class Point { + static initClass() { + /* + Section: Properties + */ + + // Public: A zero-indexed {Number} representing the row of the {Point}. + this.prototype.row = null; + + // Public: A zero-indexed {Number} representing the column of the {Point}. + this.prototype.column = null; + + this.ZERO = Object.freeze(new Point(0, 0)); + + this.INFINITY = Object.freeze(new Point(Infinity, Infinity)); + } + + /* + Section: Construction + */ + + // Public: Convert any point-compatible object to a {Point}. + // + // * `object` This can be an object that's already a {Point}, in which case it's + // simply returned, or an array containing two {Number}s representing the + // row and column. + // * `copy` An optional boolean indicating whether to force the copying of objects + // that are already points. + // + // Returns: A {Point} based on the given object. + static fromObject(object, copy) { + if (object instanceof Point) { + if (copy) { return object.copy(); } else { return object; } + } else { + let column, row; + if (Array.isArray(object)) { + [row, column] = Array.from(object); + } else { + ({row, column} = object); + } + + return new Point(row, column); + } + } + + /* + Section: Comparison + */ + + // Public: Returns the given {Point} that is earlier in the buffer. + // + // * `point1` {Point} + // * `point2` {Point} + static min(point1, point2) { + point1 = this.fromObject(point1); + point2 = this.fromObject(point2); + if (point1.isLessThanOrEqual(point2)) { + return point1; + } else { + return point2; + } + } + + static max(point1, point2) { + point1 = Point.fromObject(point1); + point2 = Point.fromObject(point2); + if (point1.compare(point2) >= 0) { + return point1; + } else { + return point2; + } + } + + static assertValid(point) { + if (!isNumber(point.row) || !isNumber(point.column)) { + throw new TypeError(`Invalid Point: ${point}`); + } + } + + /* + Section: Construction + */ + + // Public: Construct a {Point} object + // + // * `row` {Number} row + // * `column` {Number} column + constructor(row=0, column=0) { + if (!(this instanceof Point)) { + return new Point(row, column); + } + this.row = row; + this.column = column; + } + + // Public: Returns a new {Point} with the same row and column. + copy() { + return new Point(this.row, this.column); + } + + // Public: Returns a new {Point} with the row and column negated. + negate() { + return new Point(-this.row, -this.column); + } + + /* + Section: Operations + */ + + // Public: Makes this point immutable and returns itself. + // + // Returns an immutable version of this {Point} + freeze() { + return Object.freeze(this); + } + + // Public: Build and return a new point by adding the rows and columns of + // the given point. + // + // * `other` A {Point} whose row and column will be added to this point's row + // and column to build the returned point. + // + // Returns a {Point}. + translate(other) { + const {row, column} = Point.fromObject(other); + return new Point(this.row + row, this.column + column); + } + + // Public: Build and return a new {Point} by traversing the rows and columns + // specified by the given point. + // + // * `other` A {Point} providing the rows and columns to traverse by. + // + // This method differs from the direct, vector-style addition offered by + // {::translate}. Rather than adding the rows and columns directly, it derives + // the new point from traversing in "typewriter space". At the end of every row + // traversed, a carriage return occurs that returns the columns to 0 before + // continuing the traversal. + // + // ## Examples + // + // Traversing 0 rows, 2 columns: + // `new Point(10, 5).traverse(new Point(0, 2)) # => [10, 7]` + // + // Traversing 2 rows, 2 columns. Note the columns reset from 0 before adding: + // `new Point(10, 5).traverse(new Point(2, 2)) # => [12, 2]` + // + // Returns a {Point}. + traverse(other) { + let column; + other = Point.fromObject(other); + const row = this.row + other.row; + if (other.row === 0) { + column = this.column + other.column; + } else { + ({ + column + } = other); + } + + return new Point(row, column); + } + + traversalFrom(other) { + other = Point.fromObject(other); + if (this.row === other.row) { + if ((this.column === Infinity) && (other.column === Infinity)) { + return new Point(0, 0); + } else { + return new Point(0, this.column - other.column); + } + } else { + return new Point(this.row - other.row, this.column); + } + } + + splitAt(column) { + let rightColumn; + if (this.row === 0) { + rightColumn = this.column - column; + } else { + rightColumn = this.column; + } + + return [new Point(0, column), new Point(this.row, rightColumn)]; + } + + /* + Section: Comparison + */ + + // Public: + // + // * `other` A {Point} or point-compatible {Array}. + // + // Returns `-1` if this point precedes the argument. + // Returns `0` if this point is equivalent to the argument. + // Returns `1` if this point follows the argument. + compare(other) { + other = Point.fromObject(other); + if (this.row > other.row) { + return 1; + } else if (this.row < other.row) { + return -1; + } else { + if (this.column > other.column) { + return 1; + } else if (this.column < other.column) { + return -1; + } else { + return 0; + } + } + } + + // Public: Returns a {Boolean} indicating whether this point has the same row + // and column as the given {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isEqual(other) { + if (!other) { return false; } + other = Point.fromObject(other); + return (this.row === other.row) && (this.column === other.column); + } + + // Public: Returns a {Boolean} indicating whether this point precedes the given + // {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isLessThan(other) { + return this.compare(other) < 0; + } + + // Public: Returns a {Boolean} indicating whether this point precedes or is + // equal to the given {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isLessThanOrEqual(other) { + return this.compare(other) <= 0; + } + + // Public: Returns a {Boolean} indicating whether this point follows the given + // {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isGreaterThan(other) { + return this.compare(other) > 0; + } + + // Public: Returns a {Boolean} indicating whether this point follows or is + // equal to the given {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isGreaterThanOrEqual(other) { + return this.compare(other) >= 0; + } + + isZero() { + return (this.row === 0) && (this.column === 0); + } + + isPositive() { + if (this.row > 0) { + return true; + } else if (this.row < 0) { + return false; + } else { + return this.column > 0; + } + } + + isNegative() { + if (this.row < 0) { + return true; + } else if (this.row > 0) { + return false; + } else { + return this.column < 0; + } + } + + /* + Section: Conversion + */ + + // Public: Returns an array of this point's row and column. + toArray() { + return [this.row, this.column]; + } + + // Public: Returns an array of this point's row and column. + serialize() { + return this.toArray(); + } + + // Public: Returns a string representation of the point. + toString() { + return `(${this.row}, ${this.column})`; + } + }; + Point.initClass(); + return Point; +})()); + +var isNumber = value => (typeof value === 'number') && (!Number.isNaN(value)); From 9a98baaf42eecfd64e07f90aabeaf156fba85e78 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Thu, 9 Mar 2023 13:17:03 -0300 Subject: [PATCH 28/64] decaffeinate: Rename is-character-pair.coffee and 4 other files from .coffee to .js --- src/{is-character-pair.coffee => is-character-pair.js} | 0 src/{marker.coffee => marker.js} | 0 src/{point-helpers.coffee => point-helpers.js} | 0 src/{range.coffee => range.js} | 0 src/{set-helpers.coffee => set-helpers.js} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/{is-character-pair.coffee => is-character-pair.js} (100%) rename src/{marker.coffee => marker.js} (100%) rename src/{point-helpers.coffee => point-helpers.js} (100%) rename src/{range.coffee => range.js} (100%) rename src/{set-helpers.coffee => set-helpers.js} (100%) diff --git a/src/is-character-pair.coffee b/src/is-character-pair.js similarity index 100% rename from src/is-character-pair.coffee rename to src/is-character-pair.js diff --git a/src/marker.coffee b/src/marker.js similarity index 100% rename from src/marker.coffee rename to src/marker.js diff --git a/src/point-helpers.coffee b/src/point-helpers.js similarity index 100% rename from src/point-helpers.coffee rename to src/point-helpers.js diff --git a/src/range.coffee b/src/range.js similarity index 100% rename from src/range.coffee rename to src/range.js diff --git a/src/set-helpers.coffee b/src/set-helpers.js similarity index 100% rename from src/set-helpers.coffee rename to src/set-helpers.js From 9285a94fd70c51de527f8d317e6d9b50a089736d Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Thu, 9 Mar 2023 13:17:04 -0300 Subject: [PATCH 29/64] decaffeinate: Convert is-character-pair.coffee and 4 other files to JS --- src/is-character-pair.js | 47 +- src/marker.js | 951 +++++++++++++++++++++------------------ src/point-helpers.js | 149 +++--- src/range.js | 706 ++++++++++++++++------------- src/set-helpers.js | 40 +- 5 files changed, 1040 insertions(+), 853 deletions(-) diff --git a/src/is-character-pair.js b/src/is-character-pair.js index 02969d5ee7..d066a2d208 100644 --- a/src/is-character-pair.js +++ b/src/is-character-pair.js @@ -1,31 +1,30 @@ -module.exports = (character1, character2) -> - charCodeA = character1.charCodeAt(0) - charCodeB = character2.charCodeAt(0) - isSurrogatePair(charCodeA, charCodeB) or - isVariationSequence(charCodeA, charCodeB) or - isCombinedCharacter(charCodeA, charCodeB) +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +module.exports = function(character1, character2) { + const charCodeA = character1.charCodeAt(0); + const charCodeB = character2.charCodeAt(0); + return isSurrogatePair(charCodeA, charCodeB) || + isVariationSequence(charCodeA, charCodeB) || + isCombinedCharacter(charCodeA, charCodeB); +}; -isCombinedCharacter = (charCodeA, charCodeB) -> - not isCombiningCharacter(charCodeA) and isCombiningCharacter(charCodeB) +var isCombinedCharacter = (charCodeA, charCodeB) => !isCombiningCharacter(charCodeA) && isCombiningCharacter(charCodeB); -isSurrogatePair = (charCodeA, charCodeB) -> - isHighSurrogate(charCodeA) and isLowSurrogate(charCodeB) +var isSurrogatePair = (charCodeA, charCodeB) => isHighSurrogate(charCodeA) && isLowSurrogate(charCodeB); -isVariationSequence = (charCodeA, charCodeB) -> - not isVariationSelector(charCodeA) and isVariationSelector(charCodeB) +var isVariationSequence = (charCodeA, charCodeB) => !isVariationSelector(charCodeA) && isVariationSelector(charCodeB); -isHighSurrogate = (charCode) -> - 0xD800 <= charCode <= 0xDBFF +var isHighSurrogate = charCode => 0xD800 <= charCode && charCode <= 0xDBFF; -isLowSurrogate = (charCode) -> - 0xDC00 <= charCode <= 0xDFFF +var isLowSurrogate = charCode => 0xDC00 <= charCode && charCode <= 0xDFFF; -isVariationSelector = (charCode) -> - 0xFE00 <= charCode <= 0xFE0F +var isVariationSelector = charCode => 0xFE00 <= charCode && charCode <= 0xFE0F; -isCombiningCharacter = (charCode) -> - 0x0300 <= charCode <= 0x036F or - 0x1AB0 <= charCode <= 0x1AFF or - 0x1DC0 <= charCode <= 0x1DFF or - 0x20D0 <= charCode <= 0x20FF or - 0xFE20 <= charCode <= 0xFE2F +var isCombiningCharacter = charCode => (0x0300 <= charCode && charCode <= 0x036F) || +(0x1AB0 <= charCode && charCode <= 0x1AFF) || +(0x1DC0 <= charCode && charCode <= 0x1DFF) || +(0x20D0 <= charCode && charCode <= 0x20FF) || +(0xFE20 <= charCode && charCode <= 0xFE2F); diff --git a/src/marker.js b/src/marker.js index a6017ddf26..866e2011ce 100644 --- a/src/marker.js +++ b/src/marker.js @@ -1,435 +1,518 @@ -{extend, isEqual, omit, pick, size} = require 'underscore-plus' -{Emitter} = require 'event-kit' -Delegator = require 'delegato' -Point = require './point' -Range = require './range' -Grim = require 'grim' - -OptionKeys = new Set(['reversed', 'tailed', 'invalidate', 'exclusive']) - -# Private: Represents a buffer annotation that remains logically stationary -# even as the buffer changes. This is used to represent cursors, folds, snippet -# targets, misspelled words, and anything else that needs to track a logical -# location in the buffer over time. -# -# Head and Tail: -# Markers always have a *head* and sometimes have a *tail*. If you think of a -# marker as an editor selection, the tail is the part that's stationary and the -# head is the part that moves when the mouse is moved. A marker without a tail -# always reports an empty range at the head position. A marker with a head position -# greater than the tail is in a "normal" orientation. If the head precedes the -# tail the marker is in a "reversed" orientation. -# -# Validity: -# Markers are considered *valid* when they are first created. Depending on the -# invalidation strategy you choose, certain changes to the buffer can cause a -# marker to become invalid, for example if the text surrounding the marker is -# deleted. See {TextBuffer::markRange} for invalidation strategies. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +let Marker; +const {extend, isEqual, omit, pick, size} = require('underscore-plus'); +const {Emitter} = require('event-kit'); +const Delegator = require('delegato'); +const Point = require('./point'); +const Range = require('./range'); +const Grim = require('grim'); + +const OptionKeys = new Set(['reversed', 'tailed', 'invalidate', 'exclusive']); + +// Private: Represents a buffer annotation that remains logically stationary +// even as the buffer changes. This is used to represent cursors, folds, snippet +// targets, misspelled words, and anything else that needs to track a logical +// location in the buffer over time. +// +// Head and Tail: +// Markers always have a *head* and sometimes have a *tail*. If you think of a +// marker as an editor selection, the tail is the part that's stationary and the +// head is the part that moves when the mouse is moved. A marker without a tail +// always reports an empty range at the head position. A marker with a head position +// greater than the tail is in a "normal" orientation. If the head precedes the +// tail the marker is in a "reversed" orientation. +// +// Validity: +// Markers are considered *valid* when they are first created. Depending on the +// invalidation strategy you choose, certain changes to the buffer can cause a +// marker to become invalid, for example if the text surrounding the marker is +// deleted. See {TextBuffer::markRange} for invalidation strategies. module.exports = -class Marker - Delegator.includeInto(this) - - @extractParams: (inputParams) -> - outputParams = {} - containsCustomProperties = false - if inputParams? - for key in Object.keys(inputParams) - if OptionKeys.has(key) - outputParams[key] = inputParams[key] - else if key is 'clipDirection' or key is 'skipSoftWrapIndentation' - # TODO: Ignore these two keys for now. Eventually, when the - # deprecation below will be gone, we can remove this conditional as - # well, and just return standard marker properties. - else - containsCustomProperties = true - outputParams.properties ?= {} - outputParams.properties[key] = inputParams[key] - - # TODO: Remove both this deprecation and the conditional above on the - # release after the one where we'll ship `DisplayLayer`. - if containsCustomProperties - Grim.deprecate(""" - Assigning custom properties to a marker when creating/copying it is - deprecated. Please, consider storing the custom properties you need in - some other object in your package, keyed by the marker's id property. - """) - - outputParams - - @delegatesMethods 'containsPoint', 'containsRange', 'intersectsRow', toMethod: 'getRange' - - constructor: (@id, @layer, range, params, exclusivitySet = false) -> - {@tailed, @reversed, @valid, @invalidate, @exclusive, @properties} = params - @emitter = new Emitter - @tailed ?= true - @reversed ?= false - @valid ?= true - @invalidate ?= 'overlap' - @properties ?= {} - @hasChangeObservers = false - Object.freeze(@properties) - @layer.setMarkerIsExclusive(@id, @isExclusive()) unless exclusivitySet - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the marker is destroyed. - # - # * `callback` {Function} to be called when the marker is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @layer.markersWithDestroyListeners.add(this) - @emitter.on 'did-destroy', callback - - # Public: Invoke the given callback when the state of the marker changes. - # - # * `callback` {Function} to be called when the marker changes. - # * `event` {Object} with the following keys: - # * `oldHeadPosition` {Point} representing the former head position - # * `newHeadPosition` {Point} representing the new head position - # * `oldTailPosition` {Point} representing the former tail position - # * `newTailPosition` {Point} representing the new tail position - # * `wasValid` {Boolean} indicating whether the marker was valid before the change - # * `isValid` {Boolean} indicating whether the marker is now valid - # * `hadTail` {Boolean} indicating whether the marker had a tail before the change - # * `hasTail` {Boolean} indicating whether the marker now has a tail - # * `oldProperties` {Object} containing the marker's custom properties before the change. - # * `newProperties` {Object} containing the marker's custom properties after the change. - # * `textChanged` {Boolean} indicating whether this change was caused by a textual change - # to the buffer or whether the marker was manipulated directly via its public API. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - unless @hasChangeObservers - @previousEventState = @getSnapshot(@getRange()) - @hasChangeObservers = true - @layer.markersWithChangeListeners.add(this) - @emitter.on 'did-change', callback - - # Public: Returns the current {Range} of the marker. The range is immutable. - getRange: -> @layer.getMarkerRange(@id) - - # Public: Sets the range of the marker. - # - # * `range` A {Range} or range-compatible {Array}. The range will be clipped - # before it is assigned. - # * `params` (optional) An {Object} with the following keys: - # * `reversed` {Boolean} indicating the marker will to be in a reversed - # orientation. - # * `exclusive` {Boolean} indicating that changes occurring at either end of - # the marker will be considered *outside* the marker rather than inside. - # This defaults to `false` unless the marker's invalidation strategy is - # `inside` or the marker has no tail, in which case it defaults to `true`. - setRange: (range, params) -> - params ?= {} - @update(@getRange(), {reversed: params.reversed, tailed: true, range: Range.fromObject(range, true), exclusive: params.exclusive}) - - # Public: Returns a {Point} representing the marker's current head position. - getHeadPosition: -> - if @reversed - @getStartPosition() - else - @getEndPosition() - - # Public: Sets the head position of the marker. - # - # * `position` A {Point} or point-compatible {Array}. The position will be - # clipped before it is assigned. - setHeadPosition: (position) -> - position = Point.fromObject(position) - oldRange = @getRange() - params = {} - - if @hasTail() - if @isReversed() - if position.isLessThan(oldRange.end) - params.range = new Range(position, oldRange.end) - else - params.reversed = false - params.range = new Range(oldRange.end, position) - else - if position.isLessThan(oldRange.start) - params.reversed = true - params.range = new Range(position, oldRange.start) - else - params.range = new Range(oldRange.start, position) - else - params.range = new Range(position, position) - @update(oldRange, params) - - # Public: Returns a {Point} representing the marker's current tail position. - # If the marker has no tail, the head position will be returned instead. - getTailPosition: -> - if @reversed - @getEndPosition() - else - @getStartPosition() - - # Public: Sets the tail position of the marker. If the marker doesn't have a - # tail, it will after calling this method. - # - # * `position` A {Point} or point-compatible {Array}. The position will be - # clipped before it is assigned. - setTailPosition: (position) -> - position = Point.fromObject(position) - oldRange = @getRange() - params = {tailed: true} - - if @reversed - if position.isLessThan(oldRange.start) - params.reversed = false - params.range = new Range(position, oldRange.start) - else - params.range = new Range(oldRange.start, position) - else - if position.isLessThan(oldRange.end) - params.range = new Range(position, oldRange.end) - else - params.reversed = true - params.range = new Range(oldRange.end, position) - - @update(oldRange, params) - - # Public: Returns a {Point} representing the start position of the marker, - # which could be the head or tail position, depending on its orientation. - getStartPosition: -> @layer.getMarkerStartPosition(@id) - - # Public: Returns a {Point} representing the end position of the marker, - # which could be the head or tail position, depending on its orientation. - getEndPosition: -> @layer.getMarkerEndPosition(@id) - - # Public: Removes the marker's tail. After calling the marker's head position - # will be reported as its current tail position until the tail is planted - # again. - clearTail: -> - headPosition = @getHeadPosition() - @update(@getRange(), {tailed: false, reversed: false, range: Range(headPosition, headPosition)}) - - # Public: Plants the marker's tail at the current head position. After calling - # the marker's tail position will be its head position at the time of the - # call, regardless of where the marker's head is moved. - plantTail: -> - unless @hasTail() - headPosition = @getHeadPosition() - @update(@getRange(), {tailed: true, range: new Range(headPosition, headPosition)}) - - # Public: Returns a {Boolean} indicating whether the head precedes the tail. - isReversed: -> - @tailed and @reversed - - # Public: Returns a {Boolean} indicating whether the marker has a tail. - hasTail: -> - @tailed - - # Public: Is the marker valid? - # - # Returns a {Boolean}. - isValid: -> - not @isDestroyed() and @valid - - # Public: Is the marker destroyed? - # - # Returns a {Boolean}. - isDestroyed: -> - not @layer.hasMarker(@id) - - # Public: Returns a {Boolean} indicating whether changes that occur exactly at - # the marker's head or tail cause it to move. - isExclusive: -> - if @exclusive? - @exclusive - else - @getInvalidationStrategy() is 'inside' or not @hasTail() - - # Public: Returns a {Boolean} indicating whether this marker is equivalent to - # another marker, meaning they have the same range and options. - # - # * `other` {Marker} other marker - isEqual: (other) -> - @invalidate is other.invalidate and - @tailed is other.tailed and - @reversed is other.reversed and - @exclusive is other.exclusive and - isEqual(@properties, other.properties) and - @getRange().isEqual(other.getRange()) - - # Public: Get the invalidation strategy for this marker. - # - # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. - # - # Returns a {String}. - getInvalidationStrategy: -> - @invalidate - - # Public: Returns an {Object} containing any custom properties associated with - # the marker. - getProperties: -> - @properties - - # Public: Merges an {Object} containing new properties into the marker's - # existing properties. - # - # * `properties` {Object} - setProperties: (properties) -> - @update(@getRange(), properties: extend({}, @properties, properties)) - - # Public: Creates and returns a new {Marker} with the same properties as this - # marker. - # - # * `params` {Object} - copy: (options={}) -> - snapshot = @getSnapshot() - options = Marker.extractParams(options) - @layer.createMarker(@getRange(), extend( - {} - snapshot, - options, - properties: extend({}, snapshot.properties, options.properties) - )) - - # Public: Destroys the marker, causing it to emit the 'destroyed' event. - destroy: (suppressMarkerLayerUpdateEvents) -> - return if @isDestroyed() - - if @trackDestruction - error = new Error - Error.captureStackTrace(error) - @destroyStackTrace = error.stack - - @layer.destroyMarker(this, suppressMarkerLayerUpdateEvents) - @emitter.emit 'did-destroy' - @emitter.clear() - - # Public: Compares this marker to another based on their ranges. - # - # * `other` {Marker} - compare: (other) -> - @layer.compareMarkers(@id, other.id) - - # Returns whether this marker matches the given parameters. The parameters - # are the same as {MarkerLayer::findMarkers}. - matchesParams: (params) -> - for key in Object.keys(params) - return false unless @matchesParam(key, params[key]) - true - - # Returns whether this marker matches the given parameter name and value. - # The parameters are the same as {MarkerLayer::findMarkers}. - matchesParam: (key, value) -> - switch key - when 'startPosition' - @getStartPosition().isEqual(value) - when 'endPosition' - @getEndPosition().isEqual(value) - when 'containsPoint', 'containsPosition' - @containsPoint(value) - when 'containsRange' - @containsRange(value) - when 'startRow' - @getStartPosition().row is value - when 'endRow' - @getEndPosition().row is value - when 'intersectsRow' - @intersectsRow(value) - when 'invalidate', 'reversed', 'tailed' - isEqual(@[key], value) - when 'valid' - @isValid() is value - else - isEqual(@properties[key], value) - - update: (oldRange, {range, reversed, tailed, valid, exclusive, properties}, textChanged=false, suppressMarkerLayerUpdateEvents=false) -> - return if @isDestroyed() - - oldRange = Range.fromObject(oldRange) - range = Range.fromObject(range) if range? - - wasExclusive = @isExclusive() - updated = propertiesChanged = false - - if range? and not range.isEqual(oldRange) - @layer.setMarkerRange(@id, range) - updated = true - - if reversed? and reversed isnt @reversed - @reversed = reversed - updated = true - - if tailed? and tailed isnt @tailed - @tailed = tailed - updated = true - - if valid? and valid isnt @valid - @valid = valid - updated = true - - if exclusive? and exclusive isnt @exclusive - @exclusive = exclusive - updated = true - - if wasExclusive isnt @isExclusive() - @layer.setMarkerIsExclusive(@id, @isExclusive()) - updated = true - - if properties? and not isEqual(properties, @properties) - @properties = Object.freeze(properties) - propertiesChanged = true - updated = true - - @emitChangeEvent(range ? oldRange, textChanged, propertiesChanged) - @layer.markerUpdated() if updated and not suppressMarkerLayerUpdateEvents - updated - - getSnapshot: (range, includeMarker=true) -> - snapshot = {range, @properties, @reversed, @tailed, @valid, @invalidate, @exclusive} - snapshot.marker = this if includeMarker - Object.freeze(snapshot) - - toString: -> - "[Marker #{@id}, #{@getRange()}]" - - ### - Section: Private - ### - - inspect: -> - @toString() - - emitChangeEvent: (currentRange, textChanged, propertiesChanged) -> - return unless @hasChangeObservers - oldState = @previousEventState - - currentRange ?= @getRange() - - return false unless propertiesChanged or - oldState.valid isnt @valid or - oldState.tailed isnt @tailed or - oldState.reversed isnt @reversed or - oldState.range.compare(currentRange) isnt 0 - - newState = @previousEventState = @getSnapshot(currentRange) - - if oldState.reversed - oldHeadPosition = oldState.range.start - oldTailPosition = oldState.range.end - else - oldHeadPosition = oldState.range.end - oldTailPosition = oldState.range.start - - if newState.reversed - newHeadPosition = newState.range.start - newTailPosition = newState.range.end - else - newHeadPosition = newState.range.end - newTailPosition = newState.range.start - - @emitter.emit("did-change", { - wasValid: oldState.valid, isValid: newState.valid - hadTail: oldState.tailed, hasTail: newState.tailed - oldProperties: oldState.properties, newProperties: newState.properties - oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition - textChanged - }) - true +(Marker = (function() { + Marker = class Marker { + static initClass() { + Delegator.includeInto(this); + + this.delegatesMethods('containsPoint', 'containsRange', 'intersectsRow', {toMethod: 'getRange'}); + } + + static extractParams(inputParams) { + const outputParams = {}; + let containsCustomProperties = false; + if (inputParams != null) { + for (var key of Array.from(Object.keys(inputParams))) { + if (OptionKeys.has(key)) { + outputParams[key] = inputParams[key]; + } else if ((key === 'clipDirection') || (key === 'skipSoftWrapIndentation')) { + // TODO: Ignore these two keys for now. Eventually, when the + // deprecation below will be gone, we can remove this conditional as + // well, and just return standard marker properties. + } else { + containsCustomProperties = true; + if (outputParams.properties == null) { outputParams.properties = {}; } + outputParams.properties[key] = inputParams[key]; + } + } + } + + // TODO: Remove both this deprecation and the conditional above on the + // release after the one where we'll ship `DisplayLayer`. + if (containsCustomProperties) { + Grim.deprecate(`\ +Assigning custom properties to a marker when creating/copying it is +deprecated. Please, consider storing the custom properties you need in +some other object in your package, keyed by the marker's id property.\ +`); + } + + return outputParams; + } + + constructor(id, layer, range, params, exclusivitySet) { + this.id = id; + this.layer = layer; + if (exclusivitySet == null) { exclusivitySet = false; } + ({tailed: this.tailed, reversed: this.reversed, valid: this.valid, invalidate: this.invalidate, exclusive: this.exclusive, properties: this.properties} = params); + this.emitter = new Emitter; + if (this.tailed == null) { this.tailed = true; } + if (this.reversed == null) { this.reversed = false; } + if (this.valid == null) { this.valid = true; } + if (this.invalidate == null) { this.invalidate = 'overlap'; } + if (this.properties == null) { this.properties = {}; } + this.hasChangeObservers = false; + Object.freeze(this.properties); + if (!exclusivitySet) { this.layer.setMarkerIsExclusive(this.id, this.isExclusive()); } + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the marker is destroyed. + // + // * `callback` {Function} to be called when the marker is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy(callback) { + this.layer.markersWithDestroyListeners.add(this); + return this.emitter.on('did-destroy', callback); + } + + // Public: Invoke the given callback when the state of the marker changes. + // + // * `callback` {Function} to be called when the marker changes. + // * `event` {Object} with the following keys: + // * `oldHeadPosition` {Point} representing the former head position + // * `newHeadPosition` {Point} representing the new head position + // * `oldTailPosition` {Point} representing the former tail position + // * `newTailPosition` {Point} representing the new tail position + // * `wasValid` {Boolean} indicating whether the marker was valid before the change + // * `isValid` {Boolean} indicating whether the marker is now valid + // * `hadTail` {Boolean} indicating whether the marker had a tail before the change + // * `hasTail` {Boolean} indicating whether the marker now has a tail + // * `oldProperties` {Object} containing the marker's custom properties before the change. + // * `newProperties` {Object} containing the marker's custom properties after the change. + // * `textChanged` {Boolean} indicating whether this change was caused by a textual change + // to the buffer or whether the marker was manipulated directly via its public API. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange(callback) { + if (!this.hasChangeObservers) { + this.previousEventState = this.getSnapshot(this.getRange()); + this.hasChangeObservers = true; + this.layer.markersWithChangeListeners.add(this); + } + return this.emitter.on('did-change', callback); + } + + // Public: Returns the current {Range} of the marker. The range is immutable. + getRange() { return this.layer.getMarkerRange(this.id); } + + // Public: Sets the range of the marker. + // + // * `range` A {Range} or range-compatible {Array}. The range will be clipped + // before it is assigned. + // * `params` (optional) An {Object} with the following keys: + // * `reversed` {Boolean} indicating the marker will to be in a reversed + // orientation. + // * `exclusive` {Boolean} indicating that changes occurring at either end of + // the marker will be considered *outside* the marker rather than inside. + // This defaults to `false` unless the marker's invalidation strategy is + // `inside` or the marker has no tail, in which case it defaults to `true`. + setRange(range, params) { + if (params == null) { params = {}; } + return this.update(this.getRange(), {reversed: params.reversed, tailed: true, range: Range.fromObject(range, true), exclusive: params.exclusive}); + } + + // Public: Returns a {Point} representing the marker's current head position. + getHeadPosition() { + if (this.reversed) { + return this.getStartPosition(); + } else { + return this.getEndPosition(); + } + } + + // Public: Sets the head position of the marker. + // + // * `position` A {Point} or point-compatible {Array}. The position will be + // clipped before it is assigned. + setHeadPosition(position) { + position = Point.fromObject(position); + const oldRange = this.getRange(); + const params = {}; + + if (this.hasTail()) { + if (this.isReversed()) { + if (position.isLessThan(oldRange.end)) { + params.range = new Range(position, oldRange.end); + } else { + params.reversed = false; + params.range = new Range(oldRange.end, position); + } + } else { + if (position.isLessThan(oldRange.start)) { + params.reversed = true; + params.range = new Range(position, oldRange.start); + } else { + params.range = new Range(oldRange.start, position); + } + } + } else { + params.range = new Range(position, position); + } + return this.update(oldRange, params); + } + + // Public: Returns a {Point} representing the marker's current tail position. + // If the marker has no tail, the head position will be returned instead. + getTailPosition() { + if (this.reversed) { + return this.getEndPosition(); + } else { + return this.getStartPosition(); + } + } + + // Public: Sets the tail position of the marker. If the marker doesn't have a + // tail, it will after calling this method. + // + // * `position` A {Point} or point-compatible {Array}. The position will be + // clipped before it is assigned. + setTailPosition(position) { + position = Point.fromObject(position); + const oldRange = this.getRange(); + const params = {tailed: true}; + + if (this.reversed) { + if (position.isLessThan(oldRange.start)) { + params.reversed = false; + params.range = new Range(position, oldRange.start); + } else { + params.range = new Range(oldRange.start, position); + } + } else { + if (position.isLessThan(oldRange.end)) { + params.range = new Range(position, oldRange.end); + } else { + params.reversed = true; + params.range = new Range(oldRange.end, position); + } + } + + return this.update(oldRange, params); + } + + // Public: Returns a {Point} representing the start position of the marker, + // which could be the head or tail position, depending on its orientation. + getStartPosition() { return this.layer.getMarkerStartPosition(this.id); } + + // Public: Returns a {Point} representing the end position of the marker, + // which could be the head or tail position, depending on its orientation. + getEndPosition() { return this.layer.getMarkerEndPosition(this.id); } + + // Public: Removes the marker's tail. After calling the marker's head position + // will be reported as its current tail position until the tail is planted + // again. + clearTail() { + const headPosition = this.getHeadPosition(); + return this.update(this.getRange(), {tailed: false, reversed: false, range: Range(headPosition, headPosition)}); + } + + // Public: Plants the marker's tail at the current head position. After calling + // the marker's tail position will be its head position at the time of the + // call, regardless of where the marker's head is moved. + plantTail() { + if (!this.hasTail()) { + const headPosition = this.getHeadPosition(); + return this.update(this.getRange(), {tailed: true, range: new Range(headPosition, headPosition)}); + } + } + + // Public: Returns a {Boolean} indicating whether the head precedes the tail. + isReversed() { + return this.tailed && this.reversed; + } + + // Public: Returns a {Boolean} indicating whether the marker has a tail. + hasTail() { + return this.tailed; + } + + // Public: Is the marker valid? + // + // Returns a {Boolean}. + isValid() { + return !this.isDestroyed() && this.valid; + } + + // Public: Is the marker destroyed? + // + // Returns a {Boolean}. + isDestroyed() { + return !this.layer.hasMarker(this.id); + } + + // Public: Returns a {Boolean} indicating whether changes that occur exactly at + // the marker's head or tail cause it to move. + isExclusive() { + if (this.exclusive != null) { + return this.exclusive; + } else { + return (this.getInvalidationStrategy() === 'inside') || !this.hasTail(); + } + } + + // Public: Returns a {Boolean} indicating whether this marker is equivalent to + // another marker, meaning they have the same range and options. + // + // * `other` {Marker} other marker + isEqual(other) { + return (this.invalidate === other.invalidate) && + (this.tailed === other.tailed) && + (this.reversed === other.reversed) && + (this.exclusive === other.exclusive) && + isEqual(this.properties, other.properties) && + this.getRange().isEqual(other.getRange()); + } + + // Public: Get the invalidation strategy for this marker. + // + // Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. + // + // Returns a {String}. + getInvalidationStrategy() { + return this.invalidate; + } + + // Public: Returns an {Object} containing any custom properties associated with + // the marker. + getProperties() { + return this.properties; + } + + // Public: Merges an {Object} containing new properties into the marker's + // existing properties. + // + // * `properties` {Object} + setProperties(properties) { + return this.update(this.getRange(), {properties: extend({}, this.properties, properties)}); + } + + // Public: Creates and returns a new {Marker} with the same properties as this + // marker. + // + // * `params` {Object} + copy(options) { + if (options == null) { options = {}; } + const snapshot = this.getSnapshot(); + options = Marker.extractParams(options); + return this.layer.createMarker(this.getRange(), extend( + {}, + snapshot, + options, + {properties: extend({}, snapshot.properties, options.properties)} + )); + } + + // Public: Destroys the marker, causing it to emit the 'destroyed' event. + destroy(suppressMarkerLayerUpdateEvents) { + if (this.isDestroyed()) { return; } + + if (this.trackDestruction) { + const error = new Error; + Error.captureStackTrace(error); + this.destroyStackTrace = error.stack; + } + + this.layer.destroyMarker(this, suppressMarkerLayerUpdateEvents); + this.emitter.emit('did-destroy'); + return this.emitter.clear(); + } + + // Public: Compares this marker to another based on their ranges. + // + // * `other` {Marker} + compare(other) { + return this.layer.compareMarkers(this.id, other.id); + } + + // Returns whether this marker matches the given parameters. The parameters + // are the same as {MarkerLayer::findMarkers}. + matchesParams(params) { + for (var key of Array.from(Object.keys(params))) { + if (!this.matchesParam(key, params[key])) { return false; } + } + return true; + } + + // Returns whether this marker matches the given parameter name and value. + // The parameters are the same as {MarkerLayer::findMarkers}. + matchesParam(key, value) { + switch (key) { + case 'startPosition': + return this.getStartPosition().isEqual(value); + case 'endPosition': + return this.getEndPosition().isEqual(value); + case 'containsPoint': case 'containsPosition': + return this.containsPoint(value); + case 'containsRange': + return this.containsRange(value); + case 'startRow': + return this.getStartPosition().row === value; + case 'endRow': + return this.getEndPosition().row === value; + case 'intersectsRow': + return this.intersectsRow(value); + case 'invalidate': case 'reversed': case 'tailed': + return isEqual(this[key], value); + case 'valid': + return this.isValid() === value; + default: + return isEqual(this.properties[key], value); + } + } + + update(oldRange, {range, reversed, tailed, valid, exclusive, properties}, textChanged, suppressMarkerLayerUpdateEvents) { + let propertiesChanged; + if (textChanged == null) { textChanged = false; } + if (suppressMarkerLayerUpdateEvents == null) { suppressMarkerLayerUpdateEvents = false; } + if (this.isDestroyed()) { return; } + + oldRange = Range.fromObject(oldRange); + if (range != null) { range = Range.fromObject(range); } + + const wasExclusive = this.isExclusive(); + let updated = (propertiesChanged = false); + + if ((range != null) && !range.isEqual(oldRange)) { + this.layer.setMarkerRange(this.id, range); + updated = true; + } + + if ((reversed != null) && (reversed !== this.reversed)) { + this.reversed = reversed; + updated = true; + } + + if ((tailed != null) && (tailed !== this.tailed)) { + this.tailed = tailed; + updated = true; + } + + if ((valid != null) && (valid !== this.valid)) { + this.valid = valid; + updated = true; + } + + if ((exclusive != null) && (exclusive !== this.exclusive)) { + this.exclusive = exclusive; + updated = true; + } + + if (wasExclusive !== this.isExclusive()) { + this.layer.setMarkerIsExclusive(this.id, this.isExclusive()); + updated = true; + } + + if ((properties != null) && !isEqual(properties, this.properties)) { + this.properties = Object.freeze(properties); + propertiesChanged = true; + updated = true; + } + + this.emitChangeEvent(range != null ? range : oldRange, textChanged, propertiesChanged); + if (updated && !suppressMarkerLayerUpdateEvents) { this.layer.markerUpdated(); } + return updated; + } + + getSnapshot(range, includeMarker) { + if (includeMarker == null) { includeMarker = true; } + const snapshot = {range, properties: this.properties, reversed: this.reversed, tailed: this.tailed, valid: this.valid, invalidate: this.invalidate, exclusive: this.exclusive}; + if (includeMarker) { snapshot.marker = this; } + return Object.freeze(snapshot); + } + + toString() { + return `[Marker ${this.id}, ${this.getRange()}]`; + } + + /* + Section: Private + */ + + inspect() { + return this.toString(); + } + + emitChangeEvent(currentRange, textChanged, propertiesChanged) { + let newHeadPosition, newTailPosition, oldHeadPosition, oldTailPosition; + if (!this.hasChangeObservers) { return; } + const oldState = this.previousEventState; + + if (currentRange == null) { currentRange = this.getRange(); } + + if (!propertiesChanged && + (oldState.valid === this.valid) && + (oldState.tailed === this.tailed) && + (oldState.reversed === this.reversed) && + (oldState.range.compare(currentRange) === 0)) { return false; } + + const newState = (this.previousEventState = this.getSnapshot(currentRange)); + + if (oldState.reversed) { + oldHeadPosition = oldState.range.start; + oldTailPosition = oldState.range.end; + } else { + oldHeadPosition = oldState.range.end; + oldTailPosition = oldState.range.start; + } + + if (newState.reversed) { + newHeadPosition = newState.range.start; + newTailPosition = newState.range.end; + } else { + newHeadPosition = newState.range.end; + newTailPosition = newState.range.start; + } + + this.emitter.emit("did-change", { + wasValid: oldState.valid, isValid: newState.valid, + hadTail: oldState.tailed, hasTail: newState.tailed, + oldProperties: oldState.properties, newProperties: newState.properties, + oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, + textChanged + }); + return true; + } + }; + Marker.initClass(); + return Marker; +})()); diff --git a/src/point-helpers.js b/src/point-helpers.js index 3f2a86949b..980756ea7b 100644 --- a/src/point-helpers.js +++ b/src/point-helpers.js @@ -1,62 +1,87 @@ -Point = require './point' - -exports.compare = (a, b) -> - if a.row is b.row - compareNumbers(a.column, b.column) - else - compareNumbers(a.row, b.row) - -compareNumbers = (a, b) -> - if a < b - -1 - else if a > b - 1 - else - 0 - -exports.isEqual = (a, b) -> - a.row is b.row and a.column is b.column - -exports.traverse = (start, distance) -> - if distance.row is 0 - Point(start.row, start.column + distance.column) - else - Point(start.row + distance.row, distance.column) - -exports.traversal = (end, start) -> - if end.row is start.row - Point(0, end.column - start.column) - else - Point(end.row - start.row, end.column) - -NEWLINE_REG_EXP = /\n/g - -exports.characterIndexForPoint = (text, point) -> - row = point.row - column = point.column - NEWLINE_REG_EXP.lastIndex = 0 - while row-- > 0 - unless NEWLINE_REG_EXP.exec(text) - return text.length - - NEWLINE_REG_EXP.lastIndex + column - -exports.clipNegativePoint = (point) -> - if point.row < 0 - Point(0, 0) - else if point.column < 0 - Point(point.row, 0) - else - point - -exports.max = (a, b) -> - if exports.compare(a, b) >= 0 - a - else - b - -exports.min = (a, b) -> - if exports.compare(a, b) <= 0 - a - else - b +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Point = require('./point'); + +exports.compare = function(a, b) { + if (a.row === b.row) { + return compareNumbers(a.column, b.column); + } else { + return compareNumbers(a.row, b.row); + } +}; + +var compareNumbers = function(a, b) { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } +}; + +exports.isEqual = (a, b) => (a.row === b.row) && (a.column === b.column); + +exports.traverse = function(start, distance) { + if (distance.row === 0) { + return Point(start.row, start.column + distance.column); + } else { + return Point(start.row + distance.row, distance.column); + } +}; + +exports.traversal = function(end, start) { + if (end.row === start.row) { + return Point(0, end.column - start.column); + } else { + return Point(end.row - start.row, end.column); + } +}; + +const NEWLINE_REG_EXP = /\n/g; + +exports.characterIndexForPoint = function(text, point) { + let { + row + } = point; + const { + column + } = point; + NEWLINE_REG_EXP.lastIndex = 0; + while (row-- > 0) { + if (!NEWLINE_REG_EXP.exec(text)) { + return text.length; + } + } + + return NEWLINE_REG_EXP.lastIndex + column; +}; + +exports.clipNegativePoint = function(point) { + if (point.row < 0) { + return Point(0, 0); + } else if (point.column < 0) { + return Point(point.row, 0); + } else { + return point; + } +}; + +exports.max = function(a, b) { + if (exports.compare(a, b) >= 0) { + return a; + } else { + return b; + } +}; + +exports.min = function(a, b) { + if (exports.compare(a, b) <= 0) { + return a; + } else { + return b; + } +}; diff --git a/src/range.js b/src/range.js index 10bc381e3d..b861e15ccb 100644 --- a/src/range.js +++ b/src/range.js @@ -1,318 +1,390 @@ -Point = require './point' -newlineRegex = null - -# Public: Represents a region in a buffer in row/column coordinates. -# -# Every public method that takes a range also accepts a *range-compatible* -# {Array}. This means a 2-element array containing {Point}s or point-compatible -# arrays. So the following are equivalent: -# -# ## Examples -# -# ```coffee -# new Range(new Point(0, 1), new Point(2, 3)) -# new Range([0, 1], [2, 3]) -# [[0, 1], [2, 3]] # Range compatible array -# ``` +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +let Range; +const Point = require('./point'); +let newlineRegex = null; + +// Public: Represents a region in a buffer in row/column coordinates. +// +// Every public method that takes a range also accepts a *range-compatible* +// {Array}. This means a 2-element array containing {Point}s or point-compatible +// arrays. So the following are equivalent: +// +// ## Examples +// +// ```coffee +// new Range(new Point(0, 1), new Point(2, 3)) +// new Range([0, 1], [2, 3]) +// [[0, 1], [2, 3]] # Range compatible array +// ``` module.exports = -class Range - ### - Section: Properties - ### - - # Public: A {Point} representing the start of the {Range}. - start: null - - # Public: A {Point} representing the end of the {Range}. - end: null - - ### - Section: Construction - ### - - # Public: Convert any range-compatible object to a {Range}. - # - # * `object` This can be an object that's already a {Range}, in which case it's - # simply returned, or an array containing two {Point}s or point-compatible - # arrays. - # * `copy` An optional boolean indicating whether to force the copying of objects - # that are already ranges. - # - # Returns: A {Range} based on the given object. - @fromObject: (object, copy) -> - if Array.isArray(object) - new this(object[0], object[1]) - else if object instanceof this - if copy then object.copy() else object - else - new this(object.start, object.end) - - # Returns a range based on an optional starting point and the given text. If - # no starting point is given it will be assumed to be [0, 0]. - # - # * `startPoint` (optional) {Point} where the range should start. - # * `text` A {String} after which the range should end. The range will have as many - # rows as the text has lines have an end column based on the length of the - # last line. - # - # Returns: A {Range} - @fromText: (args...) -> - newlineRegex ?= require('./helpers').newlineRegex - - if args.length > 1 - startPoint = Point.fromObject(args.shift()) - else - startPoint = new Point(0, 0) - text = args.shift() - endPoint = startPoint.copy() - lines = text.split(newlineRegex) - if lines.length > 1 - lastIndex = lines.length - 1 - endPoint.row += lastIndex - endPoint.column = lines[lastIndex].length - else - endPoint.column += lines[0].length - new this(startPoint, endPoint) - - # Returns a {Range} that starts at the given point and ends at the - # start point plus the given row and column deltas. - # - # * `startPoint` A {Point} or point-compatible {Array} - # * `rowDelta` A {Number} indicating how many rows to add to the start point - # to get the end point. - # * `columnDelta` A {Number} indicating how many rows to columns to the start - # point to get the end point. - @fromPointWithDelta: (startPoint, rowDelta, columnDelta) -> - startPoint = Point.fromObject(startPoint) - endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta) - new this(startPoint, endPoint) - - @fromPointWithTraversalExtent: (startPoint, extent) -> - startPoint = Point.fromObject(startPoint) - new this(startPoint, startPoint.traverse(extent)) - - - ### - Section: Serialization and Deserialization - ### - - # Public: Call this with the result of {Range::serialize} to construct a new Range. - # - # * `array` {Array} of params to pass to the {::constructor} - @deserialize: (array) -> - if Array.isArray(array) - new this(array[0], array[1]) - else - new this() - - ### - Section: Construction - ### - - # Public: Construct a {Range} object - # - # * `pointA` {Point} or Point compatible {Array} (default: [0,0]) - # * `pointB` {Point} or Point compatible {Array} (default: [0,0]) - constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) -> - unless this instanceof Range - return new Range(pointA, pointB) - - pointA = Point.fromObject(pointA) - pointB = Point.fromObject(pointB) - - if pointA.isLessThanOrEqual(pointB) - @start = pointA - @end = pointB - else - @start = pointB - @end = pointA - - # Public: Returns a new range with the same start and end positions. - copy: -> - new @constructor(@start.copy(), @end.copy()) - - # Public: Returns a new range with the start and end positions negated. - negate: -> - new @constructor(@start.negate(), @end.negate()) - - ### - Section: Serialization and Deserialization - ### - - # Public: Returns a plain javascript object representation of the range. - serialize: -> - [@start.serialize(), @end.serialize()] - - ### - Section: Range Details - ### - - # Public: Is the start position of this range equal to the end position? - # - # Returns a {Boolean}. - isEmpty: -> - @start.isEqual(@end) - - # Public: Returns a {Boolean} indicating whether this range starts and ends on - # the same row. - isSingleLine: -> - @start.row is @end.row - - # Public: Get the number of rows in this range. - # - # Returns a {Number}. - getRowCount: -> - @end.row - @start.row + 1 - - # Public: Returns an array of all rows in the range. - getRows: -> - [@start.row..@end.row] - - ### - Section: Operations - ### - - # Public: Freezes the range and its start and end point so it becomes - # immutable and returns itself. - # - # Returns an immutable version of this {Range} - freeze: -> - @start.freeze() - @end.freeze() - Object.freeze(this) - - # Public: Returns a new range that contains this range and the given range. - # - # * `otherRange` A {Range} or range-compatible {Array} - union: (otherRange) -> - start = if @start.isLessThan(otherRange.start) then @start else otherRange.start - end = if @end.isGreaterThan(otherRange.end) then @end else otherRange.end - new @constructor(start, end) - - # Public: Build and return a new range by translating this range's start and - # end points by the given delta(s). - # - # * `startDelta` A {Point} by which to translate the start of this range. - # * `endDelta` (optional) A {Point} to by which to translate the end of this - # range. If omitted, the `startDelta` will be used instead. - # - # Returns a {Range}. - translate: (startDelta, endDelta=startDelta) -> - new @constructor(@start.translate(startDelta), @end.translate(endDelta)) - - # Public: Build and return a new range by traversing this range's start and - # end points by the given delta. - # - # See {Point::traverse} for details of how traversal differs from translation. - # - # * `delta` A {Point} containing the rows and columns to traverse to derive - # the new range. - # - # Returns a {Range}. - traverse: (delta) -> - new @constructor(@start.traverse(delta), @end.traverse(delta)) - - ### - Section: Comparison - ### - - # Public: Compare two Ranges - # - # * `otherRange` A {Range} or range-compatible {Array}. - # - # Returns `-1` if this range starts before the argument or contains it. - # Returns `0` if this range is equivalent to the argument. - # Returns `1` if this range starts after the argument or is contained by it. - compare: (other) -> - other = @constructor.fromObject(other) - if value = @start.compare(other.start) - value - else - other.end.compare(@end) - - # Public: Returns a {Boolean} indicating whether this range has the same start - # and end points as the given {Range} or range-compatible {Array}. - # - # * `otherRange` A {Range} or range-compatible {Array}. - isEqual: (other) -> - return false unless other? - other = @constructor.fromObject(other) - other.start.isEqual(@start) and other.end.isEqual(@end) - - # Public: Returns a {Boolean} indicating whether this range starts and ends on - # the same row as the argument. - # - # * `otherRange` A {Range} or range-compatible {Array}. - coversSameRows: (other) -> - @start.row is other.start.row and @end.row is other.end.row - - # Public: Determines whether this range intersects with the argument. - # - # * `otherRange` A {Range} or range-compatible {Array} - # * `exclusive` (optional) {Boolean} indicating whether to exclude endpoints - # when testing for intersection. Defaults to `false`. - # - # Returns a {Boolean}. - intersectsWith: (otherRange, exclusive) -> - if exclusive - not (@end.isLessThanOrEqual(otherRange.start) or @start.isGreaterThanOrEqual(otherRange.end)) - else - not (@end.isLessThan(otherRange.start) or @start.isGreaterThan(otherRange.end)) - - # Public: Returns a {Boolean} indicating whether this range contains the given - # range. - # - # * `otherRange` A {Range} or range-compatible {Array} - # * `exclusive` (optional) {Boolean} including that the containment should be exclusive of - # endpoints. Defaults to false. - containsRange: (otherRange, exclusive) -> - {start, end} = @constructor.fromObject(otherRange) - @containsPoint(start, exclusive) and @containsPoint(end, exclusive) - - # Public: Returns a {Boolean} indicating whether this range contains the given - # point. - # - # * `point` A {Point} or point-compatible {Array} - # * `exclusive` (optional) {Boolean} including that the containment should be exclusive of - # endpoints. Defaults to false. - containsPoint: (point, exclusive) -> - point = Point.fromObject(point) - if exclusive - point.isGreaterThan(@start) and point.isLessThan(@end) - else - point.isGreaterThanOrEqual(@start) and point.isLessThanOrEqual(@end) - - # Public: Returns a {Boolean} indicating whether this range intersects the - # given row {Number}. - # - # * `row` Row {Number} - intersectsRow: (row) -> - @start.row <= row <= @end.row - - # Public: Returns a {Boolean} indicating whether this range intersects the - # row range indicated by the given startRow and endRow {Number}s. - # - # * `startRow` {Number} start row - # * `endRow` {Number} end row - intersectsRowRange: (startRow, endRow) -> - [startRow, endRow] = [endRow, startRow] if startRow > endRow - @end.row >= startRow and endRow >= @start.row - - getExtent: -> - @end.traversalFrom(@start) - - ### - Section: Conversion - ### - - toDelta: -> - rows = @end.row - @start.row - if rows is 0 - columns = @end.column - @start.column - else - columns = @end.column - new Point(rows, columns) - - # Public: Returns a string representation of the range. - toString: -> - "[#{@start} - #{@end}]" +(Range = (function() { + Range = class Range { + static initClass() { + /* + Section: Properties + */ + + // Public: A {Point} representing the start of the {Range}. + this.prototype.start = null; + + // Public: A {Point} representing the end of the {Range}. + this.prototype.end = null; + } + + /* + Section: Construction + */ + + // Public: Convert any range-compatible object to a {Range}. + // + // * `object` This can be an object that's already a {Range}, in which case it's + // simply returned, or an array containing two {Point}s or point-compatible + // arrays. + // * `copy` An optional boolean indicating whether to force the copying of objects + // that are already ranges. + // + // Returns: A {Range} based on the given object. + static fromObject(object, copy) { + if (Array.isArray(object)) { + return new (this)(object[0], object[1]); + } else if (object instanceof this) { + if (copy) { return object.copy(); } else { return object; } + } else { + return new (this)(object.start, object.end); + } + } + + // Returns a range based on an optional starting point and the given text. If + // no starting point is given it will be assumed to be [0, 0]. + // + // * `startPoint` (optional) {Point} where the range should start. + // * `text` A {String} after which the range should end. The range will have as many + // rows as the text has lines have an end column based on the length of the + // last line. + // + // Returns: A {Range} + static fromText(...args) { + let startPoint; + if (newlineRegex == null) { ({ + newlineRegex + } = require('./helpers')); } + + if (args.length > 1) { + startPoint = Point.fromObject(args.shift()); + } else { + startPoint = new Point(0, 0); + } + const text = args.shift(); + const endPoint = startPoint.copy(); + const lines = text.split(newlineRegex); + if (lines.length > 1) { + const lastIndex = lines.length - 1; + endPoint.row += lastIndex; + endPoint.column = lines[lastIndex].length; + } else { + endPoint.column += lines[0].length; + } + return new (this)(startPoint, endPoint); + } + + // Returns a {Range} that starts at the given point and ends at the + // start point plus the given row and column deltas. + // + // * `startPoint` A {Point} or point-compatible {Array} + // * `rowDelta` A {Number} indicating how many rows to add to the start point + // to get the end point. + // * `columnDelta` A {Number} indicating how many rows to columns to the start + // point to get the end point. + static fromPointWithDelta(startPoint, rowDelta, columnDelta) { + startPoint = Point.fromObject(startPoint); + const endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta); + return new (this)(startPoint, endPoint); + } + + static fromPointWithTraversalExtent(startPoint, extent) { + startPoint = Point.fromObject(startPoint); + return new (this)(startPoint, startPoint.traverse(extent)); + } + + + /* + Section: Serialization and Deserialization + */ + + // Public: Call this with the result of {Range::serialize} to construct a new Range. + // + // * `array` {Array} of params to pass to the {::constructor} + static deserialize(array) { + if (Array.isArray(array)) { + return new (this)(array[0], array[1]); + } else { + return new (this)(); + } + } + + /* + Section: Construction + */ + + // Public: Construct a {Range} object + // + // * `pointA` {Point} or Point compatible {Array} (default: [0,0]) + // * `pointB` {Point} or Point compatible {Array} (default: [0,0]) + constructor(pointA, pointB) { + if (pointA == null) { pointA = new Point(0, 0); } + if (pointB == null) { pointB = new Point(0, 0); } + if (!(this instanceof Range)) { + return new Range(pointA, pointB); + } + + pointA = Point.fromObject(pointA); + pointB = Point.fromObject(pointB); + + if (pointA.isLessThanOrEqual(pointB)) { + this.start = pointA; + this.end = pointB; + } else { + this.start = pointB; + this.end = pointA; + } + } + + // Public: Returns a new range with the same start and end positions. + copy() { + return new this.constructor(this.start.copy(), this.end.copy()); + } + + // Public: Returns a new range with the start and end positions negated. + negate() { + return new this.constructor(this.start.negate(), this.end.negate()); + } + + /* + Section: Serialization and Deserialization + */ + + // Public: Returns a plain javascript object representation of the range. + serialize() { + return [this.start.serialize(), this.end.serialize()]; + } + + /* + Section: Range Details + */ + + // Public: Is the start position of this range equal to the end position? + // + // Returns a {Boolean}. + isEmpty() { + return this.start.isEqual(this.end); + } + + // Public: Returns a {Boolean} indicating whether this range starts and ends on + // the same row. + isSingleLine() { + return this.start.row === this.end.row; + } + + // Public: Get the number of rows in this range. + // + // Returns a {Number}. + getRowCount() { + return (this.end.row - this.start.row) + 1; + } + + // Public: Returns an array of all rows in the range. + getRows() { + return __range__(this.start.row, this.end.row, true); + } + + /* + Section: Operations + */ + + // Public: Freezes the range and its start and end point so it becomes + // immutable and returns itself. + // + // Returns an immutable version of this {Range} + freeze() { + this.start.freeze(); + this.end.freeze(); + return Object.freeze(this); + } + + // Public: Returns a new range that contains this range and the given range. + // + // * `otherRange` A {Range} or range-compatible {Array} + union(otherRange) { + const start = this.start.isLessThan(otherRange.start) ? this.start : otherRange.start; + const end = this.end.isGreaterThan(otherRange.end) ? this.end : otherRange.end; + return new this.constructor(start, end); + } + + // Public: Build and return a new range by translating this range's start and + // end points by the given delta(s). + // + // * `startDelta` A {Point} by which to translate the start of this range. + // * `endDelta` (optional) A {Point} to by which to translate the end of this + // range. If omitted, the `startDelta` will be used instead. + // + // Returns a {Range}. + translate(startDelta, endDelta) { + if (endDelta == null) { endDelta = startDelta; } + return new this.constructor(this.start.translate(startDelta), this.end.translate(endDelta)); + } + + // Public: Build and return a new range by traversing this range's start and + // end points by the given delta. + // + // See {Point::traverse} for details of how traversal differs from translation. + // + // * `delta` A {Point} containing the rows and columns to traverse to derive + // the new range. + // + // Returns a {Range}. + traverse(delta) { + return new this.constructor(this.start.traverse(delta), this.end.traverse(delta)); + } + + /* + Section: Comparison + */ + + // Public: Compare two Ranges + // + // * `otherRange` A {Range} or range-compatible {Array}. + // + // Returns `-1` if this range starts before the argument or contains it. + // Returns `0` if this range is equivalent to the argument. + // Returns `1` if this range starts after the argument or is contained by it. + compare(other) { + let value; + other = this.constructor.fromObject(other); + if ((value = this.start.compare(other.start))) { + return value; + } else { + return other.end.compare(this.end); + } + } + + // Public: Returns a {Boolean} indicating whether this range has the same start + // and end points as the given {Range} or range-compatible {Array}. + // + // * `otherRange` A {Range} or range-compatible {Array}. + isEqual(other) { + if (other == null) { return false; } + other = this.constructor.fromObject(other); + return other.start.isEqual(this.start) && other.end.isEqual(this.end); + } + + // Public: Returns a {Boolean} indicating whether this range starts and ends on + // the same row as the argument. + // + // * `otherRange` A {Range} or range-compatible {Array}. + coversSameRows(other) { + return (this.start.row === other.start.row) && (this.end.row === other.end.row); + } + + // Public: Determines whether this range intersects with the argument. + // + // * `otherRange` A {Range} or range-compatible {Array} + // * `exclusive` (optional) {Boolean} indicating whether to exclude endpoints + // when testing for intersection. Defaults to `false`. + // + // Returns a {Boolean}. + intersectsWith(otherRange, exclusive) { + if (exclusive) { + return !(this.end.isLessThanOrEqual(otherRange.start) || this.start.isGreaterThanOrEqual(otherRange.end)); + } else { + return !(this.end.isLessThan(otherRange.start) || this.start.isGreaterThan(otherRange.end)); + } + } + + // Public: Returns a {Boolean} indicating whether this range contains the given + // range. + // + // * `otherRange` A {Range} or range-compatible {Array} + // * `exclusive` (optional) {Boolean} including that the containment should be exclusive of + // endpoints. Defaults to false. + containsRange(otherRange, exclusive) { + const {start, end} = this.constructor.fromObject(otherRange); + return this.containsPoint(start, exclusive) && this.containsPoint(end, exclusive); + } + + // Public: Returns a {Boolean} indicating whether this range contains the given + // point. + // + // * `point` A {Point} or point-compatible {Array} + // * `exclusive` (optional) {Boolean} including that the containment should be exclusive of + // endpoints. Defaults to false. + containsPoint(point, exclusive) { + point = Point.fromObject(point); + if (exclusive) { + return point.isGreaterThan(this.start) && point.isLessThan(this.end); + } else { + return point.isGreaterThanOrEqual(this.start) && point.isLessThanOrEqual(this.end); + } + } + + // Public: Returns a {Boolean} indicating whether this range intersects the + // given row {Number}. + // + // * `row` Row {Number} + intersectsRow(row) { + return this.start.row <= row && row <= this.end.row; + } + + // Public: Returns a {Boolean} indicating whether this range intersects the + // row range indicated by the given startRow and endRow {Number}s. + // + // * `startRow` {Number} start row + // * `endRow` {Number} end row + intersectsRowRange(startRow, endRow) { + if (startRow > endRow) { [startRow, endRow] = Array.from([endRow, startRow]); } + return (this.end.row >= startRow) && (endRow >= this.start.row); + } + + getExtent() { + return this.end.traversalFrom(this.start); + } + + /* + Section: Conversion + */ + + toDelta() { + let columns; + const rows = this.end.row - this.start.row; + if (rows === 0) { + columns = this.end.column - this.start.column; + } else { + columns = this.end.column; + } + return new Point(rows, columns); + } + + // Public: Returns a string representation of the range. + toString() { + return `[${this.start} - ${this.end}]`; + } + }; + Range.initClass(); + return Range; +})()); + +function __range__(left, right, inclusive) { + let range = []; + let ascending = left < right; + let end = !inclusive ? right : ascending ? right + 1 : right - 1; + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i); + } + return range; +} \ No newline at end of file diff --git a/src/set-helpers.js b/src/set-helpers.js index e2f134f724..e8e059b9f8 100644 --- a/src/set-helpers.js +++ b/src/set-helpers.js @@ -1,20 +1,28 @@ -setEqual = (a, b) -> - return false unless a.size is b.size - iterator = a.values() - until (next = iterator.next()).done - return false unless b.has(next.value) - true +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const setEqual = function(a, b) { + let next; + if (a.size !== b.size) { return false; } + const iterator = a.values(); + while (!(next = iterator.next()).done) { + if (!b.has(next.value)) { return false; } + } + return true; +}; -subtractSet = (set, valuesToRemove) -> - if set.size > valuesToRemove.size - valuesToRemove.forEach (value) -> set.delete(value) - else - set.forEach (value) -> set.delete(value) if valuesToRemove.has(value) +const subtractSet = function(set, valuesToRemove) { + if (set.size > valuesToRemove.size) { + return valuesToRemove.forEach(value => set.delete(value)); + } else { + return set.forEach(function(value) { if (valuesToRemove.has(value)) { return set.delete(value); } }); + } +}; -addSet = (set, valuesToAdd) -> - valuesToAdd.forEach (value) -> set.add(value) +const addSet = (set, valuesToAdd) => valuesToAdd.forEach(value => set.add(value)); -intersectSet = (set, other) -> - set.forEach (value) -> set.delete(value) unless other.has(value) +const intersectSet = (set, other) => set.forEach(function(value) { if (!other.has(value)) { return set.delete(value); } }); -module.exports = {setEqual, subtractSet, addSet, intersectSet} +module.exports = {setEqual, subtractSet, addSet, intersectSet}; From 8b8ac8dd439cf565bbe670b8b833981d58abec7b Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Thu, 9 Mar 2023 13:17:04 -0300 Subject: [PATCH 30/64] decaffeinate: Run post-processing cleanups on is-character-pair.coffee and 4 other files --- src/is-character-pair.js | 2 ++ src/marker.js | 2 ++ src/point-helpers.js | 2 ++ src/range.js | 2 ++ src/set-helpers.js | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/is-character-pair.js b/src/is-character-pair.js index d066a2d208..3a8a18b8be 100644 --- a/src/is-character-pair.js +++ b/src/is-character-pair.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/src/marker.js b/src/marker.js index 866e2011ce..e8ed1d4a83 100644 --- a/src/marker.js +++ b/src/marker.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/src/point-helpers.js b/src/point-helpers.js index 980756ea7b..7ad5312d0a 100644 --- a/src/point-helpers.js +++ b/src/point-helpers.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns diff --git a/src/range.js b/src/range.js index b861e15ccb..2638141404 100644 --- a/src/range.js +++ b/src/range.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from diff --git a/src/set-helpers.js b/src/set-helpers.js index e8e059b9f8..f967167328 100644 --- a/src/set-helpers.js +++ b/src/set-helpers.js @@ -1,3 +1,5 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns From adf8f1a1a8691fc9542a97a276fee60a31279a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 15:44:07 -0300 Subject: [PATCH 31/64] Fixing semi-constructor stuff from Point and Range --- src/point.js | 25 +++++++++++++++++-------- src/range.js | 20 ++++++++++++++++---- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/point.js b/src/point.js index c3e6c8238c..e78ab64ed9 100644 --- a/src/point.js +++ b/src/point.js @@ -23,15 +23,15 @@ module.exports = /* Section: Properties */ - + // Public: A zero-indexed {Number} representing the row of the {Point}. this.prototype.row = null; - + // Public: A zero-indexed {Number} representing the column of the {Point}. this.prototype.column = null; - + this.ZERO = Object.freeze(new Point(0, 0)); - + this.INFINITY = Object.freeze(new Point(Infinity, Infinity)); } @@ -106,9 +106,6 @@ module.exports = // * `row` {Number} row // * `column` {Number} column constructor(row=0, column=0) { - if (!(this instanceof Point)) { - return new Point(row, column); - } this.row = row; this.column = column; } @@ -319,7 +316,19 @@ module.exports = } }; Point.initClass(); - return Point; + + function callableConstructor(c, f) { + function Point(row, col) { + if(new.target) { + return new c(row, col) + } + return f(row, col) + } + Point.prototype = c.prototype + Point.prototype.constructor = Point + return Point + } + return callableConstructor(Point, (row, col) => new Point(row, col)); })()); var isNumber = value => (typeof value === 'number') && (!Number.isNaN(value)); diff --git a/src/range.js b/src/range.js index 2638141404..d1d272a856 100644 --- a/src/range.js +++ b/src/range.js @@ -32,10 +32,10 @@ module.exports = /* Section: Properties */ - + // Public: A {Point} representing the start of the {Range}. this.prototype.start = null; - + // Public: A {Point} representing the end of the {Range}. this.prototype.end = null; } @@ -378,7 +378,19 @@ module.exports = } }; Range.initClass(); - return Range; + + function callableConstructor(c, f) { + function Range(a, b) { + if(new.target) { + return new c(a, b) + } + return f(a, b) + } + Range.prototype = c.prototype + Range.prototype.constructor = Range + return Range + } + return callableConstructor(Range, (a, b) => new Range(a, b)); })()); function __range__(left, right, inclusive) { @@ -389,4 +401,4 @@ function __range__(left, right, inclusive) { range.push(i); } return range; -} \ No newline at end of file +} From 355d9166e707b8f72b9bbfa912ebff7b9201ca87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 15:49:54 -0300 Subject: [PATCH 32/64] Adding the static functions --- src/point.js | 5 +++++ src/range.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/point.js b/src/point.js index e78ab64ed9..329514ba38 100644 --- a/src/point.js +++ b/src/point.js @@ -326,6 +326,11 @@ module.exports = } Point.prototype = c.prototype Point.prototype.constructor = Point + Point.fromObject = c.fromObject + Point.min = c.min + Point.max = c.max + Point.assertValid = c.assertValid + return Point } return callableConstructor(Point, (row, col) => new Point(row, col)); diff --git a/src/range.js b/src/range.js index d1d272a856..3ad977e09a 100644 --- a/src/range.js +++ b/src/range.js @@ -388,6 +388,11 @@ module.exports = } Range.prototype = c.prototype Range.prototype.constructor = Range + Range.fromObject = c.fromObject + Range.fromText = c.fromText + Range.fromPointWithDelta = c.fromPointWithDelta + Range.fromPointWithTraversalExtent = c.fromPointWithTraversalExtent + Range.deserialize = c.deserialize return Range } return callableConstructor(Range, (a, b) => new Range(a, b)); From 364538fb40cfb8f3b5c2ac5a717bdadd4fd34ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Thu, 9 Mar 2023 16:19:46 -0300 Subject: [PATCH 33/64] More fixes to translation --- src/point.js | 22 +++++----------------- src/range.js | 17 +++-------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/point.js b/src/point.js index 329514ba38..7b7d6583c3 100644 --- a/src/point.js +++ b/src/point.js @@ -19,22 +19,6 @@ let Point; module.exports = (Point = (function() { Point = class Point { - static initClass() { - /* - Section: Properties - */ - - // Public: A zero-indexed {Number} representing the row of the {Point}. - this.prototype.row = null; - - // Public: A zero-indexed {Number} representing the column of the {Point}. - this.prototype.column = null; - - this.ZERO = Object.freeze(new Point(0, 0)); - - this.INFINITY = Object.freeze(new Point(Infinity, Infinity)); - } - /* Section: Construction */ @@ -315,7 +299,7 @@ module.exports = return `(${this.row}, ${this.column})`; } }; - Point.initClass(); + // Point.initClass(); function callableConstructor(c, f) { function Point(row, col) { @@ -330,6 +314,10 @@ module.exports = Point.min = c.min Point.max = c.max Point.assertValid = c.assertValid + Point.prototype.row = null; + Point.prototype.column = null; + Point.ZERO = Object.freeze(new c(0, 0)); + Point.INFINITY = Object.freeze(new c(Infinity, Infinity)); return Point } diff --git a/src/range.js b/src/range.js index 3ad977e09a..2565ea6dd9 100644 --- a/src/range.js +++ b/src/range.js @@ -4,7 +4,6 @@ * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ @@ -28,18 +27,6 @@ let newlineRegex = null; module.exports = (Range = (function() { Range = class Range { - static initClass() { - /* - Section: Properties - */ - - // Public: A {Point} representing the start of the {Range}. - this.prototype.start = null; - - // Public: A {Point} representing the end of the {Range}. - this.prototype.end = null; - } - /* Section: Construction */ @@ -377,7 +364,6 @@ module.exports = return `[${this.start} - ${this.end}]`; } }; - Range.initClass(); function callableConstructor(c, f) { function Range(a, b) { @@ -393,6 +379,9 @@ module.exports = Range.fromPointWithDelta = c.fromPointWithDelta Range.fromPointWithTraversalExtent = c.fromPointWithTraversalExtent Range.deserialize = c.deserialize + Range.prototype.start = null; + Range.prototype.end = null; + return Range } return callableConstructor(Range, (a, b) => new Range(a, b)); From 12f7a42dac5dfcbf26259b53cfdf712cb1efcd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 13 Jun 2023 17:37:51 -0300 Subject: [PATCH 34/64] Upgrade pathwatcher --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8b03bf078..2b44c27e68 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#312d1fb", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f" + "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#e56a44c" }, "standard": { "env": { From 8d3e3cd5c177802f60a8556abc64337fa7de8c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 19 Sep 2023 17:38:40 -0300 Subject: [PATCH 35/64] Re-sync superstring --- package.json | 2 +- spec/helpers/test-language-mode.js | 3 +-- spec/text-buffer-io-spec.js | 3 +-- src/default-history-provider.js | 4 +--- src/display-layer.js | 4 +--- src/helpers.js | 3 +-- src/marker-layer.js | 4 +--- src/text-buffer.js | 9 +-------- 8 files changed, 8 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 2b44c27e68..fdf87dba4f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/pulsar-edit/superstring-wasm.git#312d1fb", + "superstring": "https://github.com/bongnv/superstring.git#d2a885bd2756ce73391dec3e3fb060fbe9597459", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#e56a44c" diff --git a/spec/helpers/test-language-mode.js b/spec/helpers/test-language-mode.js index a0cf464e9f..c229524054 100644 --- a/spec/helpers/test-language-mode.js +++ b/spec/helpers/test-language-mode.js @@ -1,5 +1,4 @@ -let MarkerIndex; -require('superstring').superstring.then(s => MarkerIndex = s.MarkerIndex) +const {MarkerIndex} = require('superstring') const {Emitter} = require('event-kit') const Point = require('../../src/point') const Range = require('../../src/range') diff --git a/spec/text-buffer-io-spec.js b/spec/text-buffer-io-spec.js index b5fa34ec86..8129692c4e 100644 --- a/spec/text-buffer-io-spec.js +++ b/spec/text-buffer-io-spec.js @@ -6,8 +6,7 @@ const {Disposable} = require('event-kit') const Point = require('../src/point') const Range = require('../src/range') const TextBuffer = require('../src/text-buffer') -let NativeTextBuffer; -require('superstring').superstring.then(s => NativeTextBuffer = s.TextBuffer); +const {TextBuffer: NativeTextBuffer} = require('superstring') const fsAdmin = require('fs-admin') const pathwatcher = require('pathwatcher') const winattr = require('winattr') diff --git a/src/default-history-provider.js b/src/default-history-provider.js index 65c9022344..adc4842e6b 100644 --- a/src/default-history-provider.js +++ b/src/default-history-provider.js @@ -7,8 +7,7 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ let DefaultHistoryProvider; -let Patch = null; -require('superstring').superstring.then(r => { return Patch = r.Patch; }); +const {Patch} = require('superstring') const MarkerLayer = require('./marker-layer'); const {traversal} = require('./point-helpers'); const {patchFromChanges} = require('./helpers'); @@ -588,4 +587,3 @@ var snapshotFromTransaction = function(transaction) { var transactionFromSnapshot = ({changes, markersBefore, markersAfter}) => // TODO: Return raw patch if there's no markersBefore && markersAfter new Transaction(markersBefore, patchFromChanges(changes), markersAfter); - diff --git a/src/display-layer.js b/src/display-layer.js index ed8d7097f8..87bc774f69 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -1,6 +1,4 @@ -let Patch; -const {superstring} = require('superstring') -superstring.then(r => Patch = r.Patch); +const {Patch} = require('superstring') const {Emitter} = require('event-kit') const Point = require('./point') const Range = require('./range') diff --git a/src/helpers.js b/src/helpers.js index 27dc9096e4..094685521d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,5 +1,4 @@ -let Patch; -require('superstring').superstring.then(r => Patch = r.Patch); +const {Patch} = require('superstring') const Range = require('./range') const {traversal} = require('./point-helpers') diff --git a/src/marker-layer.js b/src/marker-layer.js index 7f378a68f4..82c0e75bbc 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -3,9 +3,7 @@ const {Emitter} = require('event-kit'); const Point = require("./point"); const Range = require("./range"); const Marker = require("./marker"); -const {superstring} = require('superstring'); -let MarkerIndex = null; -superstring.then((r) => { return MarkerIndex = r.MarkerIndex; }); +const {MarkerIndex} = require("superstring") const {intersectSet} = require("./set-helpers"); const SerializationVersion = 2; diff --git a/src/text-buffer.js b/src/text-buffer.js index c268f7f549..dbf0861d3b 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -5,11 +5,7 @@ const _ = require('underscore-plus') const path = require('path') const crypto = require('crypto') const mkdirp = require('mkdirp') -const {superstring} = require('superstring'); -let NativeTextBuffer; -superstring.then(s => { - NativeTextBuffer = s.TextBuffer -}); +const {TextBuffer: NativeTextBuffer, Patch} = require('superstring'); const Point = require('./point') const Range = require('./range') const DefaultHistoryProvider = require('./default-history-provider') @@ -2489,9 +2485,6 @@ Object.assign(TextBuffer, { spliceArray: spliceArray }) -let Patch; -require('superstring').superstring.then(r => Patch = r.Patch); - Object.assign(TextBuffer.prototype, { stoppedChangingDelay: 300, fileChangeDelay: 200, From 8f0321e930cc88b974b1e66308631186532ef858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Tue, 19 Sep 2023 23:19:11 -0300 Subject: [PATCH 36/64] Forcing ID to be integer --- src/marker-layer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/marker-layer.js b/src/marker-layer.js index 82c0e75bbc..df0c0b8976 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -512,7 +512,7 @@ module.exports = class MarkerLayer { start = this.delegate.clipPosition(start); end = this.delegate.clipPosition(end); this.index.remove(id); - return this.index.insert(id, start, end); + return this.index.insert(parseInt(id), start, end); } setMarkerIsExclusive(id, exclusive) { @@ -542,7 +542,7 @@ module.exports = class MarkerLayer { range = Range.fromObject(range); Point.assertValid(range.start); Point.assertValid(range.end); - this.index.insert(id, range.start, range.end); + this.index.insert(parseInt(id), range.start, range.end); return this.markersById[id] = new Marker(id, this, range, params); } From 11be3a78a220bc94d69221d7fd701571ea049b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Szabo?= Date: Wed, 20 Sep 2023 21:37:43 -0300 Subject: [PATCH 37/64] Forcing ID to _be an int_ --- src/marker-layer.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/marker-layer.js b/src/marker-layer.js index df0c0b8976..f9a3fed652 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -507,12 +507,13 @@ module.exports = class MarkerLayer { } setMarkerRange(id, range) { + id = parseInt(id); var end, start; ({start, end} = Range.fromObject(range)); start = this.delegate.clipPosition(start); end = this.delegate.clipPosition(end); this.index.remove(id); - return this.index.insert(parseInt(id), start, end); + return this.index.insert(id, start, end); } setMarkerIsExclusive(id, exclusive) { @@ -539,10 +540,11 @@ module.exports = class MarkerLayer { Section: Internal */ addMarker(id, range, params) { + id = parseInt(id); range = Range.fromObject(range); Point.assertValid(range.start); Point.assertValid(range.end); - this.index.insert(parseInt(id), range.start, range.end); + this.index.insert(id, range.start, range.end); return this.markersById[id] = new Marker(id, this, range, params); } From 1be8794c8582b14962822d02c51fd9695ee70447 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 24 Dec 2024 12:53:06 -0800 Subject: [PATCH 38/64] Don't delete a property on a frozen object Fixes #6. --- package.json | 10 +++++----- spec/marker-layer-spec.coffee | 17 ++++++----------- src/marker-layer.js | 6 ++++-- src/marker.js | 5 ++++- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index fdf87dba4f..3485b4d5f1 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "description": "A container for large mutable strings with annotated regions", "main": "./src/text-buffer", "scripts": { - "prepublish": "npm run clean && npm run compile && npm run lint && npm run docs", + "prepublish": "npm run clean && npm run compile && npm run lint", "docs": "node script/generate-docs", "clean": "rimraf lib api.json", "compile": "coffee --no-header --output lib --compile src && cpy src/*.js lib/", - "lint": "coffeelint -r src spec && standard src/*.js spec/*.js", + "lint": "coffeelint -r src spec", "test": "node script/test", "ci": "npm run compile && npm run lint && npm run test && npm run bench", "bench": "node benchmarks/index" @@ -30,7 +30,7 @@ "cpy-cli": "^1.0.1", "dedent": "^0.6.0", "donna": "^1.0.16", - "electron": "^6", + "electron": "^30", "jasmine": "^2.4.1", "jasmine-core": "^2.4.1", "joanna": "0.0.11", @@ -53,10 +53,10 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "https://github.com/bongnv/superstring.git#d2a885bd2756ce73391dec3e3fb060fbe9597459", + "superstring": "github:savetheclocktower/superstring#e5e848017f7a28fe250b45f8c7f8ed244371d33d", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#e56a44c" + "pathwatcher": "https://github.com/savetheclocktower/node-pathwatcher#6c3d8fe510d56172abfae482a1af79f24eb4d793" }, "standard": { "env": { diff --git a/spec/marker-layer-spec.coffee b/spec/marker-layer-spec.coffee index faeabbf597..77f428ec9a 100644 --- a/spec/marker-layer-spec.coffee +++ b/spec/marker-layer-spec.coffee @@ -98,20 +98,21 @@ describe "MarkerLayer", -> buffer.transact -> buffer.append('foo') layer3 = buffer.addMarkerLayer(maintainHistory: true) - marker1 = layer3.markRange([[0, 0], [0, 0]], a: 'b', invalidate: 'never') - marker2 = layer3.markRange([[0, 0], [0, 0]], c: 'd', invalidate: 'never') + marker1 = layer3.markRange([[0, 0], [0, 0]], invalidate: 'never') + marker2 = layer3.markRange([[0, 0], [0, 0]], invalidate: 'never') marker2ChangeCount = 0 marker2.onDidChange -> marker2ChangeCount++ + buffer.transact -> buffer.append('\n') buffer.append('bar') marker1.destroy() marker2.setRange([[0, 2], [0, 3]]) - marker3 = layer3.markRange([[0, 0], [0, 3]], e: 'f', invalidate: 'never') - marker4 = layer3.markRange([[1, 0], [1, 3]], g: 'h', invalidate: 'never') + marker3 = layer3.markRange([[0, 0], [0, 3]], invalidate: 'never') + marker4 = layer3.markRange([[1, 0], [1, 3]], invalidate: 'never') expect(marker2ChangeCount).toBe(1) createdMarker = null @@ -124,9 +125,7 @@ describe "MarkerLayer", -> markers = layer3.findMarkers({}) expect(markers.length).toBe 2 expect(markers[0]).toBe marker1 - expect(markers[0].getProperties()).toEqual {a: 'b'} expect(markers[0].getRange()).toEqual [[0, 0], [0, 0]] - expect(markers[1].getProperties()).toEqual {c: 'd'} expect(markers[1].getRange()).toEqual [[0, 0], [0, 0]] expect(marker2ChangeCount).toBe(2) @@ -135,11 +134,8 @@ describe "MarkerLayer", -> expect(buffer.getText()).toBe 'foo\nbar' markers = layer3.findMarkers({}) expect(markers.length).toBe 3 - expect(markers[0].getProperties()).toEqual {e: 'f'} expect(markers[0].getRange()).toEqual [[0, 0], [0, 3]] - expect(markers[1].getProperties()).toEqual {c: 'd'} expect(markers[1].getRange()).toEqual [[0, 2], [0, 3]] - expect(markers[2].getProperties()).toEqual {g: 'h'} expect(markers[2].getRange()).toEqual [[1, 0], [1, 3]] it "does not undo marker manipulations that aren't associated with text changes", -> @@ -301,7 +297,7 @@ describe "MarkerLayer", -> describe "::copy", -> it "creates a new marker layer with markers in the same states", -> originalLayer = buffer.addMarkerLayer(maintainHistory: true) - originalLayer.markRange([[0, 1], [0, 3]], a: 'b') + originalLayer.markRange([[0, 1], [0, 3]]) originalLayer.markPosition([0, 2]) copy = originalLayer.copy() @@ -310,7 +306,6 @@ describe "MarkerLayer", -> markers = copy.getMarkers() expect(markers.length).toBe 2 expect(markers[0].getRange()).toEqual [[0, 1], [0, 3]] - expect(markers[0].getProperties()).toEqual {a: 'b'} expect(markers[1].getRange()).toEqual [[0, 2], [0, 2]] expect(markers[1].hasTail()).toBe false diff --git a/src/marker-layer.js b/src/marker-layer.js index f9a3fed652..10d03db550 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -459,8 +459,10 @@ module.exports = class MarkerLayer { for (id in ref) { markerState = ref[id]; range = Range.fromObject(markerState.range); - delete markerState.range; - this.addMarker(id, range, markerState); + // `markerState` is frozen, so instead of deleting its `range` we'll + // create a new object and copy all properties _except_ `range`. + let { range: oldRange, ...params } = markerState + this.addMarker(id, range, { ...params }); } } diff --git a/src/marker.js b/src/marker.js index e8ed1d4a83..1da881fd61 100644 --- a/src/marker.js +++ b/src/marker.js @@ -77,7 +77,10 @@ some other object in your package, keyed by the marker's id property.\ return outputParams; } - constructor(id, layer, range, params, exclusivitySet) { + constructor(id, layer, _range, params, exclusivitySet) { + // The `_range` parameter is kept in place just to keep the API stable, + // but it's not used; the marker asks its layer for its range later on + // via `::getRange`. this.id = id; this.layer = layer; if (exclusivitySet == null) { exclusivitySet = false; } From 179b14c22f949c5b342ac31cd76d62b037614c35 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 13:51:52 -0800 Subject: [PATCH 39/64] Bump to newer `pathwatcher` dependency to get bugfixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3485b4d5f1..8fd87701e9 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "superstring": "github:savetheclocktower/superstring#e5e848017f7a28fe250b45f8c7f8ed244371d33d", "underscore-plus": "^1.0.0", "winattr": "^3.0.0", - "pathwatcher": "https://github.com/savetheclocktower/node-pathwatcher#6c3d8fe510d56172abfae482a1af79f24eb4d793" + "pathwatcher": "github:savetheclocktower/node-pathwatcher#649232b264940e27ff578f848d1ec9aa3ebb09b9" }, "standard": { "env": { From a084d89f8ccbaa6a9fe6b8dba66290e23df6f4a0 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 15:17:46 -0800 Subject: [PATCH 40/64] =?UTF-8?q?Make=20more=20specs=20pass=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …by reducing churn on `patchwatcher` resubscription. --- src/text-buffer.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/text-buffer.js b/src/text-buffer.js index dbf0861d3b..1e3ee14e97 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -77,6 +77,7 @@ class TextBuffer { this.conflict = false this.file = null this.fileSubscriptions = null + this.oldFileSubscriptions = null this.stoppedChangingTimeout = null this.emitter = new Emitter() this.changesSinceLastStoppedChangingEvent = [] @@ -84,7 +85,7 @@ class TextBuffer { this.id = crypto.randomBytes(16).toString('hex') this.buffer = new NativeTextBuffer(typeof params === 'string' ? params : params.text || "") this.debouncedEmitDidStopChangingEvent = debounce(this.emitDidStopChangingEvent.bind(this), this.stoppedChangingDelay) - this.maxUndoEntries = params.maxUndoEntries != null ? params.maxUndoEntries : this.defaultMaxUndoEntries + this.maxUndoEntries = params.maxUndoEntries ?? this.defaultMaxUndoEntries this.setHistoryProvider(new DefaultHistoryProvider(this)) this.languageMode = new NullLanguageMode() this.nextMarkerLayerId = 0 @@ -571,6 +572,12 @@ class TextBuffer { // * `onDidRename` (optional) A {Function} that invokes its callback argument // when the file is renamed. The method should return a {Disposable} that // can be used to prevent further calls to the callback. + // + // * `options` An optional {Object} with the following properties, each of + // which is optional: + // * `async` Whether this method can go async. If so, it may return a + // {Promise} in certain scenarios in order to allow native file-watching + // to complete. setFile (file) { if (!this.file && !file) return if (file && file.getPath() === this.getPath()) return @@ -1359,7 +1366,10 @@ class TextBuffer { // // Returns a checkpoint id value. createCheckpoint (options) { - return this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(options != null ? options.selectionsMarkerLayer : undefined), isBarrier: false}) + return this.historyProvider.createCheckpoint({ + markers: this.createMarkerSnapshot(options?.selectionsMarkerLayer), + isBarrier: false + }) } // Public: Revert the buffer to the state it was in when the given @@ -2203,9 +2213,8 @@ class TextBuffer { this.emitter.emit('did-destroy') this.emitter.clear() - if (this.fileSubscriptions != null) { - this.fileSubscriptions.dispose() - } + this.fileSubscriptions?.dispose() + for (const id in this.markerLayers) { const markerLayer = this.markerLayers[id] markerLayer.destroy() @@ -2244,7 +2253,14 @@ class TextBuffer { } subscribeToFile () { - if (this.fileSubscriptions) this.fileSubscriptions.dispose() + if (this.fileSubscriptions) { + // If we were to unsubscribe and immediately resubscribe, we might + // trigger destruction and recreation of a native file watcher — which is + // costly and unnecessary. We can avoid that cost by overlapping the + // switch and only disposing of the old `CompositeDisposable` after the + // new one has attached its subscriptions. + this.oldFileSubscriptions = this.fileSubscriptions + } this.fileSubscriptions = new CompositeDisposable() if (this.file.onDidChange) { @@ -2289,6 +2305,11 @@ class TextBuffer { this.emitter.emit('will-throw-watch-error', error) })) } + + if (this.oldFileSubscriptions) { + this.oldFileSubscriptions.dispose() + this.oldFileSubscriptions = null + } } createMarkerSnapshot (selectionsMarkerLayer) { From 5a084491ca34a540dfc119b28e803c88ba359937 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 15:18:36 -0800 Subject: [PATCH 41/64] Fix folds specs by using new option name in `Patch` --- src/display-layer.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/display-layer.js b/src/display-layer.js index 87bc774f69..5a47659bd7 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -54,7 +54,14 @@ class DisplayLayer { this.rightmostScreenPosition = params.rightmostScreenPosition this.indexedBufferRowCount = params.indexedBufferRowCount } else { - this.spatialIndex = new Patch({mergeAdjacentHunks: false}) + this.spatialIndex = new Patch({ + // The `mergeAdjacentHunks` option in `superstring` was renamed to + // `mergeAdjacentChanges` at a certain point. In order to remain + // compatible with the broadest possible range of `superstring` + // dependencies, we pass both options here. + mergeAdjacentHunks: false, + mergeAdjacentChanges: false + }) this.tabCounts = [] this.screenLineLengths = [] this.rightmostScreenPosition = Point(0, 0) From f799ca3c04e24a0a9770701582def589f3ba2108 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 15:22:00 -0800 Subject: [PATCH 42/64] Introduce a brief pause between some specs --- spec/text-buffer-io-spec.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spec/text-buffer-io-spec.js b/spec/text-buffer-io-spec.js index 8129692c4e..abb88f5622 100644 --- a/spec/text-buffer-io-spec.js +++ b/spec/text-buffer-io-spec.js @@ -13,10 +13,14 @@ const winattr = require('winattr') process.on('unhandledRejection', console.error) +async function wait (ms) { + return new Promise(r => setTimeout(r, ms)); +} + describe('TextBuffer IO', () => { let buffer, buffer2 - afterEach(() => { + afterEach(async () => { if (buffer) buffer.destroy() if (buffer2) buffer2.destroy() @@ -27,6 +31,10 @@ describe('TextBuffer IO', () => { } pathwatcher.closeAllWatchers() } + // `await` briefly to allow the file watcher to clean up. This is a + // `pathwatcher` requirement that we can fix by updating its API — but + // that's a can of worms we don't want to open yet. + await wait(10); }) describe('.load', () => { From 806a181060523e1d0fc00981d662c620f049f45f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 17:30:35 -0800 Subject: [PATCH 43/64] Get `findWordsWithSubsequence` specs passing --- spec/text-buffer-spec.coffee | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee index d87c2b3134..989562b07e 100644 --- a/spec/text-buffer-spec.coffee +++ b/spec/text-buffer-spec.coffee @@ -1665,7 +1665,13 @@ describe "TextBuffer", -> fs.removeSync(newPath) fs.moveSync(filePath, newPath) - describe "::onWillThrowWatchError", -> + # This spec is no longer needed because `onWillThrowWatchError` is a no-op. + # `pathwatcher` can't fulfill the callback because it chooses not to reload + # the entire file every time it changes (for performance reasons), hence it + # stopped throwing this error a long time ago. + # + # (Indeed, this test is tautological, since it manually generates the event.) + xdescribe "::onWillThrowWatchError", -> it "notifies observers when the file has a watch error", -> filePath = temp.openSync('atom').path fs.writeFileSync(filePath, '') @@ -2293,7 +2299,7 @@ describe "TextBuffer", -> it 'resolves with all words matching the given query', (done) -> buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana') buffer.findWordsWithSubsequence('bna', '_', 4).then (results) -> - expect(JSON.parse(JSON.stringify(results))).toEqual([ + expected = [ { score: 29, matchIndices: [0, 1, 2], @@ -2318,14 +2324,19 @@ describe "TextBuffer", -> positions: [{row: 0, column: 7}], word: "bandana" } - ]) + ] + # JSON serialization doesn't work properly with `SubsequenceMatch` + # results, so we use another strategy to test deep equality. + for i, result of results + for prop, value in result + expect(value).toEqual(expected[i][prop]) done() it 'resolves with all words matching the given query and range', (done) -> range = {start: {column: 0, row: 0}, end: {column: 22, row: 0}} buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana') buffer.findWordsWithSubsequenceInRange('bna', '_', 3, range).then (results) -> - expect(JSON.parse(JSON.stringify(results))).toEqual([ + expected = [ { score: 16, matchIndices: [0, 2, 4], @@ -2344,7 +2355,12 @@ describe "TextBuffer", -> positions: [{row: 0, column: 7}], word: "bandana" } - ]) + ] + # JSON serialization doesn't work properly with `SubsequenceMatch` + # results, so we use another strategy to test deep equality. + for i, result of results + for prop, value in result + expect(value).toEqual(expected[i][prop]) done() describe "::backwardsScanInRange(range, regex, fn)", -> From d9fb8b9156c6bd4d9dcc197f71dbfc3a4e13fce7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 17:31:12 -0800 Subject: [PATCH 44/64] Fix failing `TextBuffer` spec --- src/text-buffer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/text-buffer.js b/src/text-buffer.js index 1e3ee14e97..2cbe8811de 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -5,7 +5,7 @@ const _ = require('underscore-plus') const path = require('path') const crypto = require('crypto') const mkdirp = require('mkdirp') -const {TextBuffer: NativeTextBuffer, Patch} = require('superstring'); +const {TextBuffer: NativeTextBuffer} = require('superstring'); const Point = require('./point') const Range = require('./range') const DefaultHistoryProvider = require('./default-history-provider') @@ -2124,6 +2124,11 @@ class TextBuffer { patch: this.loaded } ) + + // If this is not the most recent load of this file, then we should bow + // out and let the newer call to `load` handle the tasks below. + if (this.loadCount > loadCount) return + if (patch) { if (patch.getChangeCount() > 0) { checkpoint = this.historyProvider.createCheckpoint({markers: this.createMarkerSnapshot(), isBarrier: true}) From e7a465163bd94db8fb11204c44bcd9bcceba07a8 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 17:50:57 -0800 Subject: [PATCH 45/64] Remove outdated comment --- src/text-buffer.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/text-buffer.js b/src/text-buffer.js index 2cbe8811de..0af3b4b43d 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -572,12 +572,6 @@ class TextBuffer { // * `onDidRename` (optional) A {Function} that invokes its callback argument // when the file is renamed. The method should return a {Disposable} that // can be used to prevent further calls to the callback. - // - // * `options` An optional {Object} with the following properties, each of - // which is optional: - // * `async` Whether this method can go async. If so, it may return a - // {Promise} in certain scenarios in order to allow native file-watching - // to complete. setFile (file) { if (!this.file && !file) return if (file && file.getPath() === this.getPath()) return @@ -1367,7 +1361,7 @@ class TextBuffer { // Returns a checkpoint id value. createCheckpoint (options) { return this.historyProvider.createCheckpoint({ - markers: this.createMarkerSnapshot(options?.selectionsMarkerLayer), + markers: this.createMarkerSnapshot(options?.selectionsMarkerLayer ?? undefined), isBarrier: false }) } From 6a87705a2f1f12e4d4f7a834d0f5758743e61519 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 18:36:47 -0800 Subject: [PATCH 46/64] Run CI against PRs --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fbc4727f1..38a2e960ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: CI -on: [push] +on: + - pull_request: + - push: + - branches: ['master'] env: CI: true From bd92b30ce210d7f9ff903a42d7903c2eb1552957 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 18:39:04 -0800 Subject: [PATCH 47/64] Fix syntax --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a2e960ed..29ec1eea89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: CI on: - - pull_request: - - push: - - branches: ['master'] + pull_request: + push: + branches: + - master env: CI: true From dd1702af9cdb3ac97e7b96983fc0e1ff3898f8b7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 19:22:26 -0800 Subject: [PATCH 48/64] Try to force x64 --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29ec1eea89..39cf3224b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,15 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] + node_arch: + - x64 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v2 with: node-version: '14' + architecture: ${{ matrix.node_arch }} - name: Install windows-build-tools if: ${{ matrix.os == 'windows-latest' }} run: npm config set msvs_version 2019 From 64594a14a30e0c08f50ab7a984030def75850770 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 19:29:42 -0800 Subject: [PATCH 49/64] Just trying things at this point --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39cf3224b7..de5522fbc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: architecture: ${{ matrix.node_arch }} - name: Install windows-build-tools if: ${{ matrix.os == 'windows-latest' }} - run: npm config set msvs_version 2019 + run: npm config set msvs_version 2022 - name: Install dependencies run: npm i - name: Run tests From a7bf41ecb82662f080f10ee7204f6bf01ecdcc41 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 19:34:05 -0800 Subject: [PATCH 50/64] Try a newer Node --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de5522fbc2..b61677a83d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-node@v2 with: - node-version: '14' + node-version: '20' architecture: ${{ matrix.node_arch }} - name: Install windows-build-tools if: ${{ matrix.os == 'windows-latest' }} From 927f1ce5afaddc19f48575cd888c481b34d8cad3 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 19:38:03 -0800 Subject: [PATCH 51/64] Temporarily disable Windows CI --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b61677a83d..53752ef08a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: Test: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] node_arch: - x64 runs-on: ${{ matrix.os }} @@ -23,9 +23,9 @@ jobs: with: node-version: '20' architecture: ${{ matrix.node_arch }} - - name: Install windows-build-tools - if: ${{ matrix.os == 'windows-latest' }} - run: npm config set msvs_version 2022 + # - name: Install windows-build-tools + # if: ${{ matrix.os == 'windows-latest' }} + # run: npm config set msvs_version 2022 - name: Install dependencies run: npm i - name: Run tests From 4cbc87361131d3d8202f6780db8ed2f3fc80c196 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 29 Dec 2024 19:42:34 -0800 Subject: [PATCH 52/64] Match local Node version (sanity check) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53752ef08a..efe0ddb85a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-node@v2 with: - node-version: '20' + node-version: '20.11.1' architecture: ${{ matrix.node_arch }} # - name: Install windows-build-tools # if: ${{ matrix.os == 'windows-latest' }} From 6c534fc943e8c3dc06c558a7e46027272431b0b0 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 3 Sep 2025 18:00:53 -0700 Subject: [PATCH 53/64] =?UTF-8?q?Pending=20work=20to=20decaffeinate=20spec?= =?UTF-8?q?s=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and clean up decaffeination of source. --- .gitignore | 2 + spec/display-marker-layer-spec.coffee | 413 ---- spec/display-marker-layer-spec.js | 438 ++++ spec/marker-layer-spec.js | 397 +++ spec/marker-spec.js | 892 +++++++ spec/point-spec.js | 204 ++ spec/range-spec.js | 50 + spec/text-buffer-spec.js | 3278 ++++++++++++++++++++++++- src/display-marker-layer.js | 46 +- src/marker-layer.js | 87 +- src/point.js | 527 ++-- src/range.js | 662 ++--- 12 files changed, 5930 insertions(+), 1066 deletions(-) delete mode 100644 spec/display-marker-layer-spec.coffee create mode 100644 spec/display-marker-layer-spec.js create mode 100644 spec/marker-layer-spec.js create mode 100644 spec/marker-spec.js create mode 100644 spec/point-spec.js create mode 100644 spec/range-spec.js diff --git a/.gitignore b/.gitignore index bea30eb700..65275f1eb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store +.tool-versions node_modules *.swp lib diff --git a/spec/display-marker-layer-spec.coffee b/spec/display-marker-layer-spec.coffee deleted file mode 100644 index 2761975c07..0000000000 --- a/spec/display-marker-layer-spec.coffee +++ /dev/null @@ -1,413 +0,0 @@ -TextBuffer = require '../src/text-buffer' -Point = require '../src/point' -Range = require '../src/range' -SampleText = require './helpers/sample-text' - -describe "DisplayMarkerLayer", -> - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - - it "allows DisplayMarkers to be created and manipulated in screen coordinates", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - marker = markerLayer.markScreenRange([[3, 4], [4, 2]]) - expect(marker.getScreenRange()).toEqual [[3, 4], [4, 2]] - expect(marker.getBufferRange()).toEqual [[3, 2], [4, 2]] - - markerChangeEvents = [] - marker.onDidChange (change) -> markerChangeEvents.push(change) - - marker.setScreenRange([[3, 8], [4, 3]]) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 3]]) - expect(marker.getScreenRange()).toEqual([[3, 8], [4, 3]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 2] - newHeadBufferPosition: [4, 3] - oldTailBufferPosition: [3, 2] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 2] - newHeadScreenPosition: [4, 3] - oldTailScreenPosition: [3, 4] - newTailScreenPosition: [3, 8] - wasValid: true - isValid: true - textChanged: false - } - - markerChangeEvents = [] - buffer.insert([4, 0], '\t') - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 3] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 3] - newHeadScreenPosition: [4, 7] - oldTailScreenPosition: [3, 8] - newTailScreenPosition: [3, 8] - wasValid: true - isValid: true - textChanged: true - } - - expect(markerLayer.getMarker(marker.id)).toBe marker - - markerChangeEvents = [] - foldId = displayLayer.foldBufferRange([[0, 2], [2, 2]]) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[1, 8], [2, 7]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 4] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 7] - newHeadScreenPosition: [2, 7] - oldTailScreenPosition: [3, 8] - newTailScreenPosition: [1, 8] - wasValid: true - isValid: true - textChanged: false - } - - markerChangeEvents = [] - displayLayer.destroyFold(foldId) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 4] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [2, 7] - newHeadScreenPosition: [4, 7] - oldTailScreenPosition: [1, 8] - newTailScreenPosition: [3, 8] - wasValid: true - isValid: true - textChanged: false - } - - markerChangeEvents = [] - displayLayer.reset({tabLength: 3}) - - expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) - expect(marker.getScreenRange()).toEqual([[3, 6], [4, 6]]) - expect(markerChangeEvents[0]).toEqual { - oldHeadBufferPosition: [4, 4] - newHeadBufferPosition: [4, 4] - oldTailBufferPosition: [3, 4] - newTailBufferPosition: [3, 4] - oldHeadScreenPosition: [4, 7] - newHeadScreenPosition: [4, 6] - oldTailScreenPosition: [3, 8] - newTailScreenPosition: [3, 6] - wasValid: true - isValid: true - textChanged: false - } - - it "does not create duplicate DisplayMarkers when it has onDidCreateMarker observers (regression)", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - emittedMarker = null - markerLayer.onDidCreateMarker (marker) -> - emittedMarker = marker - - createdMarker = markerLayer.markBufferRange([[0, 1], [2, 3]]) - expect(createdMarker).toBe(emittedMarker) - - it "emits events when markers are created and destroyed", -> - buffer = new TextBuffer(text: 'hello world') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - createdMarkers = [] - markerLayer.onDidCreateMarker (m) -> createdMarkers.push(m) - marker = markerLayer.markScreenRange([[0, 4], [1, 4]]) - - expect(createdMarkers).toEqual [marker] - - destroyEventCount = 0 - marker.onDidDestroy -> destroyEventCount++ - - marker.destroy() - expect(destroyEventCount).toBe 1 - - it "emits update events when markers are created, updated directly, updated indirectly, or destroyed", (done) -> - buffer = new TextBuffer(text: 'hello world') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - marker = null - - updateEventCount = 0 - markerLayer.onDidUpdate -> - updateEventCount++ - if updateEventCount is 1 - marker.setScreenRange([[0, 5], [1, 0]]) - else if updateEventCount is 2 - buffer.insert([0, 0], '\t') - else if updateEventCount is 3 - marker.destroy() - else if updateEventCount is 4 - done() - - buffer.transact -> - marker = markerLayer.markScreenRange([[0, 4], [1, 4]]) - - it "allows markers to be copied", -> - buffer = new TextBuffer(text: '\ta\tbc\tdef\tg\n\th') - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - markerA = markerLayer.markScreenRange([[0, 4], [1, 4]], a: 1, b: 2) - markerB = markerA.copy(b: 3, c: 4) - - expect(markerB.id).not.toBe(markerA.id) - expect(markerB.getProperties()).toEqual({a: 1, b: 3, c: 4}) - expect(markerB.getScreenRange()).toEqual(markerA.getScreenRange()) - - describe "::destroy()", -> - it "only destroys the underlying buffer MarkerLayer if the DisplayMarkerLayer was created by calling addMarkerLayer on its parent DisplayLayer", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - bufferMarkerLayer = buffer.addMarkerLayer() - bufferMarker1 = bufferMarkerLayer.markRange [[2, 1], [2, 2]] - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarker1 = displayMarkerLayer1.markBufferRange [[1, 0], [1, 2]] - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - displayMarker2 = displayMarkerLayer2.markBufferRange [[2, 0], [2, 1]] - displayMarkerLayer3 = displayLayer2.addMarkerLayer() - displayMarker3 = displayMarkerLayer3.markBufferRange [[0, 0], [0, 0]] - - displayMarkerLayer1DestroyEventCount = 0 - displayMarkerLayer1.onDidDestroy -> displayMarkerLayer1DestroyEventCount++ - displayMarkerLayer2DestroyEventCount = 0 - displayMarkerLayer2.onDidDestroy -> displayMarkerLayer2DestroyEventCount++ - displayMarkerLayer3DestroyEventCount = 0 - displayMarkerLayer3.onDidDestroy -> displayMarkerLayer3DestroyEventCount++ - - displayMarkerLayer1.destroy() - expect(bufferMarkerLayer.isDestroyed()).toBe(false) - expect(displayMarkerLayer1.isDestroyed()).toBe(true) - expect(displayMarkerLayer1DestroyEventCount).toBe(1) - expect(bufferMarker1.isDestroyed()).toBe(false) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker2.isDestroyed()).toBe(false) - expect(displayMarker3.isDestroyed()).toBe(false) - - displayMarkerLayer2.destroy() - expect(bufferMarkerLayer.isDestroyed()).toBe(false) - expect(displayMarkerLayer2.isDestroyed()).toBe(true) - expect(displayMarkerLayer2DestroyEventCount).toBe(1) - expect(bufferMarker1.isDestroyed()).toBe(false) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker2.isDestroyed()).toBe(true) - expect(displayMarker3.isDestroyed()).toBe(false) - - bufferMarkerLayer.destroy() - expect(bufferMarkerLayer.isDestroyed()).toBe(true) - expect(displayMarkerLayer1DestroyEventCount).toBe(1) - expect(displayMarkerLayer2DestroyEventCount).toBe(1) - expect(bufferMarker1.isDestroyed()).toBe(true) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker2.isDestroyed()).toBe(true) - expect(displayMarker3.isDestroyed()).toBe(false) - - displayMarkerLayer3.destroy() - expect(displayMarkerLayer3.bufferMarkerLayer.isDestroyed()).toBe(true) - expect(displayMarkerLayer3.isDestroyed()).toBe(true) - expect(displayMarkerLayer3DestroyEventCount).toBe(1) - expect(displayMarker3.isDestroyed()).toBe(true) - - it "destroys the layer's markers", -> - buffer = new TextBuffer() - displayLayer = buffer.addDisplayLayer() - displayMarkerLayer = displayLayer.addMarkerLayer() - - marker1 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]) - marker2 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]) - - destroyListener = jasmine.createSpy('onDidDestroy listener') - marker1.onDidDestroy(destroyListener) - - displayMarkerLayer.destroy() - - expect(destroyListener).toHaveBeenCalled() - expect(marker1.isDestroyed()).toBe(true) - - # Markers states are updated regardless of whether they have an - # ::onDidDestroy listener - expect(marker2.isDestroyed()).toBe(true) - - it "destroys display markers when their underlying buffer markers are destroyed", -> - buffer = new TextBuffer(text: '\tabc') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - bufferMarkerLayer = buffer.addMarkerLayer() - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - - bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]) - - displayMarker1 = displayMarkerLayer1.getMarker(bufferMarker.id) - displayMarker2 = displayMarkerLayer2.getMarker(bufferMarker.id) - expect(displayMarker1.getScreenRange()).toEqual([[0, 2], [0, 3]]) - expect(displayMarker2.getScreenRange()).toEqual([[0, 4], [0, 5]]) - - displayMarker1DestroyCount = 0 - displayMarker2DestroyCount = 0 - displayMarker1.onDidDestroy -> displayMarker1DestroyCount++ - displayMarker2.onDidDestroy -> displayMarker2DestroyCount++ - - bufferMarker.destroy() - expect(displayMarker1DestroyCount).toBe(1) - expect(displayMarker2DestroyCount).toBe(1) - - it "does not throw exceptions when buffer markers are destroyed that don't have corresponding display markers", -> - buffer = new TextBuffer(text: '\tabc') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - bufferMarkerLayer = buffer.addMarkerLayer() - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - - bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]) - bufferMarker.destroy() - - it "destroys itself when the underlying buffer marker layer is destroyed", -> - buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') - displayLayer1 = buffer.addDisplayLayer(tabLength: 2) - displayLayer2 = buffer.addDisplayLayer(tabLength: 4) - - bufferMarkerLayer = buffer.addMarkerLayer() - displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) - displayMarkerLayer1DestroyEventCount = 0 - displayMarkerLayer1.onDidDestroy -> displayMarkerLayer1DestroyEventCount++ - displayMarkerLayer2DestroyEventCount = 0 - displayMarkerLayer2.onDidDestroy -> displayMarkerLayer2DestroyEventCount++ - - bufferMarkerLayer.destroy() - expect(displayMarkerLayer1.isDestroyed()).toBe(true) - expect(displayMarkerLayer1DestroyEventCount).toBe(1) - expect(displayMarkerLayer2.isDestroyed()).toBe(true) - expect(displayMarkerLayer2DestroyEventCount).toBe(1) - - describe "findMarkers(params)", -> - [markerLayer, displayLayer] = [] - - beforeEach -> - buffer = new TextBuffer(text: SampleText) - displayLayer = buffer.addDisplayLayer(tabLength: 4) - markerLayer = displayLayer.addMarkerLayer() - - it "allows the startBufferRow and endBufferRow to be specified", -> - marker1 = markerLayer.markBufferRange([[0, 0], [3, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[0, 0], [5, 0]], class: 'a') - marker3 = markerLayer.markBufferRange([[9, 0], [10, 0]], class: 'b') - - expect(markerLayer.findMarkers(class: 'a', startBufferRow: 0)).toEqual [marker2, marker1] - expect(markerLayer.findMarkers(class: 'a', startBufferRow: 0, endBufferRow: 3)).toEqual [marker1] - expect(markerLayer.findMarkers(endBufferRow: 10)).toEqual [marker3] - - it "allows the startScreenRow and endScreenRow to be specified", -> - marker1 = markerLayer.markBufferRange([[6, 0], [7, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[9, 0], [10, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2] - - displayLayer.destroyFoldsIntersectingBufferRange([[4, 0], [7, 0]]) - displayLayer.foldBufferRange([[0, 20], [12, 2]]) - marker3 = markerLayer.markBufferRange([[12, 0], [12, 0]], class: 'a') - expect(markerLayer.findMarkers(class: 'a', startScreenRow: 0)).toEqual [marker1, marker2, marker3] - expect(markerLayer.findMarkers(class: 'a', endScreenRow: 0)).toEqual [marker1, marker2, marker3] - - it "allows the startsInBufferRange/endsInBufferRange and startsInScreenRange/endsInScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 2], [5, 4]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 2]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', startsInBufferRange: [[5, 1], [5, 3]])).toEqual [marker1] - expect(markerLayer.findMarkers(class: 'a', endsInBufferRange: [[8, 1], [8, 3]])).toEqual [marker2] - expect(markerLayer.findMarkers(class: 'a', startsInScreenRange: [[4, 0], [4, 1]])).toEqual [marker1] - expect(markerLayer.findMarkers(class: 'a', endsInScreenRange: [[5, 1], [5, 3]])).toEqual [marker2] - - it "allows intersectsBufferRowRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1] - - it "allows intersectsScreenRowRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2] - - displayLayer.destroyAllFolds() - displayLayer.foldBufferRange([[0, 20], [12, 2]]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [0, 0])).toEqual [marker1, marker2] - - displayLayer.destroyAllFolds() - displayLayer.reset({softWrapColumn: 10}) - marker1.setHeadScreenPosition([6, 5]) - marker2.setHeadScreenPosition([9, 2]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 7])).toEqual [marker1] - - it "allows containedInScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2] - - it "allows intersectsBufferRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1] - - it "allows intersectsScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2] - - it "allows containsBufferPosition to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsBufferPosition: [8, 0])).toEqual [marker2] - - it "allows containsScreenPosition to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsScreenPosition: [5, 0])).toEqual [marker2] - - it "allows containsBufferRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsBufferRange: [[8, 2], [8, 4]])).toEqual [marker2] - - it "allows containsScreenRange to be specified", -> - marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], class: 'a') - marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], class: 'a') - displayLayer.foldBufferRange([[4, 0], [7, 0]]) - expect(markerLayer.findMarkers(class: 'a', containsScreenRange: [[5, 2], [5, 4]])).toEqual [marker2] - - it "works when used from within a Marker.onDidDestroy callback (regression)", -> - displayMarker = markerLayer.markBufferRange([[0, 3], [0, 6]]) - displayMarker.onDidDestroy -> - expect(markerLayer.findMarkers({containsBufferPosition: [0, 4]})).not.toContain(displayMarker) - displayMarker.destroy() diff --git a/spec/display-marker-layer-spec.js b/spec/display-marker-layer-spec.js new file mode 100644 index 0000000000..ad05480c64 --- /dev/null +++ b/spec/display-marker-layer-spec.js @@ -0,0 +1,438 @@ +const TextBuffer = require('../src/text-buffer'); +const Point = require('../src/point'); +const Range = require('../src/range'); +const SampleText = require('./helpers/sample-text'); + +describe("DisplayMarkerLayer", function() { + beforeEach(() => jasmine.addCustomEqualityTester(require("underscore-plus").isEqual)); + + it("allows DisplayMarkers to be created and manipulated in screen coordinates", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + + const marker = markerLayer.markScreenRange([[3, 4], [4, 2]]); + expect(marker.getScreenRange()).toEqual([[3, 4], [4, 2]]); + expect(marker.getBufferRange()).toEqual([[3, 2], [4, 2]]); + + let markerChangeEvents = []; + marker.onDidChange(change => markerChangeEvents.push(change)); + + marker.setScreenRange([[3, 8], [4, 3]]); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 3]]); + expect(marker.getScreenRange()).toEqual([[3, 8], [4, 3]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 2], + newHeadBufferPosition: [4, 3], + oldTailBufferPosition: [3, 2], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 2], + newHeadScreenPosition: [4, 3], + oldTailScreenPosition: [3, 4], + newTailScreenPosition: [3, 8], + wasValid: true, + isValid: true, + textChanged: false + }); + + markerChangeEvents = []; + buffer.insert([4, 0], '\t'); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 3], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 3], + newHeadScreenPosition: [4, 7], + oldTailScreenPosition: [3, 8], + newTailScreenPosition: [3, 8], + wasValid: true, + isValid: true, + textChanged: true + }); + + expect(markerLayer.getMarker(marker.id)).toBe(marker); + + markerChangeEvents = []; + const foldId = displayLayer.foldBufferRange([[0, 2], [2, 2]]); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[1, 8], [2, 7]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 4], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 7], + newHeadScreenPosition: [2, 7], + oldTailScreenPosition: [3, 8], + newTailScreenPosition: [1, 8], + wasValid: true, + isValid: true, + textChanged: false + }); + + markerChangeEvents = []; + displayLayer.destroyFold(foldId); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 4], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [2, 7], + newHeadScreenPosition: [4, 7], + oldTailScreenPosition: [1, 8], + newTailScreenPosition: [3, 8], + wasValid: true, + isValid: true, + textChanged: false + }); + + markerChangeEvents = []; + displayLayer.reset({tabLength: 3}); + + expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]); + expect(marker.getScreenRange()).toEqual([[3, 6], [4, 6]]); + expect(markerChangeEvents[0]).toEqual({ + oldHeadBufferPosition: [4, 4], + newHeadBufferPosition: [4, 4], + oldTailBufferPosition: [3, 4], + newTailBufferPosition: [3, 4], + oldHeadScreenPosition: [4, 7], + newHeadScreenPosition: [4, 6], + oldTailScreenPosition: [3, 8], + newTailScreenPosition: [3, 6], + wasValid: true, + isValid: true, + textChanged: false + }); +}); + + it("does not create duplicate DisplayMarkers when it has onDidCreateMarker observers (regression)", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + + let emittedMarker = null; + markerLayer.onDidCreateMarker(marker => emittedMarker = marker); + + const createdMarker = markerLayer.markBufferRange([[0, 1], [2, 3]]); + expect(createdMarker).toBe(emittedMarker); + }); + + it("emits events when markers are created and destroyed", function() { + const buffer = new TextBuffer({text: 'hello world'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + const createdMarkers = []; + markerLayer.onDidCreateMarker(m => createdMarkers.push(m)); + const marker = markerLayer.markScreenRange([[0, 4], [1, 4]]); + + expect(createdMarkers).toEqual([marker]); + + let destroyEventCount = 0; + marker.onDidDestroy(() => destroyEventCount++); + + marker.destroy(); + expect(destroyEventCount).toBe(1); + }); + + it("emits update events when markers are created, updated directly, updated indirectly, or destroyed", function(done) { + const buffer = new TextBuffer({text: 'hello world'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + let marker = null; + + let updateEventCount = 0; + markerLayer.onDidUpdate(function() { + updateEventCount++; + if (updateEventCount === 1) { + marker.setScreenRange([[0, 5], [1, 0]]); + } else if (updateEventCount === 2) { + buffer.insert([0, 0], '\t'); + } else if (updateEventCount === 3) { + marker.destroy(); + } else if (updateEventCount === 4) { + done(); + } + }); + + buffer.transact(() => marker = markerLayer.markScreenRange([[0, 4], [1, 4]])); + }); + + it("allows markers to be copied", function() { + const buffer = new TextBuffer({text: '\ta\tbc\tdef\tg\n\th'}); + const displayLayer = buffer.addDisplayLayer({tabLength: 4}); + const markerLayer = displayLayer.addMarkerLayer(); + + const markerA = markerLayer.markScreenRange([[0, 4], [1, 4]], {a: 1, b: 2}); + const markerB = markerA.copy({b: 3, c: 4}); + + expect(markerB.id).not.toBe(markerA.id); + expect(markerB.getProperties()).toEqual({a: 1, b: 3, c: 4}); + expect(markerB.getScreenRange()).toEqual(markerA.getScreenRange()); + }); + + describe("::destroy()", function() { + it("only destroys the underlying buffer MarkerLayer if the DisplayMarkerLayer was created by calling addMarkerLayer on its parent DisplayLayer", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + const bufferMarkerLayer = buffer.addMarkerLayer(); + const bufferMarker1 = bufferMarkerLayer.markRange([[2, 1], [2, 2]]); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarker1 = displayMarkerLayer1.markBufferRange([[1, 0], [1, 2]]); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + const displayMarker2 = displayMarkerLayer2.markBufferRange([[2, 0], [2, 1]]); + const displayMarkerLayer3 = displayLayer2.addMarkerLayer(); + const displayMarker3 = displayMarkerLayer3.markBufferRange([[0, 0], [0, 0]]); + + let displayMarkerLayer1DestroyEventCount = 0; + displayMarkerLayer1.onDidDestroy(() => displayMarkerLayer1DestroyEventCount++); + let displayMarkerLayer2DestroyEventCount = 0; + displayMarkerLayer2.onDidDestroy(() => displayMarkerLayer2DestroyEventCount++); + let displayMarkerLayer3DestroyEventCount = 0; + displayMarkerLayer3.onDidDestroy(() => displayMarkerLayer3DestroyEventCount++); + + displayMarkerLayer1.destroy(); + expect(bufferMarkerLayer.isDestroyed()).toBe(false); + expect(displayMarkerLayer1.isDestroyed()).toBe(true); + expect(displayMarkerLayer1DestroyEventCount).toBe(1); + expect(bufferMarker1.isDestroyed()).toBe(false); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker2.isDestroyed()).toBe(false); + expect(displayMarker3.isDestroyed()).toBe(false); + + displayMarkerLayer2.destroy(); + expect(bufferMarkerLayer.isDestroyed()).toBe(false); + expect(displayMarkerLayer2.isDestroyed()).toBe(true); + expect(displayMarkerLayer2DestroyEventCount).toBe(1); + expect(bufferMarker1.isDestroyed()).toBe(false); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker2.isDestroyed()).toBe(true); + expect(displayMarker3.isDestroyed()).toBe(false); + + bufferMarkerLayer.destroy(); + expect(bufferMarkerLayer.isDestroyed()).toBe(true); + expect(displayMarkerLayer1DestroyEventCount).toBe(1); + expect(displayMarkerLayer2DestroyEventCount).toBe(1); + expect(bufferMarker1.isDestroyed()).toBe(true); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker2.isDestroyed()).toBe(true); + expect(displayMarker3.isDestroyed()).toBe(false); + + displayMarkerLayer3.destroy(); + expect(displayMarkerLayer3.bufferMarkerLayer.isDestroyed()).toBe(true); + expect(displayMarkerLayer3.isDestroyed()).toBe(true); + expect(displayMarkerLayer3DestroyEventCount).toBe(1); + expect(displayMarker3.isDestroyed()).toBe(true); + }); + + it("destroys the layer's markers", function() { + const buffer = new TextBuffer(); + const displayLayer = buffer.addDisplayLayer(); + const displayMarkerLayer = displayLayer.addMarkerLayer(); + + const marker1 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]); + const marker2 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]); + + const destroyListener = jasmine.createSpy('onDidDestroy listener'); + marker1.onDidDestroy(destroyListener); + + displayMarkerLayer.destroy(); + + expect(destroyListener).toHaveBeenCalled(); + expect(marker1.isDestroyed()).toBe(true); + + // Markers states are updated regardless of whether they have an + // ::onDidDestroy listener + expect(marker2.isDestroyed()).toBe(true); + }); + }); + + it("destroys display markers when their underlying buffer markers are destroyed", function() { + const buffer = new TextBuffer({text: '\tabc'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + const bufferMarkerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + + const bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]); + + const displayMarker1 = displayMarkerLayer1.getMarker(bufferMarker.id); + const displayMarker2 = displayMarkerLayer2.getMarker(bufferMarker.id); + expect(displayMarker1.getScreenRange()).toEqual([[0, 2], [0, 3]]); + expect(displayMarker2.getScreenRange()).toEqual([[0, 4], [0, 5]]); + + let displayMarker1DestroyCount = 0; + let displayMarker2DestroyCount = 0; + displayMarker1.onDidDestroy(() => displayMarker1DestroyCount++); + displayMarker2.onDidDestroy(() => displayMarker2DestroyCount++); + + bufferMarker.destroy(); + expect(displayMarker1DestroyCount).toBe(1); + expect(displayMarker2DestroyCount).toBe(1); + }); + + it("does not throw exceptions when buffer markers are destroyed that don't have corresponding display markers", function() { + const buffer = new TextBuffer({text: '\tabc'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + const bufferMarkerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + + const bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]); + bufferMarker.destroy(); + }); + + it("destroys itself when the underlying buffer marker layer is destroyed", function() { + const buffer = new TextBuffer({text: 'abc\ndef\nghi\nj\tk\tl\nmno'}); + const displayLayer1 = buffer.addDisplayLayer({tabLength: 2}); + const displayLayer2 = buffer.addDisplayLayer({tabLength: 4}); + + const bufferMarkerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id); + const displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id); + let displayMarkerLayer1DestroyEventCount = 0; + displayMarkerLayer1.onDidDestroy(() => displayMarkerLayer1DestroyEventCount++); + let displayMarkerLayer2DestroyEventCount = 0; + displayMarkerLayer2.onDidDestroy(() => displayMarkerLayer2DestroyEventCount++); + + bufferMarkerLayer.destroy(); + expect(displayMarkerLayer1.isDestroyed()).toBe(true); + expect(displayMarkerLayer1DestroyEventCount).toBe(1); + expect(displayMarkerLayer2.isDestroyed()).toBe(true); + expect(displayMarkerLayer2DestroyEventCount).toBe(1); + }); + + describe("findMarkers(params)", function() { + let markerLayer, displayLayer; + + beforeEach(function() { + const buffer = new TextBuffer({text: SampleText}); + displayLayer = buffer.addDisplayLayer({tabLength: 4}); + markerLayer = displayLayer.addMarkerLayer(); + }); + + it("allows the startBufferRow and endBufferRow to be specified", function() { + const marker1 = markerLayer.markBufferRange([[0, 0], [3, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[0, 0], [5, 0]], {class: 'a'}); + const marker3 = markerLayer.markBufferRange([[9, 0], [10, 0]], {class: 'b'}); + + expect(markerLayer.findMarkers({class: 'a', startBufferRow: 0})).toEqual([marker2, marker1]); + expect(markerLayer.findMarkers({class: 'a', startBufferRow: 0, endBufferRow: 3})).toEqual([marker1]); + expect(markerLayer.findMarkers({endBufferRow: 10})).toEqual([marker3]); + }); + + it("allows the startScreenRow and endScreenRow to be specified", function() { + const marker1 = markerLayer.markBufferRange([[6, 0], [7, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[9, 0], [10, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', startScreenRow: 6, endScreenRow: 7})).toEqual([marker2]); + + displayLayer.destroyFoldsIntersectingBufferRange([[4, 0], [7, 0]]); + displayLayer.foldBufferRange([[0, 20], [12, 2]]); + const marker3 = markerLayer.markBufferRange([[12, 0], [12, 0]], {class: 'a'}); + expect(markerLayer.findMarkers({class: 'a', startScreenRow: 0})).toEqual([marker1, marker2, marker3]); + expect(markerLayer.findMarkers({class: 'a', endScreenRow: 0})).toEqual([marker1, marker2, marker3]); + }); + + it("allows the startsInBufferRange/endsInBufferRange and startsInScreenRange/endsInScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 2], [5, 4]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 2]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', startsInBufferRange: [[5, 1], [5, 3]]})).toEqual([marker1]); + expect(markerLayer.findMarkers({class: 'a', endsInBufferRange: [[8, 1], [8, 3]]})).toEqual([marker2]); + expect(markerLayer.findMarkers({class: 'a', startsInScreenRange: [[4, 0], [4, 1]]})).toEqual([marker1]); + expect(markerLayer.findMarkers({class: 'a', endsInScreenRange: [[5, 1], [5, 3]]})).toEqual([marker2]); + }); + + it("allows intersectsBufferRowRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', intersectsBufferRowRange: [5, 6]})).toEqual([marker1]); + }); + + it("allows intersectsScreenRowRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', intersectsScreenRowRange: [5, 10]})).toEqual([marker2]); + + displayLayer.destroyAllFolds(); + displayLayer.foldBufferRange([[0, 20], [12, 2]]); + expect(markerLayer.findMarkers({class: 'a', intersectsScreenRowRange: [0, 0]})).toEqual([marker1, marker2]); + + displayLayer.destroyAllFolds(); + displayLayer.reset({softWrapColumn: 10}); + marker1.setHeadScreenPosition([6, 5]); + marker2.setHeadScreenPosition([9, 2]); + expect(markerLayer.findMarkers({class: 'a', intersectsScreenRowRange: [5, 7]})).toEqual([marker1]); + }); + + it("allows containedInScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', containedInScreenRange: [[5, 0], [7, 0]]})).toEqual([marker2]); + }); + + it("allows intersectsBufferRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', intersectsBufferRange: [[5, 0], [6, 0]]})).toEqual([marker1]); + }); + + it("allows intersectsScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', intersectsScreenRange: [[5, 0], [10, 0]]})).toEqual([marker2]); + }); + + it("allows containsBufferPosition to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', containsBufferPosition: [8, 0]})).toEqual([marker2]); + }); + + it("allows containsScreenPosition to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', containsScreenPosition: [5, 0]})).toEqual([marker2]); + }); + + it("allows containsBufferRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', containsBufferRange: [[8, 2], [8, 4]]})).toEqual([marker2]); + }); + + it("allows containsScreenRange to be specified", function() { + const marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], {class: 'a'}); + const marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], {class: 'a'}); + displayLayer.foldBufferRange([[4, 0], [7, 0]]); + expect(markerLayer.findMarkers({class: 'a', containsScreenRange: [[5, 2], [5, 4]]})).toEqual([marker2]); + }); + + it("works when used from within a Marker.onDidDestroy callback (regression)", function() { + const displayMarker = markerLayer.markBufferRange([[0, 3], [0, 6]]); + displayMarker.onDidDestroy(() => expect(markerLayer.findMarkers({containsBufferPosition: [0, 4]})).not.toContain(displayMarker)); + displayMarker.destroy(); + }); + }); +}); diff --git a/spec/marker-layer-spec.js b/spec/marker-layer-spec.js new file mode 100644 index 0000000000..cd5eca06ee --- /dev/null +++ b/spec/marker-layer-spec.js @@ -0,0 +1,397 @@ +const {uniq, times} = require('underscore-plus'); +const TextBuffer = require('../src/text-buffer'); + +describe("MarkerLayer", function() { + let buffer, layer1, layer2; + + beforeEach(function() { + jasmine.addCustomEqualityTester(require("underscore-plus").isEqual); + buffer = new TextBuffer({text: "abcdefghijklmnopqrstuvwxyz"}); + layer1 = buffer.addMarkerLayer(); + layer2 = buffer.addMarkerLayer(); + }); + + it("ensures that marker ids are unique across layers", function() { + times(5, function() { + buffer.markRange([[0, 3], [0, 6]]); + layer1.markRange([[0, 4], [0, 7]]); + layer2.markRange([[0, 5], [0, 8]]); + }); + + const ids = buffer.getMarkers() + .concat(layer1.getMarkers()) + .concat(layer2.getMarkers()) + .map(marker => marker.id); + + expect(uniq(ids).length).toEqual(ids.length); + }); + + it("updates each layer's markers when the text changes", function() { + const defaultMarker = buffer.markRange([[0, 3], [0, 6]]); + const layer1Marker = layer1.markRange([[0, 4], [0, 7]]); + const layer2Marker = layer2.markRange([[0, 5], [0, 8]]); + + buffer.setTextInRange([[0, 1], [0, 2]], "BBB"); + expect(defaultMarker.getRange()).toEqual([[0, 5], [0, 8]]); + expect(layer1Marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(layer2Marker.getRange()).toEqual([[0, 7], [0, 10]]); + + layer2.destroy(); + expect(layer2.isAlive()).toBe(false); + expect(layer2.isDestroyed()).toBe(true); + + expect(layer1.isAlive()).toBe(true); + expect(layer1.isDestroyed()).toBe(false); + + buffer.undo(); + expect(defaultMarker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(layer1Marker.getRange()).toEqual([[0, 4], [0, 7]]); + + expect(layer2Marker.isDestroyed()).toBe(true); + expect(layer2Marker.getRange()).toEqual([[0, 0], [0, 0]]); +}); + + it("emits onDidCreateMarker events synchronously when markers are created", function() { + const createdMarkers = []; + layer1.onDidCreateMarker(marker => createdMarkers.push(marker)); + const marker = layer1.markRange([[0, 1], [2, 3]]); + expect(createdMarkers).toEqual([marker]); +}); + + it("does not emit marker events on the TextBuffer for non-default layers", function() { + let updateEventCount; + let createEventCount = (updateEventCount = 0); + buffer.onDidCreateMarker(() => createEventCount++); + buffer.onDidUpdateMarkers(() => updateEventCount++); + + const marker1 = buffer.markRange([[0, 1], [0, 2]]); + marker1.setRange([[0, 1], [0, 3]]); + + expect(createEventCount).toBe(1); + expect(updateEventCount).toBe(2); + + const marker2 = layer1.markRange([[0, 1], [0, 2]]); + marker2.setRange([[0, 1], [0, 3]]); + + expect(createEventCount).toBe(1); + expect(updateEventCount).toBe(2); + }); + + describe("when destroyInvalidatedMarkers is enabled for the layer", () => { + it("destroys markers when they are invalidated via a splice", function() { + const layer3 = buffer.addMarkerLayer({destroyInvalidatedMarkers: true}); + + const marker1 = layer3.markRange([[0, 0], [0, 3]], {invalidate: 'inside'}); + const marker2 = layer3.markRange([[0, 2], [0, 6]], {invalidate: 'inside'}); + + const destroyedMarkers = []; + marker1.onDidDestroy(() => destroyedMarkers.push(marker1)); + marker2.onDidDestroy(() => destroyedMarkers.push(marker2)); + + buffer.insert([0, 5], 'x'); + + expect(destroyedMarkers).toEqual([marker2]); + expect(marker2.isDestroyed()).toBe(true); + expect(marker1.isDestroyed()).toBe(false); + }) + }); + + describe("when maintainHistory is enabled for the layer", function() { + let layer3 = null; + + beforeEach(() => layer3 = buffer.addMarkerLayer({maintainHistory: true})); + + it("restores the state of all markers in the layer on undo and redo", function() { + buffer.setText(''); + buffer.transact(() => buffer.append('foo')); + layer3 = buffer.addMarkerLayer({maintainHistory: true}); + + const marker1 = layer3.markRange([[0, 0], [0, 0]], {invalidate: 'never'}); + const marker2 = layer3.markRange([[0, 0], [0, 0]], {invalidate: 'never'}); + + let marker2ChangeCount = 0; + marker2.onDidChange(() => marker2ChangeCount++); + + + buffer.transact(function() { + buffer.append('\n'); + buffer.append('bar'); + + marker1.destroy(); + marker2.setRange([[0, 2], [0, 3]]); + const marker3 = layer3.markRange([[0, 0], [0, 3]], {invalidate: 'never'}); + const marker4 = layer3.markRange([[1, 0], [1, 3]], {invalidate: 'never'}); + expect(marker2ChangeCount).toBe(1); + }); + + let createdMarker = null; + layer3.onDidCreateMarker(m => createdMarker = m); + buffer.undo(); + + expect(buffer.getText()).toBe('foo'); + expect(marker1.isDestroyed()).toBe(false); + expect(createdMarker).toBe(marker1); + let markers = layer3.findMarkers({}); + expect(markers.length).toBe(2); + expect(markers[0]).toBe(marker1); + expect(markers[0].getRange()).toEqual([[0, 0], [0, 0]]); + expect(markers[1].getRange()).toEqual([[0, 0], [0, 0]]); + expect(marker2ChangeCount).toBe(2); + + buffer.redo(); + + expect(buffer.getText()).toBe('foo\nbar'); + markers = layer3.findMarkers({}); + expect(markers.length).toBe(3); + expect(markers[0].getRange()).toEqual([[0, 0], [0, 3]]); + expect(markers[1].getRange()).toEqual([[0, 2], [0, 3]]); + expect(markers[2].getRange()).toEqual([[1, 0], [1, 3]]); + }); + + it("does not undo marker manipulations that aren't associated with text changes", function() { + const marker = layer3.markRange([[0, 6], [0, 9]]); + + // Can't undo changes in a transaction without other buffer changes + buffer.transact(() => marker.setRange([[0, 4], [0, 20]])); + buffer.undo(); + expect(marker.getRange()).toEqual([[0, 4], [0, 20]]); + + // Can undo changes in a transaction with other buffer changes + buffer.transact(function() { + marker.setRange([[0, 5], [0, 9]]); + buffer.setTextInRange([[0, 2], [0, 3]], 'XYZ'); + marker.setRange([[0, 8], [0, 12]]); + }); + + buffer.undo(); + expect(marker.getRange()).toEqual([[0, 4], [0, 20]]); + + buffer.redo(); + expect(marker.getRange()).toEqual([[0, 8], [0, 12]]); + }); + + it("ignores snapshot references to marker layers that no longer exist", function() { + layer3.markRange([[0, 6], [0, 9]]); + buffer.append("stuff"); + layer3.destroy(); + + // Should not throw an exception + buffer.undo(); + }); + }); + + describe("when a role is provided for the layer", () => { + it("getRole() returns its role and keeps track of ids of 'selections' role", function() { + expect(buffer.selectionsMarkerLayerIds.size).toBe(0); + + const selectionsMarkerLayer1 = buffer.addMarkerLayer({role: "selections"}); + expect(selectionsMarkerLayer1.getRole()).toBe("selections"); + + expect(buffer.addMarkerLayer({role: "role-1"}).getRole()).toBe("role-1"); + expect(buffer.addMarkerLayer().getRole()).toBe(undefined); + + expect(buffer.selectionsMarkerLayerIds.size).toBe(1); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe(true); + + const selectionsMarkerLayer2 = buffer.addMarkerLayer({role: "selections"}); + expect(selectionsMarkerLayer2.getRole()).toBe("selections"); + + expect(buffer.selectionsMarkerLayerIds.size).toBe(2); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe(true); + + selectionsMarkerLayer1.destroy(); + selectionsMarkerLayer2.destroy(); + expect(buffer.selectionsMarkerLayerIds.size).toBe(2); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe(true); + expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe(true); + }) + }); + + describe("::findMarkers(params)", () => { + it("does not find markers from other layers", () => { + const defaultMarker = buffer.markRange([[0, 3], [0, 6]]); + const layer1Marker = layer1.markRange([[0, 3], [0, 6]]); + const layer2Marker = layer2.markRange([[0, 3], [0, 6]]); + + expect(buffer.findMarkers({containsPoint: [0, 4]})).toEqual([defaultMarker]); + expect(layer1.findMarkers({containsPoint: [0, 4]})).toEqual([layer1Marker]); + expect(layer2.findMarkers({containsPoint: [0, 4]})).toEqual([layer2Marker]); + }) + }); + + describe("::onDidUpdate", () => { + it("notifies observers at the end of the outermost transaction when markers are created, updated, or destroyed", function() { + let marker1, marker2; + + const displayLayer = buffer.addDisplayLayer(); + let displayLayerDidChange = false; + + let changeCount = 0; + buffer.onDidChange(() => changeCount++); + + let updateCount = 0; + layer1.onDidUpdate(function() { + updateCount++; + if (updateCount === 1) { + expect(changeCount).toBe(0); + buffer.transact(function() { + marker1.setRange([[1, 2], [3, 4]]); + marker2.setRange([[4, 5], [6, 7]]); + }); + } else if (updateCount === 2) { + expect(changeCount).toBe(0); + buffer.transact(function() { + buffer.insert([0, 1], "xxx"); + buffer.insert([0, 1], "yyy"); + }); + } else if (updateCount === 3) { + expect(changeCount).toBe(1); + marker1.destroy(); + marker2.destroy(); + } else if (updateCount === 7) { + expect(changeCount).toBe(2); + expect(displayLayerDidChange).toBe(true, 'Display layer was updated after marker layer.'); + } + }); + + buffer.transact(() => buffer.transact(function() { + marker1 = layer1.markRange([[0, 2], [0, 4]]); + marker2 = layer1.markRange([[0, 6], [0, 8]]); + })); + + expect(updateCount).toBe(5); + + // update events happen immediately when there is no parent transaction + layer1.markRange([[0, 2], [0, 4]]); + expect(updateCount).toBe(6); + + // update events happen after updating display layers when there is no parent transaction. + displayLayer.onDidChange(() => displayLayerDidChange = true); + buffer.undo(); + expect(updateCount).toBe(7); + }); + }); + + describe("::clear()", () => { + it("destroys all of the layer's markers", function(done) { + buffer = new TextBuffer({text: 'abc'}); + const displayLayer = buffer.addDisplayLayer(); + const markerLayer = buffer.addMarkerLayer(); + const displayMarkerLayer = displayLayer.getMarkerLayer(markerLayer.id); + const marker1 = markerLayer.markRange([[0, 1], [0, 2]]); + const marker2 = markerLayer.markRange([[0, 1], [0, 2]]); + const marker3 = markerLayer.markRange([[0, 1], [0, 2]]); + const displayMarker1 = displayMarkerLayer.getMarker(marker1.id); + // intentionally omit a display marker for marker2 just to cover that case + const displayMarker3 = displayMarkerLayer.getMarker(marker3.id); + + let marker1DestroyCount = 0; + let marker2DestroyCount = 0; + let displayMarker1DestroyCount = 0; + let displayMarker3DestroyCount = 0; + let markerLayerUpdateCount = 0; + let displayMarkerLayerUpdateCount = 0; + marker1.onDidDestroy(() => marker1DestroyCount++); + marker2.onDidDestroy(() => marker2DestroyCount++); + displayMarker1.onDidDestroy(() => displayMarker1DestroyCount++); + displayMarker3.onDidDestroy(() => displayMarker3DestroyCount++); + markerLayer.onDidUpdate(function() { + markerLayerUpdateCount++; + if ((markerLayerUpdateCount === 1) && (displayMarkerLayerUpdateCount === 1)) { done(); } + }); + displayMarkerLayer.onDidUpdate(function() { + displayMarkerLayerUpdateCount++; + if ((markerLayerUpdateCount === 1) && (displayMarkerLayerUpdateCount === 1)) { done(); } + }); + + markerLayer.clear(); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(true); + expect(marker3.isDestroyed()).toBe(true); + expect(displayMarker1.isDestroyed()).toBe(true); + expect(displayMarker3.isDestroyed()).toBe(true); + expect(marker1DestroyCount).toBe(1); + expect(marker2DestroyCount).toBe(1); + expect(displayMarker1DestroyCount).toBe(1); + expect(displayMarker3DestroyCount).toBe(1); + expect(markerLayer.getMarkers()).toEqual([]); + expect(displayMarkerLayer.getMarkers()).toEqual([]); + expect(displayMarkerLayer.getMarker(displayMarker3.id)).toBeUndefined(); + }); + }); + + describe("::copy", function() { + it("creates a new marker layer with markers in the same states", function() { + const originalLayer = buffer.addMarkerLayer({maintainHistory: true}); + originalLayer.markRange([[0, 1], [0, 3]]); + originalLayer.markPosition([0, 2]); + + const copy = originalLayer.copy(); + expect(copy).not.toBe(originalLayer); + + const markers = copy.getMarkers(); + expect(markers.length).toBe(2); + expect(markers[0].getRange()).toEqual([[0, 1], [0, 3]]); + expect(markers[1].getRange()).toEqual([[0, 2], [0, 2]]); + expect(markers[1].hasTail()).toBe(false); + }); + + it("copies the marker layer role", function() { + const originalLayer = buffer.addMarkerLayer({maintainHistory: true, role: "selections"}); + const copy = originalLayer.copy(); + expect(copy).not.toBe(originalLayer); + expect(copy.getRole()).toBe("selections"); + expect(buffer.selectionsMarkerLayerIds.has(originalLayer.id)).toBe(true); + expect(buffer.selectionsMarkerLayerIds.has(copy.id)).toBe(true); + expect(buffer.selectionsMarkerLayerIds.size).toBe(2); + }); + }); + + describe("::destroy", () => { + it("destroys the layer's markers", () => { + buffer = new TextBuffer(); + const markerLayer = buffer.addMarkerLayer(); + + const marker1 = markerLayer.markRange([[0, 0], [0, 0]]); + const marker2 = markerLayer.markRange([[0, 0], [0, 0]]); + + const destroyListener = jasmine.createSpy('onDidDestroy listener'); + marker1.onDidDestroy(destroyListener); + + markerLayer.destroy(); + + expect(destroyListener).toHaveBeenCalled(); + expect(marker1.isDestroyed()).toBe(true); + + // Markers states are updated regardless of whether they have an + // `::onDidDestroy` listener. + expect(marker2.isDestroyed()).toBe(true); + }) + }); + + describe("trackDestructionInOnDidCreateMarkerCallbacks", () => { + it("stores a stack trace when destroy is called during onDidCreateMarker callbacks", function() { + layer1.onDidCreateMarker(function(m) { if (destroyInCreateCallback) { return m.destroy(); } }); + + layer1.trackDestructionInOnDidCreateMarkerCallbacks = true; + var destroyInCreateCallback = true; + const marker1 = layer1.markPosition([0, 0]); + expect(marker1.isDestroyed()).toBe(true); + expect(marker1.destroyStackTrace).toBeDefined(); + + destroyInCreateCallback = false; + const marker2 = layer1.markPosition([0, 0]); + expect(marker2.isDestroyed()).toBe(false); + expect(marker2.destroyStackTrace).toBeUndefined(); + marker2.destroy(); + expect(marker2.isDestroyed()).toBe(true); + expect(marker2.destroyStackTrace).toBeUndefined(); + + destroyInCreateCallback = true; + layer1.trackDestructionInOnDidCreateMarkerCallbacks = false; + const marker3 = layer1.markPosition([0, 0]); + expect(marker3.isDestroyed()).toBe(true); + expect(marker3.destroyStackTrace).toBeUndefined(); + }); + }); +}); diff --git a/spec/marker-spec.js b/spec/marker-spec.js new file mode 100644 index 0000000000..e878f9c41c --- /dev/null +++ b/spec/marker-spec.js @@ -0,0 +1,892 @@ +const {difference, times, uniq} = require('underscore-plus'); +const TextBuffer = require('../src/text-buffer'); + +describe("Marker", function() { + let buffer, markerCreations, markersUpdatedCount; + + beforeEach(function() { + jasmine.addCustomEqualityTester(require("underscore-plus").isEqual); + buffer = new TextBuffer({text: "abcdefghijklmnopqrstuvwxyz"}); + markerCreations = []; + buffer.onDidCreateMarker(marker => markerCreations.push(marker)); + markersUpdatedCount = 0; + buffer.onDidUpdateMarkers(() => markersUpdatedCount++); + }); + + describe("creation", function() { + describe("TextBuffer::markRange(range, properties)", function() { + it("creates a marker for the given range with the given properties", function() { + const marker = buffer.markRange([[0, 3], [0, 6]]); + expect(marker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(marker.getHeadPosition()).toEqual([0, 6]); + expect(marker.getTailPosition()).toEqual([0, 3]); + expect(marker.isReversed()).toBe(false); + expect(marker.hasTail()).toBe(true); + expect(markerCreations).toEqual([marker]); + expect(markersUpdatedCount).toBe(1); + }); + + it("allows a reversed marker to be created", function() { + const marker = buffer.markRange([[0, 3], [0, 6]], {reversed: true}); + expect(marker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(marker.getHeadPosition()).toEqual([0, 3]); + expect(marker.getTailPosition()).toEqual([0, 6]); + expect(marker.isReversed()).toBe(true); + expect(marker.hasTail()).toBe(true); + }); + + it("allows an invalidation strategy to be assigned", function() { + const marker = buffer.markRange([[0, 3], [0, 6]], {invalidate: 'inside'}); + expect(marker.getInvalidationStrategy()).toBe('inside'); + }); + + it("allows an exclusive marker to be created independently of its invalidation strategy", function() { + const layer = buffer.addMarkerLayer({maintainHistory: true}); + const marker1 = layer.markRange([[0, 3], [0, 6]], {invalidate: 'overlap', exclusive: true}); + const marker2 = marker1.copy(); + const marker3 = marker1.copy({exclusive: false}); + const marker4 = marker1.copy({exclusive: null, invalidate: 'inside'}); + + buffer.insert([0, 3], 'something'); + + expect(marker1.getStartPosition()).toEqual([0, 12]); + expect(marker1.isExclusive()).toBe(true); + expect(marker2.getStartPosition()).toEqual([0, 12]); + expect(marker2.isExclusive()).toBe(true); + expect(marker3.getStartPosition()).toEqual([0, 3]); + expect(marker3.isExclusive()).toBe(false); + expect(marker4.getStartPosition()).toEqual([0, 12]); + expect(marker4.isExclusive()).toBe(true); + }); + + it("allows custom state to be assigned", function() { + const marker = buffer.markRange([[0, 3], [0, 6]], {foo: 1, bar: 2}); + expect(marker.getProperties()).toEqual({foo: 1, bar: 2}); + }); + + it("clips the range before creating a marker with it", function() { + const marker = buffer.markRange([[-100, -100], [100, 100]]); + expect(marker.getRange()).toEqual([[0, 0], [0, 26]]); + }); + + it("throws an error if an invalid point is given", function() { + const marker1 = buffer.markRange([[0, 1], [0, 2]]); + + expect(() => buffer.markRange([[0, NaN], [0, 2]])) + .toThrowError("Invalid Point: (0, NaN)"); + expect(() => buffer.markRange([[0, 1], [0, NaN]])) + .toThrowError("Invalid Point: (0, NaN)"); + + expect(buffer.findMarkers({})).toEqual([marker1]); + expect(buffer.getMarkers()).toEqual([marker1]); + }); + + it("allows arbitrary properties to be assigned", function() { + const marker = buffer.markRange([[0, 6], [0, 8]], {foo: 'bar'}); + expect(marker.getProperties()).toEqual({foo: 'bar'}); + }); + }); + + describe("TextBuffer::markPosition(position, properties)", function() { + it("creates a tail-less marker at the given position", function() { + const marker = buffer.markPosition([0, 6]); + expect(marker.getRange()).toEqual([[0, 6], [0, 6]]); + expect(marker.getHeadPosition()).toEqual([0, 6]); + expect(marker.getTailPosition()).toEqual([0, 6]); + expect(marker.isReversed()).toBe(false); + expect(marker.hasTail()).toBe(false); + expect(markerCreations).toEqual([marker]); + }); + + it("allows an invalidation strategy to be assigned", function() { + const marker = buffer.markPosition([0, 3], {invalidate: 'inside'}); + expect(marker.getInvalidationStrategy()).toBe('inside'); + }); + + it("throws an error if an invalid point is given", function() { + const marker1 = buffer.markPosition([0, 1]); + + expect(() => buffer.markPosition([0, NaN])) + .toThrowError("Invalid Point: (0, NaN)"); + + expect(buffer.findMarkers({})).toEqual([marker1]); + expect(buffer.getMarkers()).toEqual([marker1]); + }); + + it("allows arbitrary properties to be assigned", function() { + const marker = buffer.markPosition([0, 6], {foo: 'bar'}); + expect(marker.getProperties()).toEqual({foo: 'bar'}); + }); + }); + }); + + describe("direct updates", function() { + let marker, changes; + + beforeEach(function() { + marker = buffer.markRange([[0, 6], [0, 9]]); + changes = []; + markersUpdatedCount = 0; + marker.onDidChange(change => changes.push(change)); + }); + + describe("::setHeadPosition(position, state)", function() { + it("sets the head position of the marker, flipping its orientation if necessary", function() { + marker.setHeadPosition([0, 12]); + expect(marker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(marker.isReversed()).toBe(false); + expect(markersUpdatedCount).toBe(1); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 12], + oldTailPosition: [0, 6], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 3]); + expect(markersUpdatedCount).toBe(2); + expect(marker.getRange()).toEqual([[0, 3], [0, 6]]); + expect(marker.isReversed()).toBe(true); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 3], + oldTailPosition: [0, 6], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 9]); + expect(markersUpdatedCount).toBe(3); + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isReversed()).toBe(false); + expect(changes).toEqual([{ + oldHeadPosition: [0, 3], newHeadPosition: [0, 9], + oldTailPosition: [0, 6], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + }); + + it("does not give the marker a tail if it doesn't have one already", function() { + marker.clearTail(); + expect(marker.hasTail()).toBe(false); + marker.setHeadPosition([0, 15]); + expect(marker.hasTail()).toBe(false); + expect(marker.getRange()).toEqual([[0, 15], [0, 15]]); + }); + + it("does not notify ::onDidChange observers and returns false if the position isn't actually changed", function() { + expect(marker.setHeadPosition(marker.getHeadPosition())).toBe(false); + expect(markersUpdatedCount).toBe(0); + expect(changes.length).toBe(0); + }); + + it("clips the assigned position", function() { + marker.setHeadPosition([100, 100]); + expect(marker.getHeadPosition()).toEqual([0, 26]); + }); + }); + + describe("::setTailPosition(position, state)", function() { + it("sets the head position of the marker, flipping its orientation if necessary", function() { + marker.setTailPosition([0, 3]); + expect(marker.getRange()).toEqual([[0, 3], [0, 9]]); + expect(marker.isReversed()).toBe(false); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 9], + oldTailPosition: [0, 6], newTailPosition: [0, 3], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setTailPosition([0, 12]); + expect(marker.getRange()).toEqual([[0, 9], [0, 12]]); + expect(marker.isReversed()).toBe(true); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 9], + oldTailPosition: [0, 3], newTailPosition: [0, 12], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setTailPosition([0, 6]); + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isReversed()).toBe(false); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 9], + oldTailPosition: [0, 12], newTailPosition: [0, 6], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + }); + + it("plants the tail of the marker if it does not have a tail", function() { + marker.clearTail(); + expect(marker.hasTail()).toBe(false); + marker.setTailPosition([0, 0]); + expect(marker.hasTail()).toBe(true); + expect(marker.getRange()).toEqual([[0, 0], [0, 9]]); + }); + + it("does not notify ::onDidChange observers and returns false if the position isn't actually changed", function() { + expect(marker.setTailPosition(marker.getTailPosition())).toBe(false); + expect(changes.length).toBe(0); + }); + + it("clips the assigned position", function() { + marker.setTailPosition([100, 100]); + expect(marker.getTailPosition()).toEqual([0, 26]); + }); + }); + + describe("::setRange(range, options)", function() { + it("sets the head and tail position simultaneously, flipping the orientation if the 'isReversed' option is true", function() { + marker.setRange([[0, 8], [0, 12]]); + expect(marker.getRange()).toEqual([[0, 8], [0, 12]]); + expect(marker.isReversed()).toBe(false); + expect(marker.getHeadPosition()).toEqual([0, 12]); + expect(marker.getTailPosition()).toEqual([0, 8]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 12], + oldTailPosition: [0, 6], newTailPosition: [0, 8], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setRange([[0, 3], [0, 9]], {reversed: true}); + expect(marker.getRange()).toEqual([[0, 3], [0, 9]]); + expect(marker.isReversed()).toBe(true); + expect(marker.getHeadPosition()).toEqual([0, 3]); + expect(marker.getTailPosition()).toEqual([0, 9]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 3], + oldTailPosition: [0, 8], newTailPosition: [0, 9], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + }); + + it("plants the tail of the marker if it does not have a tail", function() { + marker.clearTail(); + expect(marker.hasTail()).toBe(false); + marker.setRange([[0, 1], [0, 10]]); + expect(marker.hasTail()).toBe(true); + expect(marker.getRange()).toEqual([[0, 1], [0, 10]]); + }); + + it("clips the assigned range", function() { + marker.setRange([[-100, -100], [100, 100]]); + expect(marker.getRange()).toEqual([[0, 0], [0, 26]]); + }); + + it("emits the right events when called inside of an ::onDidChange handler", function() { + marker.onDidChange(function(change) { + if (marker.getHeadPosition().isEqual([0, 5])) { + marker.setHeadPosition([0, 6]); + } + }); + + marker.setHeadPosition([0, 5]); + + const headPositions = (() => { + const result = []; + for (var {oldHeadPosition, newHeadPosition} of changes) { + result.push({old: oldHeadPosition, new: newHeadPosition}); + } + return result; + })(); + + expect(headPositions).toEqual([ + {old: [0, 9], new: [0, 5]}, + {old: [0, 5], new: [0, 6]} + ]); + }); + + it("throws an error if an invalid range is given", function() { + expect(() => marker.setRange([[0, NaN], [0, 12]])) + .toThrowError("Invalid Point: (0, NaN)"); + + expect(buffer.findMarkers({})).toEqual([marker]); + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + }); + }); + + describe("::clearTail() / ::plantTail()", () => { + it("clears the tail / plants the tail at the current head position", function() { + marker.setRange([[0, 6], [0, 9]], {reversed: true}); + + changes = []; + marker.clearTail(); + expect(marker.getRange()).toEqual([[0, 6], [0, 6]]); + expect(marker.hasTail()).toBe(false); + expect(marker.isReversed()).toBe(false); + + expect(changes).toEqual([{ + oldHeadPosition: [0, 6], newHeadPosition: [0, 6], + oldTailPosition: [0, 9], newTailPosition: [0, 6], + hadTail: true, hasTail: false, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 12]); + expect(marker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 6], newHeadPosition: [0, 12], + oldTailPosition: [0, 6], newTailPosition: [0, 12], + hadTail: false, hasTail: false, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.plantTail(); + expect(marker.hasTail()).toBe(true); + expect(marker.isReversed()).toBe(false); + expect(marker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 12], + oldTailPosition: [0, 12], newTailPosition: [0, 12], + hadTail: false, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.setHeadPosition([0, 15]); + expect(marker.getRange()).toEqual([[0, 12], [0, 15]]); + expect(changes).toEqual([{ + oldHeadPosition: [0, 12], newHeadPosition: [0, 15], + oldTailPosition: [0, 12], newTailPosition: [0, 12], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: false + }]); + + changes = []; + marker.plantTail(); + expect(marker.getRange()).toEqual([[0, 12], [0, 15]]); + expect(changes).toEqual([]); + }); + }); + + describe("::setProperties(properties)", () => { + it("merges the given properties into the current properties", function() { + marker.setProperties({foo: 1}); + expect(marker.getProperties()).toEqual({foo: 1}); + marker.setProperties({bar: 2}); + expect(marker.getProperties()).toEqual({foo: 1, bar: 2}); + expect(markersUpdatedCount).toBe(2); + }); + }); + }); + + describe("indirect updates (due to buffer changes)", function() { + let allStrategies, neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker; + + beforeEach(function() { + overlapMarker = buffer.markRange([[0, 6], [0, 9]], {invalidate: 'overlap'}); + neverMarker = overlapMarker.copy({invalidate: 'never'}); + surroundMarker = overlapMarker.copy({invalidate: 'surround'}); + insideMarker = overlapMarker.copy({invalidate: 'inside'}); + touchMarker = overlapMarker.copy({invalidate: 'touch'}); + allStrategies = [neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker]; + markersUpdatedCount = 0; + }); + + it("defers notifying Marker::onDidChange observers until after notifying Buffer::onDidChange observers", function() { + let marker; + for (marker of allStrategies) { + marker.changes = []; + marker.onDidChange(change => marker.changes.push(change)); + } + + let changedCount = 0; + const changeSubscription = buffer.onDidChange(function(change) { + changedCount++; + expect(markersUpdatedCount).toBe(0); + for (marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 8], [0, 11]]); + expect(marker.isValid()).toBe(true); + expect(marker.changes.length).toBe(0); + } + }); + + buffer.setTextInRange([[0, 1], [0, 2]], "ABC"); + + expect(changedCount).toBe(1); + + for (marker of allStrategies) { + expect(marker.changes).toEqual([{ + oldHeadPosition: [0, 9], newHeadPosition: [0, 11], + oldTailPosition: [0, 6], newTailPosition: [0, 8], + hadTail: true, hasTail: true, + wasValid: true, isValid: true, + oldProperties: {}, newProperties: {}, + textChanged: true + }]); + } + expect(markersUpdatedCount).toBe(1); + + for (marker of allStrategies) { marker.changes = []; } + changeSubscription.dispose(); + changedCount = 0; + markersUpdatedCount = 0; + buffer.onDidChange(function(change) { + changedCount++; + expect(markersUpdatedCount).toBe(0); + for (marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isValid()).toBe(true); + expect(marker.changes.length).toBe(0); + } + }); + }); + + it("notifies ::onDidUpdateMarkers observers even if there are no Marker::onDidChange observers", function() { + expect(markersUpdatedCount).toBe(0); + buffer.insert([0, 0], "123"); + expect(markersUpdatedCount).toBe(1); + overlapMarker.setRange([[0, 1], [0, 2]]); + expect(markersUpdatedCount).toBe(2); + }); + + it("emits onDidChange events when undoing/redoing text changes that move the marker", function() { + const marker = buffer.markRange([[0, 4], [0, 8]]); + buffer.insert([0, 0], 'ABCD'); + + const changes = []; + marker.onDidChange(change => changes.push(change)); + buffer.undo(); + expect(changes.length).toBe(1); + expect(changes[0].newHeadPosition).toEqual([0, 8]); + buffer.redo(); + expect(changes.length).toBe(2); + expect(changes[1].newHeadPosition).toEqual([0, 12]); + }); + + describe("when a change precedes a marker", () => { + it("shifts the marker based on the characters inserted or removed by the change", function() { + let marker; + buffer.setTextInRange([[0, 1], [0, 2]], "ABC"); + for (marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 8], [0, 11]]); + expect(marker.isValid()).toBe(true); + } + + buffer.setTextInRange([[0, 1], [0, 1]], '\nDEF'); + for (marker of allStrategies) { + expect(marker.getRange()).toEqual([[1, 10], [1, 13]]); + expect(marker.isValid()).toBe(true); + } + }) + }); + + describe("when a change follows a marker", () => { + it("does not shift the marker", function() { + buffer.setTextInRange([[0, 10], [0, 12]], "ABC"); + for (var marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isValid()).toBe(true); + } + }) + }); + + describe("when a change starts at a marker's start position", function() { + describe("when the marker has a tail", () => { + it("interprets the change as being inside the marker for all invalidation strategies", function() { + buffer.setTextInRange([[0, 6], [0, 7]], "ABC"); + + for (var marker of difference(allStrategies, [insideMarker, touchMarker])) { + expect(marker.getRange()).toEqual([[0, 6], [0, 11]]); + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.getRange()).toEqual([[0, 9], [0, 11]]); + expect(insideMarker.isValid()).toBe(false); + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 11]]); + expect(touchMarker.isValid()).toBe(false); + }) + }); + + describe("when the marker has no tail", () => { + it("interprets the change as being outside the marker for all invalidation strategies", function() { + let marker; + for (marker of allStrategies) { + marker.setRange([[0, 6], [0, 11]], {reversed: true}); + marker.clearTail(); + expect(marker.getRange()).toEqual([[0, 6], [0, 6]]); + } + + buffer.setTextInRange([[0, 6], [0, 6]], "ABC"); + + for (marker of difference(allStrategies, [touchMarker])) { + expect(marker.getRange()).toEqual([[0, 9], [0, 9]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 9], [0, 9]]); + expect(touchMarker.isValid()).toBe(false); + + buffer.setTextInRange([[0, 9], [0, 9]], "DEF"); + + for (marker of difference(allStrategies, [touchMarker])) { + expect(marker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 12], [0, 12]]); + expect(touchMarker.isValid()).toBe(false); + }); + }); + }); + + describe("when a change ends at a marker's start position but starts before it", () => { + it("interprets the change as being outside the marker for all invalidation strategies", function() { + buffer.setTextInRange([[0, 4], [0, 6]], "ABC"); + + for (var marker of difference(allStrategies, [touchMarker])) { + expect(marker.getRange()).toEqual([[0, 7], [0, 10]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 7], [0, 10]]); + expect(touchMarker.isValid()).toBe(false); + }); + }); + + describe("when a change starts and ends at a marker's start position", () => { + it("interprets the change as being inside the marker for all invalidation strategies except 'inside'", function() { + buffer.insert([0, 6], "ABC"); + + for (var marker of difference(allStrategies, [insideMarker, touchMarker])) { + expect(marker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.getRange()).toEqual([[0, 9], [0, 12]]); + expect(insideMarker.isValid()).toBe(true); + + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(touchMarker.isValid()).toBe(false); + }); + }); + + describe("when a change starts at a marker's end position", function() { + describe("when the change is an insertion", () => { + it("interprets the change as being inside the marker for all invalidation strategies except 'inside'", function() { + buffer.setTextInRange([[0, 9], [0, 9]], "ABC"); + + for (var marker of difference(allStrategies, [insideMarker, touchMarker])) { + expect(marker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(insideMarker.isValid()).toBe(true); + + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 12]]); + expect(touchMarker.isValid()).toBe(false); + }); + }); + + describe("when the change replaces some existing text", () => { + it("interprets the change as being outside the marker for all invalidation strategies", function() { + buffer.setTextInRange([[0, 9], [0, 11]], "ABC"); + + for (var marker of difference(allStrategies, [touchMarker])) { + expect(marker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(marker.isValid()).toBe(true); + } + + expect(touchMarker.getRange()).toEqual([[0, 6], [0, 9]]); + expect(touchMarker.isValid()).toBe(false); + }) + }); + }); + + describe("when a change surrounds a marker", () => { + it("truncates the marker to the end of the change and invalidates every invalidation strategy except 'never'", function() { + let marker; + buffer.setTextInRange([[0, 5], [0, 10]], "ABC"); + + for (marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 8], [0, 8]]); + } + + for (marker of difference(allStrategies, [neverMarker])) { + expect(marker.isValid()).toBe(false); + } + + expect(neverMarker.isValid()).toBe(true); + }) + }); + + describe("when a change is inside a marker", () => { + it("adjusts the marker's end position and invalidates markers with an 'inside' or 'touch' strategy", function() { + let marker; + buffer.setTextInRange([[0, 7], [0, 8]], "AB"); + + for (marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 6], [0, 10]]); + } + + for (marker of difference(allStrategies, [insideMarker, touchMarker])) { + expect(marker.isValid()).toBe(true); + } + + expect(insideMarker.isValid()).toBe(false); + expect(touchMarker.isValid()).toBe(false); + }); + }); + + describe("when a change overlaps the start of a marker", () => { + it("moves the start of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", function() { + buffer.setTextInRange([[0, 5], [0, 7]], "ABC"); + + for (var marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 8], [0, 10]]); + } + + expect(neverMarker.isValid()).toBe(true); + expect(surroundMarker.isValid()).toBe(true); + expect(overlapMarker.isValid()).toBe(false); + expect(insideMarker.isValid()).toBe(false); + expect(touchMarker.isValid()).toBe(false); + }); + }); + + describe("when a change overlaps the end of a marker", () => { + it("moves the end of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", function() { + buffer.setTextInRange([[0, 8], [0, 10]], "ABC"); + + for (var marker of allStrategies) { + expect(marker.getRange()).toEqual([[0, 6], [0, 11]]); + } + + expect(neverMarker.isValid()).toBe(true); + expect(surroundMarker.isValid()).toBe(true); + expect(overlapMarker.isValid()).toBe(false); + expect(insideMarker.isValid()).toBe(false); + expect(touchMarker.isValid()).toBe(false); + }); + }); + + describe("when multiple changes occur in a transaction", () => { + it("emits one change event for each marker that was indirectly updated", function() { + let strategy; + for (strategy of allStrategies) { + strategy.changes = []; + strategy.onDidChange(change => strategy.changes.push(change)); + } + + buffer.transact(function() { + buffer.insert([0, 7], "."); + buffer.append("!"); + + for (strategy of allStrategies) { + expect(strategy.changes.length).toBe(0); + } + + neverMarker.setRange([[0, 0], [0, 1]]); + }); + + expect(neverMarker.changes).toEqual([{ + oldHeadPosition: [0, 9], + newHeadPosition: [0, 1], + oldTailPosition: [0, 6], + newTailPosition: [0, 0], + wasValid: true, + isValid: true, + hadTail: true, + hasTail: true, + oldProperties: {}, + newProperties: {}, + textChanged: false + }]); + + expect(insideMarker.changes).toEqual([{ + oldHeadPosition: [0, 9], + newHeadPosition: [0, 10], + oldTailPosition: [0, 6], + newTailPosition: [0, 6], + wasValid: true, + isValid: false, + hadTail: true, + hasTail: true, + oldProperties: {}, + newProperties: {}, + textChanged: true + }]); + }); + }); + }); + + describe("destruction", function() { + it("removes the marker from the buffer, marks it destroyed and invalid, and notifies ::onDidDestroy observers", function() { + let destroyedHandler; + const marker = buffer.markRange([[0, 3], [0, 6]]); + expect(buffer.getMarker(marker.id)).toBe(marker); + marker.onDidDestroy(destroyedHandler = jasmine.createSpy("destroyedHandler")); + + marker.destroy(); + + expect(destroyedHandler.calls.count()).toBe(1); + expect(buffer.getMarker(marker.id)).toBeUndefined(); + expect(marker.isDestroyed()).toBe(true); + expect(marker.isValid()).toBe(false); + expect(marker.getRange()).toEqual([[0, 0], [0, 0]]); + }); + + it("handles markers deleted in event handlers", function() { + let marker1 = buffer.markRange([[0, 3], [0, 6]]); + let marker2 = marker1.copy(); + let marker3 = marker1.copy(); + + marker1.onDidChange(function() { + marker1.destroy(); + marker2.destroy(); + marker3.destroy(); + }); + + // doesn't blow up. + buffer.insert([0, 0], "!"); + + marker1 = buffer.markRange([[0, 3], [0, 6]]); + marker2 = marker1.copy(); + marker3 = marker1.copy(); + + marker1.onDidChange(function() { + marker1.destroy(); + marker2.destroy(); + marker3.destroy(); + }); + + // doesn't blow up. + buffer.undo(); + }); + + it("does not reinsert the marker if its range is later updated", function() { + const marker = buffer.markRange([[0, 3], [0, 6]]); + marker.destroy(); + expect(buffer.findMarkers({intersectsRow: 0})).toEqual([]); + marker.setRange([[0, 0], [0, 9]]); + expect(buffer.findMarkers({intersectsRow: 0})).toEqual([]); + }); + + it("does not blow up when destroy is called twice", function() { + const marker = buffer.markRange([[0, 3], [0, 6]]); + marker.destroy(); + marker.destroy(); + }); + }); + + describe("TextBuffer::findMarkers(properties)", function() { + let marker1, marker2, marker3, marker4; + + beforeEach(function() { + marker1 = buffer.markRange([[0, 0], [0, 3]], {class: 'a'}); + marker2 = buffer.markRange([[0, 0], [0, 5]], {class: 'a', invalidate: 'surround'}); + marker3 = buffer.markRange([[0, 4], [0, 7]], {class: 'a'}); + marker4 = buffer.markRange([[0, 0], [0, 7]], {class: 'b', invalidate: 'never'}); + }); + + it("can find markers based on custom properties", function() { + expect(buffer.findMarkers({class: 'a'})).toEqual([marker2, marker1, marker3]); + expect(buffer.findMarkers({class: 'b'})).toEqual([marker4]); + }); + + it("can find markers based on their invalidation strategy", function() { + expect(buffer.findMarkers({invalidate: 'overlap'})).toEqual([marker1, marker3]); + expect(buffer.findMarkers({invalidate: 'surround'})).toEqual([marker2]); + expect(buffer.findMarkers({invalidate: 'never'})).toEqual([marker4]); + }); + + it("can find markers that start or end at a given position", function() { + expect(buffer.findMarkers({startPosition: [0, 0]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({startPosition: [0, 0], class: 'a'})).toEqual([marker2, marker1]); + expect(buffer.findMarkers({startPosition: [0, 0], endPosition: [0, 3], class: 'a'})).toEqual([marker1]); + expect(buffer.findMarkers({startPosition: [0, 4], endPosition: [0, 7]})).toEqual([marker3]); + expect(buffer.findMarkers({endPosition: [0, 7]})).toEqual([marker4, marker3]); + expect(buffer.findMarkers({endPosition: [0, 7], class: 'b'})).toEqual([marker4]); + }); + + it("can find markers that start or end at a given range", function() { + expect(buffer.findMarkers({startsInRange: [[0, 0], [0, 4]]})).toEqual([marker4, marker2, marker1, marker3]); + expect(buffer.findMarkers({startsInRange: [[0, 0], [0, 4]], class: 'a'})).toEqual([marker2, marker1, marker3]); + expect(buffer.findMarkers({startsInRange: [[0, 0], [0, 4]], endsInRange: [[0, 3], [0, 6]]})).toEqual([marker2, marker1]); + expect(buffer.findMarkers({endsInRange: [[0, 5], [0, 7]]})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that contain a given point", function() { + expect(buffer.findMarkers({containsPosition: [0, 0]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({containsPoint: [0, 0]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({containsPoint: [0, 1], class: 'a'})).toEqual([marker2, marker1]); + expect(buffer.findMarkers({containsPoint: [0, 4]})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that contain a given range", function() { + expect(buffer.findMarkers({containsRange: [[0, 1], [0, 4]]})).toEqual([marker4, marker2]); + expect(buffer.findMarkers({containsRange: [[0, 4], [0, 1]]})).toEqual([marker4, marker2]); + expect(buffer.findMarkers({containsRange: [[0, 1], [0, 3]]})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({containsRange: [[0, 6], [0, 7]]})).toEqual([marker4, marker3]); + }); + + it("can find markers that intersect a given range", function() { + expect(buffer.findMarkers({intersectsRange: [[0, 4], [0, 6]]})).toEqual([marker4, marker2, marker3]); + expect(buffer.findMarkers({intersectsRange: [[0, 0], [0, 2]]})).toEqual([marker4, marker2, marker1]); + }); + + it("can find markers that start or end at a given row", function() { + buffer.setTextInRange([[0, 7], [0, 7]], '\n'); + buffer.setTextInRange([[0, 3], [0, 4]], ' \n'); + expect(buffer.findMarkers({startRow: 0})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({startRow: 1})).toEqual([marker3]); + expect(buffer.findMarkers({endRow: 2})).toEqual([marker4, marker3]); + expect(buffer.findMarkers({startRow: 0, endRow: 2})).toEqual([marker4]); + }); + + it("can find markers that intersect a given row", function() { + buffer.setTextInRange([[0, 7], [0, 7]], '\n'); + buffer.setTextInRange([[0, 3], [0, 4]], ' \n'); + expect(buffer.findMarkers({intersectsRow: 0})).toEqual([marker4, marker2, marker1]); + expect(buffer.findMarkers({intersectsRow: 1})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that intersect a given range", function() { + buffer.setTextInRange([[0, 7], [0, 7]], '\n'); + buffer.setTextInRange([[0, 3], [0, 4]], ' \n'); + expect(buffer.findMarkers({intersectsRowRange: [1, 2]})).toEqual([marker4, marker2, marker3]); + }); + + it("can find markers that are contained within a certain range, inclusive", function() { + expect(buffer.findMarkers({containedInRange: [[0, 0], [0, 6]]})).toEqual([marker2, marker1]); + expect(buffer.findMarkers({containedInRange: [[0, 4], [0, 7]]})).toEqual([marker3]); + }); + }); +}); diff --git a/spec/point-spec.js b/spec/point-spec.js new file mode 100644 index 0000000000..e3c16c53e8 --- /dev/null +++ b/spec/point-spec.js @@ -0,0 +1,204 @@ +/* +* decaffeinate suggestions: +* DS102: Remove unnecessary code created because of implicit returns +* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md +*/ +const Point = require('../src/point'); + +describe("Point", function() { + beforeEach(() => jasmine.addCustomEqualityTester(require("underscore-plus").isEqual)); + + describe("::negate()", () => it("should negate the row and column", function() { + expect(new Point( 0, 0).negate().toString()).toBe("(0, 0)"); + expect(new Point( 1, 2).negate().toString()).toBe("(-1, -2)"); + expect(new Point(-1, -2).negate().toString()).toBe("(1, 2)"); + expect(new Point(-1, 2).negate().toString()).toBe("(1, -2)"); + })); + + describe("::fromObject(object, copy)", function() { + it("returns a new Point if object is point-compatible array ", function() { + expect(Point.fromObject([1, 3])).toEqual(Point(1, 3)); + expect(Point.fromObject([Infinity, Infinity])).toEqual(Point.INFINITY); + }); + + it("returns the copy of object if it is an instanceof Point", function() { + const origin = Point(0, 0); + expect(Point.fromObject(origin, false) === origin).toBe(true); + expect(Point.fromObject(origin, true) === origin).toBe(false); + }); + }); + + describe("::copy()", () => it("returns a copy of the object", function() { + expect(Point(3, 4).copy()).toEqual(Point(3, 4)); + expect(Point.ZERO.copy()).toEqual([0, 0]); + })); + + describe("::negate()", () => it("returns a new point with row and column negated", function() { + expect(Point(3, 4).negate()).toEqual(Point(-3, -4)); + expect(Point.ZERO.negate()).toEqual([0, 0]); + })); + + describe("::freeze()", () => it("makes the Point object immutable", function() { + expect(Object.isFrozen(Point(3, 4).freeze())).toBe(true); + expect(Object.isFrozen(Point.ZERO.freeze())).toBe(true); + })); + + describe("::compare(other)", () => it("returns -1 for <, 0 for =, 1 for > comparisions", function() { + expect(Point(2, 3).compare(Point(2, 6))).toBe(-1); + expect(Point(2, 3).compare(Point(3, 4))).toBe(-1); + expect(Point(1, 1).compare(Point(1, 1))).toBe(0); + expect(Point(2, 3).compare(Point(2, 0))).toBe(1); + expect(Point(2, 3).compare(Point(1, 3))).toBe(1); + + expect(Point(2, 3).compare([2, 6])).toBe(-1); + expect(Point(2, 3).compare([3, 4])).toBe(-1); + expect(Point(1, 1).compare([1, 1])).toBe(0); + expect(Point(2, 3).compare([2, 0])).toBe(1); + expect(Point(2, 3).compare([1, 3])).toBe(1); + })); + + describe("::isLessThan(other)", () => it("returns a boolean indicating whether a point precedes the given Point ", function() { + expect(Point(2, 3).isLessThan(Point(2, 5))).toBe(true); + expect(Point(2, 3).isLessThan(Point(3, 4))).toBe(true); + expect(Point(2, 3).isLessThan(Point(2, 3))).toBe(false); + expect(Point(2, 3).isLessThan(Point(2, 1))).toBe(false); + expect(Point(2, 3).isLessThan(Point(1, 2))).toBe(false); + + expect(Point(2, 3).isLessThan([2, 5])).toBe(true); + expect(Point(2, 3).isLessThan([3, 4])).toBe(true); + expect(Point(2, 3).isLessThan([2, 3])).toBe(false); + expect(Point(2, 3).isLessThan([2, 1])).toBe(false); + expect(Point(2, 3).isLessThan([1, 2])).toBe(false); + })); + + describe("::isLessThanOrEqual(other)", () => it("returns a boolean indicating whether a point precedes or equal the given Point ", function() { + expect(Point(2, 3).isLessThanOrEqual(Point(2, 5))).toBe(true); + expect(Point(2, 3).isLessThanOrEqual(Point(3, 4))).toBe(true); + expect(Point(2, 3).isLessThanOrEqual(Point(2, 3))).toBe(true); + expect(Point(2, 3).isLessThanOrEqual(Point(2, 1))).toBe(false); + expect(Point(2, 3).isLessThanOrEqual(Point(1, 2))).toBe(false); + + expect(Point(2, 3).isLessThanOrEqual([2, 5])).toBe(true); + expect(Point(2, 3).isLessThanOrEqual([3, 4])).toBe(true); + expect(Point(2, 3).isLessThanOrEqual([2, 3])).toBe(true); + expect(Point(2, 3).isLessThanOrEqual([2, 1])).toBe(false); + expect(Point(2, 3).isLessThanOrEqual([1, 2])).toBe(false); + })); + + describe("::isGreaterThan(other)", () => it("returns a boolean indicating whether a point follows the given Point ", function() { + expect(Point(2, 3).isGreaterThan(Point(2, 5))).toBe(false); + expect(Point(2, 3).isGreaterThan(Point(3, 4))).toBe(false); + expect(Point(2, 3).isGreaterThan(Point(2, 3))).toBe(false); + expect(Point(2, 3).isGreaterThan(Point(2, 1))).toBe(true); + expect(Point(2, 3).isGreaterThan(Point(1, 2))).toBe(true); + + expect(Point(2, 3).isGreaterThan([2, 5])).toBe(false); + expect(Point(2, 3).isGreaterThan([3, 4])).toBe(false); + expect(Point(2, 3).isGreaterThan([2, 3])).toBe(false); + expect(Point(2, 3).isGreaterThan([2, 1])).toBe(true); + expect(Point(2, 3).isGreaterThan([1, 2])).toBe(true); + })); + + describe("::isGreaterThanOrEqual(other)", () => it("returns a boolean indicating whether a point follows or equal the given Point ", function() { + expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 5))).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual(Point(3, 4))).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 3))).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 1))).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual(Point(1, 2))).toBe(true); + + expect(Point(2, 3).isGreaterThanOrEqual([2, 5])).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual([3, 4])).toBe(false); + expect(Point(2, 3).isGreaterThanOrEqual([2, 3])).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual([2, 1])).toBe(true); + expect(Point(2, 3).isGreaterThanOrEqual([1, 2])).toBe(true); + })); + + describe("::isEqual()", () => it("returns if whether two points are equal", function() { + expect(Point(1, 1).isEqual(Point(1, 1))).toBe(true); + expect(Point(1, 1).isEqual([1, 1])).toBe(true); + expect(Point(1, 2).isEqual(Point(3, 3))).toBe(false); + expect(Point(1, 2).isEqual([3, 3])).toBe(false); + })); + + describe("::isPositive()", () => it("returns true if the point represents a forward traversal", function() { + expect(Point(-1, -1).isPositive()).toBe(false); + expect(Point(-1, 0).isPositive()).toBe(false); + expect(Point(-1, Infinity).isPositive()).toBe(false); + expect(Point(0, 0).isPositive()).toBe(false); + + expect(Point(0, 1).isPositive()).toBe(true); + expect(Point(5, 0).isPositive()).toBe(true); + expect(Point(5, -1).isPositive()).toBe(true); + })); + + describe("::isZero()", () => it("returns true if the point is zero", function() { + expect(Point(1, 1).isZero()).toBe(false); + expect(Point(0, 1).isZero()).toBe(false); + expect(Point(1, 0).isZero()).toBe(false); + expect(Point(0, 0).isZero()).toBe(true); + })); + + describe("::min(a, b)", () => it("returns the minimum of two points", function() { + expect(Point.min(Point(3, 4), Point(1, 1))).toEqual(Point(1, 1)); + expect(Point.min(Point(1, 2), Point(5, 6))).toEqual(Point(1, 2)); + expect(Point.min([3, 4], [1, 1])).toEqual([1, 1]); + expect(Point.min([1, 2], [5, 6])).toEqual([1, 2]); + })); + + describe("::max(a, b)", () => it("returns the minimum of two points", function() { + expect(Point.max(Point(3, 4), Point(1, 1))).toEqual(Point(3, 4)); + expect(Point.max(Point(1, 2), Point(5, 6))).toEqual(Point(5, 6)); + expect(Point.max([3, 4], [1, 1])).toEqual([3, 4]); + expect(Point.max([1, 2], [5, 6])).toEqual([5, 6]); + })); + + describe("::translate(delta)", () => it("returns a new point by adding corresponding coordinates", function() { + expect(Point(1, 1).translate(Point(2, 3))).toEqual(Point(3, 4)); + expect(Point.INFINITY.translate(Point(2, 3))).toEqual(Point.INFINITY); + + expect(Point.ZERO.translate([5, 6])).toEqual([5, 6]); + expect(Point(1, 1).translate([3, 4])).toEqual([4, 5]); + })); + + describe("::traverse(delta)", () => it("returns a new point by traversing given rows and columns", function() { + expect(Point(2, 3).traverse(Point(0, 3))).toEqual(Point(2, 6)); + expect(Point(2, 3).traverse([0, 3])).toEqual([2, 6]); + + expect(Point(1, 3).traverse(Point(4, 2))).toEqual([5, 2]); + expect(Point(1, 3).traverse([5, 4])).toEqual([6, 4]); + })); + + describe("::traversalFrom(other)", () => it("returns a point that other has to traverse to get to given point", function() { + expect(Point(2, 5).traversalFrom(Point(2, 3))).toEqual(Point(0, 2)); + expect(Point(2, 3).traversalFrom(Point(2, 5))).toEqual(Point(0, -2)); + expect(Point(2, 3).traversalFrom(Point(2, 3))).toEqual(Point(0, 0)); + + expect(Point(3, 4).traversalFrom(Point(2, 3))).toEqual(Point(1, 4)); + expect(Point(2, 3).traversalFrom(Point(3, 5))).toEqual(Point(-1, 3)); + + expect(Point(2, 5).traversalFrom([2, 3])).toEqual([0, 2]); + expect(Point(2, 3).traversalFrom([2, 5])).toEqual([0, -2]); + expect(Point(2, 3).traversalFrom([2, 3])).toEqual([0, 0]); + + expect(Point(3, 4).traversalFrom([2, 3])).toEqual([1, 4]); + expect(Point(2, 3).traversalFrom([3, 5])).toEqual([-1, 3]); + })); + + describe("::toArray()", () => it("returns an array of row and column", function() { + expect(Point(1, 3).toArray()).toEqual([1, 3]); + expect(Point.ZERO.toArray()).toEqual([0, 0]); + expect(Point.INFINITY.toArray()).toEqual([Infinity, Infinity]); + })); + + describe("::serialize()", () => it("returns an array of row and column", function() { + expect(Point(1, 3).serialize()).toEqual([1, 3]); + expect(Point.ZERO.serialize()).toEqual([0, 0]); + expect(Point.INFINITY.serialize()).toEqual([Infinity, Infinity]); + })); + + describe("::toString()", () => it("returns string representation of Point", function() { + expect(Point(4, 5).toString()).toBe("(4, 5)"); + expect(Point.ZERO.toString()).toBe("(0, 0)"); + expect(Point.INFINITY.toString()).toBe("(Infinity, Infinity)"); + })); +}); diff --git a/spec/range-spec.js b/spec/range-spec.js new file mode 100644 index 0000000000..dc78411e83 --- /dev/null +++ b/spec/range-spec.js @@ -0,0 +1,50 @@ +const Range = require('../src/range'); + +describe("Range", function() { + beforeEach(() => jasmine.addCustomEqualityTester(require("underscore-plus").isEqual)); + + describe("::intersectsWith(other, [exclusive])", function() { + const intersectsWith = function(range1, range2, exclusive) { + range1 = Range.fromObject(range1); + range2 = Range.fromObject(range2); + return range1.intersectsWith(range2, exclusive); + }; + + describe("when the exclusive argument is false (the default)", () => { + it("returns true if the ranges intersect, exclusive of their endpoints", function() { + expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]])).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]])).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]])).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe(false); + }); + }); + + describe("when the exclusive argument is true", () => { + it("returns true if the ranges intersect, exclusive of their endpoints", function() { + expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]], true)).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]], true)).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]], true)).toBe(true); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe(false); + expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe(false); + }) + }); + }); + + describe("::negate()", () => { + it("should negate the start and end points", function() { + expect(new Range([ 0, 0], [ 0, 0]).negate().toString()).toBe("[(0, 0) - (0, 0)]"); + expect(new Range([ 1, 2], [ 3, 4]).negate().toString()).toBe("[(-3, -4) - (-1, -2)]"); + expect(new Range([-1, -2], [-3, -4]).negate().toString()).toBe("[(1, 2) - (3, 4)]"); + expect(new Range([-1, 2], [ 3, -4]).negate().toString()).toBe("[(-3, 4) - (1, -2)]"); + }) + }); +}); diff --git a/spec/text-buffer-spec.js b/spec/text-buffer-spec.js index 1e44cc05ac..4111898c28 100644 --- a/spec/text-buffer-spec.js +++ b/spec/text-buffer-spec.js @@ -1,5 +1,3266 @@ -const path = require('path') -const TextBuffer = require('../src/text-buffer') +/* + * decaffeinate suggestions: + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const fs = require('fs-plus'); +const path = require('path'); +const {join} = path; +const temp = require('temp'); +const {File} = require('pathwatcher'); +const Random = require('random-seed'); +const Point = require('../src/point'); +const Range = require('../src/range'); +const DisplayLayer = require('../src/display-layer'); +const DefaultHistoryProvider = require('../src/default-history-provider'); +const TextBuffer = require('../src/text-buffer'); +const SampleText = fs.readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8'); +const {buildRandomLines, getRandomBufferRange} = require('./helpers/random'); +const NullLanguageMode = require('../src/null-language-mode'); + +describe("TextBuffer", function() { + let buffer = null; + + beforeEach(function() { + temp.track(); + jasmine.addCustomEqualityTester(require("underscore-plus").isEqual); + // When running specs in Atom, setTimeout is spied on by default. + jasmine.useRealClock?.(); + }); + + afterEach(function() { + buffer?.destroy(); + buffer = null; + }); + + describe("construction", function() { + it("can be constructed empty", function() { + buffer = new TextBuffer; + expect(buffer.getLineCount()).toBe(1); + expect(buffer.getText()).toBe(''); + expect(buffer.lineForRow(0)).toBe(''); + expect(buffer.lineEndingForRow(0)).toBe(''); + }); + + it("can be constructed with initial text containing no trailing newline", function() { + const text = "hello\nworld\r\nhow are you doing?\r\nlast"; + buffer = new TextBuffer(text); + expect(buffer.getLineCount()).toBe(4); + expect(buffer.getText()).toBe(text); + expect(buffer.lineForRow(0)).toBe('hello'); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + expect(buffer.lineForRow(1)).toBe('world'); + expect(buffer.lineEndingForRow(1)).toBe('\r\n'); + expect(buffer.lineForRow(2)).toBe('how are you doing?'); + expect(buffer.lineEndingForRow(2)).toBe('\r\n'); + expect(buffer.lineForRow(3)).toBe('last'); + expect(buffer.lineEndingForRow(3)).toBe(''); + }); + + it("can be constructed with initial text containing a trailing newline", function() { + const text = "first\n"; + buffer = new TextBuffer(text); + expect(buffer.getLineCount()).toBe(2); + expect(buffer.getText()).toBe(text); + expect(buffer.lineForRow(0)).toBe('first'); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + expect(buffer.lineForRow(1)).toBe(''); + expect(buffer.lineEndingForRow(1)).toBe(''); + }); + + it("automatically assigns a unique identifier to new buffers", function() { + const bufferIds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(() => new TextBuffer().getId()); + const uniqueBufferIds = new Set(bufferIds); + + expect(uniqueBufferIds.size).toBe(bufferIds.length); + }); + }); + + describe("::destroy()", () => it("clears the buffer's state", function(done) { + const filePath = temp.openSync('atom').path; + buffer = new TextBuffer(); + buffer.setPath(filePath); + buffer.append("a"); + buffer.append("b"); + buffer.destroy(); + + expect(buffer.getText()).toBe(''); + buffer.undo(); + expect(buffer.getText()).toBe(''); + buffer.save().catch(function(error) { + expect(error.message).toMatch(/Can't save destroyed buffer/); + done(); + }); + })); + + describe("::setTextInRange(range, text)", function() { + beforeEach(() => buffer = new TextBuffer("hello\nworld\r\nhow are you doing?")); + + it("can replace text on a single line with a standard newline", function() { + buffer.setTextInRange([[0, 2], [0, 4]], "y y"); + expect(buffer.getText()).toEqual("hey yo\nworld\r\nhow are you doing?"); + }); + + it("can replace text on a single line with a carriage-return/newline", function() { + buffer.setTextInRange([[1, 3], [1, 5]], "ms"); + expect(buffer.getText()).toEqual("hello\nworms\r\nhow are you doing?"); + }); + + it("can replace text in a region spanning multiple lines, ending on the last line", function() { + buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", {normalizeLineEndings: false}); + expect(buffer.getText()).toEqual("hey there\r\ncat\nwhat are you doing?"); + }); + + it("can replace text in a region spanning multiple lines, ending with a carriage-return/newline", function() { + buffer.setTextInRange([[0, 2], [1, 3]], "y\nyou're o", {normalizeLineEndings: false}); + expect(buffer.getText()).toEqual("hey\nyou're old\r\nhow are you doing?"); + }); + + describe("after a change", () => it("notifies, in order: the language mode, display layers, and display layer ::onDidChange observers with the relevant details", function() { + buffer = new TextBuffer("hello\nworld\r\nhow are you doing?"); + + const events = []; + const languageMode = { + bufferDidChange(e) { events.push({source: 'language-mode', event: e}); }, + bufferDidFinishTransaction() {}, + onDidChangeHighlighting() { return {dispose() {}}; } + }; + const displayLayer1 = buffer.addDisplayLayer(); + const displayLayer2 = buffer.addDisplayLayer(); + spyOn(displayLayer1, 'bufferDidChange').and.callFake(function(e) { + events.push({source: 'display-layer-1', event: e}); + return DisplayLayer.prototype.bufferDidChange.call(displayLayer1, e); + }); + spyOn(displayLayer2, 'bufferDidChange').and.callFake(function(e) { + events.push({source: 'display-layer-2', event: e}); + return DisplayLayer.prototype.bufferDidChange.call(displayLayer2, e); + }); + buffer.setLanguageMode(languageMode); + buffer.onDidChange(e => events.push({source: 'buffer', event: JSON.parse(JSON.stringify(e))})); + displayLayer1.onDidChange(e => events.push({source: 'display-layer-event', event: e})); + + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", {normalizeLineEndings: false}); + buffer.setTextInRange([[1, 1], [1, 2]], "abc", {normalizeLineEndings: false}); + }); + + const changeEvent1 = { + oldRange: [[0, 2], [2, 3]], newRange: [[0, 2], [2, 4]], + oldText: "llo\nworld\r\nhow", newText: "y there\r\ncat\nwhat", + }; + const changeEvent2 = { + oldRange: [[1, 1], [1, 2]], newRange: [[1, 1], [1, 4]], + oldText: "a", newText: "abc", + }; + expect(events).toEqual([ + {source: 'language-mode', event: changeEvent1}, + {source: 'display-layer-1', event: changeEvent1}, + {source: 'display-layer-2', event: changeEvent1}, + + {source: 'language-mode', event: changeEvent2}, + {source: 'display-layer-1', event: changeEvent2}, + {source: 'display-layer-2', event: changeEvent2}, + + { + source: 'buffer', + event: { + oldRange: Range(Point(0, 2), Point(2, 3)), + newRange: Range(Point(0, 2), Point(2, 4)), + changes: [ + { + oldRange: Range(Point(0, 2), Point(2, 3)), + newRange: Range(Point(0, 2), Point(2, 4)), + oldText: "llo\nworld\r\nhow", + newText: "y there\r\ncabct\nwhat" + } + ] + } + }, + { + source: 'display-layer-event', + event: [{ + oldRange: Range(Point(0, 0), Point(3, 0)), + newRange: Range(Point(0, 0), Point(3, 0)) + }] + } + ]); + })); + + it("returns the newRange of the change", () => expect(buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat"), {normalizeLineEndings: false}).toEqual([[0, 2], [2, 4]])); + + it("clips the given range", function() { + buffer.setTextInRange([[-1, -1], [0, 1]], "y"); + buffer.setTextInRange([[0, 10], [0, 100]], "w"); + expect(buffer.lineForRow(0)).toBe("yellow"); + }); + + it("preserves the line endings of existing lines", function() { + buffer.setTextInRange([[0, 1], [0, 2]], 'o'); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + buffer.setTextInRange([[1, 1], [1, 3]], 'i'); + expect(buffer.lineEndingForRow(1)).toBe('\r\n'); + }); + + it("freezes change event ranges", function() { + let changedOldRange = null; + let changedNewRange = null; + buffer.onDidChange(function({oldRange, newRange}) { + oldRange.start = Point(0, 3); + oldRange.start.row = 1; + newRange.start = Point(4, 4); + newRange.end.row = 2; + changedOldRange = oldRange; + changedNewRange = newRange; + }); + + buffer.setTextInRange(Range(Point(0, 2), Point(0, 4)), "y y"); + + expect(changedOldRange).toEqual([[0, 2], [0, 4]]); + expect(changedNewRange).toEqual([[0, 2], [0, 5]]); + }); + + describe("when the undo option is 'skip'", function() { + it("replaces the contents of the buffer with the given text", function() { + buffer.setTextInRange([[0, 0], [0, 1]], "y"); + buffer.setTextInRange([[0, 10], [0, 100]], "w", {undo: 'skip'}); + expect(buffer.lineForRow(0)).toBe("yellow"); + + expect(buffer.undo()).toBe(true); + expect(buffer.lineForRow(0)).toBe("hello"); + }); + + it("still emits marker change events (regression)", function() { + const markerLayer = buffer.addMarkerLayer(); + const marker = markerLayer.markRange([[0, 0], [0, 3]]); + + let markerLayerUpdateEventsCount = 0; + const markerChangeEvents = []; + markerLayer.onDidUpdate(() => markerLayerUpdateEventsCount++); + marker.onDidChange(event => markerChangeEvents.push(event)); + + buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'}); + expect(markerLayerUpdateEventsCount).toBe(1); + expect(markerChangeEvents).toEqual([{ + wasValid: true, isValid: true, + hadTail: true, hasTail: true, + oldProperties: {}, newProperties: {}, + oldHeadPosition: Point(0, 3), newHeadPosition: Point(0, 2), + oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), + textChanged: true + }]); + markerChangeEvents.length = 0; + + buffer.transact(() => buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'})); + expect(markerLayerUpdateEventsCount).toBe(2); + expect(markerChangeEvents).toEqual([{ + wasValid: true, isValid: true, + hadTail: true, hasTail: true, + oldProperties: {}, newProperties: {}, + oldHeadPosition: Point(0, 2), newHeadPosition: Point(0, 1), + oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), + textChanged: true + }]); + }); + + it("still emits text change events (regression)", function(done) { + const didChangeEvents = []; + buffer.onDidChange(event => didChangeEvents.push(event)); + + buffer.onDidStopChanging(function({changes}) { + assertChangesEqual(changes, [{ + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 1]], + oldText: 'h', + newText: 'z' + }]); + done(); + }); + + buffer.setTextInRange([[0, 0], [0, 1]], 'y', {undo: 'skip'}); + expect(didChangeEvents.length).toBe(1); + assertChangesEqual(didChangeEvents[0].changes, [{ + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 1]], + oldText: 'h', + newText: 'y' + }]); + + buffer.transact(() => buffer.setTextInRange([[0, 0], [0, 1]], 'z', {undo: 'skip'})); + expect(didChangeEvents.length).toBe(2); + assertChangesEqual(didChangeEvents[1].changes, [{ + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 1]], + oldText: 'y', + newText: 'z' + }]); + }); + }); + + describe("when the normalizeLineEndings argument is true (the default)", function() { + describe("when the range's start row has a line ending", () => it("normalizes inserted line endings to match the line ending of the range's start row", function() { + const changeEvents = []; + buffer.onDidChange(e => changeEvents.push(e)); + + expect(buffer.lineEndingForRow(0)).toBe('\n'); + buffer.setTextInRange([[0, 2], [0, 5]], "y\r\nthere\r\ncrazy"); + expect(buffer.lineEndingForRow(0)).toBe('\n'); + expect(buffer.lineEndingForRow(1)).toBe('\n'); + expect(buffer.lineEndingForRow(2)).toBe('\n'); + expect(changeEvents[0].newText).toBe("y\nthere\ncrazy"); + + expect(buffer.lineEndingForRow(3)).toBe('\r\n'); + buffer.setTextInRange([[3, 3], [4, Infinity]], "ms\ndo you\r\nlike\ndirt"); + expect(buffer.lineEndingForRow(3)).toBe('\r\n'); + expect(buffer.lineEndingForRow(4)).toBe('\r\n'); + expect(buffer.lineEndingForRow(5)).toBe('\r\n'); + expect(buffer.lineEndingForRow(6)).toBe(''); + expect(changeEvents[1].newText).toBe("ms\r\ndo you\r\nlike\r\ndirt"); + + buffer.setTextInRange([[5, 1], [5, 3]], '\r'); + expect(changeEvents[2].changes).toEqual([{ + oldRange: [[5, 1], [5, 3]], + newRange: [[5, 1], [6, 0]], + oldText: 'ik', + newText: '\r\n' + }]); + + buffer.undo(); + expect(changeEvents[3].changes).toEqual([{ + oldRange: [[5, 1], [6, 0]], + newRange: [[5, 1], [5, 3]], + oldText: '\r\n', + newText: 'ik' + }]); + + buffer.redo(); + expect(changeEvents[4].changes).toEqual([{ + oldRange: [[5, 1], [5, 3]], + newRange: [[5, 1], [6, 0]], + oldText: 'ik', + newText: '\r\n' + }]); + })); + + describe("when the range's start row has no line ending (because it's the last line of the buffer)", function() { + describe("when the buffer contains no newlines", () => it("honors the newlines in the inserted text", function() { + buffer = new TextBuffer("hello"); + buffer.setTextInRange([[0, 2], [0, Infinity]], "hey\r\nthere\nworld"); + expect(buffer.lineEndingForRow(0)).toBe('\r\n'); + expect(buffer.lineEndingForRow(1)).toBe('\n'); + expect(buffer.lineEndingForRow(2)).toBe(''); + })); + + describe("when the buffer contains newlines", () => it("normalizes inserted line endings to match the line ending of the penultimate row", function() { + expect(buffer.lineEndingForRow(1)).toBe('\r\n'); + buffer.setTextInRange([[2, 0], [2, Infinity]], "what\ndo\r\nyou\nwant?"); + expect(buffer.lineEndingForRow(2)).toBe('\r\n'); + expect(buffer.lineEndingForRow(3)).toBe('\r\n'); + expect(buffer.lineEndingForRow(4)).toBe('\r\n'); + expect(buffer.lineEndingForRow(5)).toBe(''); + })); + }); + }); + + describe("when the normalizeLineEndings argument is false", () => it("honors the newlines in the inserted text", function() { + buffer.setTextInRange([[1, 0], [1, 5]], "moon\norbiting\r\nhappily\nthere", {normalizeLineEndings: false}); + expect(buffer.lineEndingForRow(1)).toBe('\n'); + expect(buffer.lineEndingForRow(2)).toBe('\r\n'); + expect(buffer.lineEndingForRow(3)).toBe('\n'); + expect(buffer.lineEndingForRow(4)).toBe('\r\n'); + expect(buffer.lineEndingForRow(5)).toBe(''); + })); + }); + + describe("::setText(text)", () => it("replaces the contents of the buffer with the given text", function() { + buffer = new TextBuffer("hello\nworld\r\nyou are cool"); + buffer.setText("goodnight\r\nmoon\nit's been good"); + expect(buffer.getText()).toBe("goodnight\r\nmoon\nit's been good"); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nyou are cool"); + })); + + describe("::insert(position, text, normalizeNewlinesn)", function() { + it("inserts text at the given position", function() { + buffer = new TextBuffer("hello world"); + buffer.insert([0, 5], " there"); + expect(buffer.getText()).toBe("hello there world"); + }); + + it("honors the normalizeNewlines option", function() { + buffer = new TextBuffer("hello\nworld"); + buffer.insert([0, 5], "\r\nthere\r\nlittle", {normalizeLineEndings: false}); + expect(buffer.getText()).toBe("hello\r\nthere\r\nlittle\nworld"); + }); + }); + + describe("::append(text, normalizeNewlines)", function() { + it("appends text to the end of the buffer", function() { + buffer = new TextBuffer("hello world"); + buffer.append(", how are you?"); + expect(buffer.getText()).toBe("hello world, how are you?"); + }); + + it("honors the normalizeNewlines option", function() { + buffer = new TextBuffer("hello\nworld"); + buffer.append("\r\nhow\r\nare\nyou?", {normalizeLineEndings: false}); + expect(buffer.getText()).toBe("hello\nworld\r\nhow\r\nare\nyou?"); + }); + }); + + describe("::delete(range)", () => it("deletes text in the given range", function() { + buffer = new TextBuffer("hello world"); + buffer.delete([[0, 5], [0, 11]]); + expect(buffer.getText()).toBe("hello"); + })); + + describe("::deleteRows(startRow, endRow)", function() { + beforeEach(() => buffer = new TextBuffer("first\nsecond\nthird\nlast")); + + describe("when the endRow is less than the last row of the buffer", () => it("deletes the specified rows", function() { + buffer.deleteRows(1, 2); + expect(buffer.getText()).toBe("first\nlast"); + buffer.deleteRows(0, 0); + expect(buffer.getText()).toBe("last"); + })); + + describe("when the endRow is the last row of the buffer", () => it("deletes the specified rows", function() { + buffer.deleteRows(2, 3); + expect(buffer.getText()).toBe("first\nsecond"); + buffer.deleteRows(0, 1); + expect(buffer.getText()).toBe(""); + })); + + it("clips the given row range", function() { + buffer.deleteRows(-1, 0); + expect(buffer.getText()).toBe("second\nthird\nlast"); + buffer.deleteRows(1, 5); + expect(buffer.getText()).toBe("second"); + + buffer.deleteRows(-2, -1); + expect(buffer.getText()).toBe("second"); + buffer.deleteRows(1, 2); + expect(buffer.getText()).toBe("second"); + }); + + it("handles out of order row ranges", function() { + buffer.deleteRows(2, 1); + expect(buffer.getText()).toBe("first\nlast"); + }); + }); + + describe("::getText()", () => it("returns the contents of the buffer as a single string", function() { + buffer = new TextBuffer("hello\nworld\r\nhow are you?"); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you?"); + buffer.setTextInRange([[1, 0], [1, 5]], "mom"); + expect(buffer.getText()).toBe("hello\nmom\r\nhow are you?"); + })); + + describe("::undo() and ::redo()", function() { + beforeEach(() => buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"})); + + it("undoes and redoes multiple changes", function() { + buffer.setTextInRange([[0, 5], [0, 5]], " there"); + buffer.setTextInRange([[1, 0], [1, 5]], "friend"); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + }); + + it("clears the redo stack upon a fresh change", function() { + buffer.setTextInRange([[0, 5], [0, 5]], " there"); + buffer.setTextInRange([[1, 0], [1, 5]], "friend"); + expect(buffer.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.setTextInRange([[1, 3], [1, 5]], "m"); + expect(buffer.getText()).toBe("hello there\nworm\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello there\nworm\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("does not allow the undo stack to grow without bound", function() { + buffer = new TextBuffer({maxUndoEntries: 12}); + + // Each transaction is treated as a single undo entry. We can undo up + // to 12 of them. + buffer.setText(""); + buffer.clearUndoStack(); + for (var i = 0; i < 13; i++) { + buffer.transact(function() { + buffer.append(String(i)); + buffer.append("\n"); + }); + } + expect(buffer.getLineCount()).toBe(14); + + let undoCount = 0; + while (buffer.undo()) { undoCount++; } + expect(undoCount).toBe(12); + expect(buffer.getText()).toBe('0\n'); + }); + }); + + describe("::createMarkerSnapshot", function() { + let markerLayers = null; + + beforeEach(function() { + buffer = new TextBuffer; + + markerLayers = [ + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true}), + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true}) + ];}); + + describe("when selectionsMarkerLayer is not passed", () => it("takes a snapshot of all markerLayers", function() { + const snapshot = buffer.createMarkerSnapshot(); + const markerLayerIdsInSnapshot = Object.keys(snapshot); + expect(markerLayerIdsInSnapshot.length).toBe(4); + expect(markerLayerIdsInSnapshot.includes(markerLayers[0].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[1].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[2].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[3].id)).toBe(true); + })); + + describe("when selectionsMarkerLayer is passed", () => it("skips snapshotting of other 'selection' role marker layers", function() { + let snapshot = buffer.createMarkerSnapshot(markerLayers[0]); + let markerLayerIdsInSnapshot = Object.keys(snapshot); + expect(markerLayerIdsInSnapshot.length).toBe(3); + expect(markerLayerIdsInSnapshot.includes(markerLayers[0].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[1].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[2].id)).toBe(false); + expect(markerLayerIdsInSnapshot.includes(markerLayers[3].id)).toBe(true); + + snapshot = buffer.createMarkerSnapshot(markerLayers[2]); + markerLayerIdsInSnapshot = Object.keys(snapshot); + expect(markerLayerIdsInSnapshot.length).toBe(3); + expect(markerLayerIdsInSnapshot.includes(markerLayers[0].id)).toBe(false); + expect(markerLayerIdsInSnapshot.includes(markerLayers[1].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[2].id)).toBe(true); + expect(markerLayerIdsInSnapshot.includes(markerLayers[3].id)).toBe(true); + })); + }); + + describe("selective snapshotting and restoration on transact/undo/redo for selections marker layer", function() { + let markerLayers, marker0, marker1, marker2, textUndo, textRedo, rangesBefore, rangesAfter; + const ensureMarkerLayer = function(markerLayer, range) { + const markers = markerLayer.findMarkers({}); + expect(markers.length).toBe(1); + expect(markers[0].getRange()).toEqual(range); + }; + + const getFirstMarker = markerLayer => markerLayer.findMarkers({})[0]; + + beforeEach(function() { + buffer = new TextBuffer({text: "00000000\n11111111\n22222222\n33333333\n"}); + + markerLayers = [ + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}), + buffer.addMarkerLayer({maintainHistory: true, role: "selections"}) + ]; + + textUndo = "00000000\n11111111\n22222222\n33333333\n"; + textRedo = "00000000\n11111111\n22222222\n33333333\n44444444\n"; + + rangesBefore = [ + [[0, 1], [0, 1]], + [[0, 2], [0, 2]], + [[0, 3], [0, 3]] + ]; + rangesAfter = [ + [[2, 1], [2, 1]], + [[2, 2], [2, 2]], + [[2, 3], [2, 3]] + ]; + + marker0 = markerLayers[0].markRange(rangesBefore[0]); + marker1 = markerLayers[1].markRange(rangesBefore[1]); + marker2 = markerLayers[2].markRange(rangesBefore[2]); + }); + + it("restores a snapshot from other selections marker layers on undo/redo", function() { + // Snapshot is taken for markerLayers[0] only, markerLayer[1] and markerLayer[2] are skipped + buffer.transact({selectionsMarkerLayer: markerLayers[0]}, function() { + buffer.append("44444444\n"); + marker0.setRange(rangesAfter[0]); + marker1.setRange(rangesAfter[1]); + marker2.setRange(rangesAfter[2]); + }); + + buffer.undo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesBefore[0]); + ensureMarkerLayer(markerLayers[1], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + + buffer.redo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textRedo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + + buffer.undo({selectionsMarkerLayer: markerLayers[1]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[0]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).not.toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + expect(marker1.isDestroyed()).toBe(true); + + buffer.redo({selectionsMarkerLayer: markerLayers[2]}); + expect(buffer.getText()).toBe(textRedo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[0]); + ensureMarkerLayer(markerLayers[2], rangesAfter[0]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).not.toBe(marker1); + expect(getFirstMarker(markerLayers[2])).not.toBe(marker2); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(true); + + buffer.undo({selectionsMarkerLayer: markerLayers[2]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[0]); + ensureMarkerLayer(markerLayers[2], rangesBefore[0]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).not.toBe(marker1); + expect(getFirstMarker(markerLayers[2])).not.toBe(marker2); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(true); + }); + + it("can restore a snapshot taken at a destroyed selections marker layer given selectionsMarkerLayer", function() { + buffer.transact({selectionsMarkerLayer: markerLayers[1]}, function() { + buffer.append("44444444\n"); + marker0.setRange(rangesAfter[0]); + marker1.setRange(rangesAfter[1]); + marker2.setRange(rangesAfter[2]); + }); + + markerLayers[1].destroy(); + expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeTruthy(); + expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeTruthy(); + expect(marker0.isDestroyed()).toBe(false); + expect(marker1.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(false); + + buffer.undo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesBefore[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(marker0.isDestroyed()).toBe(true); + expect(marker2.isDestroyed()).toBe(false); + + buffer.redo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textRedo); + ensureMarkerLayer(markerLayers[0], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + + markerLayers[3] = markerLayers[2].copy(); + ensureMarkerLayer(markerLayers[3], rangesAfter[2]); + markerLayers[0].destroy(); + markerLayers[2].destroy(); + expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeFalsy(); + expect(buffer.getMarkerLayer(markerLayers[3].id)).toBeTruthy(); + + buffer.undo({selectionsMarkerLayer: markerLayers[3]}); + expect(buffer.getText()).toBe(textUndo); + ensureMarkerLayer(markerLayers[3], rangesBefore[1]); + buffer.redo({selectionsMarkerLayer: markerLayers[3]}); + expect(buffer.getText()).toBe(textRedo); + ensureMarkerLayer(markerLayers[3], rangesAfter[1]); + }); + + it("falls back to normal behavior when the snaphot includes multiple layerSnapshots of selections marker layers", function() { + // Transact without selectionsMarkerLayer. + // Taken snapshot includes layerSnapshot of markerLayer[0], markerLayer[1] and markerLayer[2] + buffer.transact(function() { + buffer.append("44444444\n"); + marker0.setRange(rangesAfter[0]); + marker1.setRange(rangesAfter[1]); + marker2.setRange(rangesAfter[2]); + }); + + buffer.undo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textUndo); + + ensureMarkerLayer(markerLayers[0], rangesBefore[0]); + ensureMarkerLayer(markerLayers[1], rangesBefore[1]); + ensureMarkerLayer(markerLayers[2], rangesBefore[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + + buffer.redo({selectionsMarkerLayer: markerLayers[0]}); + expect(buffer.getText()).toBe(textRedo); + + ensureMarkerLayer(markerLayers[0], rangesAfter[0]); + ensureMarkerLayer(markerLayers[1], rangesAfter[1]); + ensureMarkerLayer(markerLayers[2], rangesAfter[2]); + expect(getFirstMarker(markerLayers[0])).toBe(marker0); + expect(getFirstMarker(markerLayers[1])).toBe(marker1); + expect(getFirstMarker(markerLayers[2])).toBe(marker2); + }); + + describe("selections marker layer's selective snapshotting on createCheckpoint, groupChangesSinceCheckpoint", () => it("skips snapshotting of other marker layers with the same role as the selectionsMarkerLayer", function() { + const eventHandler = jasmine.createSpy('eventHandler'); + + const args = []; + spyOn(buffer, 'createMarkerSnapshot').and.callFake(arg => args.push(arg)); + + const checkpoint1 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[0]}); + const checkpoint2 = buffer.createCheckpoint(); + const checkpoint3 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[2]}); + const checkpoint4 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[1]}); + expect(args).toEqual([ + markerLayers[0], + undefined, + markerLayers[2], + markerLayers[1], + ]); + + buffer.groupChangesSinceCheckpoint(checkpoint4, {selectionsMarkerLayer: markerLayers[0]}); + buffer.groupChangesSinceCheckpoint(checkpoint3, {selectionsMarkerLayer: markerLayers[2]}); + buffer.groupChangesSinceCheckpoint(checkpoint2); + buffer.groupChangesSinceCheckpoint(checkpoint1, {selectionsMarkerLayer: markerLayers[1]}); + expect(args).toEqual([ + markerLayers[0], + undefined, + markerLayers[2], + markerLayers[1], + + markerLayers[0], + markerLayers[2], + undefined, + markerLayers[1], + ]); + })); + }); + + describe("transactions", function() { + let now = null; + + beforeEach(function() { + now = 0; + spyOn(Date, 'now').and.callFake(() => now); + + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + buffer.setTextInRange([[1, 3], [1, 5]], 'ms'); + }); + + describe("::transact(groupingInterval, fn)", function() { + it("groups all operations in the given function in a single transaction", function() { + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.transact(() => buffer.setTextInRange([[2, 13], [2, 14]], "igg")); + }); + + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("halts execution of the function if the transaction is aborted", function() { + let innerContinued = false; + let outerContinued = false; + + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.transact(function() { + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + buffer.abortTransaction(); + innerContinued = true; + }); + outerContinued = true; + }); + + expect(innerContinued).toBe(false); + expect(outerContinued).toBe(true); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you doing?"); + }); + + it("groups all operations performed within the given function into a single undo/redo operation", function() { + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + }); + + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + + // subsequent changes are not included in the transaction + buffer.setTextInRange([[1, 0], [1, 0]], "little "); + buffer.undo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + + // this should undo all changes in the transaction + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + // previous changes are not included in the transaction + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + // this should redo all changes in the transaction + buffer.redo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you digging?"); + + // this should redo the change following the transaction + buffer.redo(); + expect(buffer.getText()).toBe("hey\nlittle worms\r\nhow are you digging?"); + }); + + it("does not push the transaction to the undo stack if it is empty", function() { + buffer.transact(function() {}); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + buffer.transact(() => buffer.abortTransaction()); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("halts execution undoes all operations since the beginning of the transaction if ::abortTransaction() is called", function() { + let continuedPastAbort = false; + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + buffer.abortTransaction(); + continuedPastAbort = true; + }); + + expect(continuedPastAbort).toBe(false); + + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + }); + + it("preserves the redo stack until a content change occurs", function() { + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + // no changes occur in this transaction before aborting + buffer.transact(function() { + buffer.markRange([[0, 0], [0, 5]]); + buffer.abortTransaction(); + buffer.setTextInRange([[0, 0], [0, 5]], "hey"); + }); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.transact(function() { + buffer.setTextInRange([[0, 0], [0, 5]], "hey"); + buffer.abortTransaction(); + }); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("allows nested transactions", function() { + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.transact(function() { + buffer.setTextInRange([[0, 2], [0, 5]], "y"); + buffer.transact(function() { + buffer.setTextInRange([[2, 13], [2, 14]], "igg"); + buffer.setTextInRange([[2, 18], [2, 19]], "'"); + }); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you diggin'?"); + buffer.undo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you doing?"); + buffer.redo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you diggin'?"); + }); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("hey\nworms\r\nhow are you diggin'?"); + + buffer.undo(); + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworld\r\nhow are you doing?"); + }); + + it("groups adjacent transactions within each other's grouping intervals", function() { + now += 1000; + buffer.transact(101, () => buffer.setTextInRange([[0, 2], [0, 5]], "y")); + + now += 100; + buffer.transact(201, () => buffer.setTextInRange([[0, 3], [0, 3]], "yy")); + + now += 200; + buffer.transact(201, () => buffer.setTextInRange([[0, 5], [0, 5]], "yy")); + + // not grouped because the previous transaction's grouping interval + // is only 200ms and we've advanced 300ms + now += 300; + buffer.transact(301, () => buffer.setTextInRange([[0, 7], [0, 7]], "!!")); + + expect(buffer.getText()).toBe("heyyyyy!!\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("heyyyyy\nworms\r\nhow are you doing?"); + + buffer.undo(); + expect(buffer.getText()).toBe("hello\nworms\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("heyyyyy\nworms\r\nhow are you doing?"); + + buffer.redo(); + expect(buffer.getText()).toBe("heyyyyy!!\nworms\r\nhow are you doing?"); + }); + + it("allows undo/redo within transactions, but not beyond the start of the containing transaction", function() { + buffer.setText(""); + buffer.markPosition([0, 0]); + + buffer.append("a"); + + buffer.transact(function() { + buffer.append("b"); + buffer.transact(() => buffer.append("c")); + buffer.append("d"); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("abc"); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("ab"); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("a"); + + expect(buffer.undo()).toBe(false); + expect(buffer.getText()).toBe("a"); + + expect(buffer.redo()).toBe(true); + expect(buffer.getText()).toBe("ab"); + + expect(buffer.redo()).toBe(true); + expect(buffer.getText()).toBe("abc"); + + expect(buffer.redo()).toBe(true); + expect(buffer.getText()).toBe("abcd"); + + expect(buffer.redo()).toBe(false); + expect(buffer.getText()).toBe("abcd"); + }); + + expect(buffer.undo()).toBe(true); + expect(buffer.getText()).toBe("a"); + }); + + it("does not error if the buffer is destroyed in a change callback within the transaction", function() { + buffer.onDidChange(() => buffer.destroy()); + const result = buffer.transact(function() { + buffer.append('!'); + return 'hi'; + }); + expect(result).toBe('hi'); + }); + }); + }); + + describe("checkpoints", function() { + beforeEach(() => buffer = new TextBuffer); + + describe("::getChangesSinceCheckpoint(checkpoint)", function() { + it("returns a list of changes that have been made since the checkpoint", function() { + buffer.setText('abc\ndef\nghi\njkl\n'); + buffer.append("mno\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.transact(function() { + buffer.append('pqr\n'); + buffer.append('stu\n'); + }); + buffer.append('vwx\n'); + buffer.setTextInRange([[1, 0], [1, 2]], 'yz'); + + expect(buffer.getText()).toBe('abc\nyzf\nghi\njkl\nmno\npqr\nstu\nvwx\n'); + assertChangesEqual(buffer.getChangesSinceCheckpoint(checkpoint), [ + { + oldRange: [[1, 0], [1, 2]], + newRange: [[1, 0], [1, 2]], + oldText: "de", + newText: "yz", + }, + { + oldRange: [[5, 0], [5, 0]], + newRange: [[5, 0], [8, 0]], + oldText: "", + newText: "pqr\nstu\nvwx\n", + } + ]); + }); + + it("returns an empty list of changes when no change has been made since the checkpoint", function() { + const checkpoint = buffer.createCheckpoint(); + expect(buffer.getChangesSinceCheckpoint(checkpoint)).toEqual([]); + }); + + it("returns an empty list of changes when the checkpoint doesn't exist", function() { + buffer.transact(function() { + buffer.append('abc\n'); + buffer.append('def\n'); + }); + buffer.append('ghi\n'); + expect(buffer.getChangesSinceCheckpoint(-1)).toEqual([]); + }); + }); + + describe("::revertToCheckpoint(checkpoint)", () => it("undoes all changes following the checkpoint", function() { + buffer.append("hello"); + const checkpoint = buffer.createCheckpoint(); + + buffer.transact(function() { + buffer.append("\n"); + buffer.append("world"); + }); + + buffer.append("\n"); + buffer.append("how are you?"); + + const result = buffer.revertToCheckpoint(checkpoint); + expect(result).toBe(true); + expect(buffer.getText()).toBe("hello"); + + buffer.redo(); + expect(buffer.getText()).toBe("hello"); + })); + + describe("::groupChangesSinceCheckpoint(checkpoint)", function() { + it("combines all changes since the checkpoint into a single transaction", function() { + const historyLayer = buffer.addMarkerLayer({maintainHistory: true}); + + buffer.append("one\n"); + const marker = historyLayer.markRange([[0, 1], [0, 2]]); + marker.setProperties({a: 'b'}); + + const checkpoint = buffer.createCheckpoint(); + buffer.append("two\n"); + buffer.transact(function() { + buffer.append("three\n"); + buffer.append("four"); + }); + + marker.setRange([[0, 1], [2, 3]]); + marker.setProperties({a: 'c'}); + const result = buffer.groupChangesSinceCheckpoint(checkpoint); + + expect(result).toBeTruthy(); + expect(buffer.getText()).toBe(`\ +one +two +three +four\ +` + ); + expect(marker.getRange()).toEqual([[0, 1], [2, 3]]); + expect(marker.getProperties()).toEqual({a: 'c'}); + + buffer.undo(); + expect(buffer.getText()).toBe("one\n"); + expect(marker.getRange()).toEqual([[0, 1], [0, 2]]); + expect(marker.getProperties()).toEqual({a: 'b'}); + + buffer.redo(); + expect(buffer.getText()).toBe(`\ +one +two +three +four\ +` + ); + expect(marker.getRange()).toEqual([[0, 1], [2, 3]]); + expect(marker.getProperties()).toEqual({a: 'c'}); + }); + + it("skips any later checkpoints when grouping changes", function() { + buffer.append("one\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.append("two\n"); + const checkpoint2 = buffer.createCheckpoint(); + buffer.append("three"); + + buffer.groupChangesSinceCheckpoint(checkpoint); + expect(buffer.revertToCheckpoint(checkpoint2)).toBe(false); + + expect(buffer.getText()).toBe(`\ +one +two +three\ +` + ); + + buffer.undo(); + expect(buffer.getText()).toBe("one\n"); + + buffer.redo(); + expect(buffer.getText()).toBe(`\ +one +two +three\ +` + ); + }); + + it("does nothing when no changes have been made since the checkpoint", function() { + buffer.append("one\n"); + const checkpoint = buffer.createCheckpoint(); + const result = buffer.groupChangesSinceCheckpoint(checkpoint); + expect(result).toBeTruthy(); + buffer.undo(); + expect(buffer.getText()).toBe(""); + }); + + it("returns false and does nothing when the checkpoint is not in the buffer's history", function() { + buffer.append("hello\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.undo(); + buffer.append("world"); + const result = buffer.groupChangesSinceCheckpoint(checkpoint); + expect(result).toBeFalsy(); + buffer.undo(); + expect(buffer.getText()).toBe(""); + }); + }); + + it("skips checkpoints when undoing", function() { + buffer.append("hello"); + buffer.createCheckpoint(); + buffer.createCheckpoint(); + buffer.createCheckpoint(); + buffer.undo(); + expect(buffer.getText()).toBe(""); + }); + + it("preserves checkpoints across undo and redo", function() { + buffer.append("a"); + buffer.append("b"); + const checkpoint1 = buffer.createCheckpoint(); + buffer.append("c"); + const checkpoint2 = buffer.createCheckpoint(); + + buffer.undo(); + expect(buffer.getText()).toBe("ab"); + + buffer.redo(); + expect(buffer.getText()).toBe("abc"); + + buffer.append("d"); + + expect(buffer.revertToCheckpoint(checkpoint2)).toBe(true); + expect(buffer.getText()).toBe("abc"); + expect(buffer.revertToCheckpoint(checkpoint1)).toBe(true); + expect(buffer.getText()).toBe("ab"); + }); + + it("handles checkpoints created when there have been no changes", function() { + const checkpoint = buffer.createCheckpoint(); + buffer.undo(); + buffer.append("hello"); + buffer.revertToCheckpoint(checkpoint); + expect(buffer.getText()).toBe(""); + }); + + it("returns false when the checkpoint is not in the buffer's history", function() { + buffer.append("hello\n"); + const checkpoint = buffer.createCheckpoint(); + buffer.undo(); + buffer.append("world"); + expect(buffer.revertToCheckpoint(checkpoint)).toBe(false); + expect(buffer.getText()).toBe("world"); + }); + + it("does not allow changes based on checkpoints outside of the current transaction", function() { + const checkpoint = buffer.createCheckpoint(); + + buffer.append("a"); + + buffer.transact(function() { + expect(buffer.revertToCheckpoint(checkpoint)).toBe(false); + expect(buffer.getText()).toBe("a"); + + buffer.append("b"); + + expect(buffer.groupChangesSinceCheckpoint(checkpoint)).toBeFalsy(); + }); + + buffer.undo(); + expect(buffer.getText()).toBe("a"); + }); + }); + + describe("::groupLastChanges()", () => it("groups the last two changes into a single transaction", function() { + buffer = new TextBuffer(); + const layer = buffer.addMarkerLayer({maintainHistory: true}); + + buffer.append('a'); + + // Group two transactions, ensure before/after markers snapshots are preserved + const marker = layer.markPosition([0, 0]); + buffer.transact(() => buffer.append('b')); + buffer.createCheckpoint(); + buffer.transact(function() { + buffer.append('ccc'); + marker.setHeadPosition([0, 2]); + }); + + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + expect(marker.getHeadPosition()).toEqual([0, 0]); + expect(buffer.getText()).toBe('a'); + buffer.redo(); + expect(marker.getHeadPosition()).toEqual([0, 2]); + buffer.undo(); + + // Group two bare changes + buffer.transact(function() { + buffer.append('b'); + buffer.createCheckpoint(); + buffer.append('c'); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + expect(buffer.getText()).toBe('a'); + }); + + // Group a transaction with a bare change + buffer.transact(function() { + buffer.transact(function() { + buffer.append('b'); + buffer.append('c'); + }); + buffer.append('d'); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + expect(buffer.getText()).toBe('a'); + }); + + // Group a bare change with a transaction + buffer.transact(function() { + buffer.append('b'); + buffer.transact(function() { + buffer.append('c'); + buffer.append('d'); + }); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + expect(buffer.getText()).toBe('a'); + }); + + // Can't group past the beginning of an open transaction + buffer.transact(function() { + expect(buffer.groupLastChanges()).toBe(false); + buffer.append('b'); + expect(buffer.groupLastChanges()).toBe(false); + buffer.append('c'); + expect(buffer.groupLastChanges()).toBe(true); + buffer.undo(); + expect(buffer.getText()).toBe('a'); + }); + })); + + describe("::setHistoryProvider(provider)", () => it("replaces the currently active history provider with the passed one", function() { + buffer = new TextBuffer({text: ''}); + buffer.insert([0, 0], 'Lorem '); + buffer.insert([0, 6], 'ipsum '); + expect(buffer.getText()).toBe('Lorem ipsum '); + + buffer.undo(); + expect(buffer.getText()).toBe('Lorem '); + + buffer.setHistoryProvider(new DefaultHistoryProvider(buffer)); + buffer.undo(); + expect(buffer.getText()).toBe('Lorem '); + + buffer.insert([0, 6], 'dolor '); + expect(buffer.getText()).toBe('Lorem dolor '); + + buffer.undo(); + expect(buffer.getText()).toBe('Lorem '); + })); + + describe("::getHistory(maxEntries) and restoreDefaultHistoryProvider(history)", function() { + it("returns a base text and the state of the last `maxEntries` entries in the undo and redo stacks", function() { + buffer = new TextBuffer({text: ''}); + const markerLayer = buffer.addMarkerLayer({maintainHistory: true}); + + buffer.append('Lorem '); + buffer.append('ipsum '); + buffer.append('dolor '); + markerLayer.markPosition([0, 2]); + const markersSnapshotAtCheckpoint1 = buffer.createMarkerSnapshot(); + const checkpoint1 = buffer.createCheckpoint(); + buffer.append('sit '); + buffer.append('amet '); + buffer.append('consecteur '); + markerLayer.markPosition([0, 4]); + const markersSnapshotAtCheckpoint2 = buffer.createMarkerSnapshot(); + const checkpoint2 = buffer.createCheckpoint(); + buffer.append('adipiscit '); + buffer.append('elit '); + buffer.undo(); + buffer.undo(); + buffer.undo(); + + const history = buffer.getHistory(3); + expect(history.baseText).toBe('Lorem ipsum dolor '); + expect(history.nextCheckpointId).toBe(buffer.createCheckpoint()); + expect(history.undoStack).toEqual([ + { + type: 'checkpoint', + id: checkpoint1, + markers: markersSnapshotAtCheckpoint1 + }, + { + type: 'transaction', + changes: [{oldStart: Point(0, 18), oldEnd: Point(0, 18), newStart: Point(0, 18), newEnd: Point(0, 22), oldText: '', newText: 'sit '}], + markersBefore: markersSnapshotAtCheckpoint1, + markersAfter: markersSnapshotAtCheckpoint1 + }, + { + type: 'transaction', + changes: [{oldStart: Point(0, 22), oldEnd: Point(0, 22), newStart: Point(0, 22), newEnd: Point(0, 27), oldText: '', newText: 'amet '}], + markersBefore: markersSnapshotAtCheckpoint1, + markersAfter: markersSnapshotAtCheckpoint1 + } + ]); + expect(history.redoStack).toEqual([ + { + type: 'transaction', + changes: [{oldStart: Point(0, 38), oldEnd: Point(0, 38), newStart: Point(0, 38), newEnd: Point(0, 48), oldText: '', newText: 'adipiscit '}], + markersBefore: markersSnapshotAtCheckpoint2, + markersAfter: markersSnapshotAtCheckpoint2 + }, + { + type: 'checkpoint', + id: checkpoint2, + markers: markersSnapshotAtCheckpoint2 + }, + { + type: 'transaction', + changes: [{oldStart: Point(0, 27), oldEnd: Point(0, 27), newStart: Point(0, 27), newEnd: Point(0, 38), oldText: '', newText: 'consecteur '}], + markersBefore: markersSnapshotAtCheckpoint1, + markersAfter: markersSnapshotAtCheckpoint1 + } + ]); + + buffer.createCheckpoint(); + buffer.append('x'); + buffer.undo(); + buffer.clearUndoStack(); + + expect(buffer.getHistory()).not.toEqual(history); + buffer.restoreDefaultHistoryProvider(history); + expect(buffer.getHistory()).toEqual(history); + }); + + it("throws an error when called within a transaction", function() { + buffer = new TextBuffer(); + expect(() => buffer.transact(() => buffer.getHistory(3))).toThrowError(); + }); + }); + + describe("::getTextInRange(range)", function() { + it("returns the text in a given range", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(buffer.getTextInRange([[1, 1], [1, 4]])).toBe("orl"); + expect(buffer.getTextInRange([[0, 3], [2, 3]])).toBe("lo\nworld\r\nhow"); + expect(buffer.getTextInRange([[0, 0], [2, 18]])).toBe(buffer.getText()); + }); + + it("clips the given range", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(buffer.getTextInRange([[-100, -100], [100, 100]])).toBe(buffer.getText()); + }); + }); + + describe("::clipPosition(position)", function() { + it("returns a valid position closest to the given position", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(buffer.clipPosition([-1, -1])).toEqual([0, 0]); + expect(buffer.clipPosition([-1, 2])).toEqual([0, 0]); + expect(buffer.clipPosition([0, -1])).toEqual([0, 0]); + expect(buffer.clipPosition([0, 20])).toEqual([0, 5]); + expect(buffer.clipPosition([1, -1])).toEqual([1, 0]); + expect(buffer.clipPosition([1, 20])).toEqual([1, 5]); + expect(buffer.clipPosition([10, 0])).toEqual([2, 18]); + expect(buffer.clipPosition([Infinity, 0])).toEqual([2, 18]); + }); + + it("throws an error when given an invalid point", function() { + buffer = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + expect(() => buffer.clipPosition([NaN, 1])) + .toThrowError("Invalid Point: (NaN, 1)"); + expect(() => buffer.clipPosition([0, NaN])) + .toThrowError("Invalid Point: (0, NaN)"); + expect(() => buffer.clipPosition([0, {}])) + .toThrowError("Invalid Point: (0, [object Object])"); + }); + }); + + describe("::characterIndexForPosition(position)", function() { + beforeEach(() => buffer = new TextBuffer({text: "zero\none\r\ntwo\nthree"})); + + it("returns the absolute character offset for the given position", function() { + expect(buffer.characterIndexForPosition([0, 0])).toBe(0); + expect(buffer.characterIndexForPosition([0, 1])).toBe(1); + expect(buffer.characterIndexForPosition([0, 4])).toBe(4); + expect(buffer.characterIndexForPosition([1, 0])).toBe(5); + expect(buffer.characterIndexForPosition([1, 1])).toBe(6); + expect(buffer.characterIndexForPosition([1, 3])).toBe(8); + expect(buffer.characterIndexForPosition([2, 0])).toBe(10); + expect(buffer.characterIndexForPosition([2, 1])).toBe(11); + expect(buffer.characterIndexForPosition([3, 0])).toBe(14); + expect(buffer.characterIndexForPosition([3, 5])).toBe(19); + }); + + it("clips the given position before translating", function() { + expect(buffer.characterIndexForPosition([-1, -1])).toBe(0); + expect(buffer.characterIndexForPosition([1, 100])).toBe(8); + expect(buffer.characterIndexForPosition([100, 100])).toBe(19); + }); + }); + + describe("::positionForCharacterIndex(offset)", function() { + beforeEach(() => buffer = new TextBuffer({text: "zero\none\r\ntwo\nthree"})); + + it("returns the position for the given absolute character offset", function() { + expect(buffer.positionForCharacterIndex(0)).toEqual([0, 0]); + expect(buffer.positionForCharacterIndex(1)).toEqual([0, 1]); + expect(buffer.positionForCharacterIndex(4)).toEqual([0, 4]); + expect(buffer.positionForCharacterIndex(5)).toEqual([1, 0]); + expect(buffer.positionForCharacterIndex(6)).toEqual([1, 1]); + expect(buffer.positionForCharacterIndex(8)).toEqual([1, 3]); + expect(buffer.positionForCharacterIndex(10)).toEqual([2, 0]); + expect(buffer.positionForCharacterIndex(11)).toEqual([2, 1]); + expect(buffer.positionForCharacterIndex(14)).toEqual([3, 0]); + expect(buffer.positionForCharacterIndex(19)).toEqual([3, 5]); + }); + + it("clips the given offset before translating", function() { + expect(buffer.positionForCharacterIndex(-1)).toEqual([0, 0]); + expect(buffer.positionForCharacterIndex(20)).toEqual([3, 5]); + }); +}); + + describe("serialization", function() { + const expectSameMarkers = function(left, right) { + const markers1 = left.getMarkers().sort((a, b) => a.compare(b)); + const markers2 = right.getMarkers().sort((a, b) => a.compare(b)); + expect(markers1.length).toBe(markers2.length); + for (let i = 0; i < markers1.length; i++) { + var marker1 = markers1[i]; + expect(marker1).toEqual(markers2[i]); + } + }; + + it("can serialize / deserialize the buffer along with its history, marker layers, and display layers", function(done) { + const bufferA = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + const displayLayer1A = bufferA.addDisplayLayer(); + const displayLayer2A = bufferA.addDisplayLayer(); + displayLayer1A.foldBufferRange([[0, 1], [0, 3]]); + displayLayer2A.foldBufferRange([[0, 0], [0, 2]]); + bufferA.createCheckpoint(); + bufferA.setTextInRange([[0, 5], [0, 5]], " there"); + bufferA.transact(() => bufferA.setTextInRange([[1, 0], [1, 5]], "friend")); + const layerA = bufferA.addMarkerLayer({maintainHistory: true, persistent: true}); + layerA.markRange([[0, 6], [0, 8]], {reversed: true, foo: 1}); + const layerB = bufferA.addMarkerLayer({maintainHistory: true, persistent: true, role: "selections"}); + const marker2A = bufferA.markPosition([2, 2], {bar: 2}); + bufferA.transact(function() { + bufferA.setTextInRange([[1, 0], [1, 0]], "good "); + bufferA.append("?"); + marker2A.setProperties({bar: 3, baz: 4}); + }); + layerA.markRange([[0, 4], [0, 5]], {invalidate: 'inside'}); + bufferA.setTextInRange([[0, 5], [0, 5]], "oo"); + bufferA.undo(); + + const state = JSON.parse(JSON.stringify(bufferA.serialize())); + TextBuffer.deserialize(state).then(function(bufferB) { + expect(bufferB.getText()).toBe("hello there\ngood friend\r\nhow are you doing??"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + expect(bufferB.getDisplayLayer(displayLayer1A.id).foldsIntersectingBufferRange([[0, 1], [0, 3]]).length).toBe(1); + expect(bufferB.getDisplayLayer(displayLayer2A.id).foldsIntersectingBufferRange([[0, 0], [0, 2]]).length).toBe(1); + const displayLayer3B = bufferB.addDisplayLayer(); + expect(displayLayer3B.id).toBeGreaterThan(displayLayer1A.id); + expect(displayLayer3B.id).toBeGreaterThan(displayLayer2A.id); + + expect(bufferB.getMarkerLayer(layerB.id).getRole()).toBe("selections"); + expect(bufferB.selectionsMarkerLayerIds.has(layerB.id)).toBe(true); + expect(bufferB.selectionsMarkerLayerIds.size).toBe(1); + + bufferA.redo(); + bufferB.redo(); + expect(bufferB.getText()).toBe("hellooo there\ngood friend\r\nhow are you doing??"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + expect(bufferB.getMarkerLayer(layerA.id).maintainHistory).toBe(true); + expect(bufferB.getMarkerLayer(layerA.id).persistent).toBe(true); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello there\ngood friend\r\nhow are you doing??"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello there\nfriend\r\nhow are you doing?"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello there\nworld\r\nhow are you doing?"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + bufferA.undo(); + bufferB.undo(); + expect(bufferB.getText()).toBe("hello\nworld\r\nhow are you doing?"); + expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA); + + // Accounts for deserialized markers when selecting the next marker's id + const marker3A = layerA.markRange([[0, 1], [2, 3]]); + const marker3B = bufferB.getMarkerLayer(layerA.id).markRange([[0, 1], [2, 3]]); + expect(marker3B.id).toBe(marker3A.id); + + // Doesn't try to reload the buffer since it has no file. + setTimeout(function() { + expect(bufferB.getText()).toBe("hello\nworld\r\nhow are you doing?"); + done(); + }, 50); + }); + }); + + it("serializes / deserializes the buffer's persistent custom marker layers", function(done) { + const bufferA = new TextBuffer("abcdefghijklmnopqrstuvwxyz"); + + const layer1A = bufferA.addMarkerLayer(); + const layer2A = bufferA.addMarkerLayer({persistent: true}); + + layer1A.markRange([[0, 1], [0, 2]]); + layer1A.markRange([[0, 3], [0, 4]]); + + layer2A.markRange([[0, 5], [0, 6]]); + layer2A.markRange([[0, 7], [0, 8]]); + + TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then(function(bufferB) { + const layer1B = bufferB.getMarkerLayer(layer1A.id); + const layer2B = bufferB.getMarkerLayer(layer2A.id); + expect(layer2B.persistent).toBe(true); + + expect(layer1B).toBe(undefined); + expectSameMarkers(layer2A, layer2B); + done(); + }); + }); + + it("doesn't serialize the default marker layer", function(done) { + const bufferA = new TextBuffer({text: "hello\nworld\r\nhow are you doing?"}); + const markerLayerA = bufferA.getDefaultMarkerLayer(); + const marker1A = bufferA.markRange([[0, 1], [1, 2]], {foo: 1}); + + TextBuffer.deserialize(bufferA.serialize()).then(function(bufferB) { + const markerLayerB = bufferB.getDefaultMarkerLayer(); + expect(bufferB.getMarker(marker1A.id)).toBeUndefined(); + done(); + }); + }); + + it("doesn't attempt to serialize snapshots for destroyed marker layers", function() { + buffer = new TextBuffer({text: "abc"}); + const markerLayer = buffer.addMarkerLayer({maintainHistory: true, persistent: true}); + markerLayer.markPosition([0, 3]); + buffer.insert([0, 0], 'x'); + markerLayer.destroy(); + + expect(() => buffer.serialize()).not.toThrowError(); + }); + + it("doesn't remember marker layers when calling serialize with {markerLayers: false}", function(done) { + const bufferA = new TextBuffer({text: "world"}); + const layerA = bufferA.addMarkerLayer({maintainHistory: true}); + const markerA = layerA.markPosition([0, 3]); + let markerB = null; + bufferA.transact(function() { + bufferA.insert([0, 0], 'hello '); + markerB = layerA.markPosition([0, 5]); + }); + bufferA.undo(); + + TextBuffer.deserialize(bufferA.serialize({markerLayers: false})).then(function(bufferB) { + expect(bufferB.getText()).toBe("world"); + expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined(); + expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined(); + + bufferB.redo(); + expect(bufferB.getText()).toBe("hello world"); + expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined(); + expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined(); + + bufferB.undo(); + expect(bufferB.getText()).toBe("world"); + expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined(); + expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined(); + done(); + }); + }); + + it("doesn't remember history when calling serialize with {history: false}", function(done) { + const bufferA = new TextBuffer({text: 'abc'}); + bufferA.append('def'); + bufferA.append('ghi'); + + TextBuffer.deserialize(bufferA.serialize({history: false})).then(function(bufferB) { + expect(bufferB.getText()).toBe("abcdefghi"); + expect(bufferB.undo()).toBe(false); + expect(bufferB.getText()).toBe("abcdefghi"); + done(); + }); + }); + + it("serializes / deserializes the buffer's unique identifier", function(done) { + const bufferA = new TextBuffer(); + TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then(function(bufferB) { + expect(bufferB.getId()).toEqual(bufferA.getId()); + done(); + }); + }); + + it("doesn't deserialize a state that was serialized with a different buffer version", function(done) { + const bufferA = new TextBuffer(); + const serializedBuffer = JSON.parse(JSON.stringify(bufferA.serialize())); + serializedBuffer.version = 123456789; + + TextBuffer.deserialize(serializedBuffer).then(function(bufferB) { + expect(bufferB).toBeUndefined(); + done(); + }); + }); + + it("doesn't deserialize a state referencing a file that no longer exists", function(done) { + const tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')); + const filePath = join(tempDir, 'file.txt'); + fs.writeFileSync(filePath, "something\n"); + + const bufferA = TextBuffer.loadSync(filePath); + const state = bufferA.serialize(); + + fs.unlinkSync(filePath); + + state.mustExist = true; + TextBuffer.deserialize(state).then( + () => expect('serialization succeeded with mustExist: true').toBeUndefined(), + err => expect(err.code).toBe('ENOENT')).then(done, done); + }); + + describe("when the serialized buffer was unsaved and had no path", () => it("restores the previous unsaved state of the buffer", function(done) { + buffer = new TextBuffer(); + buffer.setText("abc"); + + TextBuffer.deserialize(buffer.serialize()).then(function(buffer2) { + expect(buffer2.getPath()).toBeUndefined(); + expect(buffer2.getText()).toBe("abc"); + done(); + }); + })); + }); + + describe("::getRange()", () => it("returns the range of the entire buffer text", function() { + buffer = new TextBuffer("abc\ndef\nghi"); + expect(buffer.getRange()).toEqual([[0, 0], [2, 3]]); +})); + + describe("::getLength()", () => it("returns the lenght of the entire buffer text", function() { + buffer = new TextBuffer("abc\ndef\nghi"); + expect(buffer.getLength()).toBe("abc\ndef\nghi".length); + })); + + describe("::rangeForRow(row, includeNewline)", function() { + beforeEach(() => buffer = new TextBuffer("this\nis a test\r\ntesting")); + + describe("if includeNewline is false (the default)", () => it("returns a range from the beginning of the line to the end of the line", function() { + expect(buffer.rangeForRow(0)).toEqual([[0, 0], [0, 4]]); + expect(buffer.rangeForRow(1)).toEqual([[1, 0], [1, 9]]); + expect(buffer.rangeForRow(2)).toEqual([[2, 0], [2, 7]]); + })); + + describe("if includeNewline is true", () => it("returns a range from the beginning of the line to the beginning of the next (if it exists)", function() { + expect(buffer.rangeForRow(0, true)).toEqual([[0, 0], [1, 0]]); + expect(buffer.rangeForRow(1, true)).toEqual([[1, 0], [2, 0]]); + expect(buffer.rangeForRow(2, true)).toEqual([[2, 0], [2, 7]]); + })); + + describe("if the given row is out of range", () => it("returns the range of the nearest valid row", function() { + expect(buffer.rangeForRow(-1)).toEqual([[0, 0], [0, 4]]); + expect(buffer.rangeForRow(10)).toEqual([[2, 0], [2, 7]]); + })); + }); + + describe("::onDidChangePath()", function() { + let filePath, newPath, bufferToChange, eventHandler; + + beforeEach(function() { + const tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')); + filePath = join(tempDir, "manipulate-me"); + newPath = `${filePath}-i-moved`; + fs.writeFileSync(filePath, ""); + bufferToChange = TextBuffer.loadSync(filePath); + }); + + afterEach(function() { + bufferToChange.destroy(); + fs.removeSync(filePath); + fs.removeSync(newPath); + }); + + it("notifies observers when the buffer is saved to a new path", function(done) { + bufferToChange.onDidChangePath(function(p) { + expect(p).toBe(newPath); + done(); + }); + bufferToChange.saveAs(newPath); + }); + + it("notifies observers when the buffer's file is moved", function(done) { + // FIXME: This doesn't pass on Linux + if (['linux', 'win32'].includes(process.platform)) { + done(); + return; + } + + bufferToChange.onDidChangePath(function(p) { + expect(p).toBe(newPath); + done(); + }); + + fs.removeSync(newPath); + fs.moveSync(filePath, newPath); + }); + }); + + // This spec is no longer needed because `onWillThrowWatchError` is a no-op. + // `pathwatcher` can't fulfill the callback because it chooses not to reload + // the entire file every time it changes (for performance reasons), hence it + // stopped throwing this error a long time ago. + // + // (Indeed, this test is tautological, since it manually generates the event.) + xdescribe("::onWillThrowWatchError", () => it("notifies observers when the file has a watch error", function() { + const filePath = temp.openSync('atom').path; + fs.writeFileSync(filePath, ''); + + buffer = TextBuffer.loadSync(filePath); + + const eventHandler = jasmine.createSpy('eventHandler'); + buffer.onWillThrowWatchError(eventHandler); + + buffer.file.emitter.emit('will-throw-watch-error', 'arg'); + expect(eventHandler).toHaveBeenCalledWith('arg'); + })); + + describe("::getLines()", () => it("returns an array of lines in the text contents", function() { + const filePath = require.resolve('./fixtures/sample.js'); + const fileContents = fs.readFileSync(filePath, 'utf8'); + buffer = TextBuffer.loadSync(filePath); + expect(buffer.getLines().length).toBe(fileContents.split("\n").length); + expect(buffer.getLines().join('\n')).toBe(fileContents); + })); + + describe("::setTextInRange(range, string)", function() { + let changeHandler = null; + + beforeEach(function(done) { + const filePath = require.resolve('./fixtures/sample.js'); + const fileContents = fs.readFileSync(filePath, 'utf8'); + TextBuffer.load(filePath).then(function(result) { + buffer = result; + changeHandler = jasmine.createSpy('changeHandler'); + buffer.onDidChange(changeHandler); + done(); + }); + }); + + describe("when used to insert (called with an empty range and a non-empty string)", function() { + describe("when the given string has no newlines", () => it("inserts the string at the location of the given range", function() { + const range = [[3, 4], [3, 4]]; + buffer.setTextInRange(range, "foo"); + + expect(buffer.lineForRow(2)).toBe(" if (items.length <= 1) return items;"); + expect(buffer.lineForRow(3)).toBe(" foovar pivot = items.shift(), current, left = [], right = [];"); + expect(buffer.lineForRow(4)).toBe(" while(items.length > 0) {"); + + expect(changeHandler).toHaveBeenCalled(); + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 4], [3, 7]]); + expect(event.oldText).toBe(""); + expect(event.newText).toBe("foo"); + })); + + describe("when the given string has newlines", () => it("inserts the lines at the location of the given range", function() { + const range = [[3, 4], [3, 4]]; + + buffer.setTextInRange(range, "foo\n\nbar\nbaz"); + + expect(buffer.lineForRow(2)).toBe(" if (items.length <= 1) return items;"); + expect(buffer.lineForRow(3)).toBe(" foo"); + expect(buffer.lineForRow(4)).toBe(""); + expect(buffer.lineForRow(5)).toBe("bar"); + expect(buffer.lineForRow(6)).toBe("bazvar pivot = items.shift(), current, left = [], right = [];"); + expect(buffer.lineForRow(7)).toBe(" while(items.length > 0) {"); + + expect(changeHandler).toHaveBeenCalled(); + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 4], [6, 3]]); + expect(event.oldText).toBe(""); + expect(event.newText).toBe("foo\n\nbar\nbaz"); + })); + }); + + describe("when used to remove (called with a non-empty range and an empty string)", function() { + describe("when the range is contained within a single line", () => it("removes the characters within the range", function() { + const range = [[3, 4], [3, 7]]; + buffer.setTextInRange(range, ""); + + expect(buffer.lineForRow(2)).toBe(" if (items.length <= 1) return items;"); + expect(buffer.lineForRow(3)).toBe(" pivot = items.shift(), current, left = [], right = [];"); + expect(buffer.lineForRow(4)).toBe(" while(items.length > 0) {"); + + expect(changeHandler).toHaveBeenCalled(); + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 4], [3, 4]]); + expect(event.oldText).toBe("var"); + expect(event.newText).toBe(""); + })); + + describe("when the range spans 2 lines", () => it("removes the characters within the range and joins the lines", function() { + const range = [[3, 16], [4, 4]]; + buffer.setTextInRange(range, ""); + + expect(buffer.lineForRow(2)).toBe(" if (items.length <= 1) return items;"); + expect(buffer.lineForRow(3)).toBe(" var pivot = while(items.length > 0) {"); + expect(buffer.lineForRow(4)).toBe(" current = items.shift();"); + + expect(changeHandler).toHaveBeenCalled(); + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 16], [3, 16]]); + expect(event.oldText).toBe("items.shift(), current, left = [], right = [];\n "); + expect(event.newText).toBe(""); + })); + + describe("when the range spans more than 2 lines", () => it("removes the characters within the range, joining the first and last line and removing the lines in-between", function() { + buffer.setTextInRange([[3, 16], [11, 9]], ""); + + expect(buffer.lineForRow(2)).toBe(" if (items.length <= 1) return items;"); + expect(buffer.lineForRow(3)).toBe(" var pivot = sort(Array.apply(this, arguments));"); + expect(buffer.lineForRow(4)).toBe("};"); + })); + }); + + describe("when used to replace text with other text (called with non-empty range and non-empty string)", () => it("replaces the old text with the new text", function() { + const range = [[3, 16], [11, 9]]; + const oldText = buffer.getTextInRange(range); + + buffer.setTextInRange(range, "foo\nbar"); + + expect(buffer.lineForRow(2)).toBe(" if (items.length <= 1) return items;"); + expect(buffer.lineForRow(3)).toBe(" var pivot = foo"); + expect(buffer.lineForRow(4)).toBe("barsort(Array.apply(this, arguments));"); + expect(buffer.lineForRow(5)).toBe("};"); + + expect(changeHandler).toHaveBeenCalled(); + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.oldRange).toEqual(range); + expect(event.newRange).toEqual([[3, 16], [4, 3]]); + expect(event.oldText).toBe(oldText); + expect(event.newText).toBe("foo\nbar"); + })); + + it("allows a change to be undone safely from an ::onDidChange callback", function() { + buffer.onDidChange(() => buffer.undo()); + buffer.setTextInRange([[0, 0], [0, 0]], "hello"); + expect(buffer.lineForRow(0)).toBe("var quicksort = function () {"); + }); + }); + + describe("::setText(text)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + describe("when the buffer contains newlines", () => it("changes the entire contents of the buffer and emits a change event", function() { + const lastRow = buffer.getLastRow(); + const expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]]; + const changeHandler = jasmine.createSpy('changeHandler'); + buffer.onDidChange(changeHandler); + + const newText = "I know you are.\nBut what am I?"; + buffer.setText(newText); + + expect(buffer.getText()).toBe(newText); + expect(changeHandler).toHaveBeenCalled(); + + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.newText).toBe(newText); + expect(event.oldRange).toEqual(expectedPreRange); + expect(event.newRange).toEqual([[0, 0], [1, 14]]); + })); + + describe("with windows newlines", () => it("changes the entire contents of the buffer", function() { + buffer = new TextBuffer("first\r\nlast"); + const lastRow = buffer.getLastRow(); + const expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]]; + const changeHandler = jasmine.createSpy('changeHandler'); + buffer.onDidChange(changeHandler); + + const newText = "new first\r\nnew last"; + buffer.setText(newText); + + expect(buffer.getText()).toBe(newText); + expect(changeHandler).toHaveBeenCalled(); + + const [event] = changeHandler.calls.allArgs()[0]; + expect(event.newText).toBe(newText); + expect(event.oldRange).toEqual(expectedPreRange); + expect(event.newRange).toEqual([[0, 0], [1, 8]]); + })); +}); + + describe("::setTextViaDiff(text)", function() { + beforeEach(function(done) { + const filePath = require.resolve('./fixtures/sample.js'); + TextBuffer.load(filePath).then(function(result) { + buffer = result; + done(); + }); + }); + + it("can change the entire contents of the buffer when there are no newlines", function() { + buffer.setText('BUFFER CHANGE'); + const newText = 'DISK CHANGE'; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("can change a buffer that contains lone carriage returns", function() { + const oldText = 'one\rtwo\nthree\rfour\n'; + const newText = 'one\rtwo and\nthree\rfour\n'; + buffer.setText(oldText); + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + buffer.undo(); + expect(buffer.getText()).toBe(oldText); + }); + + describe("with standard newlines", function() { + it("can change the entire contents of the buffer with no newline at the end", function() { + const newText = "I know you are.\nBut what am I?"; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("can change the entire contents of the buffer with a newline at the end", function() { + const newText = "I know you are.\nBut what am I?\n"; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("can change a few lines at the beginning in the buffer", function() { + const newText = buffer.getText().replace(/function/g, 'omgwow'); + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("can change a few lines in the middle of the buffer", function() { + const newText = buffer.getText().replace(/shift/g, 'omgwow'); + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("can adds a newline at the end", function() { + const newText = buffer.getText() + '\n'; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + }); + + describe("with windows newlines", function() { + beforeEach(() => buffer.setText(buffer.getText().replace(/\n/g, '\r\n'))); + + it("adds a newline at the end", function() { + const newText = buffer.getText() + '\r\n'; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("changes the entire contents of the buffer with smaller content with no newline at the end", function() { + const newText = "I know you are.\r\nBut what am I?"; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("changes the entire contents of the buffer with smaller content with newline at the end", function() { + const newText = "I know you are.\r\nBut what am I?\r\n"; + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("changes a few lines at the beginning in the buffer", function() { + const newText = buffer.getText().replace(/function/g, 'omgwow'); + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + + it("changes a few lines in the middle of the buffer", function() { + const newText = buffer.getText().replace(/shift/g, 'omgwow'); + buffer.setTextViaDiff(newText); + expect(buffer.getText()).toBe(newText); + }); + }); + }); + + describe("::getTextInRange(range)", function() { + beforeEach(function(done) { + const filePath = require.resolve('./fixtures/sample.js'); + TextBuffer.load(filePath).then(function(result) { + buffer = result; + done(); + }); + }); + + describe("when range is empty", () => it("returns an empty string", function() { + const range = [[1, 1], [1, 1]]; + expect(buffer.getTextInRange(range)).toBe(""); + })); + + describe("when range spans one line", () => it("returns characters in range", function() { + let range = [[2, 8], [2, 13]]; + expect(buffer.getTextInRange(range)).toBe("items"); + + const lineLength = buffer.lineForRow(2).length; + range = [[2, 0], [2, lineLength]]; + expect(buffer.getTextInRange(range)).toBe(" if (items.length <= 1) return items;"); + })); + + describe("when range spans multiple lines", () => it("returns characters in range (including newlines)", function() { + let lineLength = buffer.lineForRow(2).length; + let range = [[2, 0], [3, 0]]; + expect(buffer.getTextInRange(range)).toBe(" if (items.length <= 1) return items;\n"); + + lineLength = buffer.lineForRow(2).length; + range = [[2, 10], [4, 10]]; + expect(buffer.getTextInRange(range)).toBe("ems.length <= 1) return items;\n var pivot = items.shift(), current, left = [], right = [];\n while("); + })); + + describe("when the range starts before the start of the buffer", () => it("clips the range to the start of the buffer", () => expect(buffer.getTextInRange([[-Infinity, -Infinity], [0, Infinity]])).toBe(buffer.lineForRow(0)))); + + describe("when the range ends after the end of the buffer", () => it("clips the range to the end of the buffer", () => expect(buffer.getTextInRange([[12], [13, Infinity]])).toBe(buffer.lineForRow(12)))); + }); + + describe("::scan(regex, fn)", function() { + beforeEach(() => buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js'))); + + it("calls the given function with the information about each match", function() { + const matches = []; + buffer.scan(/current/g, match => matches.push(match)); + expect(matches.length).toBe(5); + + expect(matches[0].matchText).toBe('current'); + expect(matches[0].range).toEqual([[3, 31], [3, 38]]); + expect(matches[0].lineText).toBe(' var pivot = items.shift(), current, left = [], right = [];'); + expect(matches[0].lineTextOffset).toBe(0); + expect(matches[0].leadingContextLines.length).toBe(0); + expect(matches[0].trailingContextLines.length).toBe(0); + + expect(matches[1].matchText).toBe('current'); + expect(matches[1].range).toEqual([[5, 6], [5, 13]]); + expect(matches[1].lineText).toBe(' current = items.shift();'); + expect(matches[1].lineTextOffset).toBe(0); + expect(matches[1].leadingContextLines.length).toBe(0); + expect(matches[1].trailingContextLines.length).toBe(0); + }); + + it("calls the given function with the information about each match including context lines", function() { + const matches = []; + buffer.scan(/current/g, {leadingContextLineCount: 1, trailingContextLineCount: 2}, match => matches.push(match)); + expect(matches.length).toBe(5); + + expect(matches[0].matchText).toBe('current'); + expect(matches[0].range).toEqual([[3, 31], [3, 38]]); + expect(matches[0].lineText).toBe(' var pivot = items.shift(), current, left = [], right = [];'); + expect(matches[0].lineTextOffset).toBe(0); + expect(matches[0].leadingContextLines.length).toBe(1); + expect(matches[0].leadingContextLines[0]).toBe(' if (items.length <= 1) return items;'); + expect(matches[0].trailingContextLines.length).toBe(2); + expect(matches[0].trailingContextLines[0]).toBe(' while(items.length > 0) {'); + expect(matches[0].trailingContextLines[1]).toBe(' current = items.shift();'); + + expect(matches[1].matchText).toBe('current'); + expect(matches[1].range).toEqual([[5, 6], [5, 13]]); + expect(matches[1].lineText).toBe(' current = items.shift();'); + expect(matches[1].lineTextOffset).toBe(0); + expect(matches[1].leadingContextLines.length).toBe(1); + expect(matches[1].leadingContextLines[0]).toBe(' while(items.length > 0) {'); + expect(matches[1].trailingContextLines.length).toBe(2); + expect(matches[1].trailingContextLines[0]).toBe(' current < pivot ? left.push(current) : right.push(current);'); + expect(matches[1].trailingContextLines[1]).toBe(' }'); + }); + }); + + describe("::backwardsScan(regex, fn)", function() { + beforeEach(() => buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js'))); + + it("calls the given function with the information about each match in backwards order", function() { + const matches = []; + buffer.backwardsScan(/current/g, match => matches.push(match)); + expect(matches.length).toBe(5); + + expect(matches[0].matchText).toBe('current'); + expect(matches[0].range).toEqual([[6, 56], [6, 63]]); + expect(matches[0].lineText).toBe(' current < pivot ? left.push(current) : right.push(current);'); + expect(matches[0].lineTextOffset).toBe(0); + expect(matches[0].leadingContextLines.length).toBe(0); + expect(matches[0].trailingContextLines.length).toBe(0); + + expect(matches[1].matchText).toBe('current'); + expect(matches[1].range).toEqual([[6, 34], [6, 41]]); + expect(matches[1].lineText).toBe(' current < pivot ? left.push(current) : right.push(current);'); + expect(matches[1].lineTextOffset).toBe(0); + expect(matches[1].leadingContextLines.length).toBe(0); + expect(matches[1].trailingContextLines.length).toBe(0); + }); + }); + + describe("::scanInRange(range, regex, fn)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + describe("when given a regex with a ignore case flag", () => it("does a case-insensitive search", function() { + const matches = []; + buffer.scanInRange(/cuRRent/i, [[0, 0], [12, 0]], ({match, range}) => matches.push(match)); + expect(matches.length).toBe(1); + })); + + describe("when given a regex with no global flag", () => it("calls the iterator with the first match for the given regex in the given range", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(rr)ent/, [[4, 0], [6, 44]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe('current'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[5, 6], [5, 13]]); + })); + + describe("when given a regex with a global flag", () => it("calls the iterator with each match for the given regex in the given range", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(3); + expect(ranges.length).toBe(3); + + expect(matches[0][0]).toBe('current'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[5, 6], [5, 13]]); + + expect(matches[1][0]).toBe('current'); + expect(matches[1][1]).toBe('rr'); + expect(ranges[1]).toEqual([[6, 6], [6, 13]]); + + expect(matches[2][0]).toBe('current'); + expect(matches[2][1]).toBe('rr'); + expect(ranges[2]).toEqual([[6, 34], [6, 41]]); + })); + + describe("when the last regex match exceeds the end of the range", function() { + describe("when the portion of the match within the range also matches the regex", () => it("calls the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(r*)/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(2); + expect(ranges.length).toBe(2); + + expect(matches[0][0]).toBe('curr'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[5, 6], [5, 10]]); + + expect(matches[1][0]).toBe('cur'); + expect(matches[1][1]).toBe('r'); + expect(ranges[1]).toEqual([[6, 6], [6, 9]]); + })); + + describe("when the portion of the match within the range does not matches the regex", () => it("does not call the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.scanInRange(/cu(r*)e/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe('curre'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[5, 6], [5, 11]]); + })); + }); + + describe("when the iterator calls the 'replace' control function with a replacement string", function() { + it("replaces each occurrence of the regex match with the string", function() { + const ranges = []; + buffer.scanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, replace}) { + ranges.push(range); + replace("foo"); + }); + + expect(ranges[0]).toEqual([[5, 6], [5, 13]]); + expect(ranges[1]).toEqual([[6, 6], [6, 13]]); + expect(ranges[2]).toEqual([[6, 30], [6, 37]]); + + expect(buffer.lineForRow(5)).toBe(' foo = items.shift();'); + expect(buffer.lineForRow(6)).toBe(' foo < pivot ? left.push(foo) : right.push(current);'); + }); + + it("allows the match to be replaced with the empty string", function() { + buffer.scanInRange(/current/g, [[4, 0], [6, 59]], ({replace}) => replace("")); + + expect(buffer.lineForRow(5)).toBe(' = items.shift();'); + expect(buffer.lineForRow(6)).toBe(' < pivot ? left.push() : right.push(current);'); + }); + }); + + describe("when the iterator calls the 'stop' control function", () => it("stops the traversal", function() { + const ranges = []; + buffer.scanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, stop}) { + ranges.push(range); + if (ranges.length === 2) { stop(); } + }); + + expect(ranges.length).toBe(2); + })); + + it("returns the same results as a regex match on a regular string", function() { + const regexps = [ + /\w+/g, // 1 word + /\w+\n\s*\w+/g, // 2 words separated by an newline (escape sequence) + RegExp("\\w+\n\\s*\w+", 'g'), // 2 words separated by a newline (literal) + /\w+\s+\w+/g, // 2 words separated by some whitespace + /\w+[^\w]+\w+/g, // 2 words separated by anything + /\w+\n\s*\w+\n\s*\w+/g, // 3 words separated by newlines (escape sequence) + RegExp("\\w+\n\\s*\\w+\n\\s*\\w+", 'g'), // 3 words separated by newlines (literal) + /\w+[^\w]+\w+[^\w]+\w+/g, // 3 words separated by anything + ]; + + let i = 0; + while (i < 20) { + var left; + var seed = Date.now(); + var random = new Random(seed); + + var text = buildRandomLines(random, 40); + buffer = new TextBuffer({text}); + buffer.backwardsScanChunkSize = random.intBetween(100, 1000); + + var range = getRandomBufferRange(random, buffer) + .union(getRandomBufferRange(random, buffer)) + .union(getRandomBufferRange(random, buffer)); + var regex = regexps[random(regexps.length)]; + + var expectedMatches = (left = buffer.getTextInRange(range).match(regex)) != null ? left : []; + if (!(expectedMatches.length > 0)) { continue; } + i++; + + var forwardRanges = []; + var forwardMatches = []; + buffer.scanInRange(regex, range, function({range, matchText}) { + forwardRanges.push(range); + forwardMatches.push(matchText); + }); + expect(forwardMatches).toEqual(expectedMatches, `Seed: ${seed}`); + + var backwardRanges = []; + var backwardMatches = []; + buffer.backwardsScanInRange(regex, range, function({range, matchText}) { + backwardRanges.push(range); + backwardMatches.push(matchText); + }); + expect(backwardMatches).toEqual(expectedMatches.reverse(), `Seed: ${seed}`); + } + }); + + it("does not return empty matches at the end of the range", function() { + const ranges = []; + buffer.scanInRange(/[ ]*/gm, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[0, 29], [0, 29]], [[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.scanInRange(/[ ]*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.scanInRange(/\s*/gm, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[0, 29], [1, 2]]]); + + ranges.length = 0; + buffer.scanInRange(/\s*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + }); + + it("allows empty matches at the end of a range, when the range ends at column 0", function() { + const ranges = []; + buffer.scanInRange(/^[ ]*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^[ ]*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^\s*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^\s*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.scanInRange(/^\s*/gm, [[11, 0], [12, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[11, 0], [11, 2]], [[12, 0], [12, 0]]]); + }); + + it("handles multi-line patterns", function() { + const matchStrings = []; + + // The '\s' character class + buffer.scan(/{\s+var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A literal newline character + matchStrings.length = 0; + buffer.scan(RegExp("{\n var"), ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A '\n' escape sequence + matchStrings.length = 0; + buffer.scan(/{\n var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A negated character class in the middle of the pattern + matchStrings.length = 0; + buffer.scan(/{[^a] var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['{\n var']); + + // A negated character class at the beginning of the pattern + matchStrings.length = 0; + buffer.scan(/[^a] var/, ({matchText}) => matchStrings.push(matchText)); + expect(matchStrings).toEqual(['\n var']); + }); + }); + + describe("::find(regex)", () => it("resolves with the first range that matches the given regex", function(done) { + buffer = new TextBuffer('abc\ndefghi'); + buffer.find(/\wf\w*/).then(function(range) { + expect(range).toEqual(Range(Point(1, 1), Point(1, 6))); + done(); + }); + })); + + describe("::findAllSync(regex)", () => it("returns all the ranges that match the given regex", function() { + buffer = new TextBuffer('abc\ndefghi'); + expect(buffer.findAllSync(/[bf]\w+/)).toEqual([ + Range(Point(0, 1), Point(0, 3)), + Range(Point(1, 2), Point(1, 6)), + ]); + })); + + describe("::findAndMarkAllInRangeSync(markerLayer, regex, range, options)", () => it("populates the marker index with the matching ranges", function() { + buffer = new TextBuffer('abc def\nghi jkl\n'); + const layer = buffer.addMarkerLayer(); + let markers = buffer.findAndMarkAllInRangeSync(layer, /\w+/g, [[0, 1], [1, 6]], {invalidate: 'inside'}); + expect(markers.map(marker => marker.getRange())).toEqual([ + [[0, 1], [0, 3]], + [[0, 4], [0, 7]], + [[1, 0], [1, 3]], + [[1, 4], [1, 6]] + ]); + expect(markers[0].getInvalidationStrategy()).toBe('inside'); + expect(markers[0].isExclusive()).toBe(true); + + markers = buffer.findAndMarkAllInRangeSync(layer, /abc/g, [[0, 0], [1, 0]], {invalidate: 'touch'}); + expect(markers.map(marker => marker.getRange())).toEqual([ + [[0, 0], [0, 3]] + ]); + expect(markers[0].getInvalidationStrategy()).toBe('touch'); + expect(markers[0].isExclusive()).toBe(false); + })); + + describe("::findWordsWithSubsequence and ::findWordsWithSubsequenceInRange", function() { + it('resolves with all words matching the given query', function(done) { + buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana'); + buffer.findWordsWithSubsequence('bna', '_', 4).then(function(results) { + const expected = [ + { + score: 29, + matchIndices: [0, 1, 2], + positions: [{row: 0, column: 36}], + word: "bNa" + }, + { + score: 16, + matchIndices: [0, 2, 4], + positions: [{row: 0, column: 15}], + word: "ban_ana" + }, + { + score: 12, + matchIndices: [0, 2, 3], + positions: [{row: 0, column: 0}, {row: 1, column: 0}], + word: "banana" + }, + { + score: 7, + matchIndices: [0, 5, 6], + positions: [{row: 0, column: 7}], + word: "bandana" + } + ]; + // JSON serialization doesn't work properly with `SubsequenceMatch` + // results, so we use another strategy to test deep equality. + for (var i in results) { + var result = results[i]; + for (var value = 0; value < result.length; value++) { + var prop = result[value]; + expect(value).toEqual(expected[i][prop]); + } + } + done(); + }); + }); + + it('resolves with all words matching the given query and range', function(done) { + const range = {start: {column: 0, row: 0}, end: {column: 22, row: 0}}; + buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana'); + buffer.findWordsWithSubsequenceInRange('bna', '_', 3, range).then(function(results) { + const expected = [ + { + score: 16, + matchIndices: [0, 2, 4], + positions: [{row: 0, column: 15}], + word: "ban_ana" + }, + { + score: 12, + matchIndices: [0, 2, 3], + positions: [{row: 0, column: 0}], + word: "banana" + }, + { + score: 7, + matchIndices: [0, 5, 6], + positions: [{row: 0, column: 7}], + word: "bandana" + } + ]; + // JSON serialization doesn't work properly with `SubsequenceMatch` + // results, so we use another strategy to test deep equality. + for (var i in results) { + var result = results[i]; + for (var value = 0; value < result.length; value++) { + var prop = result[value]; + expect(value).toEqual(expected[i][prop]); + } + } + done(); + }); + }); + }); + + describe("::backwardsScanInRange(range, regex, fn)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + describe("when given a regex with no global flag", () => it("calls the iterator with the last match for the given regex in the given range", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/, [[4, 0], [6, 44]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe('current'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[6, 34], [6, 41]]); + })); + + describe("when given a regex with a global flag", () => it("calls the iterator with each match for the given regex in the given range, starting with the last match", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(3); + expect(ranges.length).toBe(3); + + expect(matches[0][0]).toBe('current'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[6, 34], [6, 41]]); + + expect(matches[1][0]).toBe('current'); + expect(matches[1][1]).toBe('rr'); + expect(ranges[1]).toEqual([[6, 6], [6, 13]]); + + expect(matches[2][0]).toBe('current'); + expect(matches[2][1]).toBe('rr'); + expect(ranges[2]).toEqual([[5, 6], [5, 13]]); + })); + + describe("when the last regex match starts at the beginning of the range", () => it("calls the iterator with the match", function() { + let matches = []; + let ranges = []; + buffer.scanInRange(/quick/g, [[0, 4], [2, 0]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe('quick'); + expect(ranges[0]).toEqual([[0, 4], [0, 9]]); + + matches = []; + ranges = []; + buffer.scanInRange(/^/, [[0, 0], [2, 0]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe(""); + expect(ranges[0]).toEqual([[0, 0], [0, 0]]); + })); + + describe("when the first regex match exceeds the end of the range", function() { + describe("when the portion of the match within the range also matches the regex", () => it("calls the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(r*)/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(2); + expect(ranges.length).toBe(2); + + expect(matches[0][0]).toBe('cur'); + expect(matches[0][1]).toBe('r'); + expect(ranges[0]).toEqual([[6, 6], [6, 9]]); + + expect(matches[1][0]).toBe('curr'); + expect(matches[1][1]).toBe('rr'); + expect(ranges[1]).toEqual([[5, 6], [5, 10]]); + })); + + describe("when the portion of the match within the range does not matches the regex", () => it("does not call the iterator with the truncated match", function() { + const matches = []; + const ranges = []; + buffer.backwardsScanInRange(/cu(r*)e/g, [[4, 0], [6, 9]], function({match, range}) { + matches.push(match); + ranges.push(range); + }); + + expect(matches.length).toBe(1); + expect(ranges.length).toBe(1); + + expect(matches[0][0]).toBe('curre'); + expect(matches[0][1]).toBe('rr'); + expect(ranges[0]).toEqual([[5, 6], [5, 11]]); + })); + }); + + describe("when the iterator calls the 'replace' control function with a replacement string", () => it("replaces each occurrence of the regex match with the string", function() { + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, replace}) { + ranges.push(range); + if (!range.start.isEqual([6, 6])) { replace("foo"); } + }); + + expect(ranges[0]).toEqual([[6, 34], [6, 41]]); + expect(ranges[1]).toEqual([[6, 6], [6, 13]]); + expect(ranges[2]).toEqual([[5, 6], [5, 13]]); + + expect(buffer.lineForRow(5)).toBe(' foo = items.shift();'); + expect(buffer.lineForRow(6)).toBe(' current < pivot ? left.push(foo) : right.push(current);'); + })); + + describe("when the iterator calls the 'stop' control function", () => it("stops the traversal", function() { + const ranges = []; + buffer.backwardsScanInRange(/cu(rr)ent/g, [[4, 0], [6, 59]], function({range, stop}) { + ranges.push(range); + if (ranges.length === 2) { stop(); } + }); + + expect(ranges.length).toBe(2); + expect(ranges[0]).toEqual([[6, 34], [6, 41]]); + expect(ranges[1]).toEqual([[6, 6], [6, 13]]); + })); + + describe("when called with a random range", () => { + it("returns the same results as ::scanInRange, but in the opposite order", () => { + for (let i = 1; i < 50; i++) { + var seed = Date.now(); + var random = new Random(seed); + + buffer.backwardsScanChunkSize = random.intBetween(1, 80); + + var [startRow, endRow] = [random(buffer.getLineCount()), random(buffer.getLineCount())].sort(); + var startColumn = random(buffer.lineForRow(startRow).length); + var endColumn = random(buffer.lineForRow(endRow).length); + var range = [[startRow, startColumn], [endRow, endColumn]]; + + var regex = [ + /\w/g, + /\w{2}/g, + /\w{3}/g, + /.{5}/g + ][random(4)]; + + if (random(2) > 0) { + var forwardRanges = []; + var backwardRanges = []; + var forwardMatches = []; + var backwardMatches = []; + + buffer.scanInRange(regex, range, function({range, matchText}) { + forwardMatches.push(matchText); + forwardRanges.push(range); + }); + + buffer.backwardsScanInRange(regex, range, function({range, matchText}) { + backwardMatches.unshift(matchText); + backwardRanges.unshift(range); + }); + + expect(backwardRanges).toEqual(forwardRanges, `Seed: ${seed}`); + expect(backwardMatches).toEqual(forwardMatches, `Seed: ${seed}`); + } else { + var referenceBuffer = new TextBuffer({text: buffer.getText()}); + referenceBuffer.scanInRange(regex, range, ({matchText, replace}) => replace(matchText + '.')); + + buffer.backwardsScanInRange(regex, range, ({matchText, replace}) => replace(matchText + '.')); + + expect(buffer.getText()).toBe(referenceBuffer.getText(), `Seed: ${seed}`); + } + } + }); + }); + + it("does not return empty matches at the end of the range", function() { + const ranges = []; + + buffer.backwardsScanInRange(/[ ]*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/[ ]*/m, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/\s*/gm, [[1, 0], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[1, 0], [1, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/\s*/m, [[0, 29], [1, 2]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[0, 29], [1, 2]]]); + }); + + it("allows empty matches at the end of a range, when the range ends at column 0", function() { + const ranges = []; + buffer.backwardsScanInRange(/^[ ]*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/^[ ]*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/^\s*/gm, [[9, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]); + + ranges.length = 0; + buffer.backwardsScanInRange(/^\s*/gm, [[10, 0], [10, 0]], ({range}) => ranges.push(range)); + expect(ranges).toEqual([[[10, 0], [10, 0]]]); + }); + }); + + describe("::characterIndexForPosition(position)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + it("returns the total number of characters that precede the given position", function() { + expect(buffer.characterIndexForPosition([0, 0])).toBe(0); + expect(buffer.characterIndexForPosition([0, 1])).toBe(1); + expect(buffer.characterIndexForPosition([0, 29])).toBe(29); + expect(buffer.characterIndexForPosition([1, 0])).toBe(30); + expect(buffer.characterIndexForPosition([2, 0])).toBe(61); + expect(buffer.characterIndexForPosition([12, 2])).toBe(408); + expect(buffer.characterIndexForPosition([Infinity])).toBe(408); + }); + + describe("when the buffer contains crlf line endings", () => it("returns the total number of characters that precede the given position", function() { + buffer.setText("line1\r\nline2\nline3\r\nline4"); + expect(buffer.characterIndexForPosition([1])).toBe(7); + expect(buffer.characterIndexForPosition([2])).toBe(13); + expect(buffer.characterIndexForPosition([3])).toBe(20); + })); + }); + + describe("::positionForCharacterIndex(position)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + it("returns the position based on character index", function() { + expect(buffer.positionForCharacterIndex(0)).toEqual([0, 0]); + expect(buffer.positionForCharacterIndex(1)).toEqual([0, 1]); + expect(buffer.positionForCharacterIndex(29)).toEqual([0, 29]); + expect(buffer.positionForCharacterIndex(30)).toEqual([1, 0]); + expect(buffer.positionForCharacterIndex(61)).toEqual([2, 0]); + expect(buffer.positionForCharacterIndex(408)).toEqual([12, 2]); + }); + + describe("when the buffer contains crlf line endings", () => it("returns the position based on character index", function() { + buffer.setText("line1\r\nline2\nline3\r\nline4"); + expect(buffer.positionForCharacterIndex(7)).toEqual([1, 0]); + expect(buffer.positionForCharacterIndex(13)).toEqual([2, 0]); + expect(buffer.positionForCharacterIndex(20)).toEqual([3, 0]); + })); +}); + + describe("::isEmpty()", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + it("returns true for an empty buffer", function() { + buffer.setText(''); + expect(buffer.isEmpty()).toBeTruthy(); + }); + + it("returns false for a non-empty buffer", function() { + buffer.setText('a'); + expect(buffer.isEmpty()).toBeFalsy(); + buffer.setText('a\nb\nc'); + expect(buffer.isEmpty()).toBeFalsy(); + buffer.setText('\n'); + expect(buffer.isEmpty()).toBeFalsy(); + }); + }); + + describe("::hasAstral()", function() { + it("returns true for buffers containing surrogate pairs", () => expect(new TextBuffer('hooray 😄').hasAstral()).toBeTruthy()); + + it("returns false for buffers that do not contain surrogate pairs", () => expect(new TextBuffer('nope').hasAstral()).toBeFalsy()); + }); + + describe("::onWillChange(callback)", () => it("notifies observers before a transaction, an undo or a redo", function() { + let changeCount = 0; + let expectedText = ''; + + buffer = new TextBuffer(); + const checkpoint = buffer.createCheckpoint(); + + buffer.onWillChange(function(change) { + expect(buffer.getText()).toBe(expectedText); + changeCount++; + }); + + buffer.append('a'); + expect(changeCount).toBe(1); + expectedText = 'a'; + + buffer.transact(function() { + buffer.append('b'); + buffer.append('c'); + }); + expect(changeCount).toBe(2); + expectedText = 'abc'; + + // Empty transactions do not cause onWillChange listeners to be called + buffer.transact(function() {}); + expect(changeCount).toBe(2); + + buffer.undo(); + expect(changeCount).toBe(3); + expectedText = 'a'; + + buffer.redo(); + expect(changeCount).toBe(4); + expectedText = 'abc'; + + buffer.revertToCheckpoint(checkpoint); + expect(changeCount).toBe(5); + })); + + describe("::onDidChange(callback)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + it("notifies observers after a transaction, an undo or a redo", function() { + let textChanges = []; + buffer.onDidChange(({changes}) => textChanges.push(...changes || [])); + + buffer.insert([0, 0], "abc"); + buffer.delete([[0, 0], [0, 1]]); + + assertChangesEqual(textChanges, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 3]], + oldText: "", + newText: "abc" + }, + { + oldRange: [[0, 0], [0, 1]], + newRange: [[0, 0], [0, 0]], + oldText: "a", + newText: "" + } + ]); + + textChanges = []; + buffer.transact(function() { + buffer.insert([1, 0], "v"); + buffer.insert([1, 1], "x"); + buffer.insert([1, 2], "y"); + buffer.insert([2, 3], "zw"); + buffer.delete([[2, 3], [2, 4]]); + }); + + assertChangesEqual(textChanges, [ + { + oldRange: [[1, 0], [1, 0]], + newRange: [[1, 0], [1, 3]], + oldText: "", + newText: "vxy", + }, + { + oldRange: [[2, 3], [2, 3]], + newRange: [[2, 3], [2, 4]], + oldText: "", + newText: "w", + } + ]); + + textChanges = []; + buffer.undo(); + assertChangesEqual(textChanges, [ + { + oldRange: [[1, 0], [1, 3]], + newRange: [[1, 0], [1, 0]], + oldText: "vxy", + newText: "", + }, + { + oldRange: [[2, 3], [2, 4]], + newRange: [[2, 3], [2, 3]], + oldText: "w", + newText: "", + } + ]); + + textChanges = []; + buffer.redo(); + assertChangesEqual(textChanges, [ + { + oldRange: [[1, 0], [1, 0]], + newRange: [[1, 0], [1, 3]], + oldText: "", + newText: "vxy", + }, + { + oldRange: [[2, 3], [2, 3]], + newRange: [[2, 3], [2, 4]], + oldText: "", + newText: "w", + } + ]); + + textChanges = []; + buffer.transact(() => buffer.transact(() => buffer.insert([0, 0], "j"))); + + // we emit only one event for nested transactions + assertChangesEqual(textChanges, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 1]], + oldText: "", + newText: "j", + } + ]); + }); + + it("doesn't notify observers after an empty transaction", function() { + const didChangeTextSpy = jasmine.createSpy(); + buffer.onDidChange(didChangeTextSpy); + buffer.transact(function() {}); + expect(didChangeTextSpy).not.toHaveBeenCalled(); + }); + + it("doesn't throw an error when clearing the undo stack within a transaction", function() { + let didChangeTextSpy; + buffer.onDidChange(didChangeTextSpy = jasmine.createSpy()); + expect(() => buffer.transact(() => buffer.clearUndoStack())).not.toThrowError(); + expect(didChangeTextSpy).not.toHaveBeenCalled(); + }); + }); + + describe("::onDidStopChanging(callback)", function() { + let delay, didStopChangingCallback; + + const wait = (milliseconds, callback) => setTimeout(callback, milliseconds); + + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + delay = buffer.stoppedChangingDelay; + didStopChangingCallback = jasmine.createSpy("didStopChangingCallback"); + buffer.onDidStopChanging(didStopChangingCallback); + }); + + it("notifies observers after a delay passes following changes", function(done) { + buffer.insert([0, 0], 'a'); + expect(didStopChangingCallback).not.toHaveBeenCalled(); + + wait(delay / 2, function() { + buffer.transact(() => buffer.transact(function() { + buffer.insert([0, 0], 'b'); + buffer.insert([1, 0], 'c'); + buffer.insert([1, 1], 'd'); + })); + expect(didStopChangingCallback).not.toHaveBeenCalled(); + + wait(delay / 2, function() { + expect(didStopChangingCallback).not.toHaveBeenCalled(); + + wait(delay, function() { + expect(didStopChangingCallback).toHaveBeenCalled(); + assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 2]], + oldText: "", + newText: "ba", + }, + { + oldRange: [[1, 0], [1, 0]], + newRange: [[1, 0], [1, 2]], + oldText: "", + newText: "cd", + } + ]); + + didStopChangingCallback.calls.reset(); + buffer.undo(); + buffer.undo(); + wait(delay * 2, function() { + expect(didStopChangingCallback).toHaveBeenCalled(); + assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ + { + oldRange: [[0, 0], [0, 2]], + newRange: [[0, 0], [0, 0]], + oldText: "ba", + newText: "", + }, + { + oldRange: [[1, 0], [1, 2]], + newRange: [[1, 0], [1, 0]], + oldText: "cd", + newText: "", + }, + ]); + done(); + }); + }); + }); + }); + }); + + it("provides the correct changes when the buffer is mutated in the onDidChange callback", function(done) { + buffer.onDidChange(function({changes}) { + switch (changes[0].newText) { + case 'a': + buffer.insert(changes[0].newRange.end, 'b'); + case 'b': + buffer.insert(changes[0].newRange.end, 'c'); + case 'c': + buffer.insert(changes[0].newRange.end, 'd'); + } + }); + + buffer.insert([0, 0], 'a'); + + wait(delay * 2, function() { + expect(didStopChangingCallback).toHaveBeenCalled(); + assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ + { + oldRange: [[0, 0], [0, 0]], + newRange: [[0, 0], [0, 4]], + oldText: "", + newText: "abcd", + } + ]); + done(); + }); + }); + }); + + describe("::append(text)", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + it("adds text to the end of the buffer", function() { + buffer.setText(""); + buffer.append("a"); + expect(buffer.getText()).toBe("a"); + buffer.append("b\nc"); + expect(buffer.getText()).toBe("ab\nc"); + }); + }); + + describe("::setLanguageMode", function() { + it("destroys the previous language mode", function() { + buffer = new TextBuffer(); + + const languageMode1 = { + alive: true, + destroy() { this.alive = false; }, + onDidChangeHighlighting() { return {dispose() {}}; } + }; + + const languageMode2 = { + alive: true, + destroy() { this.alive = false; }, + onDidChangeHighlighting() { return {dispose() {}}; } + }; + + buffer.setLanguageMode(languageMode1); + expect(languageMode1.alive).toBe(true); + expect(languageMode2.alive).toBe(true); + + buffer.setLanguageMode(languageMode2); + expect(languageMode1.alive).toBe(false); + expect(languageMode2.alive).toBe(true); + + buffer.destroy(); + expect(languageMode1.alive).toBe(false); + expect(languageMode2.alive).toBe(false); + }); + + it("notifies ::onDidChangeLanguageMode observers when the language mode changes", function() { + buffer = new TextBuffer(); + expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true); + + const events = []; + buffer.onDidChangeLanguageMode((newMode, oldMode) => events.push({newMode, oldMode})); + + const languageMode = { + onDidChangeHighlighting() { return {dispose() {}}; } + }; + + buffer.setLanguageMode(languageMode); + expect(buffer.getLanguageMode()).toBe(languageMode); + expect(events.length).toBe(1); + expect(events[0].newMode).toBe(languageMode); + expect(events[0].oldMode instanceof NullLanguageMode).toBe(true); + + buffer.setLanguageMode(null); + expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true); + expect(events.length).toBe(2); + expect(events[1].newMode).toBe(buffer.getLanguageMode()); + expect(events[1].oldMode).toBe(languageMode); + }); + }); + + describe("line ending support", function() { + beforeEach(function() { + const filePath = require.resolve('./fixtures/sample.js'); + buffer = TextBuffer.loadSync(filePath); + }); + + describe(".getText()", () => it("returns the text with the corrent line endings for each row", function() { + buffer.setText("a\r\nb\nc"); + expect(buffer.getText()).toBe("a\r\nb\nc"); + buffer.setText("a\r\nb\nc\n"); + expect(buffer.getText()).toBe("a\r\nb\nc\n"); + })); + + describe("when editing a line", () => it("preserves the existing line ending", function() { + buffer.setText("a\r\nb\nc"); + buffer.insert([0, 1], "1"); + expect(buffer.getText()).toBe("a1\r\nb\nc"); + })); + + describe("when inserting text with multiple lines", function() { + describe("when the current line has a line ending", () => it("uses the same line ending as the line where the text is inserted", function() { + buffer.setText("a\r\n"); + buffer.insert([0, 1], "hello\n1\n\n2"); + expect(buffer.getText()).toBe("ahello\r\n1\r\n\r\n2\r\n"); + })); + + describe("when the current line has no line ending (because it's the last line of the buffer)", function() { + describe("when the buffer contains only a single line", () => it("honors the line endings in the inserted text", function() { + buffer.setText("initialtext"); + buffer.append("hello\n1\r\n2\n"); + expect(buffer.getText()).toBe("initialtexthello\n1\r\n2\n"); + })); + + describe("when the buffer contains a preceding line", () => it("uses the line ending of the preceding line", function() { + buffer.setText("\ninitialtext"); + buffer.append("hello\n1\r\n2\n"); + expect(buffer.getText()).toBe("\ninitialtexthello\n1\n2\n"); + })); + }); + }); + + describe("::setPreferredLineEnding(lineEnding)", function() { + it("uses the given line ending when normalizing, rather than inferring one from the surrounding text", function() { + buffer = new TextBuffer({text: "a \r\n"}); + + expect(buffer.getPreferredLineEnding()).toBe(null); + buffer.append(" b \n"); + expect(buffer.getText()).toBe("a \r\n b \r\n"); + + buffer.setPreferredLineEnding("\n"); + expect(buffer.getPreferredLineEnding()).toBe("\n"); + buffer.append(" c \n"); + expect(buffer.getText()).toBe("a \r\n b \r\n c \n"); + + buffer.setPreferredLineEnding(null); + buffer.append(" d \r\n"); + expect(buffer.getText()).toBe("a \r\n b \r\n c \n d \n"); + }); + + it("persists across serialization and deserialization", function(done) { + const bufferA = new TextBuffer; + bufferA.setPreferredLineEnding("\r\n"); + + TextBuffer.deserialize(bufferA.serialize()).then(function(bufferB) { + expect(bufferB.getPreferredLineEnding()).toBe("\r\n"); + done(); + }); + }); + }); + }); +}); describe('when a buffer is already open', () => { const filePath = path.join(__dirname, 'fixtures', 'sample.js') @@ -30,3 +3291,16 @@ describe('when a buffer is already open', () => { }) }) }) + + +function assertChangesEqual(actualChanges, expectedChanges) { + expect(actualChanges.length).toBe(expectedChanges.length); + for (let i = 0; i < actualChanges.length; i++) { + var actualChange = actualChanges[i]; + var expectedChange = expectedChanges[i]; + expect(actualChange.oldRange).toEqual(expectedChange.oldRange); + expect(actualChange.newRange).toEqual(expectedChange.newRange); + expect(actualChange.oldText).toEqual(expectedChange.oldText); + expect(actualChange.newText).toEqual(expectedChange.newText); + } +} diff --git a/src/display-marker-layer.js b/src/display-marker-layer.js index 7c3cb521ca..eb9eab1455 100644 --- a/src/display-marker-layer.js +++ b/src/display-marker-layer.js @@ -1,10 +1,8 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ -let DisplayMarkerLayer; const {Emitter, CompositeDisposable} = require('event-kit'); const DisplayMarker = require('./display-marker'); const Range = require('./range'); @@ -14,20 +12,21 @@ const Point = require('./point'); // {DisplayLayer} level. Wraps an underlying {MarkerLayer} on the {TextBuffer}. // // This API is experimental and subject to change on any release. -module.exports = -(DisplayMarkerLayer = class DisplayMarkerLayer { +class DisplayMarkerLayer { constructor(displayLayer, bufferMarkerLayer, ownsBufferMarkerLayer) { this.displayLayer = displayLayer; this.bufferMarkerLayer = bufferMarkerLayer; this.ownsBufferMarkerLayer = ownsBufferMarkerLayer; - ({id: this.id} = this.bufferMarkerLayer); + this.id = this.bufferMarkerLayer.id; this.bufferMarkerLayer.displayMarkerLayers.add(this); this.markersById = {}; this.destroyed = false; this.emitter = new Emitter; this.subscriptions = new CompositeDisposable; this.markersWithDestroyListeners = new Set; - this.subscriptions.add(this.bufferMarkerLayer.onDidUpdate(this.emitDidUpdate.bind(this))); + this.subscriptions.add( + this.bufferMarkerLayer.onDidUpdate(this.emitDidUpdate.bind(this)) + ); } /* @@ -38,11 +37,13 @@ module.exports = destroy() { if (this.destroyed) { return; } this.destroyed = true; + if (this.ownsBufferMarkerLayer) { this.clear(); } this.subscriptions.dispose(); this.bufferMarkerLayer.displayMarkerLayers.delete(this); if (this.ownsBufferMarkerLayer) { this.bufferMarkerLayer.destroy(); } this.displayLayer.didDestroyMarkerLayer(this.id); + this.emitter.emit('did-destroy'); return this.emitter.clear(); } @@ -53,8 +54,10 @@ module.exports = } didClearBufferMarkerLayer() { - this.markersWithDestroyListeners.forEach(marker => marker.didDestroyBufferMarker()); - return this.markersById = {}; + for (let marker of this.markersWithDestroyListeners) { + marker.didDestroyBufferMarker(); + } + this.markersById = {}; } // Essential: Determine whether this layer has been destroyed. @@ -190,7 +193,9 @@ module.exports = markScreenPosition(screenPosition, options) { screenPosition = Point.fromObject(screenPosition); const bufferPosition = this.displayLayer.translateScreenPosition(screenPosition, options); - return this.getMarker(this.bufferMarkerLayer.markPosition(bufferPosition, options).id); + return this.getMarker( + this.bufferMarkerLayer.markPosition(bufferPosition, options).id + ); } // Public: Create a marker with the given buffer range. @@ -224,7 +229,9 @@ module.exports = // Returns a {DisplayMarker}. markBufferRange(bufferRange, options) { bufferRange = Range.fromObject(bufferRange); - return this.getMarker(this.bufferMarkerLayer.markRange(bufferRange, options).id); + return this.getMarker( + this.bufferMarkerLayer.markRange(bufferRange, options).id + ); } // Public: Create a marker on this layer with its head at the given buffer @@ -255,7 +262,12 @@ module.exports = // // Returns a {DisplayMarker}. markBufferPosition(bufferPosition, options) { - return this.getMarker(this.bufferMarkerLayer.markPosition(Point.fromObject(bufferPosition), options).id); + return this.getMarker( + this.bufferMarkerLayer.markPosition( + Point.fromObject(bufferPosition), + options + ).id + ); } /* @@ -278,7 +290,9 @@ module.exports = // // Returns an {Array} of {DisplayMarker}s. getMarkers() { - return this.bufferMarkerLayer.getMarkers().map(({id}) => this.getMarker(id)); + return this.bufferMarkerLayer.getMarkers().map( + ({ id }) => this.getMarker(id) + ); } // Public: Get the number of markers in the marker layer. @@ -325,7 +339,9 @@ module.exports = // Returns an {Array} of {DisplayMarker}s findMarkers(params) { params = this.translateToBufferMarkerLayerFindParams(params); - return this.bufferMarkerLayer.findMarkers(params).map(stringMarker => this.getMarker(stringMarker.id)); + return this.bufferMarkerLayer.findMarkers(params).map( + stringMarker => this.getMarker(stringMarker.id) + ); } /* @@ -465,4 +481,6 @@ module.exports = return bufferMarkerLayerFindParams; } -}); +} + +module.exports = DisplayMarkerLayer; diff --git a/src/marker-layer.js b/src/marker-layer.js index 10d03db550..1196e3ccbd 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -36,17 +36,26 @@ module.exports = class MarkerLayer { /* Section: Lifecycle */ - constructor(delegate1, id3, options) { - var ref, ref1, ref2; - this.delegate = delegate1; - this.id = id3; - this.maintainHistory = (ref = options != null ? options.maintainHistory : void 0) != null ? ref : false; - this.destroyInvalidatedMarkers = (ref1 = options != null ? options.destroyInvalidatedMarkers : void 0) != null ? ref1 : false; - this.role = options != null ? options.role : void 0; + constructor( + delegate, + id, + { + destroyInvalidatedMarkers = false, + maintainHistory = false, + persistent = false, + role + } = {} + ) { + this.delegate = delegate; + this.id = id; + this.maintainHistory = maintainHistory; + this.destroyInvalidatedMarkers = destroyInvalidatedMarkers; + this.role = role; if (this.role === "selections") { this.delegate.registerSelectionsMarkerLayer(this); } - this.persistent = (ref2 = options != null ? options.persistent : void 0) != null ? ref2 : false; + this.persistent = persistent; + this.emitter = new Emitter(); this.index = new MarkerIndex(); this.markersById = {}; @@ -60,13 +69,13 @@ module.exports = class MarkerLayer { // Public: Create a copy of this layer with markers in the same state and // locations. copy() { - var copy, marker, markerId, ref, snapshot; - copy = this.delegate.addMarkerLayer({maintainHistory: this.maintainHistory, role: this.role}); - ref = this.markersById; - for (markerId in ref) { - marker = ref[markerId]; - snapshot = marker.getSnapshot(null); - copy.createMarker(marker.getRange(), marker.getSnapshot()); + let copy = this.delegate.addMarkerLayer({ + maintainHistory: this.maintainHistory, + role: this.role + }); + for (let marker of Object.values(this.markersById)) { + let snapshot = marker.getSnapshot(null); + copy.createMarker(marker.getRange(), snapshot); } return copy; } @@ -124,14 +133,7 @@ module.exports = class MarkerLayer { // Returns an {Array} of {Marker}s. getMarkers() { - var id, marker, ref, results; - ref = this.markersById; - results = []; - for (id in ref) { - marker = ref[id]; - results.push(marker); - } - return results; + return Object.values(this.markersById); } // Public: Get the number of markers in the marker layer. @@ -145,18 +147,21 @@ module.exports = class MarkerLayer { // See the documentation for {TextBuffer::findMarkers}. findMarkers(params) { - var end, i, key, len, markerIds, position, ref, result, start, value; - markerIds = null; - ref = Object.keys(params); - for (i = 0, len = ref.length; i < len; i++) { - key = ref[i]; - value = params[key]; + let markerIds = null; + for (let [key, value] of Object.entries(params)) { + let start, end, position; switch (key) { case 'startPosition': - markerIds = filterSet(markerIds, this.index.findStartingAt(Point.fromObject(value))); + markerIds = filterSet( + markerIds, + this.index.findStartingAt(Point.fromObject(value)) + ); break; case 'endPosition': - markerIds = filterSet(markerIds, this.index.findEndingAt(Point.fromObject(value))); + markerIds = filterSet( + markerIds, + this.index.findEndingAt(Point.fromObject(value)) + ); break; case 'startsInRange': ({start, end} = Range.fromObject(value)); @@ -203,18 +208,14 @@ module.exports = class MarkerLayer { if (markerIds == null) { markerIds = new Set(Object.keys(this.markersById)); } - result = []; - markerIds.forEach((markerId) => { - var marker; - marker = this.markersById[markerId]; - if (!marker.matchesParams(params)) { - return; - } - return result.push(marker); - }); - return result.sort(function(a, b) { - return a.compare(b); - }); + let result = []; + for (let markerId of markerIds) { + let marker = this.markersById[markerId]; + if (!marker.matchesParams(params)) continue; + result.push(marker); + } + result.sort((a, b) => a.compare(b)); + return result; } // Public: Get the role of the marker layer e.g. `atom.selection`. diff --git a/src/point.js b/src/point.js index 7b7d6583c3..a105228023 100644 --- a/src/point.js +++ b/src/point.js @@ -15,313 +15,310 @@ // new Point(1, 2) // [1, 2] # Point compatible Array // ``` -let Point; -module.exports = -(Point = (function() { - Point = class Point { - /* - Section: Construction - */ - - // Public: Convert any point-compatible object to a {Point}. - // - // * `object` This can be an object that's already a {Point}, in which case it's - // simply returned, or an array containing two {Number}s representing the - // row and column. - // * `copy` An optional boolean indicating whether to force the copying of objects - // that are already points. - // - // Returns: A {Point} based on the given object. - static fromObject(object, copy) { - if (object instanceof Point) { - if (copy) { return object.copy(); } else { return object; } - } else { - let column, row; - if (Array.isArray(object)) { - [row, column] = Array.from(object); - } else { - ({row, column} = object); - } - - return new Point(row, column); - } - } - /* - Section: Comparison - */ - - // Public: Returns the given {Point} that is earlier in the buffer. - // - // * `point1` {Point} - // * `point2` {Point} - static min(point1, point2) { - point1 = this.fromObject(point1); - point2 = this.fromObject(point2); - if (point1.isLessThanOrEqual(point2)) { - return point1; +class Point { + /* + Section: Construction + */ + + // Public: Convert any point-compatible object to a {Point}. + // + // * `object` This can be an object that's already a {Point}, in which case it's + // simply returned, or an array containing two {Number}s representing the + // row and column. + // * `copy` An optional boolean indicating whether to force the copying of objects + // that are already points. + // + // Returns: A {Point} based on the given object. + static fromObject(object, copy) { + if (object instanceof Point) { + if (copy) { return object.copy(); } else { return object; } + } else { + let column, row; + if (Array.isArray(object)) { + [row, column] = Array.from(object); } else { - return point2; + ({row, column} = object); } - } - static max(point1, point2) { - point1 = Point.fromObject(point1); - point2 = Point.fromObject(point2); - if (point1.compare(point2) >= 0) { - return point1; - } else { - return point2; - } + return new Point(row, column); } + } - static assertValid(point) { - if (!isNumber(point.row) || !isNumber(point.column)) { - throw new TypeError(`Invalid Point: ${point}`); - } + /* + Section: Comparison + */ + + // Public: Returns the given {Point} that is earlier in the buffer. + // + // * `point1` {Point} + // * `point2` {Point} + static min(point1, point2) { + point1 = this.fromObject(point1); + point2 = this.fromObject(point2); + if (point1.isLessThanOrEqual(point2)) { + return point1; + } else { + return point2; } + } - /* - Section: Construction - */ - - // Public: Construct a {Point} object - // - // * `row` {Number} row - // * `column` {Number} column - constructor(row=0, column=0) { - this.row = row; - this.column = column; + static max(point1, point2) { + point1 = Point.fromObject(point1); + point2 = Point.fromObject(point2); + if (point1.compare(point2) >= 0) { + return point1; + } else { + return point2; } + } - // Public: Returns a new {Point} with the same row and column. - copy() { - return new Point(this.row, this.column); + static assertValid(point) { + if (!isActualNumber(point.row) || !isActualNumber(point.column)) { + throw new TypeError(`Invalid Point: ${point}`); } + } - // Public: Returns a new {Point} with the row and column negated. - negate() { - return new Point(-this.row, -this.column); - } + /* + Section: Construction + */ + + // Public: Construct a {Point} object + // + // * `row` {Number} row + // * `column` {Number} column + constructor(row=0, column=0) { + this.row = row; + this.column = column; + } + + // Public: Returns a new {Point} with the same row and column. + copy() { + return new Point(this.row, this.column); + } - /* - Section: Operations - */ + // Public: Returns a new {Point} with the row and column negated. + negate() { + return new Point(-this.row, -this.column); + } - // Public: Makes this point immutable and returns itself. - // - // Returns an immutable version of this {Point} - freeze() { - return Object.freeze(this); - } + /* + Section: Operations + */ - // Public: Build and return a new point by adding the rows and columns of - // the given point. - // - // * `other` A {Point} whose row and column will be added to this point's row - // and column to build the returned point. - // - // Returns a {Point}. - translate(other) { - const {row, column} = Point.fromObject(other); - return new Point(this.row + row, this.column + column); - } + // Public: Makes this point immutable and returns itself. + // + // Returns an immutable version of this {Point} + freeze() { + return Object.freeze(this); + } - // Public: Build and return a new {Point} by traversing the rows and columns - // specified by the given point. - // - // * `other` A {Point} providing the rows and columns to traverse by. - // - // This method differs from the direct, vector-style addition offered by - // {::translate}. Rather than adding the rows and columns directly, it derives - // the new point from traversing in "typewriter space". At the end of every row - // traversed, a carriage return occurs that returns the columns to 0 before - // continuing the traversal. - // - // ## Examples - // - // Traversing 0 rows, 2 columns: - // `new Point(10, 5).traverse(new Point(0, 2)) # => [10, 7]` - // - // Traversing 2 rows, 2 columns. Note the columns reset from 0 before adding: - // `new Point(10, 5).traverse(new Point(2, 2)) # => [12, 2]` - // - // Returns a {Point}. - traverse(other) { - let column; - other = Point.fromObject(other); - const row = this.row + other.row; - if (other.row === 0) { - column = this.column + other.column; - } else { - ({ - column - } = other); - } + // Public: Build and return a new point by adding the rows and columns of + // the given point. + // + // * `other` A {Point} whose row and column will be added to this point's row + // and column to build the returned point. + // + // Returns a {Point}. + translate(other) { + const {row, column} = Point.fromObject(other); + return new Point(this.row + row, this.column + column); + } - return new Point(row, column); + // Public: Build and return a new {Point} by traversing the rows and columns + // specified by the given point. + // + // * `other` A {Point} providing the rows and columns to traverse by. + // + // This method differs from the direct, vector-style addition offered by + // {::translate}. Rather than adding the rows and columns directly, it derives + // the new point from traversing in "typewriter space". At the end of every row + // traversed, a carriage return occurs that returns the columns to 0 before + // continuing the traversal. + // + // ## Examples + // + // Traversing 0 rows, 2 columns: + // `new Point(10, 5).traverse(new Point(0, 2)) # => [10, 7]` + // + // Traversing 2 rows, 2 columns. Note the columns reset from 0 before adding: + // `new Point(10, 5).traverse(new Point(2, 2)) # => [12, 2]` + // + // Returns a {Point}. + traverse(other) { + let column; + other = Point.fromObject(other); + const row = this.row + other.row; + if (other.row === 0) { + column = this.column + other.column; + } else { + ({ + column + } = other); } - traversalFrom(other) { - other = Point.fromObject(other); - if (this.row === other.row) { - if ((this.column === Infinity) && (other.column === Infinity)) { - return new Point(0, 0); - } else { - return new Point(0, this.column - other.column); - } - } else { - return new Point(this.row - other.row, this.column); - } - } + return new Point(row, column); + } - splitAt(column) { - let rightColumn; - if (this.row === 0) { - rightColumn = this.column - column; + traversalFrom(other) { + other = Point.fromObject(other); + if (this.row === other.row) { + if ((this.column === Infinity) && (other.column === Infinity)) { + return new Point(0, 0); } else { - rightColumn = this.column; + return new Point(0, this.column - other.column); } + } else { + return new Point(this.row - other.row, this.column); + } + } - return [new Point(0, column), new Point(this.row, rightColumn)]; + splitAt(column) { + let rightColumn; + if (this.row === 0) { + rightColumn = this.column - column; + } else { + rightColumn = this.column; } - /* - Section: Comparison - */ - - // Public: - // - // * `other` A {Point} or point-compatible {Array}. - // - // Returns `-1` if this point precedes the argument. - // Returns `0` if this point is equivalent to the argument. - // Returns `1` if this point follows the argument. - compare(other) { - other = Point.fromObject(other); - if (this.row > other.row) { + return [new Point(0, column), new Point(this.row, rightColumn)]; + } + + /* + Section: Comparison + */ + + // Public: + // + // * `other` A {Point} or point-compatible {Array}. + // + // Returns `-1` if this point precedes the argument. + // Returns `0` if this point is equivalent to the argument. + // Returns `1` if this point follows the argument. + compare(other) { + other = Point.fromObject(other); + if (this.row > other.row) { + return 1; + } else if (this.row < other.row) { + return -1; + } else { + if (this.column > other.column) { return 1; - } else if (this.row < other.row) { + } else if (this.column < other.column) { return -1; } else { - if (this.column > other.column) { - return 1; - } else if (this.column < other.column) { - return -1; - } else { - return 0; - } + return 0; } } + } - // Public: Returns a {Boolean} indicating whether this point has the same row - // and column as the given {Point} or point-compatible {Array}. - // - // * `other` A {Point} or point-compatible {Array}. - isEqual(other) { - if (!other) { return false; } - other = Point.fromObject(other); - return (this.row === other.row) && (this.column === other.column); - } - - // Public: Returns a {Boolean} indicating whether this point precedes the given - // {Point} or point-compatible {Array}. - // - // * `other` A {Point} or point-compatible {Array}. - isLessThan(other) { - return this.compare(other) < 0; - } + // Public: Returns a {Boolean} indicating whether this point has the same row + // and column as the given {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isEqual(other) { + if (!other) { return false; } + other = Point.fromObject(other); + return (this.row === other.row) && (this.column === other.column); + } - // Public: Returns a {Boolean} indicating whether this point precedes or is - // equal to the given {Point} or point-compatible {Array}. - // - // * `other` A {Point} or point-compatible {Array}. - isLessThanOrEqual(other) { - return this.compare(other) <= 0; - } + // Public: Returns a {Boolean} indicating whether this point precedes the given + // {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isLessThan(other) { + return this.compare(other) < 0; + } - // Public: Returns a {Boolean} indicating whether this point follows the given - // {Point} or point-compatible {Array}. - // - // * `other` A {Point} or point-compatible {Array}. - isGreaterThan(other) { - return this.compare(other) > 0; - } + // Public: Returns a {Boolean} indicating whether this point precedes or is + // equal to the given {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isLessThanOrEqual(other) { + return this.compare(other) <= 0; + } - // Public: Returns a {Boolean} indicating whether this point follows or is - // equal to the given {Point} or point-compatible {Array}. - // - // * `other` A {Point} or point-compatible {Array}. - isGreaterThanOrEqual(other) { - return this.compare(other) >= 0; - } + // Public: Returns a {Boolean} indicating whether this point follows the given + // {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isGreaterThan(other) { + return this.compare(other) > 0; + } - isZero() { - return (this.row === 0) && (this.column === 0); - } + // Public: Returns a {Boolean} indicating whether this point follows or is + // equal to the given {Point} or point-compatible {Array}. + // + // * `other` A {Point} or point-compatible {Array}. + isGreaterThanOrEqual(other) { + return this.compare(other) >= 0; + } - isPositive() { - if (this.row > 0) { - return true; - } else if (this.row < 0) { - return false; - } else { - return this.column > 0; - } - } + isZero() { + return (this.row === 0) && (this.column === 0); + } - isNegative() { - if (this.row < 0) { - return true; - } else if (this.row > 0) { - return false; - } else { - return this.column < 0; - } + isPositive() { + if (this.row > 0) { + return true; + } else if (this.row < 0) { + return false; + } else { + return this.column > 0; } + } - /* - Section: Conversion - */ - - // Public: Returns an array of this point's row and column. - toArray() { - return [this.row, this.column]; + isNegative() { + if (this.row < 0) { + return true; + } else if (this.row > 0) { + return false; + } else { + return this.column < 0; } + } - // Public: Returns an array of this point's row and column. - serialize() { - return this.toArray(); - } + /* + Section: Conversion + */ - // Public: Returns a string representation of the point. - toString() { - return `(${this.row}, ${this.column})`; - } - }; - // Point.initClass(); + // Public: Returns an array of this point's row and column. + toArray() { + return [this.row, this.column]; + } - function callableConstructor(c, f) { - function Point(row, col) { - if(new.target) { - return new c(row, col) - } - return f(row, col) - } - Point.prototype = c.prototype - Point.prototype.constructor = Point - Point.fromObject = c.fromObject - Point.min = c.min - Point.max = c.max - Point.assertValid = c.assertValid - Point.prototype.row = null; - Point.prototype.column = null; - Point.ZERO = Object.freeze(new c(0, 0)); - Point.INFINITY = Object.freeze(new c(Infinity, Infinity)); - - return Point + // Public: Returns an array of this point's row and column. + serialize() { + return this.toArray(); } - return callableConstructor(Point, (row, col) => new Point(row, col)); -})()); -var isNumber = value => (typeof value === 'number') && (!Number.isNaN(value)); + // Public: Returns a string representation of the point. + toString() { + return `(${this.row}, ${this.column})`; + } +} + +function _Point (...args) { + return new Point(...args); +} +_Point.displayName = 'Point'; +_Point.prototype = Point.prototype; +_Point.prototype.constructor = _Point; +Object.assign(_Point, { + fromObject: Point.fromObject, + min: Point.min, + max: Point.max, + assertValid: Point.assertValid, + ZERO: Object.freeze(new Point(0, 0)), + INFINITY: Object.freeze(new Point(Infinity, Infinity)) +}); +Object.assign(_Point.prototype, { + row: null, + column: null +}); + +function isActualNumber (value) { + return (typeof value === 'number') && (!Number.isNaN(value)); +} + +module.exports = _Point; diff --git a/src/range.js b/src/range.js index 2565ea6dd9..2238b626ee 100644 --- a/src/range.js +++ b/src/range.js @@ -7,10 +7,30 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ -let Range; +// let Range; const Point = require('./point'); let newlineRegex = null; +function __range__(left, right, inclusive) { + let range = []; + let ascending = left < right; + let end = !inclusive ? right : ascending ? right + 1 : right - 1; + for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { + range.push(i); + } + return range; +} + +// NOTE: The convoluted mess in this file represents the constraint of "formal" +// JavaScript classes that you cannot treat them as functions: +// +// Point(0, 0); // <- TypeError: Class constructor client cannot be invoked +// without 'new' +// +// Hence we keep the ugliness from the decaffeinated version of `Range` until +// we find a better idiom. + + // Public: Represents a region in a buffer in row/column coordinates. // // Every public method that takes a range also accepts a *range-compatible* @@ -24,375 +44,359 @@ let newlineRegex = null; // new Range([0, 1], [2, 3]) // [[0, 1], [2, 3]] # Range compatible array // ``` -module.exports = -(Range = (function() { - Range = class Range { - /* - Section: Construction - */ - - // Public: Convert any range-compatible object to a {Range}. - // - // * `object` This can be an object that's already a {Range}, in which case it's - // simply returned, or an array containing two {Point}s or point-compatible - // arrays. - // * `copy` An optional boolean indicating whether to force the copying of objects - // that are already ranges. - // - // Returns: A {Range} based on the given object. - static fromObject(object, copy) { - if (Array.isArray(object)) { - return new (this)(object[0], object[1]); - } else if (object instanceof this) { - if (copy) { return object.copy(); } else { return object; } - } else { - return new (this)(object.start, object.end); - } +class Range { + /* + Section: Construction + */ + + // Public: Convert any range-compatible object to a {Range}. + // + // * `object` This can be an object that's already a {Range}, in which case it's + // simply returned, or an array containing two {Point}s or point-compatible + // arrays. + // * `copy` An optional boolean indicating whether to force the copying of objects + // that are already ranges. + // + // Returns: A {Range} based on the given object. + static fromObject(object, copy) { + if (Array.isArray(object)) { + return new (this)(object[0], object[1]); + } else if (object instanceof this) { + if (copy) { return object.copy(); } else { return object; } + } else { + return new (this)(object.start, object.end); } + } - // Returns a range based on an optional starting point and the given text. If - // no starting point is given it will be assumed to be [0, 0]. - // - // * `startPoint` (optional) {Point} where the range should start. - // * `text` A {String} after which the range should end. The range will have as many - // rows as the text has lines have an end column based on the length of the - // last line. - // - // Returns: A {Range} - static fromText(...args) { - let startPoint; - if (newlineRegex == null) { ({ - newlineRegex - } = require('./helpers')); } - - if (args.length > 1) { - startPoint = Point.fromObject(args.shift()); - } else { - startPoint = new Point(0, 0); - } - const text = args.shift(); - const endPoint = startPoint.copy(); - const lines = text.split(newlineRegex); - if (lines.length > 1) { - const lastIndex = lines.length - 1; - endPoint.row += lastIndex; - endPoint.column = lines[lastIndex].length; - } else { - endPoint.column += lines[0].length; - } - return new (this)(startPoint, endPoint); + // Returns a range based on an optional starting point and the given text. If + // no starting point is given it will be assumed to be [0, 0]. + // + // * `startPoint` (optional) {Point} where the range should start. + // * `text` A {String} after which the range should end. The range will have as many + // rows as the text has lines have an end column based on the length of the + // last line. + // + // Returns: A {Range} + static fromText(...args) { + let startPoint; + if (newlineRegex == null) { ({ + newlineRegex + } = require('./helpers')); } + + if (args.length > 1) { + startPoint = Point.fromObject(args.shift()); + } else { + startPoint = new Point(0, 0); } - - // Returns a {Range} that starts at the given point and ends at the - // start point plus the given row and column deltas. - // - // * `startPoint` A {Point} or point-compatible {Array} - // * `rowDelta` A {Number} indicating how many rows to add to the start point - // to get the end point. - // * `columnDelta` A {Number} indicating how many rows to columns to the start - // point to get the end point. - static fromPointWithDelta(startPoint, rowDelta, columnDelta) { - startPoint = Point.fromObject(startPoint); - const endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta); - return new (this)(startPoint, endPoint); + const text = args.shift(); + const endPoint = startPoint.copy(); + const lines = text.split(newlineRegex); + if (lines.length > 1) { + const lastIndex = lines.length - 1; + endPoint.row += lastIndex; + endPoint.column = lines[lastIndex].length; + } else { + endPoint.column += lines[0].length; } + return new (this)(startPoint, endPoint); + } - static fromPointWithTraversalExtent(startPoint, extent) { - startPoint = Point.fromObject(startPoint); - return new (this)(startPoint, startPoint.traverse(extent)); - } + // Returns a {Range} that starts at the given point and ends at the + // start point plus the given row and column deltas. + // + // * `startPoint` A {Point} or point-compatible {Array} + // * `rowDelta` A {Number} indicating how many rows to add to the start point + // to get the end point. + // * `columnDelta` A {Number} indicating how many rows to columns to the start + // point to get the end point. + static fromPointWithDelta(startPoint, rowDelta, columnDelta) { + startPoint = Point.fromObject(startPoint); + const endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta); + return new (this)(startPoint, endPoint); + } + static fromPointWithTraversalExtent(startPoint, extent) { + startPoint = Point.fromObject(startPoint); + return new (this)(startPoint, startPoint.traverse(extent)); + } - /* - Section: Serialization and Deserialization - */ - // Public: Call this with the result of {Range::serialize} to construct a new Range. - // - // * `array` {Array} of params to pass to the {::constructor} - static deserialize(array) { - if (Array.isArray(array)) { - return new (this)(array[0], array[1]); - } else { - return new (this)(); - } - } + /* + Section: Serialization and Deserialization + */ - /* - Section: Construction - */ - - // Public: Construct a {Range} object - // - // * `pointA` {Point} or Point compatible {Array} (default: [0,0]) - // * `pointB` {Point} or Point compatible {Array} (default: [0,0]) - constructor(pointA, pointB) { - if (pointA == null) { pointA = new Point(0, 0); } - if (pointB == null) { pointB = new Point(0, 0); } - if (!(this instanceof Range)) { - return new Range(pointA, pointB); - } - - pointA = Point.fromObject(pointA); - pointB = Point.fromObject(pointB); - - if (pointA.isLessThanOrEqual(pointB)) { - this.start = pointA; - this.end = pointB; - } else { - this.start = pointB; - this.end = pointA; - } + // Public: Call this with the result of {Range::serialize} to construct a new Range. + // + // * `array` {Array} of params to pass to the {::constructor} + static deserialize(array) { + if (Array.isArray(array)) { + return new (this)(array[0], array[1]); + } else { + return new (this)(); } + } - // Public: Returns a new range with the same start and end positions. - copy() { - return new this.constructor(this.start.copy(), this.end.copy()); + /* + Section: Construction + */ + + // Public: Construct a {Range} object + // + // * `pointA` {Point} or Point compatible {Array} (default: [0,0]) + // * `pointB` {Point} or Point compatible {Array} (default: [0,0]) + constructor(pointA, pointB) { + if (pointA == null) { pointA = new Point(0, 0); } + if (pointB == null) { pointB = new Point(0, 0); } + if (!(this instanceof Range)) { + return new Range(pointA, pointB); } - // Public: Returns a new range with the start and end positions negated. - negate() { - return new this.constructor(this.start.negate(), this.end.negate()); + pointA = Point.fromObject(pointA); + pointB = Point.fromObject(pointB); + + if (pointA.isLessThanOrEqual(pointB)) { + this.start = pointA; + this.end = pointB; + } else { + this.start = pointB; + this.end = pointA; } + } - /* - Section: Serialization and Deserialization - */ + // Public: Returns a new range with the same start and end positions. + copy() { + return new this.constructor(this.start.copy(), this.end.copy()); + } - // Public: Returns a plain javascript object representation of the range. - serialize() { - return [this.start.serialize(), this.end.serialize()]; - } + // Public: Returns a new range with the start and end positions negated. + negate() { + return new this.constructor(this.start.negate(), this.end.negate()); + } - /* - Section: Range Details - */ + /* + Section: Serialization and Deserialization + */ - // Public: Is the start position of this range equal to the end position? - // - // Returns a {Boolean}. - isEmpty() { - return this.start.isEqual(this.end); - } + // Public: Returns a plain javascript object representation of the range. + serialize() { + return [this.start.serialize(), this.end.serialize()]; + } - // Public: Returns a {Boolean} indicating whether this range starts and ends on - // the same row. - isSingleLine() { - return this.start.row === this.end.row; - } + /* + Section: Range Details + */ - // Public: Get the number of rows in this range. - // - // Returns a {Number}. - getRowCount() { - return (this.end.row - this.start.row) + 1; - } + // Public: Is the start position of this range equal to the end position? + // + // Returns a {Boolean}. + isEmpty() { + return this.start.isEqual(this.end); + } - // Public: Returns an array of all rows in the range. - getRows() { - return __range__(this.start.row, this.end.row, true); - } + // Public: Returns a {Boolean} indicating whether this range starts and ends on + // the same row. + isSingleLine() { + return this.start.row === this.end.row; + } - /* - Section: Operations - */ - - // Public: Freezes the range and its start and end point so it becomes - // immutable and returns itself. - // - // Returns an immutable version of this {Range} - freeze() { - this.start.freeze(); - this.end.freeze(); - return Object.freeze(this); - } + // Public: Get the number of rows in this range. + // + // Returns a {Number}. + getRowCount() { + return (this.end.row - this.start.row) + 1; + } - // Public: Returns a new range that contains this range and the given range. - // - // * `otherRange` A {Range} or range-compatible {Array} - union(otherRange) { - const start = this.start.isLessThan(otherRange.start) ? this.start : otherRange.start; - const end = this.end.isGreaterThan(otherRange.end) ? this.end : otherRange.end; - return new this.constructor(start, end); - } + // Public: Returns an array of all rows in the range. + getRows() { + return __range__(this.start.row, this.end.row, true); + } - // Public: Build and return a new range by translating this range's start and - // end points by the given delta(s). - // - // * `startDelta` A {Point} by which to translate the start of this range. - // * `endDelta` (optional) A {Point} to by which to translate the end of this - // range. If omitted, the `startDelta` will be used instead. - // - // Returns a {Range}. - translate(startDelta, endDelta) { - if (endDelta == null) { endDelta = startDelta; } - return new this.constructor(this.start.translate(startDelta), this.end.translate(endDelta)); - } + /* + Section: Operations + */ + + // Public: Freezes the range and its start and end point so it becomes + // immutable and returns itself. + // + // Returns an immutable version of this {Range} + freeze() { + this.start.freeze(); + this.end.freeze(); + return Object.freeze(this); + } - // Public: Build and return a new range by traversing this range's start and - // end points by the given delta. - // - // See {Point::traverse} for details of how traversal differs from translation. - // - // * `delta` A {Point} containing the rows and columns to traverse to derive - // the new range. - // - // Returns a {Range}. - traverse(delta) { - return new this.constructor(this.start.traverse(delta), this.end.traverse(delta)); - } + // Public: Returns a new range that contains this range and the given range. + // + // * `otherRange` A {Range} or range-compatible {Array} + union(otherRange) { + const start = this.start.isLessThan(otherRange.start) ? this.start : otherRange.start; + const end = this.end.isGreaterThan(otherRange.end) ? this.end : otherRange.end; + return new this.constructor(start, end); + } - /* - Section: Comparison - */ - - // Public: Compare two Ranges - // - // * `otherRange` A {Range} or range-compatible {Array}. - // - // Returns `-1` if this range starts before the argument or contains it. - // Returns `0` if this range is equivalent to the argument. - // Returns `1` if this range starts after the argument or is contained by it. - compare(other) { - let value; - other = this.constructor.fromObject(other); - if ((value = this.start.compare(other.start))) { - return value; - } else { - return other.end.compare(this.end); - } - } + // Public: Build and return a new range by translating this range's start and + // end points by the given delta(s). + // + // * `startDelta` A {Point} by which to translate the start of this range. + // * `endDelta` (optional) A {Point} to by which to translate the end of this + // range. If omitted, the `startDelta` will be used instead. + // + // Returns a {Range}. + translate(startDelta, endDelta) { + if (endDelta == null) { endDelta = startDelta; } + return new this.constructor(this.start.translate(startDelta), this.end.translate(endDelta)); + } - // Public: Returns a {Boolean} indicating whether this range has the same start - // and end points as the given {Range} or range-compatible {Array}. - // - // * `otherRange` A {Range} or range-compatible {Array}. - isEqual(other) { - if (other == null) { return false; } - other = this.constructor.fromObject(other); - return other.start.isEqual(this.start) && other.end.isEqual(this.end); - } + // Public: Build and return a new range by traversing this range's start and + // end points by the given delta. + // + // See {Point::traverse} for details of how traversal differs from translation. + // + // * `delta` A {Point} containing the rows and columns to traverse to derive + // the new range. + // + // Returns a {Range}. + traverse(delta) { + return new this.constructor(this.start.traverse(delta), this.end.traverse(delta)); + } - // Public: Returns a {Boolean} indicating whether this range starts and ends on - // the same row as the argument. - // - // * `otherRange` A {Range} or range-compatible {Array}. - coversSameRows(other) { - return (this.start.row === other.start.row) && (this.end.row === other.end.row); + /* + Section: Comparison + */ + + // Public: Compare two Ranges + // + // * `otherRange` A {Range} or range-compatible {Array}. + // + // Returns `-1` if this range starts before the argument or contains it. + // Returns `0` if this range is equivalent to the argument. + // Returns `1` if this range starts after the argument or is contained by it. + compare(other) { + let value; + other = this.constructor.fromObject(other); + if ((value = this.start.compare(other.start))) { + return value; + } else { + return other.end.compare(this.end); } + } - // Public: Determines whether this range intersects with the argument. - // - // * `otherRange` A {Range} or range-compatible {Array} - // * `exclusive` (optional) {Boolean} indicating whether to exclude endpoints - // when testing for intersection. Defaults to `false`. - // - // Returns a {Boolean}. - intersectsWith(otherRange, exclusive) { - if (exclusive) { - return !(this.end.isLessThanOrEqual(otherRange.start) || this.start.isGreaterThanOrEqual(otherRange.end)); - } else { - return !(this.end.isLessThan(otherRange.start) || this.start.isGreaterThan(otherRange.end)); - } - } + // Public: Returns a {Boolean} indicating whether this range has the same start + // and end points as the given {Range} or range-compatible {Array}. + // + // * `otherRange` A {Range} or range-compatible {Array}. + isEqual(other) { + if (other == null) { return false; } + other = this.constructor.fromObject(other); + return other.start.isEqual(this.start) && other.end.isEqual(this.end); + } - // Public: Returns a {Boolean} indicating whether this range contains the given - // range. - // - // * `otherRange` A {Range} or range-compatible {Array} - // * `exclusive` (optional) {Boolean} including that the containment should be exclusive of - // endpoints. Defaults to false. - containsRange(otherRange, exclusive) { - const {start, end} = this.constructor.fromObject(otherRange); - return this.containsPoint(start, exclusive) && this.containsPoint(end, exclusive); - } + // Public: Returns a {Boolean} indicating whether this range starts and ends on + // the same row as the argument. + // + // * `otherRange` A {Range} or range-compatible {Array}. + coversSameRows(other) { + return (this.start.row === other.start.row) && (this.end.row === other.end.row); + } - // Public: Returns a {Boolean} indicating whether this range contains the given - // point. - // - // * `point` A {Point} or point-compatible {Array} - // * `exclusive` (optional) {Boolean} including that the containment should be exclusive of - // endpoints. Defaults to false. - containsPoint(point, exclusive) { - point = Point.fromObject(point); - if (exclusive) { - return point.isGreaterThan(this.start) && point.isLessThan(this.end); - } else { - return point.isGreaterThanOrEqual(this.start) && point.isLessThanOrEqual(this.end); - } + // Public: Determines whether this range intersects with the argument. + // + // * `otherRange` A {Range} or range-compatible {Array} + // * `exclusive` (optional) {Boolean} indicating whether to exclude endpoints + // when testing for intersection. Defaults to `false`. + // + // Returns a {Boolean}. + intersectsWith(otherRange, exclusive) { + if (exclusive) { + return !(this.end.isLessThanOrEqual(otherRange.start) || this.start.isGreaterThanOrEqual(otherRange.end)); + } else { + return !(this.end.isLessThan(otherRange.start) || this.start.isGreaterThan(otherRange.end)); } + } - // Public: Returns a {Boolean} indicating whether this range intersects the - // given row {Number}. - // - // * `row` Row {Number} - intersectsRow(row) { - return this.start.row <= row && row <= this.end.row; - } + // Public: Returns a {Boolean} indicating whether this range contains the given + // range. + // + // * `otherRange` A {Range} or range-compatible {Array} + // * `exclusive` (optional) {Boolean} including that the containment should be exclusive of + // endpoints. Defaults to false. + containsRange(otherRange, exclusive) { + const {start, end} = this.constructor.fromObject(otherRange); + return this.containsPoint(start, exclusive) && this.containsPoint(end, exclusive); + } - // Public: Returns a {Boolean} indicating whether this range intersects the - // row range indicated by the given startRow and endRow {Number}s. - // - // * `startRow` {Number} start row - // * `endRow` {Number} end row - intersectsRowRange(startRow, endRow) { - if (startRow > endRow) { [startRow, endRow] = Array.from([endRow, startRow]); } - return (this.end.row >= startRow) && (endRow >= this.start.row); + // Public: Returns a {Boolean} indicating whether this range contains the given + // point. + // + // * `point` A {Point} or point-compatible {Array} + // * `exclusive` (optional) {Boolean} including that the containment should be exclusive of + // endpoints. Defaults to false. + containsPoint(point, exclusive) { + point = Point.fromObject(point); + if (exclusive) { + return point.isGreaterThan(this.start) && point.isLessThan(this.end); + } else { + return point.isGreaterThanOrEqual(this.start) && point.isLessThanOrEqual(this.end); } + } - getExtent() { - return this.end.traversalFrom(this.start); - } + // Public: Returns a {Boolean} indicating whether this range intersects the + // given row {Number}. + // + // * `row` Row {Number} + intersectsRow(row) { + return this.start.row <= row && row <= this.end.row; + } - /* - Section: Conversion - */ - - toDelta() { - let columns; - const rows = this.end.row - this.start.row; - if (rows === 0) { - columns = this.end.column - this.start.column; - } else { - columns = this.end.column; - } - return new Point(rows, columns); - } + // Public: Returns a {Boolean} indicating whether this range intersects the + // row range indicated by the given startRow and endRow {Number}s. + // + // * `startRow` {Number} start row + // * `endRow` {Number} end row + intersectsRowRange(startRow, endRow) { + if (startRow > endRow) { [startRow, endRow] = Array.from([endRow, startRow]); } + return (this.end.row >= startRow) && (endRow >= this.start.row); + } - // Public: Returns a string representation of the range. - toString() { - return `[${this.start} - ${this.end}]`; - } - }; - - function callableConstructor(c, f) { - function Range(a, b) { - if(new.target) { - return new c(a, b) - } - return f(a, b) + getExtent() { + return this.end.traversalFrom(this.start); + } + + /* + Section: Conversion + */ + + toDelta() { + let columns; + const rows = this.end.row - this.start.row; + if (rows === 0) { + columns = this.end.column - this.start.column; + } else { + columns = this.end.column; } - Range.prototype = c.prototype - Range.prototype.constructor = Range - Range.fromObject = c.fromObject - Range.fromText = c.fromText - Range.fromPointWithDelta = c.fromPointWithDelta - Range.fromPointWithTraversalExtent = c.fromPointWithTraversalExtent - Range.deserialize = c.deserialize - Range.prototype.start = null; - Range.prototype.end = null; - - return Range + return new Point(rows, columns); } - return callableConstructor(Range, (a, b) => new Range(a, b)); -})()); -function __range__(left, right, inclusive) { - let range = []; - let ascending = left < right; - let end = !inclusive ? right : ascending ? right + 1 : right - 1; - for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) { - range.push(i); + // Public: Returns a string representation of the range. + toString() { + return `[${this.start} - ${this.end}]`; } - return range; } + +function _Range (...args) { + return new Range(...args); +}; +_Range.displayName = 'Range'; +_Range.prototype = Range.prototype; +_Range.prototype.constructor = _Range; +Object.assign(_Range, { + fromObject: Range.fromObject, + fromText: Range.fromText, + fromPointWithDelta: Range.fromPointWithDelta, + fromPointWithTraversalExtent: Range.fromPointWithTraversalExtent, + deserialize: Range.deserialize +}); +_Range.prototype.start = null; +_Range.prototype.end = null; + +module.exports = _Range; From e2247d4b89508f40acdbaad407615fd87c40ffd6 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 4 Sep 2025 16:34:49 -0700 Subject: [PATCH 54/64] Fix failing specs --- spec/marker-spec.js | 12 +++++------- spec/text-buffer-spec.js | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/marker-spec.js b/spec/marker-spec.js index e878f9c41c..7e5c29ba34 100644 --- a/spec/marker-spec.js +++ b/spec/marker-spec.js @@ -420,8 +420,7 @@ describe("Marker", function() { }); it("defers notifying Marker::onDidChange observers until after notifying Buffer::onDidChange observers", function() { - let marker; - for (marker of allStrategies) { + for (let marker of allStrategies) { marker.changes = []; marker.onDidChange(change => marker.changes.push(change)); } @@ -430,7 +429,7 @@ describe("Marker", function() { const changeSubscription = buffer.onDidChange(function(change) { changedCount++; expect(markersUpdatedCount).toBe(0); - for (marker of allStrategies) { + for (let marker of allStrategies) { expect(marker.getRange()).toEqual([[0, 8], [0, 11]]); expect(marker.isValid()).toBe(true); expect(marker.changes.length).toBe(0); @@ -441,7 +440,7 @@ describe("Marker", function() { expect(changedCount).toBe(1); - for (marker of allStrategies) { + for (let marker of allStrategies) { expect(marker.changes).toEqual([{ oldHeadPosition: [0, 9], newHeadPosition: [0, 11], oldTailPosition: [0, 6], newTailPosition: [0, 8], @@ -699,8 +698,7 @@ describe("Marker", function() { describe("when multiple changes occur in a transaction", () => { it("emits one change event for each marker that was indirectly updated", function() { - let strategy; - for (strategy of allStrategies) { + for (let strategy of allStrategies) { strategy.changes = []; strategy.onDidChange(change => strategy.changes.push(change)); } @@ -709,7 +707,7 @@ describe("Marker", function() { buffer.insert([0, 7], "."); buffer.append("!"); - for (strategy of allStrategies) { + for (let strategy of allStrategies) { expect(strategy.changes.length).toBe(0); } diff --git a/spec/text-buffer-spec.js b/spec/text-buffer-spec.js index 4111898c28..4da1f6d0c5 100644 --- a/spec/text-buffer-spec.js +++ b/spec/text-buffer-spec.js @@ -3097,10 +3097,13 @@ three\ switch (changes[0].newText) { case 'a': buffer.insert(changes[0].newRange.end, 'b'); + break; case 'b': buffer.insert(changes[0].newRange.end, 'c'); + break; case 'c': buffer.insert(changes[0].newRange.end, 'd'); + break; } }); From 587dd18641924ba8accdfcdbae8389d819793145 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 4 Sep 2025 17:14:59 -0700 Subject: [PATCH 55/64] Streamline definitions of callable `Point` and `Range` constructors --- src/point.js | 19 +++++++++---------- src/range.js | 25 ++++--------------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/point.js b/src/point.js index a105228023..fa86c3b0db 100644 --- a/src/point.js +++ b/src/point.js @@ -1,10 +1,3 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ // Public: Represents a point in a buffer in row/column coordinates. // // Every public method that takes a point also accepts a *point-compatible* @@ -36,7 +29,7 @@ class Point { } else { let column, row; if (Array.isArray(object)) { - [row, column] = Array.from(object); + [row, column] = object; } else { ({row, column} = object); } @@ -79,6 +72,9 @@ class Point { } } + static ZERO = Object.freeze(new Point(0, 0)); + static INFINITY = Object.freeze(new Point(Infinity, Infinity)); + /* Section: Construction */ @@ -298,6 +294,9 @@ class Point { } } +// ES5 classes differ from their predecessors in that you are not allowed to +// call them like ordinary functions. Hence we must write this wrapper function +// which delegates to `new Point` whether it was called with `new` or not. function _Point (...args) { return new Point(...args); } @@ -309,8 +308,8 @@ Object.assign(_Point, { min: Point.min, max: Point.max, assertValid: Point.assertValid, - ZERO: Object.freeze(new Point(0, 0)), - INFINITY: Object.freeze(new Point(Infinity, Infinity)) + ZERO: Point.ZERO, + INFINITY: Point.INFINITY }); Object.assign(_Point.prototype, { row: null, diff --git a/src/range.js b/src/range.js index 2238b626ee..5cf795f5ba 100644 --- a/src/range.js +++ b/src/range.js @@ -1,13 +1,3 @@ -// TODO: This file was created by bulk-decaffeinate. -// Sanity-check the conversion and remove this comment. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ -// let Range; const Point = require('./point'); let newlineRegex = null; @@ -21,16 +11,6 @@ function __range__(left, right, inclusive) { return range; } -// NOTE: The convoluted mess in this file represents the constraint of "formal" -// JavaScript classes that you cannot treat them as functions: -// -// Point(0, 0); // <- TypeError: Class constructor client cannot be invoked -// without 'new' -// -// Hence we keep the ugliness from the decaffeinated version of `Range` until -// we find a better idiom. - - // Public: Represents a region in a buffer in row/column coordinates. // // Every public method that takes a range also accepts a *range-compatible* @@ -354,7 +334,7 @@ class Range { // * `startRow` {Number} start row // * `endRow` {Number} end row intersectsRowRange(startRow, endRow) { - if (startRow > endRow) { [startRow, endRow] = Array.from([endRow, startRow]); } + if (startRow > endRow) { [startRow, endRow] = [endRow, startRow]; } return (this.end.row >= startRow) && (endRow >= this.start.row); } @@ -383,6 +363,9 @@ class Range { } } +// ES5 classes differ from their predecessors in that you are not allowed to +// call them like ordinary functions. Hence we must write this wrapper function +// which delegates to `new Range` whether it was called with `new` or not. function _Range (...args) { return new Range(...args); }; From 61d757f87bece00a41d54e82ac7135a27cf9f602 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 5 Sep 2025 00:13:50 -0700 Subject: [PATCH 56/64] Further decaffeination cleanup --- src/default-history-provider.js | 184 +++---- src/display-layer.js | 3 +- src/display-marker-layer.js | 9 +- src/display-marker.js | 18 +- src/helpers.js | 8 +- src/is-character-pair.js | 36 +- src/marker-layer.js | 113 ++-- src/marker.js | 911 ++++++++++++++++---------------- src/point-helpers.js | 25 +- src/set-helpers.js | 39 +- 10 files changed, 686 insertions(+), 660 deletions(-) diff --git a/src/default-history-provider.js b/src/default-history-provider.js index adc4842e6b..8b14655e67 100644 --- a/src/default-history-provider.js +++ b/src/default-history-provider.js @@ -1,12 +1,3 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ -let DefaultHistoryProvider; const {Patch} = require('superstring') const MarkerLayer = require('./marker-layer'); const {traversal} = require('./point-helpers'); @@ -51,8 +42,7 @@ class Transaction { } // Manages undo/redo for {TextBuffer} -module.exports = -(DefaultHistoryProvider = class DefaultHistoryProvider { +class DefaultHistoryProvider { constructor(buffer) { this.buffer = buffer; this.maxUndoEntries = this.buffer.maxUndoEntries; @@ -68,7 +58,7 @@ module.exports = } groupChangesSinceCheckpoint(checkpointId, options) { - const deleteCheckpoint = options?.deleteCheckpoint != null ? options?.deleteCheckpoint : false; + const deleteCheckpoint = options?.deleteCheckpoint ?? false; const markerSnapshotAfter = options?.markers; let checkpointIndex = null; let markerSnapshotBefore = null; @@ -186,7 +176,7 @@ module.exports = enforceUndoStackSizeLimit() { if (this.undoStack.length > this.maxUndoEntries) { - return this.undoStack.splice(0, this.undoStack.length - this.maxUndoEntries); + this.undoStack.splice(0, this.undoStack.length - this.maxUndoEntries); } } @@ -203,19 +193,19 @@ module.exports = if (groupingInterval === 0) { return; } if (previousEntry instanceof Transaction && topEntry.shouldGroupWith(previousEntry)) { - return this.undoStack.splice(this.undoStack.length - 2, 2, topEntry.groupWith(previousEntry)); + this.undoStack.splice(this.undoStack.length - 2, 2, topEntry.groupWith(previousEntry)); } } pushChange({newStart, oldExtent, newExtent, oldText, newText}) { const patch = new Patch; patch.splice(newStart, oldExtent, newExtent, oldText, newText); - return this.pushPatch(patch); + this.pushPatch(patch); } pushPatch(patch) { this.undoStack.push(patch); - return this.clearRedoStack(); + this.clearRedoStack(); } undo() { @@ -248,7 +238,12 @@ module.exports = } if (spliceIndex != null) { - this.redoStack.push(...Array.from(this.undoStack.splice(spliceIndex).reverse() || [])); + // This feels strange, but what it actually does is remove N elements + // from the undo stack in place (as is expected)… and then the `reverse` + // is applied to the items that were removed from the stack. That's + // appropriate, since they're suppopsed to flip their order when going + // onto the redo stack. + this.redoStack.push(...this.undoStack.splice(spliceIndex).reverse() || []); return { textUpdates: patch.getChanges(), markers: snapshotBelow @@ -294,7 +289,12 @@ module.exports = } if (spliceIndex != null) { - this.undoStack.push(...Array.from(this.redoStack.splice(spliceIndex).reverse() || [])); + // This feels strange, but what it actually does is remove N elements + // from the redo stack in place (as is expected)… and then the `reverse` + // is applied to the items that were removed from the stack. That's + // appropriate, since they're suppopsed to flip their order when going + // onto the undo stack. + this.undoStack.push(...this.redoStack.splice(spliceIndex).reverse() || []); return { textUpdates: patch.getChanges(), markers: snapshotBelow @@ -343,15 +343,15 @@ module.exports = clear() { this.clearUndoStack(); - return this.clearRedoStack(); + this.clearRedoStack(); } clearUndoStack() { - return this.undoStack.length = 0; + this.undoStack.length = 0; } clearRedoStack() { - return this.redoStack.length = 0; + this.redoStack.length = 0; } toString() { @@ -389,7 +389,7 @@ module.exports = this.nextCheckpointId = state.nextCheckpointId; this.maxUndoEntries = state.maxUndoEntries; this.undoStack = this.deserializeStack(state.undoStack); - return this.redoStack = this.deserializeStack(state.redoStack); + this.redoStack = this.deserializeStack(state.redoStack); } getSnapshot(maxEntries) { @@ -470,68 +470,64 @@ module.exports = } serializeStack(stack, options) { - return (() => { - const result = []; - for (let entry of stack) { - switch (entry.constructor) { - case Checkpoint: - result.push({ - type: 'checkpoint', - id: entry.id, - snapshot: this.serializeSnapshot(entry.snapshot, options), - isBarrier: entry.isBarrier - }); - break; - case Transaction: - result.push({ - type: 'transaction', - markerSnapshotBefore: this.serializeSnapshot(entry.markerSnapshotBefore, options), - markerSnapshotAfter: this.serializeSnapshot(entry.markerSnapshotAfter, options), - patch: entry.patch.serialize().toString('base64') - }); - break; - case Patch: - result.push({ - type: 'patch', - data: entry.serialize().toString('base64') - }); - break; - default: - throw new Error(`Unexpected undoStack entry type during serialization: ${entry.constructor.name}`); - } + const result = []; + for (let entry of stack) { + switch (entry.constructor) { + case Checkpoint: + result.push({ + type: 'checkpoint', + id: entry.id, + snapshot: this.serializeSnapshot(entry.snapshot, options), + isBarrier: entry.isBarrier + }); + break; + case Transaction: + result.push({ + type: 'transaction', + markerSnapshotBefore: this.serializeSnapshot(entry.markerSnapshotBefore, options), + markerSnapshotAfter: this.serializeSnapshot(entry.markerSnapshotAfter, options), + patch: entry.patch.serialize().toString('base64') + }); + break; + case Patch: + result.push({ + type: 'patch', + data: entry.serialize().toString('base64') + }); + break; + default: + throw new Error(`Unexpected undoStack entry type during serialization: ${entry.constructor.name}`); } - return result; - })(); + } + return result; } deserializeStack(stack) { - return (() => { - const result = []; - for (let entry of stack) { - switch (entry.type) { - case 'checkpoint': - result.push(new Checkpoint( - entry.id, - MarkerLayer.deserializeSnapshot(entry.snapshot), - entry.isBarrier - )); - break; - case 'transaction': - result.push(new Transaction( - MarkerLayer.deserializeSnapshot(entry.markerSnapshotBefore), - Patch.deserialize(Buffer.from(entry.patch, 'base64')), - MarkerLayer.deserializeSnapshot(entry.markerSnapshotAfter) - )); - break; - case 'patch': - result.push(Patch.deserialize(Buffer.from(entry.data, 'base64'))); - break; - default: - throw new Error(`Unexpected undoStack entry type during deserialization: ${entry.type}`); - } + let result = []; + for (let entry of stack) { + switch (entry.type) { + case 'checkpoint': + result.push(new Checkpoint( + entry.id, + MarkerLayer.deserializeSnapshot(entry.snapshot), + entry.isBarrier + )); + break; + case 'transaction': + result.push(new Transaction( + MarkerLayer.deserializeSnapshot(entry.markerSnapshotBefore), + Patch.deserialize(Buffer.from(entry.patch, 'base64')), + MarkerLayer.deserializeSnapshot(entry.markerSnapshotAfter) + )); + break; + case 'patch': + result.push(Patch.deserialize(Buffer.from(entry.data, 'base64'))); + break; + default: + throw new Error(`Unexpected undoStack entry type during deserialization: ${entry.type}`); } - return result; - })(); + } + return result; } serializeSnapshot(snapshot, options) { @@ -552,17 +548,21 @@ module.exports = } return serializedLayerSnapshots; } -}); +} -var snapshotFromCheckpoint = checkpoint => ({ - type: 'checkpoint', - id: checkpoint.id, - markers: checkpoint.snapshot -}); +function snapshotFromCheckpoint(checkpoint) { + return { + type: 'checkpoint', + id: checkpoint.id, + markers: checkpoint.snapshot + }; +} -var checkpointFromSnapshot = ({id, markers}) => new Checkpoint(id, markers, false); +function checkpointFromSnapshot ({id, markers}) { + return new Checkpoint(id, markers, false); +} -var snapshotFromTransaction = function(transaction) { +function snapshotFromTransaction (transaction) { const changes = []; const iterable = transaction.patch.getChanges(); for (let i = 0; i < iterable.length; i++) { @@ -583,7 +583,11 @@ var snapshotFromTransaction = function(transaction) { markersBefore: transaction.markerSnapshotBefore, markersAfter: transaction.markerSnapshotAfter }; -}; +} + +function transactionFromSnapshot ({changes, markersBefore, markersAfter}) { + // TODO: Return raw patch if there's no markersBefore && markersAfter + return new Transaction(markersBefore, patchFromChanges(changes), markersAfter); +} -var transactionFromSnapshot = ({changes, markersBefore, markersAfter}) => // TODO: Return raw patch if there's no markersBefore && markersAfter - new Transaction(markersBefore, patchFromChanges(changes), markersAfter); +module.exports = DefaultHistoryProvider; diff --git a/src/display-layer.js b/src/display-layer.js index 5a47659bd7..3e1f1528b1 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -9,7 +9,6 @@ const ScreenLineBuilder = require('./screen-line-builder') const {spliceArray} = require('./helpers') const {MAX_BUILT_IN_SCOPE_ID} = require('./constants') -module.exports = class DisplayLayer { constructor (id, buffer, params = {}) { this.id = id @@ -1253,3 +1252,5 @@ const NullDeadline = { didTimeout: false, timeRemaining () { return Infinity } } + +module.exports = DisplayLayer diff --git a/src/display-marker-layer.js b/src/display-marker-layer.js index eb9eab1455..f01c099bfe 100644 --- a/src/display-marker-layer.js +++ b/src/display-marker-layer.js @@ -1,8 +1,3 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ const {Emitter, CompositeDisposable} = require('event-kit'); const DisplayMarker = require('./display-marker'); const Range = require('./range'); @@ -45,12 +40,12 @@ class DisplayMarkerLayer { this.displayLayer.didDestroyMarkerLayer(this.id); this.emitter.emit('did-destroy'); - return this.emitter.clear(); + this.emitter.clear(); } // Public: Destroy all markers in this layer. clear() { - return this.bufferMarkerLayer.clear(); + this.bufferMarkerLayer.clear(); } didClearBufferMarkerLayer() { diff --git a/src/display-marker.js b/src/display-marker.js index 961dadc034..ecbd335a99 100644 --- a/src/display-marker.js +++ b/src/display-marker.js @@ -1,10 +1,3 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ -let DisplayMarker; const {Emitter} = require('event-kit'); // Essential: Represents a buffer annotation that remains logically stationary @@ -46,8 +39,7 @@ const {Emitter} = require('event-kit'); // start or start at the marker's end. This is the most fragile strategy. // // See {TextBuffer::markRange} for usage. -module.exports = -(DisplayMarker = class DisplayMarker { +class DisplayMarker { /* Section: Construction and Destruction */ @@ -65,7 +57,7 @@ module.exports = // destroyed, a marker cannot be restored by undo/redo operations. destroy() { if (!this.isDestroyed()) { - return this.bufferMarker.destroy(); + this.bufferMarker.destroy(); } } @@ -74,7 +66,7 @@ module.exports = this.layer.didDestroyMarker(this); this.emitter.dispose(); this.emitter.clear(); - return this.bufferMarkerSubscription?.dispose(); + this.bufferMarkerSubscription?.dispose(); } // Essential: Creates and returns a new {DisplayMarker} with the same properties as @@ -461,4 +453,6 @@ module.exports = return this.emitter.emit('did-change', changeEvent); } -}); +} + +module.exports = DisplayMarker; diff --git a/src/helpers.js b/src/helpers.js index 094685521d..0cd3bc511b 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -25,7 +25,7 @@ exports.debounce = function debounce (fn, wait) { } } -exports.spliceArray = function (array, start, removedCount, insertedItems = []) { +exports.spliceArray = function spliceArray(array, start, removedCount, insertedItems = []) { const oldLength = array.length const insertedCount = insertedItems.length removedCount = Math.min(removedCount, oldLength - start) @@ -49,7 +49,7 @@ exports.spliceArray = function (array, start, removedCount, insertedItems = []) } } -exports.patchFromChanges = function (changes) { +exports.patchFromChanges = function patchFromChanges(changes) { const patch = new Patch() for (let i = 0; i < changes.length; i++) { const {oldStart, oldEnd, oldText, newStart, newEnd, newText} = changes[i] @@ -60,7 +60,7 @@ exports.patchFromChanges = function (changes) { return patch } -exports.normalizePatchChanges = function (changes) { +exports.normalizePatchChanges = function normalizePatchChanges(changes) { return changes.map((change) => new TextChange( Range(change.oldStart, change.oldEnd), @@ -70,7 +70,7 @@ exports.normalizePatchChanges = function (changes) { ) } -exports.extentForText = function (text) { +exports.extentForText = function extentForText(text) { let lastLineStartIndex = 0 let row = 0 LF_REGEX.lastIndex = 0 diff --git a/src/is-character-pair.js b/src/is-character-pair.js index 3a8a18b8be..5566ce36cd 100644 --- a/src/is-character-pair.js +++ b/src/is-character-pair.js @@ -13,20 +13,34 @@ module.exports = function(character1, character2) { isCombinedCharacter(charCodeA, charCodeB); }; -var isCombinedCharacter = (charCodeA, charCodeB) => !isCombiningCharacter(charCodeA) && isCombiningCharacter(charCodeB); +function isCombinedCharacter (charCodeA, charCodeB) { + return !isCombiningCharacter(charCodeA) && isCombiningCharacter(charCodeB); +} -var isSurrogatePair = (charCodeA, charCodeB) => isHighSurrogate(charCodeA) && isLowSurrogate(charCodeB); +function isSurrogatePair (charCodeA, charCodeB) { + return isHighSurrogate(charCodeA) && isLowSurrogate(charCodeB); +} -var isVariationSequence = (charCodeA, charCodeB) => !isVariationSelector(charCodeA) && isVariationSelector(charCodeB); +function isVariationSequence (charCodeA, charCodeB) { + return !isVariationSelector(charCodeA) && isVariationSelector(charCodeB); +} -var isHighSurrogate = charCode => 0xD800 <= charCode && charCode <= 0xDBFF; +function isHighSurrogate (charCode) { + return 0xD800 <= charCode && charCode <= 0xDBFF; +} -var isLowSurrogate = charCode => 0xDC00 <= charCode && charCode <= 0xDFFF; +function isLowSurrogate (charCode) { + return 0xDC00 <= charCode && charCode <= 0xDFFF; +} -var isVariationSelector = charCode => 0xFE00 <= charCode && charCode <= 0xFE0F; +function isVariationSelector (charCode) { + return 0xFE00 <= charCode && charCode <= 0xFE0F; +} -var isCombiningCharacter = charCode => (0x0300 <= charCode && charCode <= 0x036F) || -(0x1AB0 <= charCode && charCode <= 0x1AFF) || -(0x1DC0 <= charCode && charCode <= 0x1DFF) || -(0x20D0 <= charCode && charCode <= 0x20FF) || -(0xFE20 <= charCode && charCode <= 0xFE2F); +function isCombiningCharacter (charCode) { + return (0x0300 <= charCode && charCode <= 0x036F) || + (0x1AB0 <= charCode && charCode <= 0x1AFF) || + (0x1DC0 <= charCode && charCode <= 0x1DFF) || + (0x20D0 <= charCode && charCode <= 0x20FF) || + (0xFE20 <= charCode && charCode <= 0xFE2F); +} diff --git a/src/marker-layer.js b/src/marker-layer.js index 1196e3ccbd..b624146523 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -10,7 +10,7 @@ const SerializationVersion = 2; // Public: *Experimental:* A container for a related set of markers. // This API is experimental and subject to change on any release. -module.exports = class MarkerLayer { +class MarkerLayer { static deserialize(delegate, state) { var store; store = new MarkerLayer(delegate, 0); @@ -343,95 +343,82 @@ module.exports = class MarkerLayer { Section: Private - TextBuffer interface */ splice(start, oldExtent, newExtent) { - var invalidated; - invalidated = this.index.splice(start, oldExtent, newExtent); - return invalidated.touch.forEach((id) => { - var marker, ref; - marker = this.markersById[id]; - if ((ref = invalidated[marker.getInvalidationStrategy()]) != null ? ref.has(id) : void 0) { + let invalidated = this.index.splice(start, oldExtent, newExtent); + for (let id of invalidated.touch) { + let marker = this.markersById[id]; + if (invalidated[marker.getInvalidationStrategy()]?.has(id)) { if (this.destroyInvalidatedMarkers) { - return marker.destroy(); + marker.destroy(); } else { - return marker.valid = false; + marker.valid = false; } } - }); + } } restoreFromSnapshot(snapshots, alwaysCreate) { - var existingMarkerIds, i, id, j, len, len1, marker, newMarker, range, results, snapshot, snapshotIds; - if (snapshots == null) { - return; - } - snapshotIds = Object.keys(snapshots); - existingMarkerIds = Object.keys(this.markersById); - for (i = 0, len = snapshotIds.length; i < len; i++) { - id = snapshotIds[i]; - snapshot = snapshots[id]; + if (snapshots == null) return; + + let snapshotIds = Object.keys(snapshots); + let existingMarkerIds = Object.keys(this.markersById); + + for (let id of snapshotIds) { + let snapshot = snapshots[id]; if (alwaysCreate) { this.createMarker(snapshot.range, snapshot, true); continue; } - if (marker = this.markersById[id]) { + let marker = this.markersById[id]; + if (marker) { marker.update(marker.getRange(), snapshot, true, true); } else { - ({marker} = snapshot); + marker = snapshot.marker; if (marker) { this.markersById[marker.id] = marker; - ({range} = snapshot); + let { range } = snapshot; this.index.insert(marker.id, range.start, range.end); marker.update(marker.getRange(), snapshot, true, true); if (this.emitCreateMarkerEvents) { this.emitter.emit('did-create-marker', marker); } } else { - newMarker = this.createMarker(snapshot.range, snapshot, true); + this.createMarker(snapshot.range, snapshot, true); } } } - results = []; - for (j = 0, len1 = existingMarkerIds.length; j < len1; j++) { - id = existingMarkerIds[j]; - if ((marker = this.markersById[id]) && (snapshots[id] == null)) { - results.push(marker.destroy(true)); - } else { - results.push(void 0); + + for (let id of existingMarkerIds) { + let marker = this.markersById[id]; + if (marker && !snapshots[id]) { + marker.destroy(true); } } - return results; } createSnapshot() { - var i, id, len, marker, ranges, ref, result; - result = {}; - ranges = this.index.dump(); - ref = Object.keys(this.markersById); - for (i = 0, len = ref.length; i < len; i++) { - id = ref[i]; - marker = this.markersById[id]; + let result = {}; + let ranges = this.index.dump(); + for (let id of Object.keys(this.markersById)) { + let marker = this.markersById[id]; result[id] = marker.getSnapshot(Range.fromObject(ranges[id])); } return result; } emitChangeEvents(snapshot) { - return this.markersWithChangeListeners.forEach(function(marker) { - var ref; + this.markersWithChangeListeners.forEach(function(marker) { if (!marker.isDestroyed()) { // event handlers could destroy markers - return marker.emitChangeEvent(snapshot != null ? (ref = snapshot[marker.id]) != null ? ref.range : void 0 : void 0, true, false); + return marker.emitChangeEvent(snapshot?.[marker.id]?.range, true, false); } }); } serialize() { - var i, id, len, marker, markersById, ranges, ref, snapshot; - ranges = this.index.dump(); - markersById = {}; - ref = Object.keys(this.markersById); - for (i = 0, len = ref.length; i < len; i++) { - id = ref[i]; - marker = this.markersById[id]; - snapshot = marker.getSnapshot(Range.fromObject(ranges[id]), false); + let ranges = this.index.dump(); + let markersById = {}; + for (let id of Object.keys(this.markersById)) { + let marker = this.markersById[id]; + let snapshot = marker.getSnapshot(Range.fromObject(ranges[id]), false); markersById[id] = snapshot; } return { @@ -445,7 +432,7 @@ module.exports = class MarkerLayer { } deserialize(state) { - var id, markerState, range, ref; + // var id, markerState, range, ref; if (state.version !== SerializationVersion) { return; } @@ -456,13 +443,11 @@ module.exports = class MarkerLayer { this.delegate.registerSelectionsMarkerLayer(this); } this.persistent = state.persistent; - ref = state.markersById; - for (id in ref) { - markerState = ref[id]; - range = Range.fromObject(markerState.range); + for (let [id, markerState] of Object.entries(state.markersById)) { + let range = Range.fromObject(markerState.range); // `markerState` is frozen, so instead of deleting its `range` we'll // create a new object and copy all properties _except_ `range`. - let { range: oldRange, ...params } = markerState + let { range: oldRange, ...params } = markerState; this.addMarker(id, range, { ...params }); } } @@ -481,10 +466,10 @@ module.exports = class MarkerLayer { this.markersWithChangeListeners.delete(marker); this.markersWithDestroyListeners.delete(marker); this.displayMarkerLayers.forEach(function(displayMarkerLayer) { - return displayMarkerLayer.destroyMarker(marker.id); + displayMarkerLayer.destroyMarker(marker.id); }); if (!suppressMarkerLayerUpdateEvents) { - return this.delegate.markersUpdated(this); + this.delegate.markersUpdated(this); } } } @@ -511,8 +496,7 @@ module.exports = class MarkerLayer { setMarkerRange(id, range) { id = parseInt(id); - var end, start; - ({start, end} = Range.fromObject(range)); + let {start, end} = Range.fromObject(range); start = this.delegate.clipPosition(start); end = this.delegate.clipPosition(end); this.index.remove(id); @@ -524,14 +508,13 @@ module.exports = class MarkerLayer { } createMarker(range, params, suppressMarkerLayerUpdateEvents = false) { - var id, marker, ref; - id = this.delegate.getNextMarkerId(); - marker = this.addMarker(id, range, params); + let id = this.delegate.getNextMarkerId(); + let marker = this.addMarker(id, range, params); this.delegate.markerCreated(this, marker); if (!suppressMarkerLayerUpdateEvents) { this.delegate.markersUpdated(this); } - marker.trackDestruction = (ref = this.trackDestructionInOnDidCreateMarkerCallbacks) != null ? ref : false; + marker.trackDestruction = this.trackDestructionInOnDidCreateMarkerCallbacks ?? false; if (this.emitCreateMarkerEvents) { this.emitter.emit('did-create-marker', marker); } @@ -557,7 +540,7 @@ module.exports = class MarkerLayer { }; -const filterSet = function(set1, set2) { +function filterSet(set1, set2) { if (set1) { intersectSet(set1, set2); return set1; @@ -565,3 +548,5 @@ const filterSet = function(set1, set2) { return set2; } }; + +module.exports = MarkerLayer; diff --git a/src/marker.js b/src/marker.js index 1da881fd61..cc8b24c70a 100644 --- a/src/marker.js +++ b/src/marker.js @@ -1,15 +1,4 @@ -// TODO: This file was created by bulk-decaffeinate. -// Sanity-check the conversion and remove this comment. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ -let Marker; -const {extend, isEqual, omit, pick, size} = require('underscore-plus'); +const {extend, isEqual} = require('underscore-plus'); const {Emitter} = require('event-kit'); const Delegator = require('delegato'); const Point = require('./point'); @@ -36,488 +25,528 @@ const OptionKeys = new Set(['reversed', 'tailed', 'invalidate', 'exclusive']); // invalidation strategy you choose, certain changes to the buffer can cause a // marker to become invalid, for example if the text surrounding the marker is // deleted. See {TextBuffer::markRange} for invalidation strategies. -module.exports = -(Marker = (function() { - Marker = class Marker { - static initClass() { - Delegator.includeInto(this); - - this.delegatesMethods('containsPoint', 'containsRange', 'intersectsRow', {toMethod: 'getRange'}); - } - - static extractParams(inputParams) { - const outputParams = {}; - let containsCustomProperties = false; - if (inputParams != null) { - for (var key of Array.from(Object.keys(inputParams))) { - if (OptionKeys.has(key)) { - outputParams[key] = inputParams[key]; - } else if ((key === 'clipDirection') || (key === 'skipSoftWrapIndentation')) { - // TODO: Ignore these two keys for now. Eventually, when the - // deprecation below will be gone, we can remove this conditional as - // well, and just return standard marker properties. - } else { - containsCustomProperties = true; - if (outputParams.properties == null) { outputParams.properties = {}; } - outputParams.properties[key] = inputParams[key]; - } +class Marker { + static extractParams(inputParams) { + const outputParams = {}; + let containsCustomProperties = false; + if (inputParams != null) { + for (var key of Object.keys(inputParams)) { + if (OptionKeys.has(key)) { + outputParams[key] = inputParams[key]; + } else if ((key === 'clipDirection') || (key === 'skipSoftWrapIndentation')) { + // TODO: Ignore these two keys for now. Eventually, when the + // deprecation below will be gone, we can remove this conditional as + // well, and just return standard marker properties. + } else { + containsCustomProperties = true; + if (outputParams.properties == null) { outputParams.properties = {}; } + outputParams.properties[key] = inputParams[key]; } } + } - // TODO: Remove both this deprecation and the conditional above on the - // release after the one where we'll ship `DisplayLayer`. - if (containsCustomProperties) { - Grim.deprecate(`\ + // TODO: Remove both this deprecation and the conditional above on the + // release after the one where we'll ship `DisplayLayer`. + if (containsCustomProperties) { + Grim.deprecate(`\ Assigning custom properties to a marker when creating/copying it is deprecated. Please, consider storing the custom properties you need in some other object in your package, keyed by the marker's id property.\ `); - } - - return outputParams; } - constructor(id, layer, _range, params, exclusivitySet) { - // The `_range` parameter is kept in place just to keep the API stable, - // but it's not used; the marker asks its layer for its range later on - // via `::getRange`. - this.id = id; - this.layer = layer; - if (exclusivitySet == null) { exclusivitySet = false; } - ({tailed: this.tailed, reversed: this.reversed, valid: this.valid, invalidate: this.invalidate, exclusive: this.exclusive, properties: this.properties} = params); - this.emitter = new Emitter; - if (this.tailed == null) { this.tailed = true; } - if (this.reversed == null) { this.reversed = false; } - if (this.valid == null) { this.valid = true; } - if (this.invalidate == null) { this.invalidate = 'overlap'; } - if (this.properties == null) { this.properties = {}; } - this.hasChangeObservers = false; - Object.freeze(this.properties); - if (!exclusivitySet) { this.layer.setMarkerIsExclusive(this.id, this.isExclusive()); } + return outputParams; + } + + constructor(id, layer, _range, params, exclusivitySet) { + // The `_range` parameter is kept in place just to keep the API stable, + // but it's not used; the marker asks its layer for its range later on + // via `::getRange`. + this.id = id; + this.layer = layer; + if (exclusivitySet == null) { exclusivitySet = false; } + ({tailed: this.tailed, reversed: this.reversed, valid: this.valid, invalidate: this.invalidate, exclusive: this.exclusive, properties: this.properties} = params); + this.emitter = new Emitter; + if (this.tailed == null) { this.tailed = true; } + if (this.reversed == null) { this.reversed = false; } + if (this.valid == null) { this.valid = true; } + if (this.invalidate == null) { this.invalidate = 'overlap'; } + if (this.properties == null) { this.properties = {}; } + this.hasChangeObservers = false; + Object.freeze(this.properties); + if (!exclusivitySet) { this.layer.setMarkerIsExclusive(this.id, this.isExclusive()); } + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the marker is destroyed. + // + // * `callback` {Function} to be called when the marker is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy(callback) { + this.layer.markersWithDestroyListeners.add(this); + return this.emitter.on('did-destroy', callback); + } + + // Public: Invoke the given callback when the state of the marker changes. + // + // * `callback` {Function} to be called when the marker changes. + // * `event` {Object} with the following keys: + // * `oldHeadPosition` {Point} representing the former head position + // * `newHeadPosition` {Point} representing the new head position + // * `oldTailPosition` {Point} representing the former tail position + // * `newTailPosition` {Point} representing the new tail position + // * `wasValid` {Boolean} indicating whether the marker was valid before the change + // * `isValid` {Boolean} indicating whether the marker is now valid + // * `hadTail` {Boolean} indicating whether the marker had a tail before the change + // * `hasTail` {Boolean} indicating whether the marker now has a tail + // * `oldProperties` {Object} containing the marker's custom properties before the change. + // * `newProperties` {Object} containing the marker's custom properties after the change. + // * `textChanged` {Boolean} indicating whether this change was caused by a textual change + // to the buffer or whether the marker was manipulated directly via its public API. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange(callback) { + if (!this.hasChangeObservers) { + this.previousEventState = this.getSnapshot(this.getRange()); + this.hasChangeObservers = true; + this.layer.markersWithChangeListeners.add(this); } - - /* - Section: Event Subscription - */ - - // Public: Invoke the given callback when the marker is destroyed. - // - // * `callback` {Function} to be called when the marker is destroyed. - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy(callback) { - this.layer.markersWithDestroyListeners.add(this); - return this.emitter.on('did-destroy', callback); - } - - // Public: Invoke the given callback when the state of the marker changes. - // - // * `callback` {Function} to be called when the marker changes. - // * `event` {Object} with the following keys: - // * `oldHeadPosition` {Point} representing the former head position - // * `newHeadPosition` {Point} representing the new head position - // * `oldTailPosition` {Point} representing the former tail position - // * `newTailPosition` {Point} representing the new tail position - // * `wasValid` {Boolean} indicating whether the marker was valid before the change - // * `isValid` {Boolean} indicating whether the marker is now valid - // * `hadTail` {Boolean} indicating whether the marker had a tail before the change - // * `hasTail` {Boolean} indicating whether the marker now has a tail - // * `oldProperties` {Object} containing the marker's custom properties before the change. - // * `newProperties` {Object} containing the marker's custom properties after the change. - // * `textChanged` {Boolean} indicating whether this change was caused by a textual change - // to the buffer or whether the marker was manipulated directly via its public API. - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange(callback) { - if (!this.hasChangeObservers) { - this.previousEventState = this.getSnapshot(this.getRange()); - this.hasChangeObservers = true; - this.layer.markersWithChangeListeners.add(this); - } - return this.emitter.on('did-change', callback); - } - - // Public: Returns the current {Range} of the marker. The range is immutable. - getRange() { return this.layer.getMarkerRange(this.id); } - - // Public: Sets the range of the marker. - // - // * `range` A {Range} or range-compatible {Array}. The range will be clipped - // before it is assigned. - // * `params` (optional) An {Object} with the following keys: - // * `reversed` {Boolean} indicating the marker will to be in a reversed - // orientation. - // * `exclusive` {Boolean} indicating that changes occurring at either end of - // the marker will be considered *outside* the marker rather than inside. - // This defaults to `false` unless the marker's invalidation strategy is - // `inside` or the marker has no tail, in which case it defaults to `true`. - setRange(range, params) { - if (params == null) { params = {}; } - return this.update(this.getRange(), {reversed: params.reversed, tailed: true, range: Range.fromObject(range, true), exclusive: params.exclusive}); + return this.emitter.on('did-change', callback); + } + + // Public: Returns the current {Range} of the marker. The range is immutable. + getRange() { + return this.layer.getMarkerRange(this.id); + } + + // Public: Sets the range of the marker. + // + // * `range` A {Range} or range-compatible {Array}. The range will be clipped + // before it is assigned. + // * `params` (optional) An {Object} with the following keys: + // * `reversed` {Boolean} indicating the marker will to be in a reversed + // orientation. + // * `exclusive` {Boolean} indicating that changes occurring at either end of + // the marker will be considered *outside* the marker rather than inside. + // This defaults to `false` unless the marker's invalidation strategy is + // `inside` or the marker has no tail, in which case it defaults to `true`. + setRange(range, params) { + if (params == null) { params = {}; } + return this.update(this.getRange(), { + reversed: params.reversed, + tailed: true, + range: Range.fromObject(range, true), + exclusive: params.exclusive + }); + } + + // Public: Returns a {Point} representing the marker's current head position. + getHeadPosition() { + if (this.reversed) { + return this.getStartPosition(); + } else { + return this.getEndPosition(); } - - // Public: Returns a {Point} representing the marker's current head position. - getHeadPosition() { - if (this.reversed) { - return this.getStartPosition(); - } else { - return this.getEndPosition(); - } - } - - // Public: Sets the head position of the marker. - // - // * `position` A {Point} or point-compatible {Array}. The position will be - // clipped before it is assigned. - setHeadPosition(position) { - position = Point.fromObject(position); - const oldRange = this.getRange(); - const params = {}; - - if (this.hasTail()) { - if (this.isReversed()) { - if (position.isLessThan(oldRange.end)) { - params.range = new Range(position, oldRange.end); - } else { - params.reversed = false; - params.range = new Range(oldRange.end, position); - } + } + + // Public: Sets the head position of the marker. + // + // * `position` A {Point} or point-compatible {Array}. The position will be + // clipped before it is assigned. + setHeadPosition(position) { + position = Point.fromObject(position); + const oldRange = this.getRange(); + const params = {}; + + if (this.hasTail()) { + if (this.isReversed()) { + if (position.isLessThan(oldRange.end)) { + params.range = new Range(position, oldRange.end); } else { - if (position.isLessThan(oldRange.start)) { - params.reversed = true; - params.range = new Range(position, oldRange.start); - } else { - params.range = new Range(oldRange.start, position); - } + params.reversed = false; + params.range = new Range(oldRange.end, position); } } else { - params.range = new Range(position, position); - } - return this.update(oldRange, params); - } - - // Public: Returns a {Point} representing the marker's current tail position. - // If the marker has no tail, the head position will be returned instead. - getTailPosition() { - if (this.reversed) { - return this.getEndPosition(); - } else { - return this.getStartPosition(); - } - } - - // Public: Sets the tail position of the marker. If the marker doesn't have a - // tail, it will after calling this method. - // - // * `position` A {Point} or point-compatible {Array}. The position will be - // clipped before it is assigned. - setTailPosition(position) { - position = Point.fromObject(position); - const oldRange = this.getRange(); - const params = {tailed: true}; - - if (this.reversed) { if (position.isLessThan(oldRange.start)) { - params.reversed = false; + params.reversed = true; params.range = new Range(position, oldRange.start); } else { params.range = new Range(oldRange.start, position); } - } else { - if (position.isLessThan(oldRange.end)) { - params.range = new Range(position, oldRange.end); - } else { - params.reversed = true; - params.range = new Range(oldRange.end, position); - } } - - return this.update(oldRange, params); + } else { + params.range = new Range(position, position); } - - // Public: Returns a {Point} representing the start position of the marker, - // which could be the head or tail position, depending on its orientation. - getStartPosition() { return this.layer.getMarkerStartPosition(this.id); } - - // Public: Returns a {Point} representing the end position of the marker, - // which could be the head or tail position, depending on its orientation. - getEndPosition() { return this.layer.getMarkerEndPosition(this.id); } - - // Public: Removes the marker's tail. After calling the marker's head position - // will be reported as its current tail position until the tail is planted - // again. - clearTail() { - const headPosition = this.getHeadPosition(); - return this.update(this.getRange(), {tailed: false, reversed: false, range: Range(headPosition, headPosition)}); + return this.update(oldRange, params); + } + + // Public: Returns a {Point} representing the marker's current tail position. + // If the marker has no tail, the head position will be returned instead. + getTailPosition() { + if (this.reversed) { + return this.getEndPosition(); + } else { + return this.getStartPosition(); } - - // Public: Plants the marker's tail at the current head position. After calling - // the marker's tail position will be its head position at the time of the - // call, regardless of where the marker's head is moved. - plantTail() { - if (!this.hasTail()) { - const headPosition = this.getHeadPosition(); - return this.update(this.getRange(), {tailed: true, range: new Range(headPosition, headPosition)}); + } + + // Public: Sets the tail position of the marker. If the marker doesn't have a + // tail, it will after calling this method. + // + // * `position` A {Point} or point-compatible {Array}. The position will be + // clipped before it is assigned. + setTailPosition(position) { + position = Point.fromObject(position); + const oldRange = this.getRange(); + const params = {tailed: true}; + + if (this.reversed) { + if (position.isLessThan(oldRange.start)) { + params.reversed = false; + params.range = new Range(position, oldRange.start); + } else { + params.range = new Range(oldRange.start, position); + } + } else { + if (position.isLessThan(oldRange.end)) { + params.range = new Range(position, oldRange.end); + } else { + params.reversed = true; + params.range = new Range(oldRange.end, position); } } - // Public: Returns a {Boolean} indicating whether the head precedes the tail. - isReversed() { - return this.tailed && this.reversed; + return this.update(oldRange, params); + } + + // Public: Returns a {Point} representing the start position of the marker, + // which could be the head or tail position, depending on its orientation. + getStartPosition() { + return this.layer.getMarkerStartPosition(this.id); + } + + // Public: Returns a {Point} representing the end position of the marker, + // which could be the head or tail position, depending on its orientation. + getEndPosition() { + return this.layer.getMarkerEndPosition(this.id); + } + + // Public: Removes the marker's tail. After calling the marker's head position + // will be reported as its current tail position until the tail is planted + // again. + clearTail() { + const headPosition = this.getHeadPosition(); + return this.update(this.getRange(), { + tailed: false, + reversed: false, + range: Range(headPosition, headPosition) + }); + } + + // Public: Plants the marker's tail at the current head position. After calling + // the marker's tail position will be its head position at the time of the + // call, regardless of where the marker's head is moved. + plantTail() { + if (!this.hasTail()) { + const headPosition = this.getHeadPosition(); + return this.update(this.getRange(), { + tailed: true, + range: new Range(headPosition, headPosition) + }); } - - // Public: Returns a {Boolean} indicating whether the marker has a tail. - hasTail() { - return this.tailed; + } + + // Public: Returns a {Boolean} indicating whether the head precedes the tail. + isReversed() { + return this.tailed && this.reversed; + } + + // Public: Returns a {Boolean} indicating whether the marker has a tail. + hasTail() { + return this.tailed; + } + + // Public: Is the marker valid? + // + // Returns a {Boolean}. + isValid() { + return !this.isDestroyed() && this.valid; + } + + // Public: Is the marker destroyed? + // + // Returns a {Boolean}. + isDestroyed() { + return !this.layer.hasMarker(this.id); + } + + // Public: Returns a {Boolean} indicating whether changes that occur exactly at + // the marker's head or tail cause it to move. + isExclusive() { + if (this.exclusive != null) { + return this.exclusive; + } else { + return (this.getInvalidationStrategy() === 'inside') || !this.hasTail(); } - - // Public: Is the marker valid? - // - // Returns a {Boolean}. - isValid() { - return !this.isDestroyed() && this.valid; + } + + // Public: Returns a {Boolean} indicating whether this marker is equivalent to + // another marker, meaning they have the same range and options. + // + // * `other` {Marker} other marker + isEqual(other) { + return (this.invalidate === other.invalidate) && + (this.tailed === other.tailed) && + (this.reversed === other.reversed) && + (this.exclusive === other.exclusive) && + isEqual(this.properties, other.properties) && + this.getRange().isEqual(other.getRange()); + } + + // Public: Get the invalidation strategy for this marker. + // + // Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. + // + // Returns a {String}. + getInvalidationStrategy() { + return this.invalidate; + } + + // Public: Returns an {Object} containing any custom properties associated with + // the marker. + getProperties() { + return this.properties; + } + + // Public: Merges an {Object} containing new properties into the marker's + // existing properties. + // + // * `properties` {Object} + setProperties(properties) { + return this.update(this.getRange(), { + properties: extend({}, this.properties, properties) + }); + } + + // Public: Creates and returns a new {Marker} with the same properties as this + // marker. + // + // * `params` {Object} + copy(options) { + if (options == null) { options = {}; } + const snapshot = this.getSnapshot(); + options = Marker.extractParams(options); + return this.layer.createMarker(this.getRange(), extend( + {}, + snapshot, + options, + {properties: extend({}, snapshot.properties, options.properties)} + )); + } + + // Public: Destroys the marker, causing it to emit the 'destroyed' event. + destroy(suppressMarkerLayerUpdateEvents) { + if (this.isDestroyed()) { return; } + + if (this.trackDestruction) { + const error = new Error; + Error.captureStackTrace(error); + this.destroyStackTrace = error.stack; } - // Public: Is the marker destroyed? - // - // Returns a {Boolean}. - isDestroyed() { - return !this.layer.hasMarker(this.id); + this.layer.destroyMarker(this, suppressMarkerLayerUpdateEvents); + this.emitter.emit('did-destroy'); + return this.emitter.clear(); + } + + // Public: Compares this marker to another based on their ranges. + // + // * `other` {Marker} + compare(other) { + return this.layer.compareMarkers(this.id, other.id); + } + + // Returns whether this marker matches the given parameters. The parameters + // are the same as {MarkerLayer::findMarkers}. + matchesParams(params) { + for (var key of Object.keys(params)) { + if (!this.matchesParam(key, params[key])) { return false; } } - - // Public: Returns a {Boolean} indicating whether changes that occur exactly at - // the marker's head or tail cause it to move. - isExclusive() { - if (this.exclusive != null) { - return this.exclusive; - } else { - return (this.getInvalidationStrategy() === 'inside') || !this.hasTail(); - } + return true; + } + + // Returns whether this marker matches the given parameter name and value. + // The parameters are the same as {MarkerLayer::findMarkers}. + matchesParam(key, value) { + switch (key) { + case 'startPosition': + return this.getStartPosition().isEqual(value); + case 'endPosition': + return this.getEndPosition().isEqual(value); + case 'containsPoint': case 'containsPosition': + return this.containsPoint(value); + case 'containsRange': + return this.containsRange(value); + case 'startRow': + return this.getStartPosition().row === value; + case 'endRow': + return this.getEndPosition().row === value; + case 'intersectsRow': + return this.intersectsRow(value); + case 'invalidate': case 'reversed': case 'tailed': + return isEqual(this[key], value); + case 'valid': + return this.isValid() === value; + default: + return isEqual(this.properties[key], value); } + } - // Public: Returns a {Boolean} indicating whether this marker is equivalent to - // another marker, meaning they have the same range and options. - // - // * `other` {Marker} other marker - isEqual(other) { - return (this.invalidate === other.invalidate) && - (this.tailed === other.tailed) && - (this.reversed === other.reversed) && - (this.exclusive === other.exclusive) && - isEqual(this.properties, other.properties) && - this.getRange().isEqual(other.getRange()); - } + update(oldRange, {range, reversed, tailed, valid, exclusive, properties}, textChanged, suppressMarkerLayerUpdateEvents) { + let propertiesChanged; + if (textChanged == null) { textChanged = false; } + if (suppressMarkerLayerUpdateEvents == null) { suppressMarkerLayerUpdateEvents = false; } + if (this.isDestroyed()) { return; } - // Public: Get the invalidation strategy for this marker. - // - // Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. - // - // Returns a {String}. - getInvalidationStrategy() { - return this.invalidate; - } + oldRange = Range.fromObject(oldRange); + if (range != null) { range = Range.fromObject(range); } - // Public: Returns an {Object} containing any custom properties associated with - // the marker. - getProperties() { - return this.properties; - } + const wasExclusive = this.isExclusive(); + let updated = (propertiesChanged = false); - // Public: Merges an {Object} containing new properties into the marker's - // existing properties. - // - // * `properties` {Object} - setProperties(properties) { - return this.update(this.getRange(), {properties: extend({}, this.properties, properties)}); + if ((range != null) && !range.isEqual(oldRange)) { + this.layer.setMarkerRange(this.id, range); + updated = true; } - // Public: Creates and returns a new {Marker} with the same properties as this - // marker. - // - // * `params` {Object} - copy(options) { - if (options == null) { options = {}; } - const snapshot = this.getSnapshot(); - options = Marker.extractParams(options); - return this.layer.createMarker(this.getRange(), extend( - {}, - snapshot, - options, - {properties: extend({}, snapshot.properties, options.properties)} - )); + if ((reversed != null) && (reversed !== this.reversed)) { + this.reversed = reversed; + updated = true; } - // Public: Destroys the marker, causing it to emit the 'destroyed' event. - destroy(suppressMarkerLayerUpdateEvents) { - if (this.isDestroyed()) { return; } - - if (this.trackDestruction) { - const error = new Error; - Error.captureStackTrace(error); - this.destroyStackTrace = error.stack; - } - - this.layer.destroyMarker(this, suppressMarkerLayerUpdateEvents); - this.emitter.emit('did-destroy'); - return this.emitter.clear(); + if ((tailed != null) && (tailed !== this.tailed)) { + this.tailed = tailed; + updated = true; } - // Public: Compares this marker to another based on their ranges. - // - // * `other` {Marker} - compare(other) { - return this.layer.compareMarkers(this.id, other.id); + if ((valid != null) && (valid !== this.valid)) { + this.valid = valid; + updated = true; } - // Returns whether this marker matches the given parameters. The parameters - // are the same as {MarkerLayer::findMarkers}. - matchesParams(params) { - for (var key of Array.from(Object.keys(params))) { - if (!this.matchesParam(key, params[key])) { return false; } - } - return true; + if ((exclusive != null) && (exclusive !== this.exclusive)) { + this.exclusive = exclusive; + updated = true; } - // Returns whether this marker matches the given parameter name and value. - // The parameters are the same as {MarkerLayer::findMarkers}. - matchesParam(key, value) { - switch (key) { - case 'startPosition': - return this.getStartPosition().isEqual(value); - case 'endPosition': - return this.getEndPosition().isEqual(value); - case 'containsPoint': case 'containsPosition': - return this.containsPoint(value); - case 'containsRange': - return this.containsRange(value); - case 'startRow': - return this.getStartPosition().row === value; - case 'endRow': - return this.getEndPosition().row === value; - case 'intersectsRow': - return this.intersectsRow(value); - case 'invalidate': case 'reversed': case 'tailed': - return isEqual(this[key], value); - case 'valid': - return this.isValid() === value; - default: - return isEqual(this.properties[key], value); - } + if (wasExclusive !== this.isExclusive()) { + this.layer.setMarkerIsExclusive(this.id, this.isExclusive()); + updated = true; } - update(oldRange, {range, reversed, tailed, valid, exclusive, properties}, textChanged, suppressMarkerLayerUpdateEvents) { - let propertiesChanged; - if (textChanged == null) { textChanged = false; } - if (suppressMarkerLayerUpdateEvents == null) { suppressMarkerLayerUpdateEvents = false; } - if (this.isDestroyed()) { return; } - - oldRange = Range.fromObject(oldRange); - if (range != null) { range = Range.fromObject(range); } - - const wasExclusive = this.isExclusive(); - let updated = (propertiesChanged = false); - - if ((range != null) && !range.isEqual(oldRange)) { - this.layer.setMarkerRange(this.id, range); - updated = true; - } - - if ((reversed != null) && (reversed !== this.reversed)) { - this.reversed = reversed; - updated = true; - } - - if ((tailed != null) && (tailed !== this.tailed)) { - this.tailed = tailed; - updated = true; - } - - if ((valid != null) && (valid !== this.valid)) { - this.valid = valid; - updated = true; - } - - if ((exclusive != null) && (exclusive !== this.exclusive)) { - this.exclusive = exclusive; - updated = true; - } - - if (wasExclusive !== this.isExclusive()) { - this.layer.setMarkerIsExclusive(this.id, this.isExclusive()); - updated = true; - } - - if ((properties != null) && !isEqual(properties, this.properties)) { - this.properties = Object.freeze(properties); - propertiesChanged = true; - updated = true; - } - - this.emitChangeEvent(range != null ? range : oldRange, textChanged, propertiesChanged); - if (updated && !suppressMarkerLayerUpdateEvents) { this.layer.markerUpdated(); } - return updated; + if ((properties != null) && !isEqual(properties, this.properties)) { + this.properties = Object.freeze(properties); + propertiesChanged = true; + updated = true; } - getSnapshot(range, includeMarker) { - if (includeMarker == null) { includeMarker = true; } - const snapshot = {range, properties: this.properties, reversed: this.reversed, tailed: this.tailed, valid: this.valid, invalidate: this.invalidate, exclusive: this.exclusive}; - if (includeMarker) { snapshot.marker = this; } - return Object.freeze(snapshot); + this.emitChangeEvent(range ?? oldRange, textChanged, propertiesChanged); + if (updated && !suppressMarkerLayerUpdateEvents) { + this.layer.markerUpdated(); } - - toString() { - return `[Marker ${this.id}, ${this.getRange()}]`; + return updated; + } + + getSnapshot(range, includeMarker) { + if (includeMarker == null) { includeMarker = true; } + const snapshot = { + range, + properties: this.properties, + reversed: this.reversed, + tailed: this.tailed, + valid: this.valid, + invalidate: this.invalidate, + exclusive: this.exclusive + }; + if (includeMarker) { snapshot.marker = this; } + return Object.freeze(snapshot); + } + + toString() { + return `[Marker ${this.id}, ${this.getRange()}]`; + } + + /* + Section: Private + */ + + inspect() { + return this.toString(); + } + + emitChangeEvent(currentRange, textChanged, propertiesChanged) { + let newHeadPosition, newTailPosition, oldHeadPosition, oldTailPosition; + if (!this.hasChangeObservers) { return; } + const oldState = this.previousEventState; + + if (currentRange == null) { currentRange = this.getRange(); } + + if ( + !propertiesChanged && + (oldState.valid === this.valid) && + (oldState.tailed === this.tailed) && + (oldState.reversed === this.reversed) && + (oldState.range.compare(currentRange) === 0) + ) { + return false; } - /* - Section: Private - */ + const newState = (this.previousEventState = this.getSnapshot(currentRange)); - inspect() { - return this.toString(); + if (oldState.reversed) { + oldHeadPosition = oldState.range.start; + oldTailPosition = oldState.range.end; + } else { + oldHeadPosition = oldState.range.end; + oldTailPosition = oldState.range.start; } - emitChangeEvent(currentRange, textChanged, propertiesChanged) { - let newHeadPosition, newTailPosition, oldHeadPosition, oldTailPosition; - if (!this.hasChangeObservers) { return; } - const oldState = this.previousEventState; - - if (currentRange == null) { currentRange = this.getRange(); } - - if (!propertiesChanged && - (oldState.valid === this.valid) && - (oldState.tailed === this.tailed) && - (oldState.reversed === this.reversed) && - (oldState.range.compare(currentRange) === 0)) { return false; } - - const newState = (this.previousEventState = this.getSnapshot(currentRange)); - - if (oldState.reversed) { - oldHeadPosition = oldState.range.start; - oldTailPosition = oldState.range.end; - } else { - oldHeadPosition = oldState.range.end; - oldTailPosition = oldState.range.start; - } - - if (newState.reversed) { - newHeadPosition = newState.range.start; - newTailPosition = newState.range.end; - } else { - newHeadPosition = newState.range.end; - newTailPosition = newState.range.start; - } - - this.emitter.emit("did-change", { - wasValid: oldState.valid, isValid: newState.valid, - hadTail: oldState.tailed, hasTail: newState.tailed, - oldProperties: oldState.properties, newProperties: newState.properties, - oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, - textChanged - }); - return true; + if (newState.reversed) { + newHeadPosition = newState.range.start; + newTailPosition = newState.range.end; + } else { + newHeadPosition = newState.range.end; + newTailPosition = newState.range.start; } - }; - Marker.initClass(); - return Marker; -})()); + + this.emitter.emit("did-change", { + wasValid: oldState.valid, + isValid: newState.valid, + hadTail: oldState.tailed, + hasTail: newState.tailed, + oldProperties: oldState.properties, + newProperties: newState.properties, + oldHeadPosition, + newHeadPosition, + oldTailPosition, + newTailPosition, + textChanged + }); + return true; + } +} + +Delegator.includeInto(Marker); + +Marker.delegatesMethods( + 'containsPoint', + 'containsRange', + 'intersectsRow', + {toMethod: 'getRange'} +); + +module.exports = Marker; diff --git a/src/point-helpers.js b/src/point-helpers.js index 7ad5312d0a..981265a1b1 100644 --- a/src/point-helpers.js +++ b/src/point-helpers.js @@ -7,7 +7,7 @@ */ const Point = require('./point'); -exports.compare = function(a, b) { +exports.compare = function compare(a, b) { if (a.row === b.row) { return compareNumbers(a.column, b.column); } else { @@ -15,7 +15,7 @@ exports.compare = function(a, b) { } }; -var compareNumbers = function(a, b) { +function compareNumbers(a, b) { if (a < b) { return -1; } else if (a > b) { @@ -23,11 +23,11 @@ var compareNumbers = function(a, b) { } else { return 0; } -}; +} exports.isEqual = (a, b) => (a.row === b.row) && (a.column === b.column); -exports.traverse = function(start, distance) { +exports.traverse = function traverse(start, distance) { if (distance.row === 0) { return Point(start.row, start.column + distance.column); } else { @@ -35,7 +35,7 @@ exports.traverse = function(start, distance) { } }; -exports.traversal = function(end, start) { +exports.traversal = function traversal(end, start) { if (end.row === start.row) { return Point(0, end.column - start.column); } else { @@ -45,13 +45,8 @@ exports.traversal = function(end, start) { const NEWLINE_REG_EXP = /\n/g; -exports.characterIndexForPoint = function(text, point) { - let { - row - } = point; - const { - column - } = point; +exports.characterIndexForPoint = function characterIndexForPoint(text, point) { + let { row, column } = point; NEWLINE_REG_EXP.lastIndex = 0; while (row-- > 0) { if (!NEWLINE_REG_EXP.exec(text)) { @@ -62,7 +57,7 @@ exports.characterIndexForPoint = function(text, point) { return NEWLINE_REG_EXP.lastIndex + column; }; -exports.clipNegativePoint = function(point) { +exports.clipNegativePoint = function clipNegativePoint(point) { if (point.row < 0) { return Point(0, 0); } else if (point.column < 0) { @@ -72,7 +67,7 @@ exports.clipNegativePoint = function(point) { } }; -exports.max = function(a, b) { +exports.max = function max(a, b) { if (exports.compare(a, b) >= 0) { return a; } else { @@ -80,7 +75,7 @@ exports.max = function(a, b) { } }; -exports.min = function(a, b) { +exports.min = function min(a, b) { if (exports.compare(a, b) <= 0) { return a; } else { diff --git a/src/set-helpers.js b/src/set-helpers.js index f967167328..4424bf5331 100644 --- a/src/set-helpers.js +++ b/src/set-helpers.js @@ -1,11 +1,4 @@ -// TODO: This file was created by bulk-decaffeinate. -// Sanity-check the conversion and remove this comment. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ -const setEqual = function(a, b) { +function setEqual(a, b) { let next; if (a.size !== b.size) { return false; } const iterator = a.values(); @@ -13,18 +6,34 @@ const setEqual = function(a, b) { if (!b.has(next.value)) { return false; } } return true; -}; +} -const subtractSet = function(set, valuesToRemove) { +function subtractSet(set, valuesToRemove) { if (set.size > valuesToRemove.size) { - return valuesToRemove.forEach(value => set.delete(value)); + for (let value of valuesToRemove) { + set.delete(value); + } } else { - return set.forEach(function(value) { if (valuesToRemove.has(value)) { return set.delete(value); } }); + for (let value of set) { + if (valuesToRemove.has(value)) { + set.delete(value); + } + } } -}; +} -const addSet = (set, valuesToAdd) => valuesToAdd.forEach(value => set.add(value)); +function addSet (set, valuesToAdd) { + for (let value of valuesToAdd) { + set.add(value); + } +} -const intersectSet = (set, other) => set.forEach(function(value) { if (!other.has(value)) { return set.delete(value); } }); +function intersectSet (set, other) { + for (let value of set) { + if (!other.has(value)) { + set.delete(value); + } + } +} module.exports = {setEqual, subtractSet, addSet, intersectSet}; From bb2dac80228a30bda2859fed072c47eada12fb16 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 5 Sep 2025 13:47:15 -0700 Subject: [PATCH 57/64] Remove remaining CoffeeScript artifacts --- .coffeelintignore | 1 - .gitignore | 1 - .npmignore | 2 -- coffeelint.json | 37 ------------------------------------- package.json | 18 ++++++------------ script/generate-docs | 7 +------ spec/helpers/coffee.js | 1 - spec/support/jasmine.json | 3 +-- src/point.js | 4 ++-- src/range.js | 4 ++-- 10 files changed, 12 insertions(+), 66 deletions(-) delete mode 100644 .coffeelintignore delete mode 100644 coffeelint.json diff --git a/.coffeelintignore b/.coffeelintignore deleted file mode 100644 index 1db51fed75..0000000000 --- a/.coffeelintignore +++ /dev/null @@ -1 +0,0 @@ -spec/fixtures diff --git a/.gitignore b/.gitignore index 65275f1eb4..d994319218 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,5 @@ node_modules *.swp lib npm-debug.log -.coffee api.json package-lock.json diff --git a/.npmignore b/.npmignore index 210aac2665..d4c42e7fe9 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,8 @@ spec script src -*.coffee .npmignore .DS_Store npm-debug.log .travis.yml .pairs -.coffee diff --git a/coffeelint.json b/coffeelint.json deleted file mode 100644 index a5dd715e38..0000000000 --- a/coffeelint.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "max_line_length": { - "level": "ignore" - }, - "no_empty_param_list": { - "level": "error" - }, - "arrow_spacing": { - "level": "error" - }, - "no_interpolation_in_single_quotes": { - "level": "error" - }, - "no_debugger": { - "level": "error" - }, - "prefer_english_operator": { - "level": "error" - }, - "colon_assignment_spacing": { - "spacing": { - "left": 0, - "right": 1 - }, - "level": "error" - }, - "braces_spacing": { - "spaces": 0, - "level": "error" - }, - "spacing_after_comma": { - "level": "error" - }, - "no_stand_alone_at": { - "level": "error" - } -} diff --git a/package.json b/package.json index 8fd87701e9..085c888504 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,33 @@ { - "name": "text-buffer", + "name": "@pulsar-edit/text-buffer", "version": "13.18.6", "description": "A container for large mutable strings with annotated regions", "main": "./src/text-buffer", "scripts": { - "prepublish": "npm run clean && npm run compile && npm run lint", + "prepublish": "npm run clean", "docs": "node script/generate-docs", "clean": "rimraf lib api.json", - "compile": "coffee --no-header --output lib --compile src && cpy src/*.js lib/", - "lint": "coffeelint -r src spec", "test": "node script/test", - "ci": "npm run compile && npm run lint && npm run test && npm run bench", + "ci": "npm run test && npm run bench", "bench": "node benchmarks/index" }, "repository": { "type": "git", - "url": "https://github.com/atom/text-buffer.git" + "url": "https://github.com/pulsar/text-buffer.git" }, "bugs": { - "url": "https://github.com/atom/text-buffer/issues" + "url": "https://github.com/pulsar/text-buffer/issues" }, "atomTestRunner": "atom-jasmine2-test-runner", "license": "MIT", "devDependencies": { "atom-jasmine2-test-runner": "^1.0.0", - "coffee-cache": "^0.2.0", - "coffee-script": "^1.10.0", - "coffeelint": "1.16.0", "cpy-cli": "^1.0.1", "dedent": "^0.6.0", - "donna": "^1.0.16", "electron": "^30", "jasmine": "^2.4.1", "jasmine-core": "^2.4.1", - "joanna": "0.0.11", + "joanna": "github:pulsar-edit/joanna", "json-diff": "^0.3.1", "random-seed": "^0.2.0", "regression": "^1.2.1", diff --git a/script/generate-docs b/script/generate-docs index 67a4d295f6..ad27101132 100755 --- a/script/generate-docs +++ b/script/generate-docs @@ -1,11 +1,8 @@ #!/usr/bin/env node -require('coffee-script').register() - const fs = require('fs') const path = require('path') const tello = require('tello') -const donna = require('donna') const joanna = require('joanna') const rootDir = path.join(__dirname, '..') @@ -19,7 +16,5 @@ for (const entry of fs.readdirSync('src')) { } const jsMetadata = joanna(jsFiles) -const coffeeMetadata = donna.generateMetadata([rootDir]) -Object.assign(coffeeMetadata[0].files, jsMetadata.files) -const docs = tello.digest(coffeeMetadata) +const docs = tello.digest(jsMetadata) fs.writeFileSync(path.join(rootDir, 'api.json'), JSON.stringify(docs, null, 2)) diff --git a/spec/helpers/coffee.js b/spec/helpers/coffee.js index f0dbe00e86..e69de29bb2 100644 --- a/spec/helpers/coffee.js +++ b/spec/helpers/coffee.js @@ -1 +0,0 @@ -require('coffee-cache') diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index d86a2b73fc..ac121f793e 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -1,8 +1,7 @@ { "spec_dir": "spec", "spec_files": [ - "**/*-spec.js", - "**/*-spec.coffee" + "**/*-spec.js" ], "helpers": [ "helpers/**/*.js" diff --git a/src/point.js b/src/point.js index fa86c3b0db..70b7039d69 100644 --- a/src/point.js +++ b/src/point.js @@ -4,9 +4,9 @@ // {Array}. This means a 2-element array containing {Number}s representing the // row and column. So the following are equivalent: // -// ```coffee +// ```js // new Point(1, 2) -// [1, 2] # Point compatible Array +// [1, 2] // Point-compatible Array // ``` class Point { diff --git a/src/range.js b/src/range.js index 5cf795f5ba..e70e5d1afe 100644 --- a/src/range.js +++ b/src/range.js @@ -19,10 +19,10 @@ function __range__(left, right, inclusive) { // // ## Examples // -// ```coffee +// ```js // new Range(new Point(0, 1), new Point(2, 3)) // new Range([0, 1], [2, 3]) -// [[0, 1], [2, 3]] # Range compatible array +// [[0, 1], [2, 3]] // Range-compatible array // ``` class Range { /* From 57e73ee9b1886d692a85263621c49aad6295f8d4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 5 Sep 2025 14:21:27 -0700 Subject: [PATCH 58/64] Fix documentation generation --- script/generate-docs | 2 +- src/marker-layer.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/script/generate-docs b/script/generate-docs index ad27101132..649d481b2c 100755 --- a/script/generate-docs +++ b/script/generate-docs @@ -16,5 +16,5 @@ for (const entry of fs.readdirSync('src')) { } const jsMetadata = joanna(jsFiles) -const docs = tello.digest(jsMetadata) +const docs = tello.digest([jsMetadata]) fs.writeFileSync(path.join(rootDir, 'api.json'), JSON.stringify(docs, null, 2)) diff --git a/src/marker-layer.js b/src/marker-layer.js index b624146523..0d8793524e 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -229,7 +229,7 @@ class MarkerLayer { Section: Marker creation */ // Public: Create a marker with the given range. - + // // * `range` A {Range} or range-compatible {Array} // * `options` A hash of key-value pairs to associate with the marker. There // are also reserved property names that have marker-specific meaning. @@ -262,7 +262,7 @@ class MarkerLayer { } // Public: Create a marker at with its head at the given position with no tail. - + // // * `position` {Point} or point-compatible {Array} // * `options` (optional) An {Object} with the following keys: // * `invalidate` (optional) {String} Determines the rules by which changes @@ -301,10 +301,10 @@ class MarkerLayer { // created, updated, or destroyed on this layer. *Prefer this method for // optimal performance when interacting with layers that could contain large // numbers of markers.* - + // // * `callback` A {Function} that will be called with no arguments when changes // occur on this layer. - + // // Subscribers are notified once, asynchronously when any number of changes // occur in a given tick of the event loop. You should re-query the layer // to determine the state of markers in which you're interested in. It may @@ -319,10 +319,10 @@ class MarkerLayer { // Public: Subscribe to be notified synchronously whenever markers are created // on this layer. *Avoid this method for optimal performance when interacting // with layers that could contain large numbers of markers.* - + // // * `callback` A {Function} that will be called with a {Marker} whenever a // new marker is created. - + // // You should prefer {::onDidUpdate} when synchronous notifications aren't // absolutely necessary. From 6c912d5907b9ad483119c345b4a6af17bf4af4c0 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 5 Sep 2025 14:23:33 -0700 Subject: [PATCH 59/64] Clean up wrapper code for callable constructors (for `Point`/`Range') --- src/point.js | 52 +++++++++++++++++++++++++--------------------------- src/range.js | 14 +++++--------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/point.js b/src/point.js index 70b7039d69..7b3abd5e3d 100644 --- a/src/point.js +++ b/src/point.js @@ -1,3 +1,7 @@ +function isActualNumber (value) { + return (typeof value === 'number') && (!Number.isNaN(value)); +} + // Public: Represents a point in a buffer in row/column coordinates. // // Every public method that takes a point also accepts a *point-compatible* @@ -8,24 +12,26 @@ // new Point(1, 2) // [1, 2] // Point-compatible Array // ``` - class Point { /* Section: Construction */ + static ZERO = Object.freeze(new Point(0, 0)); + static INFINITY = Object.freeze(new Point(Infinity, Infinity)); + // Public: Convert any point-compatible object to a {Point}. // - // * `object` This can be an object that's already a {Point}, in which case it's - // simply returned, or an array containing two {Number}s representing the - // row and column. - // * `copy` An optional boolean indicating whether to force the copying of objects - // that are already points. + // * `object` This can be an object that's already a {Point}, in which case + // it's simply returned; or an array containing two {Number}s representing + // the row and column. + // * `copy` An optional boolean indicating whether to force the copying of + // objects that are already points. // // Returns: A {Point} based on the given object. static fromObject(object, copy) { if (object instanceof Point) { - if (copy) { return object.copy(); } else { return object; } + return copy ? object.copy() : object; } else { let column, row; if (Array.isArray(object)) { @@ -42,7 +48,7 @@ class Point { Section: Comparison */ - // Public: Returns the given {Point} that is earlier in the buffer. + // Public: Returns the given {Point} that occurs earlier in the buffer. // // * `point1` {Point} // * `point2` {Point} @@ -56,6 +62,10 @@ class Point { } } + // Public: Returns the given {Point} that occurs later in the buffer. + // + // * `point1` {Point} + // * `point2` {Point} static max(point1, point2) { point1 = Point.fromObject(point1); point2 = Point.fromObject(point2); @@ -66,20 +76,19 @@ class Point { } } + // Public: Ensure the given {Point} is valid by throwing a `TypeError` if + // either its `row` or its `column` is not an integer. static assertValid(point) { if (!isActualNumber(point.row) || !isActualNumber(point.column)) { throw new TypeError(`Invalid Point: ${point}`); } } - static ZERO = Object.freeze(new Point(0, 0)); - static INFINITY = Object.freeze(new Point(Infinity, Infinity)); - /* Section: Construction */ - // Public: Construct a {Point} object + // Public: Construct a {Point} object. // // * `row` {Number} row // * `column` {Number} column @@ -102,9 +111,9 @@ class Point { Section: Operations */ - // Public: Makes this point immutable and returns itself. + // Public: Make this point immutable and return itself. // - // Returns an immutable version of this {Point} + // Returns an immutable version of this {Point}. freeze() { return Object.freeze(this); } @@ -302,22 +311,11 @@ function _Point (...args) { } _Point.displayName = 'Point'; _Point.prototype = Point.prototype; -_Point.prototype.constructor = _Point; -Object.assign(_Point, { - fromObject: Point.fromObject, - min: Point.min, - max: Point.max, - assertValid: Point.assertValid, - ZERO: Point.ZERO, - INFINITY: Point.INFINITY -}); Object.assign(_Point.prototype, { row: null, column: null }); - -function isActualNumber (value) { - return (typeof value === 'number') && (!Number.isNaN(value)); -} +// Make the wrapper inherit the parent's static methods. +Object.setPrototypeOf(_Point, Point); module.exports = _Point; diff --git a/src/range.js b/src/range.js index e70e5d1afe..d040110a67 100644 --- a/src/range.js +++ b/src/range.js @@ -371,15 +371,11 @@ function _Range (...args) { }; _Range.displayName = 'Range'; _Range.prototype = Range.prototype; -_Range.prototype.constructor = _Range; -Object.assign(_Range, { - fromObject: Range.fromObject, - fromText: Range.fromText, - fromPointWithDelta: Range.fromPointWithDelta, - fromPointWithTraversalExtent: Range.fromPointWithTraversalExtent, - deserialize: Range.deserialize +Object.assign(_Range.prototype, { + start: null, + end: null }); -_Range.prototype.start = null; -_Range.prototype.end = null; +// Make the wrapper inherit the parent's static methods. +Object.setPrototypeOf(_Range, Range); module.exports = _Range; From 8d95fdd9ea588508f3b9ebdca0f6ef448b739e6d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 9 Oct 2025 15:54:04 -0700 Subject: [PATCH 60/64] Debounce extra file-change event causing test failure on Linux --- spec/text-buffer-io-spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/text-buffer-io-spec.js b/spec/text-buffer-io-spec.js index abb88f5622..d022c3fccd 100644 --- a/spec/text-buffer-io-spec.js +++ b/spec/text-buffer-io-spec.js @@ -973,6 +973,9 @@ describe('TextBuffer IO', () => { fs.writeFileSync(buffer.getPath(), 'abcde') const subscription = buffer.file.onDidChange(() => { + // `text-buffer` consumes this through a `debounce` helper. We don't, + // so we should manually ensure this handler runs only once. + if (subscription.disposed) return subscription.dispose() setTimeout(() => { expect(buffer.getText()).toBe('abcde') From 616d018925868ef2ec08bb5f6cd15b5a01133033 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 9 Oct 2025 16:14:24 -0700 Subject: [PATCH 61/64] Try installing `setuptools` differently --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8e1e7126a..7c04f2e047 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,11 +24,21 @@ jobs: with: node-version: '20.11.1' architecture: ${{ matrix.node_arch }} - - name: Install Python setuptools + - name: Install Python setuptools (Unix-likes) # This is needed for Python 3.12+, since many versions of node-gyp # are incompatible with Python 3.12+, which no-longer ships 'distutils' # out of the box. 'setuptools' package provides 'distutils'. - run: python3 -m pip install setuptools + if: ${{ runner.os != 'Windows' }} + run: python3 -m pip install --break-system-packages setuptools + - name: Install Python setuptools (Windows) + # This is needed for Python 3.12+, since many versions of node-gyp + # are incompatible with Python 3.12+, which no-longer ships 'distutils' + # out of the box. 'setuptools' package provides 'distutils'. + if: ${{ runner.os == 'Windows' }} + run: | + python3 -m venv CI_venv + CI_venv\Scripts\activate.bat + python3 -m pip install setuptools - name: Install windows-build-tools if: ${{ matrix.os == 'windows-latest' }} run: npm config set msvs_version 2019 From e3a8f008c21ee9f1c145b9487093f66196f5a06b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 9 Oct 2025 21:17:07 -0700 Subject: [PATCH 62/64] Point to NPM-hosted `superstring` and `pathwatcher` --- package.json | 6 +++--- spec/helpers/test-language-mode.js | 2 +- spec/text-buffer-io-spec.js | 4 ++-- spec/text-buffer-spec.js | 2 +- src/default-history-provider.js | 2 +- src/display-layer.js | 2 +- src/helpers.js | 2 +- src/marker-layer.js | 4 ++-- src/text-buffer.js | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 085c888504..74b267fa4d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "yargs": "^6.5.0" }, "dependencies": { + "@pulsar-edit/pathwatcher": "^9.0.2", + "@pulsar-edit/superstring": "^3.0.4", "delegato": "^1.0.0", "diff": "^2.2.1", "emissary": "^1.0.0", @@ -47,10 +49,8 @@ "grim": "^2.0.2", "mkdirp": "^0.5.1", "serializable": "^1.0.3", - "superstring": "github:savetheclocktower/superstring#e5e848017f7a28fe250b45f8c7f8ed244371d33d", "underscore-plus": "^1.0.0", - "winattr": "^3.0.0", - "pathwatcher": "github:savetheclocktower/node-pathwatcher#649232b264940e27ff578f848d1ec9aa3ebb09b9" + "winattr": "^3.0.0" }, "standard": { "env": { diff --git a/spec/helpers/test-language-mode.js b/spec/helpers/test-language-mode.js index c229524054..2eb3e01e49 100644 --- a/spec/helpers/test-language-mode.js +++ b/spec/helpers/test-language-mode.js @@ -1,4 +1,4 @@ -const {MarkerIndex} = require('superstring') +const {MarkerIndex} = require('@pulsar-edit/superstring') const {Emitter} = require('event-kit') const Point = require('../../src/point') const Range = require('../../src/range') diff --git a/spec/text-buffer-io-spec.js b/spec/text-buffer-io-spec.js index d022c3fccd..3f2cf8a9ab 100644 --- a/spec/text-buffer-io-spec.js +++ b/spec/text-buffer-io-spec.js @@ -6,9 +6,9 @@ const {Disposable} = require('event-kit') const Point = require('../src/point') const Range = require('../src/range') const TextBuffer = require('../src/text-buffer') -const {TextBuffer: NativeTextBuffer} = require('superstring') +const {TextBuffer: NativeTextBuffer} = require('@pulsar-edit/superstring') const fsAdmin = require('fs-admin') -const pathwatcher = require('pathwatcher') +const pathwatcher = require('@pulsar-edit/pathwatcher') const winattr = require('winattr') process.on('unhandledRejection', console.error) diff --git a/spec/text-buffer-spec.js b/spec/text-buffer-spec.js index 4da1f6d0c5..482b2bb479 100644 --- a/spec/text-buffer-spec.js +++ b/spec/text-buffer-spec.js @@ -8,7 +8,7 @@ const fs = require('fs-plus'); const path = require('path'); const {join} = path; const temp = require('temp'); -const {File} = require('pathwatcher'); +const {File} = require('@pulsar-edit/pathwatcher'); const Random = require('random-seed'); const Point = require('../src/point'); const Range = require('../src/range'); diff --git a/src/default-history-provider.js b/src/default-history-provider.js index 8b14655e67..59af006be0 100644 --- a/src/default-history-provider.js +++ b/src/default-history-provider.js @@ -1,4 +1,4 @@ -const {Patch} = require('superstring') +const {Patch} = require('@pulsar-edit/superstring') const MarkerLayer = require('./marker-layer'); const {traversal} = require('./point-helpers'); const {patchFromChanges} = require('./helpers'); diff --git a/src/display-layer.js b/src/display-layer.js index 3e1f1528b1..705002185e 100644 --- a/src/display-layer.js +++ b/src/display-layer.js @@ -1,4 +1,4 @@ -const {Patch} = require('superstring') +const {Patch} = require('@pulsar-edit/superstring') const {Emitter} = require('event-kit') const Point = require('./point') const Range = require('./range') diff --git a/src/helpers.js b/src/helpers.js index 0cd3bc511b..8066aa1f10 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,4 +1,4 @@ -const {Patch} = require('superstring') +const {Patch} = require('@pulsar-edit/superstring') const Range = require('./range') const {traversal} = require('./point-helpers') diff --git a/src/marker-layer.js b/src/marker-layer.js index 0d8793524e..9ff06fba5f 100644 --- a/src/marker-layer.js +++ b/src/marker-layer.js @@ -3,7 +3,7 @@ const {Emitter} = require('event-kit'); const Point = require("./point"); const Range = require("./range"); const Marker = require("./marker"); -const {MarkerIndex} = require("superstring") +const {MarkerIndex} = require("@pulsar-edit/superstring") const {intersectSet} = require("./set-helpers"); const SerializationVersion = 2; @@ -322,7 +322,7 @@ class MarkerLayer { // // * `callback` A {Function} that will be called with a {Marker} whenever a // new marker is created. - // + // // You should prefer {::onDidUpdate} when synchronous notifications aren't // absolutely necessary. diff --git a/src/text-buffer.js b/src/text-buffer.js index 0af3b4b43d..19a1bcbb21 100644 --- a/src/text-buffer.js +++ b/src/text-buffer.js @@ -1,11 +1,11 @@ const {Emitter, CompositeDisposable} = require('event-kit') -const {File} = require('pathwatcher') +const {File} = require('@pulsar-edit/pathwatcher') const diff = require('diff') const _ = require('underscore-plus') const path = require('path') const crypto = require('crypto') const mkdirp = require('mkdirp') -const {TextBuffer: NativeTextBuffer} = require('superstring'); +const {TextBuffer: NativeTextBuffer} = require('@pulsar-edit/superstring'); const Point = require('./point') const Range = require('./range') const DefaultHistoryProvider = require('./default-history-provider') From ad87d1c559b4d613cedcc0e5d909ded429556eb4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 10 Oct 2025 19:23:16 -0700 Subject: [PATCH 63/64] Remove old `.coffee` specs --- spec/marker-layer-spec.coffee | 363 ---- spec/marker-spec.coffee | 765 --------- spec/point-spec.coffee | 196 --- spec/range-spec.coffee | 42 - spec/text-buffer-spec.coffee | 3010 --------------------------------- 5 files changed, 4376 deletions(-) delete mode 100644 spec/marker-layer-spec.coffee delete mode 100644 spec/marker-spec.coffee delete mode 100644 spec/point-spec.coffee delete mode 100644 spec/range-spec.coffee delete mode 100644 spec/text-buffer-spec.coffee diff --git a/spec/marker-layer-spec.coffee b/spec/marker-layer-spec.coffee deleted file mode 100644 index 77f428ec9a..0000000000 --- a/spec/marker-layer-spec.coffee +++ /dev/null @@ -1,363 +0,0 @@ -{uniq, times} = require 'underscore-plus' -TextBuffer = require '../src/text-buffer' - -describe "MarkerLayer", -> - [buffer, layer1, layer2] = [] - - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - buffer = new TextBuffer(text: "abcdefghijklmnopqrstuvwxyz") - layer1 = buffer.addMarkerLayer() - layer2 = buffer.addMarkerLayer() - - it "ensures that marker ids are unique across layers", -> - times 5, -> - buffer.markRange([[0, 3], [0, 6]]) - layer1.markRange([[0, 4], [0, 7]]) - layer2.markRange([[0, 5], [0, 8]]) - - ids = buffer.getMarkers() - .concat(layer1.getMarkers()) - .concat(layer2.getMarkers()) - .map (marker) -> marker.id - - expect(uniq(ids).length).toEqual ids.length - - it "updates each layer's markers when the text changes", -> - defaultMarker = buffer.markRange([[0, 3], [0, 6]]) - layer1Marker = layer1.markRange([[0, 4], [0, 7]]) - layer2Marker = layer2.markRange([[0, 5], [0, 8]]) - - buffer.setTextInRange([[0, 1], [0, 2]], "BBB") - expect(defaultMarker.getRange()).toEqual [[0, 5], [0, 8]] - expect(layer1Marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(layer2Marker.getRange()).toEqual [[0, 7], [0, 10]] - - layer2.destroy() - expect(layer2.isAlive()).toBe false - expect(layer2.isDestroyed()).toBe true - - expect(layer1.isAlive()).toBe true - expect(layer1.isDestroyed()).toBe false - - buffer.undo() - expect(defaultMarker.getRange()).toEqual [[0, 3], [0, 6]] - expect(layer1Marker.getRange()).toEqual [[0, 4], [0, 7]] - - expect(layer2Marker.isDestroyed()).toBe true - expect(layer2Marker.getRange()).toEqual [[0, 0], [0, 0]] - - it "emits onDidCreateMarker events synchronously when markers are created", -> - createdMarkers = [] - layer1.onDidCreateMarker (marker) -> createdMarkers.push(marker) - marker = layer1.markRange([[0, 1], [2, 3]]) - expect(createdMarkers).toEqual [marker] - - it "does not emit marker events on the TextBuffer for non-default layers", -> - createEventCount = updateEventCount = 0 - buffer.onDidCreateMarker -> createEventCount++ - buffer.onDidUpdateMarkers -> updateEventCount++ - - marker1 = buffer.markRange([[0, 1], [0, 2]]) - marker1.setRange([[0, 1], [0, 3]]) - - expect(createEventCount).toBe 1 - expect(updateEventCount).toBe 2 - - marker2 = layer1.markRange([[0, 1], [0, 2]]) - marker2.setRange([[0, 1], [0, 3]]) - - expect(createEventCount).toBe 1 - expect(updateEventCount).toBe 2 - - describe "when destroyInvalidatedMarkers is enabled for the layer", -> - it "destroys markers when they are invalidated via a splice", -> - layer3 = buffer.addMarkerLayer(destroyInvalidatedMarkers: true) - - marker1 = layer3.markRange([[0, 0], [0, 3]], invalidate: 'inside') - marker2 = layer3.markRange([[0, 2], [0, 6]], invalidate: 'inside') - - destroyedMarkers = [] - marker1.onDidDestroy -> destroyedMarkers.push(marker1) - marker2.onDidDestroy -> destroyedMarkers.push(marker2) - - buffer.insert([0, 5], 'x') - - expect(destroyedMarkers).toEqual [marker2] - expect(marker2.isDestroyed()).toBe true - expect(marker1.isDestroyed()).toBe false - - describe "when maintainHistory is enabled for the layer", -> - layer3 = null - - beforeEach -> - layer3 = buffer.addMarkerLayer(maintainHistory: true) - - it "restores the state of all markers in the layer on undo and redo", -> - buffer.setText('') - buffer.transact -> buffer.append('foo') - layer3 = buffer.addMarkerLayer(maintainHistory: true) - - marker1 = layer3.markRange([[0, 0], [0, 0]], invalidate: 'never') - marker2 = layer3.markRange([[0, 0], [0, 0]], invalidate: 'never') - - marker2ChangeCount = 0 - marker2.onDidChange -> marker2ChangeCount++ - - - buffer.transact -> - buffer.append('\n') - buffer.append('bar') - - marker1.destroy() - marker2.setRange([[0, 2], [0, 3]]) - marker3 = layer3.markRange([[0, 0], [0, 3]], invalidate: 'never') - marker4 = layer3.markRange([[1, 0], [1, 3]], invalidate: 'never') - expect(marker2ChangeCount).toBe(1) - - createdMarker = null - layer3.onDidCreateMarker((m) -> createdMarker = m) - buffer.undo() - - expect(buffer.getText()).toBe 'foo' - expect(marker1.isDestroyed()).toBe false - expect(createdMarker).toBe(marker1) - markers = layer3.findMarkers({}) - expect(markers.length).toBe 2 - expect(markers[0]).toBe marker1 - expect(markers[0].getRange()).toEqual [[0, 0], [0, 0]] - expect(markers[1].getRange()).toEqual [[0, 0], [0, 0]] - expect(marker2ChangeCount).toBe(2) - - buffer.redo() - - expect(buffer.getText()).toBe 'foo\nbar' - markers = layer3.findMarkers({}) - expect(markers.length).toBe 3 - expect(markers[0].getRange()).toEqual [[0, 0], [0, 3]] - expect(markers[1].getRange()).toEqual [[0, 2], [0, 3]] - expect(markers[2].getRange()).toEqual [[1, 0], [1, 3]] - - it "does not undo marker manipulations that aren't associated with text changes", -> - marker = layer3.markRange([[0, 6], [0, 9]]) - - # Can't undo changes in a transaction without other buffer changes - buffer.transact -> marker.setRange([[0, 4], [0, 20]]) - buffer.undo() - expect(marker.getRange()).toEqual [[0, 4], [0, 20]] - - # Can undo changes in a transaction with other buffer changes - buffer.transact -> - marker.setRange([[0, 5], [0, 9]]) - buffer.setTextInRange([[0, 2], [0, 3]], 'XYZ') - marker.setRange([[0, 8], [0, 12]]) - - buffer.undo() - expect(marker.getRange()).toEqual [[0, 4], [0, 20]] - - buffer.redo() - expect(marker.getRange()).toEqual [[0, 8], [0, 12]] - - it "ignores snapshot references to marker layers that no longer exist", -> - layer3.markRange([[0, 6], [0, 9]]) - buffer.append("stuff") - layer3.destroy() - - # Should not throw an exception - buffer.undo() - - describe "when a role is provided for the layer", -> - it "getRole() returns its role and keeps track of ids of 'selections' role", -> - expect(buffer.selectionsMarkerLayerIds.size).toBe 0 - - selectionsMarkerLayer1 = buffer.addMarkerLayer(role: "selections") - expect(selectionsMarkerLayer1.getRole()).toBe "selections" - - expect(buffer.addMarkerLayer(role: "role-1").getRole()).toBe "role-1" - expect(buffer.addMarkerLayer().getRole()).toBe undefined - - expect(buffer.selectionsMarkerLayerIds.size).toBe 1 - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe true - - selectionsMarkerLayer2 = buffer.addMarkerLayer(role: "selections") - expect(selectionsMarkerLayer2.getRole()).toBe "selections" - - expect(buffer.selectionsMarkerLayerIds.size).toBe 2 - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe true - - selectionsMarkerLayer1.destroy() - selectionsMarkerLayer2.destroy() - expect(buffer.selectionsMarkerLayerIds.size).toBe 2 - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe true - expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe true - - describe "::findMarkers(params)", -> - it "does not find markers from other layers", -> - defaultMarker = buffer.markRange([[0, 3], [0, 6]]) - layer1Marker = layer1.markRange([[0, 3], [0, 6]]) - layer2Marker = layer2.markRange([[0, 3], [0, 6]]) - - expect(buffer.findMarkers(containsPoint: [0, 4])).toEqual [defaultMarker] - expect(layer1.findMarkers(containsPoint: [0, 4])).toEqual [layer1Marker] - expect(layer2.findMarkers(containsPoint: [0, 4])).toEqual [layer2Marker] - - describe "::onDidUpdate", -> - it "notifies observers at the end of the outermost transaction when markers are created, updated, or destroyed", -> - [marker1, marker2] = [] - - displayLayer = buffer.addDisplayLayer() - displayLayerDidChange = false - - changeCount = 0 - buffer.onDidChange -> - changeCount++ - - updateCount = 0 - layer1.onDidUpdate -> - updateCount++ - if updateCount is 1 - expect(changeCount).toBe(0) - buffer.transact -> - marker1.setRange([[1, 2], [3, 4]]) - marker2.setRange([[4, 5], [6, 7]]) - else if updateCount is 2 - expect(changeCount).toBe(0) - buffer.transact -> - buffer.insert([0, 1], "xxx") - buffer.insert([0, 1], "yyy") - else if updateCount is 3 - expect(changeCount).toBe(1) - marker1.destroy() - marker2.destroy() - else if updateCount is 7 - expect(changeCount).toBe(2) - expect(displayLayerDidChange).toBe(true, 'Display layer was updated after marker layer.') - - buffer.transact -> - buffer.transact -> - marker1 = layer1.markRange([[0, 2], [0, 4]]) - marker2 = layer1.markRange([[0, 6], [0, 8]]) - - expect(updateCount).toBe(5) - - # update events happen immediately when there is no parent transaction - layer1.markRange([[0, 2], [0, 4]]) - expect(updateCount).toBe(6) - - # update events happen after updating display layers when there is no parent transaction. - displayLayer.onDidChange -> - displayLayerDidChange = true - buffer.undo() - expect(updateCount).toBe(7) - - describe "::clear()", -> - it "destroys all of the layer's markers", (done) -> - buffer = new TextBuffer(text: 'abc') - displayLayer = buffer.addDisplayLayer() - markerLayer = buffer.addMarkerLayer() - displayMarkerLayer = displayLayer.getMarkerLayer(markerLayer.id) - marker1 = markerLayer.markRange([[0, 1], [0, 2]]) - marker2 = markerLayer.markRange([[0, 1], [0, 2]]) - marker3 = markerLayer.markRange([[0, 1], [0, 2]]) - displayMarker1 = displayMarkerLayer.getMarker(marker1.id) - # intentionally omit a display marker for marker2 just to cover that case - displayMarker3 = displayMarkerLayer.getMarker(marker3.id) - - marker1DestroyCount = 0 - marker2DestroyCount = 0 - displayMarker1DestroyCount = 0 - displayMarker3DestroyCount = 0 - markerLayerUpdateCount = 0 - displayMarkerLayerUpdateCount = 0 - marker1.onDidDestroy -> marker1DestroyCount++ - marker2.onDidDestroy -> marker2DestroyCount++ - displayMarker1.onDidDestroy -> displayMarker1DestroyCount++ - displayMarker3.onDidDestroy -> displayMarker3DestroyCount++ - markerLayer.onDidUpdate -> - markerLayerUpdateCount++ - done() if markerLayerUpdateCount is 1 and displayMarkerLayerUpdateCount is 1 - displayMarkerLayer.onDidUpdate -> - displayMarkerLayerUpdateCount++ - done() if markerLayerUpdateCount is 1 and displayMarkerLayerUpdateCount is 1 - - markerLayer.clear() - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(true) - expect(marker3.isDestroyed()).toBe(true) - expect(displayMarker1.isDestroyed()).toBe(true) - expect(displayMarker3.isDestroyed()).toBe(true) - expect(marker1DestroyCount).toBe(1) - expect(marker2DestroyCount).toBe(1) - expect(displayMarker1DestroyCount).toBe(1) - expect(displayMarker3DestroyCount).toBe(1) - expect(markerLayer.getMarkers()).toEqual([]) - expect(displayMarkerLayer.getMarkers()).toEqual([]) - expect(displayMarkerLayer.getMarker(displayMarker3.id)).toBeUndefined() - - describe "::copy", -> - it "creates a new marker layer with markers in the same states", -> - originalLayer = buffer.addMarkerLayer(maintainHistory: true) - originalLayer.markRange([[0, 1], [0, 3]]) - originalLayer.markPosition([0, 2]) - - copy = originalLayer.copy() - expect(copy).not.toBe originalLayer - - markers = copy.getMarkers() - expect(markers.length).toBe 2 - expect(markers[0].getRange()).toEqual [[0, 1], [0, 3]] - expect(markers[1].getRange()).toEqual [[0, 2], [0, 2]] - expect(markers[1].hasTail()).toBe false - - it "copies the marker layer role", -> - originalLayer = buffer.addMarkerLayer(maintainHistory: true, role: "selections") - copy = originalLayer.copy() - expect(copy).not.toBe originalLayer - expect(copy.getRole()).toBe("selections") - expect(buffer.selectionsMarkerLayerIds.has(originalLayer.id)).toBe true - expect(buffer.selectionsMarkerLayerIds.has(copy.id)).toBe true - expect(buffer.selectionsMarkerLayerIds.size).toBe 2 - - describe "::destroy", -> - it "destroys the layer's markers", -> - buffer = new TextBuffer() - markerLayer = buffer.addMarkerLayer() - - marker1 = markerLayer.markRange([[0, 0], [0, 0]]) - marker2 = markerLayer.markRange([[0, 0], [0, 0]]) - - destroyListener = jasmine.createSpy('onDidDestroy listener') - marker1.onDidDestroy(destroyListener) - - markerLayer.destroy() - - expect(destroyListener).toHaveBeenCalled() - expect(marker1.isDestroyed()).toBe(true) - - # Markers states are updated regardless of whether they have an - # ::onDidDestroy listener - expect(marker2.isDestroyed()).toBe(true) - - describe "trackDestructionInOnDidCreateMarkerCallbacks", -> - it "stores a stack trace when destroy is called during onDidCreateMarker callbacks", -> - layer1.onDidCreateMarker (m) -> m.destroy() if destroyInCreateCallback - - layer1.trackDestructionInOnDidCreateMarkerCallbacks = true - destroyInCreateCallback = true - marker1 = layer1.markPosition([0, 0]) - expect(marker1.isDestroyed()).toBe(true) - expect(marker1.destroyStackTrace).toBeDefined() - - destroyInCreateCallback = false - marker2 = layer1.markPosition([0, 0]) - expect(marker2.isDestroyed()).toBe(false) - expect(marker2.destroyStackTrace).toBeUndefined() - marker2.destroy() - expect(marker2.isDestroyed()).toBe(true) - expect(marker2.destroyStackTrace).toBeUndefined() - - destroyInCreateCallback = true - layer1.trackDestructionInOnDidCreateMarkerCallbacks = false - marker3 = layer1.markPosition([0, 0]) - expect(marker3.isDestroyed()).toBe(true) - expect(marker3.destroyStackTrace).toBeUndefined() diff --git a/spec/marker-spec.coffee b/spec/marker-spec.coffee deleted file mode 100644 index a3ea821bca..0000000000 --- a/spec/marker-spec.coffee +++ /dev/null @@ -1,765 +0,0 @@ -{difference, times, uniq} = require 'underscore-plus' -TextBuffer = require '../src/text-buffer' - -describe "Marker", -> - [buffer, markerCreations, markersUpdatedCount] = [] - - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - buffer = new TextBuffer(text: "abcdefghijklmnopqrstuvwxyz") - markerCreations = [] - buffer.onDidCreateMarker (marker) -> markerCreations.push(marker) - markersUpdatedCount = 0 - buffer.onDidUpdateMarkers -> markersUpdatedCount++ - - describe "creation", -> - describe "TextBuffer::markRange(range, properties)", -> - it "creates a marker for the given range with the given properties", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - expect(marker.getRange()).toEqual [[0, 3], [0, 6]] - expect(marker.getHeadPosition()).toEqual [0, 6] - expect(marker.getTailPosition()).toEqual [0, 3] - expect(marker.isReversed()).toBe false - expect(marker.hasTail()).toBe true - expect(markerCreations).toEqual [marker] - expect(markersUpdatedCount).toBe 1 - - it "allows a reversed marker to be created", -> - marker = buffer.markRange([[0, 3], [0, 6]], reversed: true) - expect(marker.getRange()).toEqual [[0, 3], [0, 6]] - expect(marker.getHeadPosition()).toEqual [0, 3] - expect(marker.getTailPosition()).toEqual [0, 6] - expect(marker.isReversed()).toBe true - expect(marker.hasTail()).toBe true - - it "allows an invalidation strategy to be assigned", -> - marker = buffer.markRange([[0, 3], [0, 6]], invalidate: 'inside') - expect(marker.getInvalidationStrategy()).toBe 'inside' - - it "allows an exclusive marker to be created independently of its invalidation strategy", -> - layer = buffer.addMarkerLayer({maintainHistory: true}) - marker1 = layer.markRange([[0, 3], [0, 6]], invalidate: 'overlap', exclusive: true) - marker2 = marker1.copy() - marker3 = marker1.copy(exclusive: false) - marker4 = marker1.copy(exclusive: null, invalidate: 'inside') - - buffer.insert([0, 3], 'something') - - expect(marker1.getStartPosition()).toEqual [0, 12] - expect(marker1.isExclusive()).toBe true - expect(marker2.getStartPosition()).toEqual [0, 12] - expect(marker2.isExclusive()).toBe true - expect(marker3.getStartPosition()).toEqual [0, 3] - expect(marker3.isExclusive()).toBe false - expect(marker4.getStartPosition()).toEqual [0, 12] - expect(marker4.isExclusive()).toBe true - - it "allows custom state to be assigned", -> - marker = buffer.markRange([[0, 3], [0, 6]], foo: 1, bar: 2) - expect(marker.getProperties()).toEqual {foo: 1, bar: 2} - - it "clips the range before creating a marker with it", -> - marker = buffer.markRange([[-100, -100], [100, 100]]) - expect(marker.getRange()).toEqual [[0, 0], [0, 26]] - - it "throws an error if an invalid point is given", -> - marker1 = buffer.markRange([[0, 1], [0, 2]]) - - expect -> buffer.markRange([[0, NaN], [0, 2]]) - .toThrowError "Invalid Point: (0, NaN)" - expect -> buffer.markRange([[0, 1], [0, NaN]]) - .toThrowError "Invalid Point: (0, NaN)" - - expect(buffer.findMarkers({})).toEqual [marker1] - expect(buffer.getMarkers()).toEqual [marker1] - - it "allows arbitrary properties to be assigned", -> - marker = buffer.markRange([[0, 6], [0, 8]], foo: 'bar') - expect(marker.getProperties()).toEqual({foo: 'bar'}) - - describe "TextBuffer::markPosition(position, properties)", -> - it "creates a tail-less marker at the given position", -> - marker = buffer.markPosition([0, 6]) - expect(marker.getRange()).toEqual [[0, 6], [0, 6]] - expect(marker.getHeadPosition()).toEqual [0, 6] - expect(marker.getTailPosition()).toEqual [0, 6] - expect(marker.isReversed()).toBe false - expect(marker.hasTail()).toBe false - expect(markerCreations).toEqual [marker] - - it "allows an invalidation strategy to be assigned", -> - marker = buffer.markPosition([0, 3], invalidate: 'inside') - expect(marker.getInvalidationStrategy()).toBe 'inside' - - it "throws an error if an invalid point is given", -> - marker1 = buffer.markPosition([0, 1]) - - expect -> buffer.markPosition([0, NaN]) - .toThrowError "Invalid Point: (0, NaN)" - - expect(buffer.findMarkers({})).toEqual [marker1] - expect(buffer.getMarkers()).toEqual [marker1] - - it "allows arbitrary properties to be assigned", -> - marker = buffer.markPosition([0, 6], foo: 'bar') - expect(marker.getProperties()).toEqual({foo: 'bar'}) - - describe "direct updates", -> - [marker, changes] = [] - - beforeEach -> - marker = buffer.markRange([[0, 6], [0, 9]]) - changes = [] - markersUpdatedCount = 0 - marker.onDidChange (change) -> changes.push(change) - - describe "::setHeadPosition(position, state)", -> - it "sets the head position of the marker, flipping its orientation if necessary", -> - marker.setHeadPosition([0, 12]) - expect(marker.getRange()).toEqual [[0, 6], [0, 12]] - expect(marker.isReversed()).toBe false - expect(markersUpdatedCount).toBe 1 - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 12] - oldTailPosition: [0, 6], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 3]) - expect(markersUpdatedCount).toBe 2 - expect(marker.getRange()).toEqual [[0, 3], [0, 6]] - expect(marker.isReversed()).toBe true - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 3] - oldTailPosition: [0, 6], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 9]) - expect(markersUpdatedCount).toBe 3 - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isReversed()).toBe false - expect(changes).toEqual [{ - oldHeadPosition: [0, 3], newHeadPosition: [0, 9] - oldTailPosition: [0, 6], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - it "does not give the marker a tail if it doesn't have one already", -> - marker.clearTail() - expect(marker.hasTail()).toBe false - marker.setHeadPosition([0, 15]) - expect(marker.hasTail()).toBe false - expect(marker.getRange()).toEqual [[0, 15], [0, 15]] - - it "does not notify ::onDidChange observers and returns false if the position isn't actually changed", -> - expect(marker.setHeadPosition(marker.getHeadPosition())).toBe false - expect(markersUpdatedCount).toBe 0 - expect(changes.length).toBe 0 - - it "clips the assigned position", -> - marker.setHeadPosition([100, 100]) - expect(marker.getHeadPosition()).toEqual [0, 26] - - describe "::setTailPosition(position, state)", -> - it "sets the head position of the marker, flipping its orientation if necessary", -> - marker.setTailPosition([0, 3]) - expect(marker.getRange()).toEqual [[0, 3], [0, 9]] - expect(marker.isReversed()).toBe false - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 9] - oldTailPosition: [0, 6], newTailPosition: [0, 3] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setTailPosition([0, 12]) - expect(marker.getRange()).toEqual [[0, 9], [0, 12]] - expect(marker.isReversed()).toBe true - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 9] - oldTailPosition: [0, 3], newTailPosition: [0, 12] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setTailPosition([0, 6]) - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isReversed()).toBe false - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 9] - oldTailPosition: [0, 12], newTailPosition: [0, 6] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - it "plants the tail of the marker if it does not have a tail", -> - marker.clearTail() - expect(marker.hasTail()).toBe false - marker.setTailPosition([0, 0]) - expect(marker.hasTail()).toBe true - expect(marker.getRange()).toEqual [[0, 0], [0, 9]] - - it "does not notify ::onDidChange observers and returns false if the position isn't actually changed", -> - expect(marker.setTailPosition(marker.getTailPosition())).toBe false - expect(changes.length).toBe 0 - - it "clips the assigned position", -> - marker.setTailPosition([100, 100]) - expect(marker.getTailPosition()).toEqual [0, 26] - - describe "::setRange(range, options)", -> - it "sets the head and tail position simultaneously, flipping the orientation if the 'isReversed' option is true", -> - marker.setRange([[0, 8], [0, 12]]) - expect(marker.getRange()).toEqual [[0, 8], [0, 12]] - expect(marker.isReversed()).toBe false - expect(marker.getHeadPosition()).toEqual [0, 12] - expect(marker.getTailPosition()).toEqual [0, 8] - expect(changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 12] - oldTailPosition: [0, 6], newTailPosition: [0, 8] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setRange([[0, 3], [0, 9]], reversed: true) - expect(marker.getRange()).toEqual [[0, 3], [0, 9]] - expect(marker.isReversed()).toBe true - expect(marker.getHeadPosition()).toEqual [0, 3] - expect(marker.getTailPosition()).toEqual [0, 9] - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 3] - oldTailPosition: [0, 8], newTailPosition: [0, 9] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - it "plants the tail of the marker if it does not have a tail", -> - marker.clearTail() - expect(marker.hasTail()).toBe false - marker.setRange([[0, 1], [0, 10]]) - expect(marker.hasTail()).toBe true - expect(marker.getRange()).toEqual [[0, 1], [0, 10]] - - it "clips the assigned range", -> - marker.setRange([[-100, -100], [100, 100]]) - expect(marker.getRange()).toEqual [[0, 0], [0, 26]] - - it "emits the right events when called inside of an ::onDidChange handler", -> - marker.onDidChange (change) -> - if marker.getHeadPosition().isEqual([0, 5]) - marker.setHeadPosition([0, 6]) - - marker.setHeadPosition([0, 5]) - - headPositions = for {oldHeadPosition, newHeadPosition} in changes - {old: oldHeadPosition, new: newHeadPosition} - - expect(headPositions).toEqual [ - {old: [0, 9], new: [0, 5]} - {old: [0, 5], new: [0, 6]} - ] - - it "throws an error if an invalid range is given", -> - expect -> marker.setRange([[0, NaN], [0, 12]]) - .toThrowError "Invalid Point: (0, NaN)" - - expect(buffer.findMarkers({})).toEqual [marker] - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - - describe "::clearTail() / ::plantTail()", -> - it "clears the tail / plants the tail at the current head position", -> - marker.setRange([[0, 6], [0, 9]], reversed: true) - - changes = [] - marker.clearTail() - expect(marker.getRange()).toEqual [[0, 6], [0, 6]] - expect(marker.hasTail()).toBe false - expect(marker.isReversed()).toBe false - - expect(changes).toEqual [{ - oldHeadPosition: [0, 6], newHeadPosition: [0, 6] - oldTailPosition: [0, 9], newTailPosition: [0, 6] - hadTail: true, hasTail: false - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 12]) - expect(marker.getRange()).toEqual [[0, 12], [0, 12]] - expect(changes).toEqual [{ - oldHeadPosition: [0, 6], newHeadPosition: [0, 12] - oldTailPosition: [0, 6], newTailPosition: [0, 12] - hadTail: false, hasTail: false - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.plantTail() - expect(marker.hasTail()).toBe true - expect(marker.isReversed()).toBe false - expect(marker.getRange()).toEqual [[0, 12], [0, 12]] - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 12] - oldTailPosition: [0, 12], newTailPosition: [0, 12] - hadTail: false, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.setHeadPosition([0, 15]) - expect(marker.getRange()).toEqual [[0, 12], [0, 15]] - expect(changes).toEqual [{ - oldHeadPosition: [0, 12], newHeadPosition: [0, 15] - oldTailPosition: [0, 12], newTailPosition: [0, 12] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: false - }] - - changes = [] - marker.plantTail() - expect(marker.getRange()).toEqual [[0, 12], [0, 15]] - expect(changes).toEqual [] - - describe "::setProperties(properties)", -> - it "merges the given properties into the current properties", -> - marker.setProperties(foo: 1) - expect(marker.getProperties()).toEqual {foo: 1} - marker.setProperties(bar: 2) - expect(marker.getProperties()).toEqual {foo: 1, bar: 2} - expect(markersUpdatedCount).toBe 2 - - describe "indirect updates (due to buffer changes)", -> - [allStrategies, neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker] = [] - - beforeEach -> - overlapMarker = buffer.markRange([[0, 6], [0, 9]], invalidate: 'overlap') - neverMarker = overlapMarker.copy(invalidate: 'never') - surroundMarker = overlapMarker.copy(invalidate: 'surround') - insideMarker = overlapMarker.copy(invalidate: 'inside') - touchMarker = overlapMarker.copy(invalidate: 'touch') - allStrategies = [neverMarker, surroundMarker, overlapMarker, insideMarker, touchMarker] - markersUpdatedCount = 0 - - it "defers notifying Marker::onDidChange observers until after notifying Buffer::onDidChange observers", -> - for marker in allStrategies - do (marker) -> - marker.changes = [] - marker.onDidChange (change) -> - marker.changes.push(change) - - changedCount = 0 - changeSubscription = - buffer.onDidChange (change) -> - changedCount++ - expect(markersUpdatedCount).toBe 0 - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 11]] - expect(marker.isValid()).toBe true - expect(marker.changes.length).toBe 0 - - buffer.setTextInRange([[0, 1], [0, 2]], "ABC") - - expect(changedCount).toBe 1 - - for marker in allStrategies - expect(marker.changes).toEqual [{ - oldHeadPosition: [0, 9], newHeadPosition: [0, 11] - oldTailPosition: [0, 6], newTailPosition: [0, 8] - hadTail: true, hasTail: true - wasValid: true, isValid: true - oldProperties: {}, newProperties: {} - textChanged: true - }] - expect(markersUpdatedCount).toBe 1 - - marker.changes = [] for marker in allStrategies - changeSubscription.dispose() - changedCount = 0 - markersUpdatedCount = 0 - buffer.onDidChange (change) -> - changedCount++ - expect(markersUpdatedCount).toBe 0 - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isValid()).toBe true - expect(marker.changes.length).toBe 0 - - it "notifies ::onDidUpdateMarkers observers even if there are no Marker::onDidChange observers", -> - expect(markersUpdatedCount).toBe 0 - buffer.insert([0, 0], "123") - expect(markersUpdatedCount).toBe 1 - overlapMarker.setRange([[0, 1], [0, 2]]) - expect(markersUpdatedCount).toBe 2 - - it "emits onDidChange events when undoing/redoing text changes that move the marker", -> - marker = buffer.markRange([[0, 4], [0, 8]]) - buffer.insert([0, 0], 'ABCD') - - changes = [] - marker.onDidChange (change) -> changes.push(change) - buffer.undo() - expect(changes.length).toBe 1 - expect(changes[0].newHeadPosition).toEqual [0, 8] - buffer.redo() - expect(changes.length).toBe 2 - expect(changes[1].newHeadPosition).toEqual [0, 12] - - describe "when a change precedes a marker", -> - it "shifts the marker based on the characters inserted or removed by the change", -> - buffer.setTextInRange([[0, 1], [0, 2]], "ABC") - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 11]] - expect(marker.isValid()).toBe true - - buffer.setTextInRange([[0, 1], [0, 1]], '\nDEF') - for marker in allStrategies - expect(marker.getRange()).toEqual [[1, 10], [1, 13]] - expect(marker.isValid()).toBe true - - describe "when a change follows a marker", -> - it "does not shift the marker", -> - buffer.setTextInRange([[0, 10], [0, 12]], "ABC") - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isValid()).toBe true - - describe "when a change starts at a marker's start position", -> - describe "when the marker has a tail", -> - it "interprets the change as being inside the marker for all invalidation strategies", -> - buffer.setTextInRange([[0, 6], [0, 7]], "ABC") - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 11]] - expect(marker.isValid()).toBe true - - expect(insideMarker.getRange()).toEqual [[0, 9], [0, 11]] - expect(insideMarker.isValid()).toBe false - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 11]] - expect(touchMarker.isValid()).toBe false - - describe "when the marker has no tail", -> - it "interprets the change as being outside the marker for all invalidation strategies", -> - for marker in allStrategies - marker.setRange([[0, 6], [0, 11]], reversed: true) - marker.clearTail() - expect(marker.getRange()).toEqual [[0, 6], [0, 6]] - - buffer.setTextInRange([[0, 6], [0, 6]], "ABC") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 9], [0, 9]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 9], [0, 9]] - expect(touchMarker.isValid()).toBe false - - buffer.setTextInRange([[0, 9], [0, 9]], "DEF") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 12], [0, 12]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 12], [0, 12]] - expect(touchMarker.isValid()).toBe false - - describe "when a change ends at a marker's start position but starts before it", -> - it "interprets the change as being outside the marker for all invalidation strategies", -> - buffer.setTextInRange([[0, 4], [0, 6]], "ABC") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 7], [0, 10]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 7], [0, 10]] - expect(touchMarker.isValid()).toBe false - - describe "when a change starts and ends at a marker's start position", -> - it "interprets the change as being inside the marker for all invalidation strategies except 'inside'", -> - buffer.insert([0, 6], "ABC") - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 12]] - expect(marker.isValid()).toBe true - - expect(insideMarker.getRange()).toEqual [[0, 9], [0, 12]] - expect(insideMarker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 12]] - expect(touchMarker.isValid()).toBe false - - describe "when a change starts at a marker's end position", -> - describe "when the change is an insertion", -> - it "interprets the change as being inside the marker for all invalidation strategies except 'inside'", -> - buffer.setTextInRange([[0, 9], [0, 9]], "ABC") - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 12]] - expect(marker.isValid()).toBe true - - expect(insideMarker.getRange()).toEqual [[0, 6], [0, 9]] - expect(insideMarker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 12]] - expect(touchMarker.isValid()).toBe false - - describe "when the change replaces some existing text", -> - it "interprets the change as being outside the marker for all invalidation strategies", -> - buffer.setTextInRange([[0, 9], [0, 11]], "ABC") - - for marker in difference(allStrategies, [touchMarker]) - expect(marker.getRange()).toEqual [[0, 6], [0, 9]] - expect(marker.isValid()).toBe true - - expect(touchMarker.getRange()).toEqual [[0, 6], [0, 9]] - expect(touchMarker.isValid()).toBe false - - describe "when a change surrounds a marker", -> - it "truncates the marker to the end of the change and invalidates every invalidation strategy except 'never'", -> - buffer.setTextInRange([[0, 5], [0, 10]], "ABC") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 8]] - - for marker in difference(allStrategies, [neverMarker]) - expect(marker.isValid()).toBe false - - expect(neverMarker.isValid()).toBe true - - describe "when a change is inside a marker", -> - it "adjusts the marker's end position and invalidates markers with an 'inside' or 'touch' strategy", -> - buffer.setTextInRange([[0, 7], [0, 8]], "AB") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 10]] - - for marker in difference(allStrategies, [insideMarker, touchMarker]) - expect(marker.isValid()).toBe true - - expect(insideMarker.isValid()).toBe false - expect(touchMarker.isValid()).toBe false - - describe "when a change overlaps the start of a marker", -> - it "moves the start of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", -> - buffer.setTextInRange([[0, 5], [0, 7]], "ABC") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 8], [0, 10]] - - expect(neverMarker.isValid()).toBe true - expect(surroundMarker.isValid()).toBe true - expect(overlapMarker.isValid()).toBe false - expect(insideMarker.isValid()).toBe false - expect(touchMarker.isValid()).toBe false - - describe "when a change overlaps the end of a marker", -> - it "moves the end of the marker to the end of the change and invalidates the marker if its stategy is 'overlap', 'inside', or 'touch'", -> - buffer.setTextInRange([[0, 8], [0, 10]], "ABC") - - for marker in allStrategies - expect(marker.getRange()).toEqual [[0, 6], [0, 11]] - - expect(neverMarker.isValid()).toBe true - expect(surroundMarker.isValid()).toBe true - expect(overlapMarker.isValid()).toBe false - expect(insideMarker.isValid()).toBe false - expect(touchMarker.isValid()).toBe false - - describe "when multiple changes occur in a transaction", -> - it "emits one change event for each marker that was indirectly updated", -> - for marker in allStrategies - do (marker) -> - marker.changes = [] - marker.onDidChange (change) -> - marker.changes.push(change) - - buffer.transact -> - buffer.insert([0, 7], ".") - buffer.append("!") - - for marker in allStrategies - expect(marker.changes.length).toBe 0 - - neverMarker.setRange([[0, 0], [0, 1]]) - - expect(neverMarker.changes).toEqual [{ - oldHeadPosition: [0, 9] - newHeadPosition: [0, 1] - oldTailPosition: [0, 6] - newTailPosition: [0, 0] - wasValid: true - isValid: true - hadTail: true - hasTail: true - oldProperties: {} - newProperties: {} - textChanged: false - }] - - expect(insideMarker.changes).toEqual [{ - oldHeadPosition: [0, 9] - newHeadPosition: [0, 10] - oldTailPosition: [0, 6] - newTailPosition: [0, 6] - wasValid: true - isValid: false - hadTail: true - hasTail: true - oldProperties: {} - newProperties: {} - textChanged: true - }] - - describe "destruction", -> - it "removes the marker from the buffer, marks it destroyed and invalid, and notifies ::onDidDestroy observers", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - expect(buffer.getMarker(marker.id)).toBe marker - marker.onDidDestroy destroyedHandler = jasmine.createSpy("destroyedHandler") - - marker.destroy() - - expect(destroyedHandler.calls.count()).toBe 1 - expect(buffer.getMarker(marker.id)).toBeUndefined() - expect(marker.isDestroyed()).toBe true - expect(marker.isValid()).toBe false - expect(marker.getRange()).toEqual [[0, 0], [0, 0]] - - it "handles markers deleted in event handlers", -> - marker1 = buffer.markRange([[0, 3], [0, 6]]) - marker2 = marker1.copy() - marker3 = marker1.copy() - - marker1.onDidChange -> - marker1.destroy() - marker2.destroy() - marker3.destroy() - - # doesn't blow up. - buffer.insert([0, 0], "!") - - marker1 = buffer.markRange([[0, 3], [0, 6]]) - marker2 = marker1.copy() - marker3 = marker1.copy() - - marker1.onDidChange -> - marker1.destroy() - marker2.destroy() - marker3.destroy() - - # doesn't blow up. - buffer.undo() - - it "does not reinsert the marker if its range is later updated", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - marker.destroy() - expect(buffer.findMarkers(intersectsRow: 0)).toEqual [] - marker.setRange([[0, 0], [0, 9]]) - expect(buffer.findMarkers(intersectsRow: 0)).toEqual [] - - it "does not blow up when destroy is called twice", -> - marker = buffer.markRange([[0, 3], [0, 6]]) - marker.destroy() - marker.destroy() - - describe "TextBuffer::findMarkers(properties)", -> - [marker1, marker2, marker3, marker4] = [] - - beforeEach -> - marker1 = buffer.markRange([[0, 0], [0, 3]], class: 'a') - marker2 = buffer.markRange([[0, 0], [0, 5]], class: 'a', invalidate: 'surround') - marker3 = buffer.markRange([[0, 4], [0, 7]], class: 'a') - marker4 = buffer.markRange([[0, 0], [0, 7]], class: 'b', invalidate: 'never') - - it "can find markers based on custom properties", -> - expect(buffer.findMarkers(class: 'a')).toEqual [marker2, marker1, marker3] - expect(buffer.findMarkers(class: 'b')).toEqual [marker4] - - it "can find markers based on their invalidation strategy", -> - expect(buffer.findMarkers(invalidate: 'overlap')).toEqual [marker1, marker3] - expect(buffer.findMarkers(invalidate: 'surround')).toEqual [marker2] - expect(buffer.findMarkers(invalidate: 'never')).toEqual [marker4] - - it "can find markers that start or end at a given position", -> - expect(buffer.findMarkers(startPosition: [0, 0])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(startPosition: [0, 0], class: 'a')).toEqual [marker2, marker1] - expect(buffer.findMarkers(startPosition: [0, 0], endPosition: [0, 3], class: 'a')).toEqual [marker1] - expect(buffer.findMarkers(startPosition: [0, 4], endPosition: [0, 7])).toEqual [marker3] - expect(buffer.findMarkers(endPosition: [0, 7])).toEqual [marker4, marker3] - expect(buffer.findMarkers(endPosition: [0, 7], class: 'b')).toEqual [marker4] - - it "can find markers that start or end at a given range", -> - expect(buffer.findMarkers(startsInRange: [[0, 0], [0, 4]])).toEqual [marker4, marker2, marker1, marker3] - expect(buffer.findMarkers(startsInRange: [[0, 0], [0, 4]], class: 'a')).toEqual [marker2, marker1, marker3] - expect(buffer.findMarkers(startsInRange: [[0, 0], [0, 4]], endsInRange: [[0, 3], [0, 6]])).toEqual [marker2, marker1] - expect(buffer.findMarkers(endsInRange: [[0, 5], [0, 7]])).toEqual [marker4, marker2, marker3] - - it "can find markers that contain a given point", -> - expect(buffer.findMarkers(containsPosition: [0, 0])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(containsPoint: [0, 0])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(containsPoint: [0, 1], class: 'a')).toEqual [marker2, marker1] - expect(buffer.findMarkers(containsPoint: [0, 4])).toEqual [marker4, marker2, marker3] - - it "can find markers that contain a given range", -> - expect(buffer.findMarkers(containsRange: [[0, 1], [0, 4]])).toEqual [marker4, marker2] - expect(buffer.findMarkers(containsRange: [[0, 4], [0, 1]])).toEqual [marker4, marker2] - expect(buffer.findMarkers(containsRange: [[0, 1], [0, 3]])).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(containsRange: [[0, 6], [0, 7]])).toEqual [marker4, marker3] - - it "can find markers that intersect a given range", -> - expect(buffer.findMarkers(intersectsRange: [[0, 4], [0, 6]])).toEqual [marker4, marker2, marker3] - expect(buffer.findMarkers(intersectsRange: [[0, 0], [0, 2]])).toEqual [marker4, marker2, marker1] - - it "can find markers that start or end at a given row", -> - buffer.setTextInRange([[0, 7], [0, 7]], '\n') - buffer.setTextInRange([[0, 3], [0, 4]], ' \n') - expect(buffer.findMarkers(startRow: 0)).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(startRow: 1)).toEqual [marker3] - expect(buffer.findMarkers(endRow: 2)).toEqual [marker4, marker3] - expect(buffer.findMarkers(startRow: 0, endRow: 2)).toEqual [marker4] - - it "can find markers that intersect a given row", -> - buffer.setTextInRange([[0, 7], [0, 7]], '\n') - buffer.setTextInRange([[0, 3], [0, 4]], ' \n') - expect(buffer.findMarkers(intersectsRow: 0)).toEqual [marker4, marker2, marker1] - expect(buffer.findMarkers(intersectsRow: 1)).toEqual [marker4, marker2, marker3] - - it "can find markers that intersect a given range", -> - buffer.setTextInRange([[0, 7], [0, 7]], '\n') - buffer.setTextInRange([[0, 3], [0, 4]], ' \n') - expect(buffer.findMarkers(intersectsRowRange: [1, 2])).toEqual [marker4, marker2, marker3] - - it "can find markers that are contained within a certain range, inclusive", -> - expect(buffer.findMarkers(containedInRange: [[0, 0], [0, 6]])).toEqual [marker2, marker1] - expect(buffer.findMarkers(containedInRange: [[0, 4], [0, 7]])).toEqual [marker3] diff --git a/spec/point-spec.coffee b/spec/point-spec.coffee deleted file mode 100644 index 317a4d7152..0000000000 --- a/spec/point-spec.coffee +++ /dev/null @@ -1,196 +0,0 @@ -Point = require '../src/point' - -describe "Point", -> - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - - describe "::negate()", -> - it "should negate the row and column", -> - expect(new Point( 0, 0).negate().toString()).toBe "(0, 0)" - expect(new Point( 1, 2).negate().toString()).toBe "(-1, -2)" - expect(new Point(-1, -2).negate().toString()).toBe "(1, 2)" - expect(new Point(-1, 2).negate().toString()).toBe "(1, -2)" - - describe "::fromObject(object, copy)", -> - it "returns a new Point if object is point-compatible array ", -> - expect(Point.fromObject([1, 3])).toEqual Point(1, 3) - expect(Point.fromObject([Infinity, Infinity])).toEqual Point.INFINITY - - it "returns the copy of object if it is an instanceof Point", -> - origin = Point(0, 0) - expect(Point.fromObject(origin, false) is origin).toBe true - expect(Point.fromObject(origin, true) is origin).toBe false - - describe "::copy()", -> - it "returns a copy of the object", -> - expect(Point(3, 4).copy()).toEqual Point(3, 4) - expect(Point.ZERO.copy()).toEqual [0, 0] - - describe "::negate()", -> - it "returns a new point with row and column negated", -> - expect(Point(3, 4).negate()).toEqual Point(-3, -4) - expect(Point.ZERO.negate()).toEqual [0, 0] - - describe "::freeze()", -> - it "makes the Point object immutable", -> - expect(Object.isFrozen(Point(3, 4).freeze())).toBe true - expect(Object.isFrozen(Point.ZERO.freeze())).toBe true - - describe "::compare(other)", -> - it "returns -1 for <, 0 for =, 1 for > comparisions", -> - expect(Point(2, 3).compare(Point(2, 6))).toBe -1 - expect(Point(2, 3).compare(Point(3, 4))).toBe -1 - expect(Point(1, 1).compare(Point(1, 1))).toBe 0 - expect(Point(2, 3).compare(Point(2, 0))).toBe 1 - expect(Point(2, 3).compare(Point(1, 3))).toBe 1 - - expect(Point(2, 3).compare([2, 6])).toBe -1 - expect(Point(2, 3).compare([3, 4])).toBe -1 - expect(Point(1, 1).compare([1, 1])).toBe 0 - expect(Point(2, 3).compare([2, 0])).toBe 1 - expect(Point(2, 3).compare([1, 3])).toBe 1 - - describe "::isLessThan(other)", -> - it "returns a boolean indicating whether a point precedes the given Point ", -> - expect(Point(2, 3).isLessThan(Point(2, 5))).toBe true - expect(Point(2, 3).isLessThan(Point(3, 4))).toBe true - expect(Point(2, 3).isLessThan(Point(2, 3))).toBe false - expect(Point(2, 3).isLessThan(Point(2, 1))).toBe false - expect(Point(2, 3).isLessThan(Point(1, 2))).toBe false - - expect(Point(2, 3).isLessThan([2, 5])).toBe true - expect(Point(2, 3).isLessThan([3, 4])).toBe true - expect(Point(2, 3).isLessThan([2, 3])).toBe false - expect(Point(2, 3).isLessThan([2, 1])).toBe false - expect(Point(2, 3).isLessThan([1, 2])).toBe false - - describe "::isLessThanOrEqual(other)", -> - it "returns a boolean indicating whether a point precedes or equal the given Point ", -> - expect(Point(2, 3).isLessThanOrEqual(Point(2, 5))).toBe true - expect(Point(2, 3).isLessThanOrEqual(Point(3, 4))).toBe true - expect(Point(2, 3).isLessThanOrEqual(Point(2, 3))).toBe true - expect(Point(2, 3).isLessThanOrEqual(Point(2, 1))).toBe false - expect(Point(2, 3).isLessThanOrEqual(Point(1, 2))).toBe false - - expect(Point(2, 3).isLessThanOrEqual([2, 5])).toBe true - expect(Point(2, 3).isLessThanOrEqual([3, 4])).toBe true - expect(Point(2, 3).isLessThanOrEqual([2, 3])).toBe true - expect(Point(2, 3).isLessThanOrEqual([2, 1])).toBe false - expect(Point(2, 3).isLessThanOrEqual([1, 2])).toBe false - - describe "::isGreaterThan(other)", -> - it "returns a boolean indicating whether a point follows the given Point ", -> - expect(Point(2, 3).isGreaterThan(Point(2, 5))).toBe false - expect(Point(2, 3).isGreaterThan(Point(3, 4))).toBe false - expect(Point(2, 3).isGreaterThan(Point(2, 3))).toBe false - expect(Point(2, 3).isGreaterThan(Point(2, 1))).toBe true - expect(Point(2, 3).isGreaterThan(Point(1, 2))).toBe true - - expect(Point(2, 3).isGreaterThan([2, 5])).toBe false - expect(Point(2, 3).isGreaterThan([3, 4])).toBe false - expect(Point(2, 3).isGreaterThan([2, 3])).toBe false - expect(Point(2, 3).isGreaterThan([2, 1])).toBe true - expect(Point(2, 3).isGreaterThan([1, 2])).toBe true - - describe "::isGreaterThanOrEqual(other)", -> - it "returns a boolean indicating whether a point follows or equal the given Point ", -> - expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 5))).toBe false - expect(Point(2, 3).isGreaterThanOrEqual(Point(3, 4))).toBe false - expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 3))).toBe true - expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 1))).toBe true - expect(Point(2, 3).isGreaterThanOrEqual(Point(1, 2))).toBe true - - expect(Point(2, 3).isGreaterThanOrEqual([2, 5])).toBe false - expect(Point(2, 3).isGreaterThanOrEqual([3, 4])).toBe false - expect(Point(2, 3).isGreaterThanOrEqual([2, 3])).toBe true - expect(Point(2, 3).isGreaterThanOrEqual([2, 1])).toBe true - expect(Point(2, 3).isGreaterThanOrEqual([1, 2])).toBe true - - describe "::isEqual()", -> - it "returns if whether two points are equal", -> - expect(Point(1, 1).isEqual(Point(1, 1))).toBe true - expect(Point(1, 1).isEqual([1, 1])).toBe true - expect(Point(1, 2).isEqual(Point(3, 3))).toBe false - expect(Point(1, 2).isEqual([3, 3])).toBe false - - describe "::isPositive()", -> - it "returns true if the point represents a forward traversal", -> - expect(Point(-1, -1).isPositive()).toBe false - expect(Point(-1, 0).isPositive()).toBe false - expect(Point(-1, Infinity).isPositive()).toBe false - expect(Point(0, 0).isPositive()).toBe false - - expect(Point(0, 1).isPositive()).toBe true - expect(Point(5, 0).isPositive()).toBe true - expect(Point(5, -1).isPositive()).toBe true - - describe "::isZero()", -> - it "returns true if the point is zero", -> - expect(Point(1, 1).isZero()).toBe false - expect(Point(0, 1).isZero()).toBe false - expect(Point(1, 0).isZero()).toBe false - expect(Point(0, 0).isZero()).toBe true - - describe "::min(a, b)", -> - it "returns the minimum of two points", -> - expect(Point.min(Point(3, 4), Point(1, 1))).toEqual Point(1, 1) - expect(Point.min(Point(1, 2), Point(5, 6))).toEqual Point(1, 2) - expect(Point.min([3, 4], [1, 1])).toEqual [1, 1] - expect(Point.min([1, 2], [5, 6])).toEqual [1, 2] - - describe "::max(a, b)", -> - it "returns the minimum of two points", -> - expect(Point.max(Point(3, 4), Point(1, 1))).toEqual Point(3, 4) - expect(Point.max(Point(1, 2), Point(5, 6))).toEqual Point(5, 6) - expect(Point.max([3, 4], [1, 1])).toEqual [3, 4] - expect(Point.max([1, 2], [5, 6])).toEqual [5, 6] - - describe "::translate(delta)", -> - it "returns a new point by adding corresponding coordinates", -> - expect(Point(1, 1).translate(Point(2, 3))).toEqual Point(3, 4) - expect(Point.INFINITY.translate(Point(2, 3))).toEqual Point.INFINITY - - expect(Point.ZERO.translate([5, 6])).toEqual [5, 6] - expect(Point(1, 1).translate([3, 4])).toEqual [4, 5] - - describe "::traverse(delta)", -> - it "returns a new point by traversing given rows and columns", -> - expect(Point(2, 3).traverse(Point(0, 3))).toEqual Point(2, 6) - expect(Point(2, 3).traverse([0, 3])).toEqual [2, 6] - - expect(Point(1, 3).traverse(Point(4, 2))).toEqual [5, 2] - expect(Point(1, 3).traverse([5, 4])).toEqual [6, 4] - - describe "::traversalFrom(other)", -> - it "returns a point that other has to traverse to get to given point", -> - expect(Point(2, 5).traversalFrom(Point(2, 3))).toEqual Point(0, 2) - expect(Point(2, 3).traversalFrom(Point(2, 5))).toEqual Point(0, -2) - expect(Point(2, 3).traversalFrom(Point(2, 3))).toEqual Point(0, 0) - - expect(Point(3, 4).traversalFrom(Point(2, 3))).toEqual Point(1, 4) - expect(Point(2, 3).traversalFrom(Point(3, 5))).toEqual Point(-1, 3) - - expect(Point(2, 5).traversalFrom([2, 3])).toEqual [0, 2] - expect(Point(2, 3).traversalFrom([2, 5])).toEqual [0, -2] - expect(Point(2, 3).traversalFrom([2, 3])).toEqual [0, 0] - - expect(Point(3, 4).traversalFrom([2, 3])).toEqual [1, 4] - expect(Point(2, 3).traversalFrom([3, 5])).toEqual [-1, 3] - - describe "::toArray()", -> - it "returns an array of row and column", -> - expect(Point(1, 3).toArray()).toEqual [1, 3] - expect(Point.ZERO.toArray()).toEqual [0, 0] - expect(Point.INFINITY.toArray()).toEqual [Infinity, Infinity] - - describe "::serialize()", -> - it "returns an array of row and column", -> - expect(Point(1, 3).serialize()).toEqual [1, 3] - expect(Point.ZERO.serialize()).toEqual [0, 0] - expect(Point.INFINITY.serialize()).toEqual [Infinity, Infinity] - - describe "::toString()", -> - it "returns string representation of Point", -> - expect(Point(4, 5).toString()).toBe "(4, 5)" - expect(Point.ZERO.toString()).toBe "(0, 0)" - expect(Point.INFINITY.toString()).toBe "(Infinity, Infinity)" diff --git a/spec/range-spec.coffee b/spec/range-spec.coffee deleted file mode 100644 index ea4caf07a3..0000000000 --- a/spec/range-spec.coffee +++ /dev/null @@ -1,42 +0,0 @@ -Range = require '../src/range' - -describe "Range", -> - beforeEach -> - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - - describe "::intersectsWith(other, [exclusive])", -> - intersectsWith = (range1, range2, exclusive) -> - range1 = Range.fromObject(range1) - range2 = Range.fromObject(range2) - range1.intersectsWith(range2, exclusive) - - describe "when the exclusive argument is false (the default)", -> - it "returns true if the ranges intersect, exclusive of their endpoints", -> - expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]])).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]])).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]])).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false - - describe "when the exclusive argument is true", -> - it "returns true if the ranges intersect, exclusive of their endpoints", -> - expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]], true)).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]], true)).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]], true)).toBe true - expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false - expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false - - describe "::negate()", -> - it "should negate the start and end points", -> - expect(new Range([ 0, 0], [ 0, 0]).negate().toString()).toBe "[(0, 0) - (0, 0)]" - expect(new Range([ 1, 2], [ 3, 4]).negate().toString()).toBe "[(-3, -4) - (-1, -2)]" - expect(new Range([-1, -2], [-3, -4]).negate().toString()).toBe "[(1, 2) - (3, 4)]" - expect(new Range([-1, 2], [ 3, -4]).negate().toString()).toBe "[(-3, 4) - (1, -2)]" diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee deleted file mode 100644 index 989562b07e..0000000000 --- a/spec/text-buffer-spec.coffee +++ /dev/null @@ -1,3010 +0,0 @@ -fs = require 'fs-plus' -{join} = require 'path' -temp = require 'temp' -{File} = require 'pathwatcher' -Random = require 'random-seed' -Point = require '../src/point' -Range = require '../src/range' -DisplayLayer = require '../src/display-layer' -DefaultHistoryProvider = require '../src/default-history-provider' -TextBuffer = require '../src/text-buffer' -SampleText = fs.readFileSync(join(__dirname, 'fixtures', 'sample.js'), 'utf8') -{buildRandomLines, getRandomBufferRange} = require './helpers/random' -NullLanguageMode = require '../src/null-language-mode' - -describe "TextBuffer", -> - buffer = null - - beforeEach -> - temp.track() - jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) - # When running specs in Atom, setTimeout is spied on by default. - jasmine.useRealClock?() - - afterEach -> - buffer?.destroy() - buffer = null - - describe "construction", -> - it "can be constructed empty", -> - buffer = new TextBuffer - expect(buffer.getLineCount()).toBe 1 - expect(buffer.getText()).toBe '' - expect(buffer.lineForRow(0)).toBe '' - expect(buffer.lineEndingForRow(0)).toBe '' - - it "can be constructed with initial text containing no trailing newline", -> - text = "hello\nworld\r\nhow are you doing?\r\nlast" - buffer = new TextBuffer(text) - expect(buffer.getLineCount()).toBe 4 - expect(buffer.getText()).toBe text - expect(buffer.lineForRow(0)).toBe 'hello' - expect(buffer.lineEndingForRow(0)).toBe '\n' - expect(buffer.lineForRow(1)).toBe 'world' - expect(buffer.lineEndingForRow(1)).toBe '\r\n' - expect(buffer.lineForRow(2)).toBe 'how are you doing?' - expect(buffer.lineEndingForRow(2)).toBe '\r\n' - expect(buffer.lineForRow(3)).toBe 'last' - expect(buffer.lineEndingForRow(3)).toBe '' - - it "can be constructed with initial text containing a trailing newline", -> - text = "first\n" - buffer = new TextBuffer(text) - expect(buffer.getLineCount()).toBe 2 - expect(buffer.getText()).toBe text - expect(buffer.lineForRow(0)).toBe 'first' - expect(buffer.lineEndingForRow(0)).toBe '\n' - expect(buffer.lineForRow(1)).toBe '' - expect(buffer.lineEndingForRow(1)).toBe '' - - it "automatically assigns a unique identifier to new buffers", -> - bufferIds = [0..16].map(-> new TextBuffer().getId()) - uniqueBufferIds = new Set(bufferIds) - - expect(uniqueBufferIds.size).toBe(bufferIds.length) - - describe "::destroy()", -> - it "clears the buffer's state", (done) -> - filePath = temp.openSync('atom').path - buffer = new TextBuffer() - buffer.setPath(filePath) - buffer.append("a") - buffer.append("b") - buffer.destroy() - - expect(buffer.getText()).toBe('') - buffer.undo() - expect(buffer.getText()).toBe('') - buffer.save().catch (error) -> - expect(error.message).toMatch(/Can't save destroyed buffer/) - done() - - describe "::setTextInRange(range, text)", -> - beforeEach -> - buffer = new TextBuffer("hello\nworld\r\nhow are you doing?") - - it "can replace text on a single line with a standard newline", -> - buffer.setTextInRange([[0, 2], [0, 4]], "y y") - expect(buffer.getText()).toEqual "hey yo\nworld\r\nhow are you doing?" - - it "can replace text on a single line with a carriage-return/newline", -> - buffer.setTextInRange([[1, 3], [1, 5]], "ms") - expect(buffer.getText()).toEqual "hello\nworms\r\nhow are you doing?" - - it "can replace text in a region spanning multiple lines, ending on the last line", -> - buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", normalizeLineEndings: false) - expect(buffer.getText()).toEqual "hey there\r\ncat\nwhat are you doing?" - - it "can replace text in a region spanning multiple lines, ending with a carriage-return/newline", -> - buffer.setTextInRange([[0, 2], [1, 3]], "y\nyou're o", normalizeLineEndings: false) - expect(buffer.getText()).toEqual "hey\nyou're old\r\nhow are you doing?" - - describe "after a change", -> - it "notifies, in order: the language mode, display layers, and display layer ::onDidChange observers with the relevant details", -> - buffer = new TextBuffer("hello\nworld\r\nhow are you doing?") - - events = [] - languageMode = { - bufferDidChange: (e) -> events.push({source: 'language-mode', event: e}), - bufferDidFinishTransaction: ->, - onDidChangeHighlighting: -> {dispose: ->} - } - displayLayer1 = buffer.addDisplayLayer() - displayLayer2 = buffer.addDisplayLayer() - spyOn(displayLayer1, 'bufferDidChange').and.callFake (e) -> - events.push({source: 'display-layer-1', event: e}) - DisplayLayer.prototype.bufferDidChange.call(displayLayer1, e) - spyOn(displayLayer2, 'bufferDidChange').and.callFake (e) -> - events.push({source: 'display-layer-2', event: e}) - DisplayLayer.prototype.bufferDidChange.call(displayLayer2, e) - buffer.setLanguageMode(languageMode) - buffer.onDidChange (e) -> events.push({source: 'buffer', event: JSON.parse(JSON.stringify(e))}) - displayLayer1.onDidChange (e) -> events.push({source: 'display-layer-event', event: e}) - - buffer.transact -> - buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat", normalizeLineEndings: false) - buffer.setTextInRange([[1, 1], [1, 2]], "abc", normalizeLineEndings: false) - - changeEvent1 = { - oldRange: [[0, 2], [2, 3]], newRange: [[0, 2], [2, 4]] - oldText: "llo\nworld\r\nhow", newText: "y there\r\ncat\nwhat", - } - changeEvent2 = { - oldRange: [[1, 1], [1, 2]], newRange: [[1, 1], [1, 4]] - oldText: "a", newText: "abc", - } - expect(events).toEqual [ - {source: 'language-mode', event: changeEvent1}, - {source: 'display-layer-1', event: changeEvent1}, - {source: 'display-layer-2', event: changeEvent1}, - - {source: 'language-mode', event: changeEvent2}, - {source: 'display-layer-1', event: changeEvent2}, - {source: 'display-layer-2', event: changeEvent2}, - - { - source: 'buffer', - event: { - oldRange: Range(Point(0, 2), Point(2, 3)), - newRange: Range(Point(0, 2), Point(2, 4)), - changes: [ - { - oldRange: Range(Point(0, 2), Point(2, 3)), - newRange: Range(Point(0, 2), Point(2, 4)), - oldText: "llo\nworld\r\nhow", - newText: "y there\r\ncabct\nwhat" - } - ] - } - }, - { - source: 'display-layer-event', - event: [{ - oldRange: Range(Point(0, 0), Point(3, 0)), - newRange: Range(Point(0, 0), Point(3, 0)) - }] - } - ] - - it "returns the newRange of the change", -> - expect(buffer.setTextInRange([[0, 2], [2, 3]], "y there\r\ncat\nwhat"), normalizeLineEndings: false).toEqual [[0, 2], [2, 4]] - - it "clips the given range", -> - buffer.setTextInRange([[-1, -1], [0, 1]], "y") - buffer.setTextInRange([[0, 10], [0, 100]], "w") - expect(buffer.lineForRow(0)).toBe "yellow" - - it "preserves the line endings of existing lines", -> - buffer.setTextInRange([[0, 1], [0, 2]], 'o') - expect(buffer.lineEndingForRow(0)).toBe '\n' - buffer.setTextInRange([[1, 1], [1, 3]], 'i') - expect(buffer.lineEndingForRow(1)).toBe '\r\n' - - it "freezes change event ranges", -> - changedOldRange = null - changedNewRange = null - buffer.onDidChange ({oldRange, newRange}) -> - oldRange.start = Point(0, 3) - oldRange.start.row = 1 - newRange.start = Point(4, 4) - newRange.end.row = 2 - changedOldRange = oldRange - changedNewRange = newRange - - buffer.setTextInRange(Range(Point(0, 2), Point(0, 4)), "y y") - - expect(changedOldRange).toEqual([[0, 2], [0, 4]]) - expect(changedNewRange).toEqual([[0, 2], [0, 5]]) - - describe "when the undo option is 'skip'", -> - it "replaces the contents of the buffer with the given text", -> - buffer.setTextInRange([[0, 0], [0, 1]], "y") - buffer.setTextInRange([[0, 10], [0, 100]], "w", {undo: 'skip'}) - expect(buffer.lineForRow(0)).toBe "yellow" - - expect(buffer.undo()).toBe true - expect(buffer.lineForRow(0)).toBe "hello" - - it "still emits marker change events (regression)", -> - markerLayer = buffer.addMarkerLayer() - marker = markerLayer.markRange([[0, 0], [0, 3]]) - - markerLayerUpdateEventsCount = 0 - markerChangeEvents = [] - markerLayer.onDidUpdate -> markerLayerUpdateEventsCount++ - marker.onDidChange (event) -> markerChangeEvents.push(event) - - buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'}) - expect(markerLayerUpdateEventsCount).toBe(1) - expect(markerChangeEvents).toEqual([{ - wasValid: true, isValid: true, - hadTail: true, hasTail: true, - oldProperties: {}, newProperties: {}, - oldHeadPosition: Point(0, 3), newHeadPosition: Point(0, 2), - oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), - textChanged: true - }]) - markerChangeEvents.length = 0 - - buffer.transact -> - buffer.setTextInRange([[0, 0], [0, 1]], '', {undo: 'skip'}) - expect(markerLayerUpdateEventsCount).toBe(2) - expect(markerChangeEvents).toEqual([{ - wasValid: true, isValid: true, - hadTail: true, hasTail: true, - oldProperties: {}, newProperties: {}, - oldHeadPosition: Point(0, 2), newHeadPosition: Point(0, 1), - oldTailPosition: Point(0, 0), newTailPosition: Point(0, 0), - textChanged: true - }]) - - it "still emits text change events (regression)", (done) -> - didChangeEvents = [] - buffer.onDidChange (event) -> didChangeEvents.push(event) - - buffer.onDidStopChanging ({changes}) -> - assertChangesEqual(changes, [{ - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 1]], - oldText: 'h', - newText: 'z' - }]) - done() - - buffer.setTextInRange([[0, 0], [0, 1]], 'y', {undo: 'skip'}) - expect(didChangeEvents.length).toBe(1) - assertChangesEqual(didChangeEvents[0].changes, [{ - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 1]], - oldText: 'h', - newText: 'y' - }]) - - buffer.transact -> buffer.setTextInRange([[0, 0], [0, 1]], 'z', {undo: 'skip'}) - expect(didChangeEvents.length).toBe(2) - assertChangesEqual(didChangeEvents[1].changes, [{ - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 1]], - oldText: 'y', - newText: 'z' - }]) - - describe "when the normalizeLineEndings argument is true (the default)", -> - describe "when the range's start row has a line ending", -> - it "normalizes inserted line endings to match the line ending of the range's start row", -> - changeEvents = [] - buffer.onDidChange (e) -> changeEvents.push(e) - - expect(buffer.lineEndingForRow(0)).toBe '\n' - buffer.setTextInRange([[0, 2], [0, 5]], "y\r\nthere\r\ncrazy") - expect(buffer.lineEndingForRow(0)).toBe '\n' - expect(buffer.lineEndingForRow(1)).toBe '\n' - expect(buffer.lineEndingForRow(2)).toBe '\n' - expect(changeEvents[0].newText).toBe "y\nthere\ncrazy" - - expect(buffer.lineEndingForRow(3)).toBe '\r\n' - buffer.setTextInRange([[3, 3], [4, Infinity]], "ms\ndo you\r\nlike\ndirt") - expect(buffer.lineEndingForRow(3)).toBe '\r\n' - expect(buffer.lineEndingForRow(4)).toBe '\r\n' - expect(buffer.lineEndingForRow(5)).toBe '\r\n' - expect(buffer.lineEndingForRow(6)).toBe '' - expect(changeEvents[1].newText).toBe "ms\r\ndo you\r\nlike\r\ndirt" - - buffer.setTextInRange([[5, 1], [5, 3]], '\r') - expect(changeEvents[2].changes).toEqual([{ - oldRange: [[5, 1], [5, 3]], - newRange: [[5, 1], [6, 0]], - oldText: 'ik', - newText: '\r\n' - }]) - - buffer.undo() - expect(changeEvents[3].changes).toEqual([{ - oldRange: [[5, 1], [6, 0]], - newRange: [[5, 1], [5, 3]], - oldText: '\r\n', - newText: 'ik' - }]) - - buffer.redo() - expect(changeEvents[4].changes).toEqual([{ - oldRange: [[5, 1], [5, 3]], - newRange: [[5, 1], [6, 0]], - oldText: 'ik', - newText: '\r\n' - }]) - - describe "when the range's start row has no line ending (because it's the last line of the buffer)", -> - describe "when the buffer contains no newlines", -> - it "honors the newlines in the inserted text", -> - buffer = new TextBuffer("hello") - buffer.setTextInRange([[0, 2], [0, Infinity]], "hey\r\nthere\nworld") - expect(buffer.lineEndingForRow(0)).toBe '\r\n' - expect(buffer.lineEndingForRow(1)).toBe '\n' - expect(buffer.lineEndingForRow(2)).toBe '' - - describe "when the buffer contains newlines", -> - it "normalizes inserted line endings to match the line ending of the penultimate row", -> - expect(buffer.lineEndingForRow(1)).toBe '\r\n' - buffer.setTextInRange([[2, 0], [2, Infinity]], "what\ndo\r\nyou\nwant?") - expect(buffer.lineEndingForRow(2)).toBe '\r\n' - expect(buffer.lineEndingForRow(3)).toBe '\r\n' - expect(buffer.lineEndingForRow(4)).toBe '\r\n' - expect(buffer.lineEndingForRow(5)).toBe '' - - describe "when the normalizeLineEndings argument is false", -> - it "honors the newlines in the inserted text", -> - buffer.setTextInRange([[1, 0], [1, 5]], "moon\norbiting\r\nhappily\nthere", {normalizeLineEndings: false}) - expect(buffer.lineEndingForRow(1)).toBe '\n' - expect(buffer.lineEndingForRow(2)).toBe '\r\n' - expect(buffer.lineEndingForRow(3)).toBe '\n' - expect(buffer.lineEndingForRow(4)).toBe '\r\n' - expect(buffer.lineEndingForRow(5)).toBe '' - - describe "::setText(text)", -> - it "replaces the contents of the buffer with the given text", -> - buffer = new TextBuffer("hello\nworld\r\nyou are cool") - buffer.setText("goodnight\r\nmoon\nit's been good") - expect(buffer.getText()).toBe "goodnight\r\nmoon\nit's been good" - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nyou are cool" - - describe "::insert(position, text, normalizeNewlinesn)", -> - it "inserts text at the given position", -> - buffer = new TextBuffer("hello world") - buffer.insert([0, 5], " there") - expect(buffer.getText()).toBe "hello there world" - - it "honors the normalizeNewlines option", -> - buffer = new TextBuffer("hello\nworld") - buffer.insert([0, 5], "\r\nthere\r\nlittle", normalizeLineEndings: false) - expect(buffer.getText()).toBe "hello\r\nthere\r\nlittle\nworld" - - describe "::append(text, normalizeNewlines)", -> - it "appends text to the end of the buffer", -> - buffer = new TextBuffer("hello world") - buffer.append(", how are you?") - expect(buffer.getText()).toBe "hello world, how are you?" - - it "honors the normalizeNewlines option", -> - buffer = new TextBuffer("hello\nworld") - buffer.append("\r\nhow\r\nare\nyou?", normalizeLineEndings: false) - expect(buffer.getText()).toBe "hello\nworld\r\nhow\r\nare\nyou?" - - describe "::delete(range)", -> - it "deletes text in the given range", -> - buffer = new TextBuffer("hello world") - buffer.delete([[0, 5], [0, 11]]) - expect(buffer.getText()).toBe "hello" - - describe "::deleteRows(startRow, endRow)", -> - beforeEach -> - buffer = new TextBuffer("first\nsecond\nthird\nlast") - - describe "when the endRow is less than the last row of the buffer", -> - it "deletes the specified rows", -> - buffer.deleteRows(1, 2) - expect(buffer.getText()).toBe "first\nlast" - buffer.deleteRows(0, 0) - expect(buffer.getText()).toBe "last" - - describe "when the endRow is the last row of the buffer", -> - it "deletes the specified rows", -> - buffer.deleteRows(2, 3) - expect(buffer.getText()).toBe "first\nsecond" - buffer.deleteRows(0, 1) - expect(buffer.getText()).toBe "" - - it "clips the given row range", -> - buffer.deleteRows(-1, 0) - expect(buffer.getText()).toBe "second\nthird\nlast" - buffer.deleteRows(1, 5) - expect(buffer.getText()).toBe "second" - - buffer.deleteRows(-2, -1) - expect(buffer.getText()).toBe "second" - buffer.deleteRows(1, 2) - expect(buffer.getText()).toBe "second" - - it "handles out of order row ranges", -> - buffer.deleteRows(2, 1) - expect(buffer.getText()).toBe "first\nlast" - - describe "::getText()", -> - it "returns the contents of the buffer as a single string", -> - buffer = new TextBuffer("hello\nworld\r\nhow are you?") - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you?" - buffer.setTextInRange([[1, 0], [1, 5]], "mom") - expect(buffer.getText()).toBe "hello\nmom\r\nhow are you?" - - describe "::undo() and ::redo()", -> - beforeEach -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - - it "undoes and redoes multiple changes", -> - buffer.setTextInRange([[0, 5], [0, 5]], " there") - buffer.setTextInRange([[1, 0], [1, 5]], "friend") - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - buffer.redo() - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - it "clears the redo stack upon a fresh change", -> - buffer.setTextInRange([[0, 5], [0, 5]], " there") - buffer.setTextInRange([[1, 0], [1, 5]], "friend") - expect(buffer.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.setTextInRange([[1, 3], [1, 5]], "m") - expect(buffer.getText()).toBe "hello there\nworm\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello there\nworm\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello there\nworld\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "does not allow the undo stack to grow without bound", -> - buffer = new TextBuffer(maxUndoEntries: 12) - - # Each transaction is treated as a single undo entry. We can undo up - # to 12 of them. - buffer.setText("") - buffer.clearUndoStack() - for i in [0...13] - buffer.transact -> - buffer.append(String(i)) - buffer.append("\n") - expect(buffer.getLineCount()).toBe 14 - - undoCount = 0 - undoCount++ while buffer.undo() - expect(undoCount).toBe 12 - expect(buffer.getText()).toBe '0\n' - - describe "::createMarkerSnapshot", -> - markerLayers = null - - beforeEach -> - buffer = new TextBuffer - - markerLayers = [ - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true) - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true) - ] - - describe "when selectionsMarkerLayer is not passed", -> - it "takes a snapshot of all markerLayers", -> - snapshot = buffer.createMarkerSnapshot() - markerLayerIdsInSnapshot = Object.keys(snapshot) - expect(markerLayerIdsInSnapshot.length).toBe(4) - expect(markerLayers[0].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[1].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[2].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[3].id in markerLayerIdsInSnapshot).toBe(true) - - describe "when selectionsMarkerLayer is passed", -> - it "skips snapshotting of other 'selection' role marker layers", -> - snapshot = buffer.createMarkerSnapshot(markerLayers[0]) - markerLayerIdsInSnapshot = Object.keys(snapshot) - expect(markerLayerIdsInSnapshot.length).toBe(3) - expect(markerLayers[0].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[1].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[2].id in markerLayerIdsInSnapshot).toBe(false) - expect(markerLayers[3].id in markerLayerIdsInSnapshot).toBe(true) - - snapshot = buffer.createMarkerSnapshot(markerLayers[2]) - markerLayerIdsInSnapshot = Object.keys(snapshot) - expect(markerLayerIdsInSnapshot.length).toBe(3) - expect(markerLayers[0].id in markerLayerIdsInSnapshot).toBe(false) - expect(markerLayers[1].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[2].id in markerLayerIdsInSnapshot).toBe(true) - expect(markerLayers[3].id in markerLayerIdsInSnapshot).toBe(true) - - describe "selective snapshotting and restoration on transact/undo/redo for selections marker layer", -> - [markerLayers, marker0, marker1, marker2, textUndo, textRedo, rangesBefore, rangesAfter] = [] - ensureMarkerLayer = (markerLayer, range) -> - markers = markerLayer.findMarkers({}) - expect(markers.length).toBe(1) - expect(markers[0].getRange()).toEqual(range) - - getFirstMarker = (markerLayer) -> - markerLayer.findMarkers({})[0] - - beforeEach -> - buffer = new TextBuffer(text: "00000000\n11111111\n22222222\n33333333\n") - - markerLayers = [ - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - buffer.addMarkerLayer(maintainHistory: true, role: "selections") - ] - - textUndo = "00000000\n11111111\n22222222\n33333333\n" - textRedo = "00000000\n11111111\n22222222\n33333333\n44444444\n" - - rangesBefore = [ - [[0, 1], [0, 1]] - [[0, 2], [0, 2]] - [[0, 3], [0, 3]] - ] - rangesAfter = [ - [[2, 1], [2, 1]] - [[2, 2], [2, 2]] - [[2, 3], [2, 3]] - ] - - marker0 = markerLayers[0].markRange(rangesBefore[0]) - marker1 = markerLayers[1].markRange(rangesBefore[1]) - marker2 = markerLayers[2].markRange(rangesBefore[2]) - - it "restores a snapshot from other selections marker layers on undo/redo", -> - # Snapshot is taken for markerLayers[0] only, markerLayer[1] and markerLayer[2] are skipped - buffer.transact {selectionsMarkerLayer: markerLayers[0]}, -> - buffer.append("44444444\n") - marker0.setRange(rangesAfter[0]) - marker1.setRange(rangesAfter[1]) - marker2.setRange(rangesAfter[2]) - - buffer.undo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesBefore[0]) - ensureMarkerLayer(markerLayers[1], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - buffer.redo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textRedo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - buffer.undo({selectionsMarkerLayer: markerLayers[1]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[0]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).not.toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - expect(marker1.isDestroyed()).toBe(true) - - buffer.redo({selectionsMarkerLayer: markerLayers[2]}) - expect(buffer.getText()).toBe(textRedo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[0]) - ensureMarkerLayer(markerLayers[2], rangesAfter[0]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).not.toBe(marker1) - expect(getFirstMarker(markerLayers[2])).not.toBe(marker2) - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(true) - - buffer.undo({selectionsMarkerLayer: markerLayers[2]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[0]) - ensureMarkerLayer(markerLayers[2], rangesBefore[0]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).not.toBe(marker1) - expect(getFirstMarker(markerLayers[2])).not.toBe(marker2) - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(true) - - it "can restore a snapshot taken at a destroyed selections marker layer given selectionsMarkerLayer", -> - buffer.transact {selectionsMarkerLayer: markerLayers[1]}, -> - buffer.append("44444444\n") - marker0.setRange(rangesAfter[0]) - marker1.setRange(rangesAfter[1]) - marker2.setRange(rangesAfter[2]) - - markerLayers[1].destroy() - expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeTruthy() - expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeTruthy() - expect(marker0.isDestroyed()).toBe(false) - expect(marker1.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(false) - - buffer.undo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesBefore[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(marker0.isDestroyed()).toBe(true) - expect(marker2.isDestroyed()).toBe(false) - - buffer.redo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textRedo) - ensureMarkerLayer(markerLayers[0], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - - markerLayers[3] = markerLayers[2].copy() - ensureMarkerLayer(markerLayers[3], rangesAfter[2]) - markerLayers[0].destroy() - markerLayers[2].destroy() - expect(buffer.getMarkerLayer(markerLayers[0].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[1].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[2].id)).toBeFalsy() - expect(buffer.getMarkerLayer(markerLayers[3].id)).toBeTruthy() - - buffer.undo({selectionsMarkerLayer: markerLayers[3]}) - expect(buffer.getText()).toBe(textUndo) - ensureMarkerLayer(markerLayers[3], rangesBefore[1]) - buffer.redo({selectionsMarkerLayer: markerLayers[3]}) - expect(buffer.getText()).toBe(textRedo) - ensureMarkerLayer(markerLayers[3], rangesAfter[1]) - - it "falls back to normal behavior when the snaphot includes multiple layerSnapshots of selections marker layers", -> - # Transact without selectionsMarkerLayer. - # Taken snapshot includes layerSnapshot of markerLayer[0], markerLayer[1] and markerLayer[2] - buffer.transact -> - buffer.append("44444444\n") - marker0.setRange(rangesAfter[0]) - marker1.setRange(rangesAfter[1]) - marker2.setRange(rangesAfter[2]) - - buffer.undo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textUndo) - - ensureMarkerLayer(markerLayers[0], rangesBefore[0]) - ensureMarkerLayer(markerLayers[1], rangesBefore[1]) - ensureMarkerLayer(markerLayers[2], rangesBefore[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - buffer.redo({selectionsMarkerLayer: markerLayers[0]}) - expect(buffer.getText()).toBe(textRedo) - - ensureMarkerLayer(markerLayers[0], rangesAfter[0]) - ensureMarkerLayer(markerLayers[1], rangesAfter[1]) - ensureMarkerLayer(markerLayers[2], rangesAfter[2]) - expect(getFirstMarker(markerLayers[0])).toBe(marker0) - expect(getFirstMarker(markerLayers[1])).toBe(marker1) - expect(getFirstMarker(markerLayers[2])).toBe(marker2) - - describe "selections marker layer's selective snapshotting on createCheckpoint, groupChangesSinceCheckpoint", -> - it "skips snapshotting of other marker layers with the same role as the selectionsMarkerLayer", -> - eventHandler = jasmine.createSpy('eventHandler') - - args = [] - spyOn(buffer, 'createMarkerSnapshot').and.callFake (arg) -> args.push(arg) - - checkpoint1 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[0]}) - checkpoint2 = buffer.createCheckpoint() - checkpoint3 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[2]}) - checkpoint4 = buffer.createCheckpoint({selectionsMarkerLayer: markerLayers[1]}) - expect(args).toEqual([ - markerLayers[0], - undefined, - markerLayers[2], - markerLayers[1], - ]) - - buffer.groupChangesSinceCheckpoint(checkpoint4, {selectionsMarkerLayer: markerLayers[0]}) - buffer.groupChangesSinceCheckpoint(checkpoint3, {selectionsMarkerLayer: markerLayers[2]}) - buffer.groupChangesSinceCheckpoint(checkpoint2) - buffer.groupChangesSinceCheckpoint(checkpoint1, {selectionsMarkerLayer: markerLayers[1]}) - expect(args).toEqual([ - markerLayers[0], - undefined, - markerLayers[2], - markerLayers[1], - - markerLayers[0], - markerLayers[2], - undefined, - markerLayers[1], - ]) - - describe "transactions", -> - now = null - - beforeEach -> - now = 0 - spyOn(Date, 'now').and.callFake -> now - - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - buffer.setTextInRange([[1, 3], [1, 5]], 'ms') - - describe "::transact(groupingInterval, fn)", -> - it "groups all operations in the given function in a single transaction", -> - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.transact -> - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "halts execution of the function if the transaction is aborted", -> - innerContinued = false - outerContinued = false - - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.transact -> - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - buffer.abortTransaction() - innerContinued = true - outerContinued = true - - expect(innerContinued).toBe false - expect(outerContinued).toBe true - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you doing?" - - it "groups all operations performed within the given function into a single undo/redo operation", -> - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - - # subsequent changes are not included in the transaction - buffer.setTextInRange([[1, 0], [1, 0]], "little ") - buffer.undo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - - # this should undo all changes in the transaction - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - # previous changes are not included in the transaction - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - # this should redo all changes in the transaction - buffer.redo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you digging?" - - # this should redo the change following the transaction - buffer.redo() - expect(buffer.getText()).toBe "hey\nlittle worms\r\nhow are you digging?" - - it "does not push the transaction to the undo stack if it is empty", -> - buffer.transact -> - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - buffer.transact -> buffer.abortTransaction() - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "halts execution undoes all operations since the beginning of the transaction if ::abortTransaction() is called", -> - continuedPastAbort = false - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - buffer.abortTransaction() - continuedPastAbort = true - - expect(continuedPastAbort).toBe false - - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - it "preserves the redo stack until a content change occurs", -> - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - # no changes occur in this transaction before aborting - buffer.transact -> - buffer.markRange([[0, 0], [0, 5]]) - buffer.abortTransaction() - buffer.setTextInRange([[0, 0], [0, 5]], "hey") - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.transact -> - buffer.setTextInRange([[0, 0], [0, 5]], "hey") - buffer.abortTransaction() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "allows nested transactions", -> - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.transact -> - buffer.setTextInRange([[0, 2], [0, 5]], "y") - buffer.transact -> - buffer.setTextInRange([[2, 13], [2, 14]], "igg") - buffer.setTextInRange([[2, 18], [2, 19]], "'") - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you diggin'?" - buffer.undo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you doing?" - buffer.redo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you diggin'?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "hey\nworms\r\nhow are you diggin'?" - - buffer.undo() - buffer.undo() - expect(buffer.getText()).toBe "hello\nworld\r\nhow are you doing?" - - it "groups adjacent transactions within each other's grouping intervals", -> - now += 1000 - buffer.transact 101, -> buffer.setTextInRange([[0, 2], [0, 5]], "y") - - now += 100 - buffer.transact 201, -> buffer.setTextInRange([[0, 3], [0, 3]], "yy") - - now += 200 - buffer.transact 201, -> buffer.setTextInRange([[0, 5], [0, 5]], "yy") - - # not grouped because the previous transaction's grouping interval - # is only 200ms and we've advanced 300ms - now += 300 - buffer.transact 301, -> buffer.setTextInRange([[0, 7], [0, 7]], "!!") - - expect(buffer.getText()).toBe "heyyyyy!!\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "heyyyyy\nworms\r\nhow are you doing?" - - buffer.undo() - expect(buffer.getText()).toBe "hello\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "heyyyyy\nworms\r\nhow are you doing?" - - buffer.redo() - expect(buffer.getText()).toBe "heyyyyy!!\nworms\r\nhow are you doing?" - - it "allows undo/redo within transactions, but not beyond the start of the containing transaction", -> - buffer.setText("") - buffer.markPosition([0, 0]) - - buffer.append("a") - - buffer.transact -> - buffer.append("b") - buffer.transact -> buffer.append("c") - buffer.append("d") - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "abc" - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "ab" - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "a" - - expect(buffer.undo()).toBe false - expect(buffer.getText()).toBe "a" - - expect(buffer.redo()).toBe true - expect(buffer.getText()).toBe "ab" - - expect(buffer.redo()).toBe true - expect(buffer.getText()).toBe "abc" - - expect(buffer.redo()).toBe true - expect(buffer.getText()).toBe "abcd" - - expect(buffer.redo()).toBe false - expect(buffer.getText()).toBe "abcd" - - expect(buffer.undo()).toBe true - expect(buffer.getText()).toBe "a" - - it "does not error if the buffer is destroyed in a change callback within the transaction", -> - buffer.onDidChange -> buffer.destroy() - result = buffer.transact -> - buffer.append('!') - 'hi' - expect(result).toBe('hi') - - describe "checkpoints", -> - beforeEach -> - buffer = new TextBuffer - - describe "::getChangesSinceCheckpoint(checkpoint)", -> - it "returns a list of changes that have been made since the checkpoint", -> - buffer.setText('abc\ndef\nghi\njkl\n') - buffer.append("mno\n") - checkpoint = buffer.createCheckpoint() - buffer.transact -> - buffer.append('pqr\n') - buffer.append('stu\n') - buffer.append('vwx\n') - buffer.setTextInRange([[1, 0], [1, 2]], 'yz') - - expect(buffer.getText()).toBe 'abc\nyzf\nghi\njkl\nmno\npqr\nstu\nvwx\n' - assertChangesEqual(buffer.getChangesSinceCheckpoint(checkpoint), [ - { - oldRange: [[1, 0], [1, 2]], - newRange: [[1, 0], [1, 2]], - oldText: "de", - newText: "yz", - }, - { - oldRange: [[5, 0], [5, 0]], - newRange: [[5, 0], [8, 0]], - oldText: "", - newText: "pqr\nstu\nvwx\n", - } - ]) - - it "returns an empty list of changes when no change has been made since the checkpoint", -> - checkpoint = buffer.createCheckpoint() - expect(buffer.getChangesSinceCheckpoint(checkpoint)).toEqual [] - - it "returns an empty list of changes when the checkpoint doesn't exist", -> - buffer.transact -> - buffer.append('abc\n') - buffer.append('def\n') - buffer.append('ghi\n') - expect(buffer.getChangesSinceCheckpoint(-1)).toEqual [] - - describe "::revertToCheckpoint(checkpoint)", -> - it "undoes all changes following the checkpoint", -> - buffer.append("hello") - checkpoint = buffer.createCheckpoint() - - buffer.transact -> - buffer.append("\n") - buffer.append("world") - - buffer.append("\n") - buffer.append("how are you?") - - result = buffer.revertToCheckpoint(checkpoint) - expect(result).toBe(true) - expect(buffer.getText()).toBe("hello") - - buffer.redo() - expect(buffer.getText()).toBe("hello") - - describe "::groupChangesSinceCheckpoint(checkpoint)", -> - it "combines all changes since the checkpoint into a single transaction", -> - historyLayer = buffer.addMarkerLayer(maintainHistory: true) - - buffer.append("one\n") - marker = historyLayer.markRange([[0, 1], [0, 2]]) - marker.setProperties(a: 'b') - - checkpoint = buffer.createCheckpoint() - buffer.append("two\n") - buffer.transact -> - buffer.append("three\n") - buffer.append("four") - - marker.setRange([[0, 1], [2, 3]]) - marker.setProperties(a: 'c') - result = buffer.groupChangesSinceCheckpoint(checkpoint) - - expect(result).toBeTruthy() - expect(buffer.getText()).toBe """ - one - two - three - four - """ - expect(marker.getRange()).toEqual [[0, 1], [2, 3]] - expect(marker.getProperties()).toEqual {a: 'c'} - - buffer.undo() - expect(buffer.getText()).toBe("one\n") - expect(marker.getRange()).toEqual [[0, 1], [0, 2]] - expect(marker.getProperties()).toEqual {a: 'b'} - - buffer.redo() - expect(buffer.getText()).toBe """ - one - two - three - four - """ - expect(marker.getRange()).toEqual [[0, 1], [2, 3]] - expect(marker.getProperties()).toEqual {a: 'c'} - - it "skips any later checkpoints when grouping changes", -> - buffer.append("one\n") - checkpoint = buffer.createCheckpoint() - buffer.append("two\n") - checkpoint2 = buffer.createCheckpoint() - buffer.append("three") - - buffer.groupChangesSinceCheckpoint(checkpoint) - expect(buffer.revertToCheckpoint(checkpoint2)).toBe(false) - - expect(buffer.getText()).toBe """ - one - two - three - """ - - buffer.undo() - expect(buffer.getText()).toBe("one\n") - - buffer.redo() - expect(buffer.getText()).toBe """ - one - two - three - """ - - it "does nothing when no changes have been made since the checkpoint", -> - buffer.append("one\n") - checkpoint = buffer.createCheckpoint() - result = buffer.groupChangesSinceCheckpoint(checkpoint) - expect(result).toBeTruthy() - buffer.undo() - expect(buffer.getText()).toBe "" - - it "returns false and does nothing when the checkpoint is not in the buffer's history", -> - buffer.append("hello\n") - checkpoint = buffer.createCheckpoint() - buffer.undo() - buffer.append("world") - result = buffer.groupChangesSinceCheckpoint(checkpoint) - expect(result).toBeFalsy() - buffer.undo() - expect(buffer.getText()).toBe "" - - it "skips checkpoints when undoing", -> - buffer.append("hello") - buffer.createCheckpoint() - buffer.createCheckpoint() - buffer.createCheckpoint() - buffer.undo() - expect(buffer.getText()).toBe("") - - it "preserves checkpoints across undo and redo", -> - buffer.append("a") - buffer.append("b") - checkpoint1 = buffer.createCheckpoint() - buffer.append("c") - checkpoint2 = buffer.createCheckpoint() - - buffer.undo() - expect(buffer.getText()).toBe("ab") - - buffer.redo() - expect(buffer.getText()).toBe("abc") - - buffer.append("d") - - expect(buffer.revertToCheckpoint(checkpoint2)).toBe true - expect(buffer.getText()).toBe("abc") - expect(buffer.revertToCheckpoint(checkpoint1)).toBe true - expect(buffer.getText()).toBe("ab") - - it "handles checkpoints created when there have been no changes", -> - checkpoint = buffer.createCheckpoint() - buffer.undo() - buffer.append("hello") - buffer.revertToCheckpoint(checkpoint) - expect(buffer.getText()).toBe("") - - it "returns false when the checkpoint is not in the buffer's history", -> - buffer.append("hello\n") - checkpoint = buffer.createCheckpoint() - buffer.undo() - buffer.append("world") - expect(buffer.revertToCheckpoint(checkpoint)).toBe(false) - expect(buffer.getText()).toBe("world") - - it "does not allow changes based on checkpoints outside of the current transaction", -> - checkpoint = buffer.createCheckpoint() - - buffer.append("a") - - buffer.transact -> - expect(buffer.revertToCheckpoint(checkpoint)).toBe false - expect(buffer.getText()).toBe "a" - - buffer.append("b") - - expect(buffer.groupChangesSinceCheckpoint(checkpoint)).toBeFalsy() - - buffer.undo() - expect(buffer.getText()).toBe "a" - - describe "::groupLastChanges()", -> - it "groups the last two changes into a single transaction", -> - buffer = new TextBuffer() - layer = buffer.addMarkerLayer({maintainHistory: true}) - - buffer.append('a') - - # Group two transactions, ensure before/after markers snapshots are preserved - marker = layer.markPosition([0, 0]) - buffer.transact -> - buffer.append('b') - buffer.createCheckpoint() - buffer.transact -> - buffer.append('ccc') - marker.setHeadPosition([0, 2]) - - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(marker.getHeadPosition()).toEqual([0, 0]) - expect(buffer.getText()).toBe('a') - buffer.redo() - expect(marker.getHeadPosition()).toEqual([0, 2]) - buffer.undo() - - # Group two bare changes - buffer.transact -> - buffer.append('b') - buffer.createCheckpoint() - buffer.append('c') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - # Group a transaction with a bare change - buffer.transact -> - buffer.transact -> - buffer.append('b') - buffer.append('c') - buffer.append('d') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - # Group a bare change with a transaction - buffer.transact -> - buffer.append('b') - buffer.transact -> - buffer.append('c') - buffer.append('d') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - # Can't group past the beginning of an open transaction - buffer.transact -> - expect(buffer.groupLastChanges()).toBe(false) - buffer.append('b') - expect(buffer.groupLastChanges()).toBe(false) - buffer.append('c') - expect(buffer.groupLastChanges()).toBe(true) - buffer.undo() - expect(buffer.getText()).toBe('a') - - describe "::setHistoryProvider(provider)", -> - it "replaces the currently active history provider with the passed one", -> - buffer = new TextBuffer({text: ''}) - buffer.insert([0, 0], 'Lorem ') - buffer.insert([0, 6], 'ipsum ') - expect(buffer.getText()).toBe('Lorem ipsum ') - - buffer.undo() - expect(buffer.getText()).toBe('Lorem ') - - buffer.setHistoryProvider(new DefaultHistoryProvider(buffer)) - buffer.undo() - expect(buffer.getText()).toBe('Lorem ') - - buffer.insert([0, 6], 'dolor ') - expect(buffer.getText()).toBe('Lorem dolor ') - - buffer.undo() - expect(buffer.getText()).toBe('Lorem ') - - describe "::getHistory(maxEntries) and restoreDefaultHistoryProvider(history)", -> - it "returns a base text and the state of the last `maxEntries` entries in the undo and redo stacks", -> - buffer = new TextBuffer({text: ''}) - markerLayer = buffer.addMarkerLayer({maintainHistory: true}) - - buffer.append('Lorem ') - buffer.append('ipsum ') - buffer.append('dolor ') - markerLayer.markPosition([0, 2]) - markersSnapshotAtCheckpoint1 = buffer.createMarkerSnapshot() - checkpoint1 = buffer.createCheckpoint() - buffer.append('sit ') - buffer.append('amet ') - buffer.append('consecteur ') - markerLayer.markPosition([0, 4]) - markersSnapshotAtCheckpoint2 = buffer.createMarkerSnapshot() - checkpoint2 = buffer.createCheckpoint() - buffer.append('adipiscit ') - buffer.append('elit ') - buffer.undo() - buffer.undo() - buffer.undo() - - history = buffer.getHistory(3) - expect(history.baseText).toBe('Lorem ipsum dolor ') - expect(history.nextCheckpointId).toBe(buffer.createCheckpoint()) - expect(history.undoStack).toEqual([ - { - type: 'checkpoint', - id: checkpoint1, - markers: markersSnapshotAtCheckpoint1 - }, - { - type: 'transaction', - changes: [{oldStart: Point(0, 18), oldEnd: Point(0, 18), newStart: Point(0, 18), newEnd: Point(0, 22), oldText: '', newText: 'sit '}], - markersBefore: markersSnapshotAtCheckpoint1, - markersAfter: markersSnapshotAtCheckpoint1 - }, - { - type: 'transaction', - changes: [{oldStart: Point(0, 22), oldEnd: Point(0, 22), newStart: Point(0, 22), newEnd: Point(0, 27), oldText: '', newText: 'amet '}], - markersBefore: markersSnapshotAtCheckpoint1, - markersAfter: markersSnapshotAtCheckpoint1 - } - ]) - expect(history.redoStack).toEqual([ - { - type: 'transaction', - changes: [{oldStart: Point(0, 38), oldEnd: Point(0, 38), newStart: Point(0, 38), newEnd: Point(0, 48), oldText: '', newText: 'adipiscit '}], - markersBefore: markersSnapshotAtCheckpoint2, - markersAfter: markersSnapshotAtCheckpoint2 - }, - { - type: 'checkpoint', - id: checkpoint2, - markers: markersSnapshotAtCheckpoint2 - }, - { - type: 'transaction', - changes: [{oldStart: Point(0, 27), oldEnd: Point(0, 27), newStart: Point(0, 27), newEnd: Point(0, 38), oldText: '', newText: 'consecteur '}], - markersBefore: markersSnapshotAtCheckpoint1, - markersAfter: markersSnapshotAtCheckpoint1 - } - ]) - - buffer.createCheckpoint() - buffer.append('x') - buffer.undo() - buffer.clearUndoStack() - - expect(buffer.getHistory()).not.toEqual(history) - buffer.restoreDefaultHistoryProvider(history) - expect(buffer.getHistory()).toEqual(history) - - it "throws an error when called within a transaction", -> - buffer = new TextBuffer() - expect(-> - buffer.transact(-> buffer.getHistory(3)) - ).toThrowError() - - describe "::getTextInRange(range)", -> - it "returns the text in a given range", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect(buffer.getTextInRange([[1, 1], [1, 4]])).toBe "orl" - expect(buffer.getTextInRange([[0, 3], [2, 3]])).toBe "lo\nworld\r\nhow" - expect(buffer.getTextInRange([[0, 0], [2, 18]])).toBe buffer.getText() - - it "clips the given range", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect(buffer.getTextInRange([[-100, -100], [100, 100]])).toBe buffer.getText() - - describe "::clipPosition(position)", -> - it "returns a valid position closest to the given position", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect(buffer.clipPosition([-1, -1])).toEqual [0, 0] - expect(buffer.clipPosition([-1, 2])).toEqual [0, 0] - expect(buffer.clipPosition([0, -1])).toEqual [0, 0] - expect(buffer.clipPosition([0, 20])).toEqual [0, 5] - expect(buffer.clipPosition([1, -1])).toEqual [1, 0] - expect(buffer.clipPosition([1, 20])).toEqual [1, 5] - expect(buffer.clipPosition([10, 0])).toEqual [2, 18] - expect(buffer.clipPosition([Infinity, 0])).toEqual [2, 18] - - it "throws an error when given an invalid point", -> - buffer = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - expect -> buffer.clipPosition([NaN, 1]) - .toThrowError("Invalid Point: (NaN, 1)") - expect -> buffer.clipPosition([0, NaN]) - .toThrowError("Invalid Point: (0, NaN)") - expect -> buffer.clipPosition([0, {}]) - .toThrowError("Invalid Point: (0, [object Object])") - - describe "::characterIndexForPosition(position)", -> - beforeEach -> - buffer = new TextBuffer(text: "zero\none\r\ntwo\nthree") - - it "returns the absolute character offset for the given position", -> - expect(buffer.characterIndexForPosition([0, 0])).toBe 0 - expect(buffer.characterIndexForPosition([0, 1])).toBe 1 - expect(buffer.characterIndexForPosition([0, 4])).toBe 4 - expect(buffer.characterIndexForPosition([1, 0])).toBe 5 - expect(buffer.characterIndexForPosition([1, 1])).toBe 6 - expect(buffer.characterIndexForPosition([1, 3])).toBe 8 - expect(buffer.characterIndexForPosition([2, 0])).toBe 10 - expect(buffer.characterIndexForPosition([2, 1])).toBe 11 - expect(buffer.characterIndexForPosition([3, 0])).toBe 14 - expect(buffer.characterIndexForPosition([3, 5])).toBe 19 - - it "clips the given position before translating", -> - expect(buffer.characterIndexForPosition([-1, -1])).toBe 0 - expect(buffer.characterIndexForPosition([1, 100])).toBe 8 - expect(buffer.characterIndexForPosition([100, 100])).toBe 19 - - describe "::positionForCharacterIndex(offset)", -> - beforeEach -> - buffer = new TextBuffer(text: "zero\none\r\ntwo\nthree") - - it "returns the position for the given absolute character offset", -> - expect(buffer.positionForCharacterIndex(0)).toEqual [0, 0] - expect(buffer.positionForCharacterIndex(1)).toEqual [0, 1] - expect(buffer.positionForCharacterIndex(4)).toEqual [0, 4] - expect(buffer.positionForCharacterIndex(5)).toEqual [1, 0] - expect(buffer.positionForCharacterIndex(6)).toEqual [1, 1] - expect(buffer.positionForCharacterIndex(8)).toEqual [1, 3] - expect(buffer.positionForCharacterIndex(10)).toEqual [2, 0] - expect(buffer.positionForCharacterIndex(11)).toEqual [2, 1] - expect(buffer.positionForCharacterIndex(14)).toEqual [3, 0] - expect(buffer.positionForCharacterIndex(19)).toEqual [3, 5] - - it "clips the given offset before translating", -> - expect(buffer.positionForCharacterIndex(-1)).toEqual [0, 0] - expect(buffer.positionForCharacterIndex(20)).toEqual [3, 5] - - describe "serialization", -> - expectSameMarkers = (left, right) -> - markers1 = left.getMarkers().sort (a, b) -> a.compare(b) - markers2 = right.getMarkers().sort (a, b) -> a.compare(b) - expect(markers1.length).toBe markers2.length - for marker1, i in markers1 - expect(marker1).toEqual(markers2[i]) - return - - it "can serialize / deserialize the buffer along with its history, marker layers, and display layers", (done) -> - bufferA = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - displayLayer1A = bufferA.addDisplayLayer() - displayLayer2A = bufferA.addDisplayLayer() - displayLayer1A.foldBufferRange([[0, 1], [0, 3]]) - displayLayer2A.foldBufferRange([[0, 0], [0, 2]]) - bufferA.createCheckpoint() - bufferA.setTextInRange([[0, 5], [0, 5]], " there") - bufferA.transact -> bufferA.setTextInRange([[1, 0], [1, 5]], "friend") - layerA = bufferA.addMarkerLayer(maintainHistory: true, persistent: true) - layerA.markRange([[0, 6], [0, 8]], reversed: true, foo: 1) - layerB = bufferA.addMarkerLayer(maintainHistory: true, persistent: true, role: "selections") - marker2A = bufferA.markPosition([2, 2], bar: 2) - bufferA.transact -> - bufferA.setTextInRange([[1, 0], [1, 0]], "good ") - bufferA.append("?") - marker2A.setProperties(bar: 3, baz: 4) - layerA.markRange([[0, 4], [0, 5]], invalidate: 'inside') - bufferA.setTextInRange([[0, 5], [0, 5]], "oo") - bufferA.undo() - - state = JSON.parse(JSON.stringify(bufferA.serialize())) - TextBuffer.deserialize(state).then (bufferB) -> - expect(bufferB.getText()).toBe "hello there\ngood friend\r\nhow are you doing??" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - expect(bufferB.getDisplayLayer(displayLayer1A.id).foldsIntersectingBufferRange([[0, 1], [0, 3]]).length).toBe(1) - expect(bufferB.getDisplayLayer(displayLayer2A.id).foldsIntersectingBufferRange([[0, 0], [0, 2]]).length).toBe(1) - displayLayer3B = bufferB.addDisplayLayer() - expect(displayLayer3B.id).toBeGreaterThan(displayLayer1A.id) - expect(displayLayer3B.id).toBeGreaterThan(displayLayer2A.id) - - expect(bufferB.getMarkerLayer(layerB.id).getRole()).toBe "selections" - expect(bufferB.selectionsMarkerLayerIds.has(layerB.id)).toBe true - expect(bufferB.selectionsMarkerLayerIds.size).toBe 1 - - bufferA.redo() - bufferB.redo() - expect(bufferB.getText()).toBe "hellooo there\ngood friend\r\nhow are you doing??" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - expect(bufferB.getMarkerLayer(layerA.id).maintainHistory).toBe true - expect(bufferB.getMarkerLayer(layerA.id).persistent).toBe true - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello there\ngood friend\r\nhow are you doing??" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello there\nfriend\r\nhow are you doing?" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello there\nworld\r\nhow are you doing?" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - bufferA.undo() - bufferB.undo() - expect(bufferB.getText()).toBe "hello\nworld\r\nhow are you doing?" - expectSameMarkers(bufferB.getMarkerLayer(layerA.id), layerA) - - # Accounts for deserialized markers when selecting the next marker's id - marker3A = layerA.markRange([[0, 1], [2, 3]]) - marker3B = bufferB.getMarkerLayer(layerA.id).markRange([[0, 1], [2, 3]]) - expect(marker3B.id).toBe marker3A.id - - # Doesn't try to reload the buffer since it has no file. - setTimeout(-> - expect(bufferB.getText()).toBe "hello\nworld\r\nhow are you doing?" - done() - , 50) - - it "serializes / deserializes the buffer's persistent custom marker layers", (done) -> - bufferA = new TextBuffer("abcdefghijklmnopqrstuvwxyz") - - layer1A = bufferA.addMarkerLayer() - layer2A = bufferA.addMarkerLayer(persistent: true) - - layer1A.markRange([[0, 1], [0, 2]]) - layer1A.markRange([[0, 3], [0, 4]]) - - layer2A.markRange([[0, 5], [0, 6]]) - layer2A.markRange([[0, 7], [0, 8]]) - - TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then (bufferB) -> - layer1B = bufferB.getMarkerLayer(layer1A.id) - layer2B = bufferB.getMarkerLayer(layer2A.id) - expect(layer2B.persistent).toBe true - - expect(layer1B).toBe undefined - expectSameMarkers(layer2A, layer2B) - done() - - it "doesn't serialize the default marker layer", (done) -> - bufferA = new TextBuffer(text: "hello\nworld\r\nhow are you doing?") - markerLayerA = bufferA.getDefaultMarkerLayer() - marker1A = bufferA.markRange([[0, 1], [1, 2]], foo: 1) - - TextBuffer.deserialize(bufferA.serialize()).then (bufferB) -> - markerLayerB = bufferB.getDefaultMarkerLayer() - expect(bufferB.getMarker(marker1A.id)).toBeUndefined() - done() - - it "doesn't attempt to serialize snapshots for destroyed marker layers", -> - buffer = new TextBuffer(text: "abc") - markerLayer = buffer.addMarkerLayer(maintainHistory: true, persistent: true) - markerLayer.markPosition([0, 3]) - buffer.insert([0, 0], 'x') - markerLayer.destroy() - - expect(-> buffer.serialize()).not.toThrowError() - - it "doesn't remember marker layers when calling serialize with {markerLayers: false}", (done) -> - bufferA = new TextBuffer(text: "world") - layerA = bufferA.addMarkerLayer(maintainHistory: true) - markerA = layerA.markPosition([0, 3]) - markerB = null - bufferA.transact -> - bufferA.insert([0, 0], 'hello ') - markerB = layerA.markPosition([0, 5]) - bufferA.undo() - - TextBuffer.deserialize(bufferA.serialize({markerLayers: false})).then (bufferB) -> - expect(bufferB.getText()).toBe("world") - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined() - - bufferB.redo() - expect(bufferB.getText()).toBe("hello world") - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined() - - bufferB.undo() - expect(bufferB.getText()).toBe("world") - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(bufferB.getMarkerLayer(layerA.id)?.getMarker(markerB.id)).toBeUndefined() - done() - - it "doesn't remember history when calling serialize with {history: false}", (done) -> - bufferA = new TextBuffer(text: 'abc') - bufferA.append('def') - bufferA.append('ghi') - - TextBuffer.deserialize(bufferA.serialize({history: false})).then (bufferB) -> - expect(bufferB.getText()).toBe("abcdefghi") - expect(bufferB.undo()).toBe(false) - expect(bufferB.getText()).toBe("abcdefghi") - done() - - it "serializes / deserializes the buffer's unique identifier", (done) -> - bufferA = new TextBuffer() - TextBuffer.deserialize(JSON.parse(JSON.stringify(bufferA.serialize()))).then (bufferB) -> - expect(bufferB.getId()).toEqual(bufferA.getId()) - done() - - it "doesn't deserialize a state that was serialized with a different buffer version", (done) -> - bufferA = new TextBuffer() - serializedBuffer = JSON.parse(JSON.stringify(bufferA.serialize())) - serializedBuffer.version = 123456789 - - TextBuffer.deserialize(serializedBuffer).then (bufferB) -> - expect(bufferB).toBeUndefined() - done() - - it "doesn't deserialize a state referencing a file that no longer exists", (done) -> - tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')) - filePath = join(tempDir, 'file.txt') - fs.writeFileSync(filePath, "something\n") - - bufferA = TextBuffer.loadSync(filePath) - state = bufferA.serialize() - - fs.unlinkSync(filePath) - - state.mustExist = true - TextBuffer.deserialize(state).then( - -> expect('serialization succeeded with mustExist: true').toBeUndefined(), - (err) -> expect(err.code).toBe('ENOENT') - ).then(done, done) - - describe "when the serialized buffer was unsaved and had no path", -> - it "restores the previous unsaved state of the buffer", (done) -> - buffer = new TextBuffer() - buffer.setText("abc") - - TextBuffer.deserialize(buffer.serialize()).then (buffer2) -> - expect(buffer2.getPath()).toBeUndefined() - expect(buffer2.getText()).toBe("abc") - done() - - describe "::getRange()", -> - it "returns the range of the entire buffer text", -> - buffer = new TextBuffer("abc\ndef\nghi") - expect(buffer.getRange()).toEqual [[0, 0], [2, 3]] - - describe "::getLength()", -> - it "returns the lenght of the entire buffer text", -> - buffer = new TextBuffer("abc\ndef\nghi") - expect(buffer.getLength()).toBe("abc\ndef\nghi".length) - - describe "::rangeForRow(row, includeNewline)", -> - beforeEach -> - buffer = new TextBuffer("this\nis a test\r\ntesting") - - describe "if includeNewline is false (the default)", -> - it "returns a range from the beginning of the line to the end of the line", -> - expect(buffer.rangeForRow(0)).toEqual([[0, 0], [0, 4]]) - expect(buffer.rangeForRow(1)).toEqual([[1, 0], [1, 9]]) - expect(buffer.rangeForRow(2)).toEqual([[2, 0], [2, 7]]) - - describe "if includeNewline is true", -> - it "returns a range from the beginning of the line to the beginning of the next (if it exists)", -> - expect(buffer.rangeForRow(0, true)).toEqual([[0, 0], [1, 0]]) - expect(buffer.rangeForRow(1, true)).toEqual([[1, 0], [2, 0]]) - expect(buffer.rangeForRow(2, true)).toEqual([[2, 0], [2, 7]]) - - describe "if the given row is out of range", -> - it "returns the range of the nearest valid row", -> - expect(buffer.rangeForRow(-1)).toEqual([[0, 0], [0, 4]]) - expect(buffer.rangeForRow(10)).toEqual([[2, 0], [2, 7]]) - - describe "::onDidChangePath()", -> - [filePath, newPath, bufferToChange, eventHandler] = [] - - beforeEach -> - tempDir = fs.realpathSync(temp.mkdirSync('text-buffer')) - filePath = join(tempDir, "manipulate-me") - newPath = "#{filePath}-i-moved" - fs.writeFileSync(filePath, "") - bufferToChange = TextBuffer.loadSync(filePath) - - afterEach -> - bufferToChange.destroy() - fs.removeSync(filePath) - fs.removeSync(newPath) - - it "notifies observers when the buffer is saved to a new path", (done) -> - bufferToChange.onDidChangePath (p) -> - expect(p).toBe(newPath) - done() - bufferToChange.saveAs(newPath) - - it "notifies observers when the buffer's file is moved", (done) -> - # FIXME: This doesn't pass on Linux - if process.platform in ['linux', 'win32'] - done() - return - - bufferToChange.onDidChangePath (p) -> - expect(p).toBe(newPath) - done() - - fs.removeSync(newPath) - fs.moveSync(filePath, newPath) - - # This spec is no longer needed because `onWillThrowWatchError` is a no-op. - # `pathwatcher` can't fulfill the callback because it chooses not to reload - # the entire file every time it changes (for performance reasons), hence it - # stopped throwing this error a long time ago. - # - # (Indeed, this test is tautological, since it manually generates the event.) - xdescribe "::onWillThrowWatchError", -> - it "notifies observers when the file has a watch error", -> - filePath = temp.openSync('atom').path - fs.writeFileSync(filePath, '') - - buffer = TextBuffer.loadSync(filePath) - - eventHandler = jasmine.createSpy('eventHandler') - buffer.onWillThrowWatchError(eventHandler) - - buffer.file.emitter.emit 'will-throw-watch-error', 'arg' - expect(eventHandler).toHaveBeenCalledWith 'arg' - - describe "::getLines()", -> - it "returns an array of lines in the text contents", -> - filePath = require.resolve('./fixtures/sample.js') - fileContents = fs.readFileSync(filePath, 'utf8') - buffer = TextBuffer.loadSync(filePath) - expect(buffer.getLines().length).toBe fileContents.split("\n").length - expect(buffer.getLines().join('\n')).toBe fileContents - - describe "::setTextInRange(range, string)", -> - changeHandler = null - - beforeEach (done) -> - filePath = require.resolve('./fixtures/sample.js') - fileContents = fs.readFileSync(filePath, 'utf8') - TextBuffer.load(filePath).then (result) -> - buffer = result - changeHandler = jasmine.createSpy('changeHandler') - buffer.onDidChange changeHandler - done() - - describe "when used to insert (called with an empty range and a non-empty string)", -> - describe "when the given string has no newlines", -> - it "inserts the string at the location of the given range", -> - range = [[3, 4], [3, 4]] - buffer.setTextInRange range, "foo" - - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " foovar pivot = items.shift(), current, left = [], right = [];" - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.calls.allArgs()[0] - expect(event.oldRange).toEqual range - expect(event.newRange).toEqual [[3, 4], [3, 7]] - expect(event.oldText).toBe "" - expect(event.newText).toBe "foo" - - describe "when the given string has newlines", -> - it "inserts the lines at the location of the given range", -> - range = [[3, 4], [3, 4]] - - buffer.setTextInRange range, "foo\n\nbar\nbaz" - - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " foo" - expect(buffer.lineForRow(4)).toBe "" - expect(buffer.lineForRow(5)).toBe "bar" - expect(buffer.lineForRow(6)).toBe "bazvar pivot = items.shift(), current, left = [], right = [];" - expect(buffer.lineForRow(7)).toBe " while(items.length > 0) {" - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.calls.allArgs()[0] - expect(event.oldRange).toEqual range - expect(event.newRange).toEqual [[3, 4], [6, 3]] - expect(event.oldText).toBe "" - expect(event.newText).toBe "foo\n\nbar\nbaz" - - describe "when used to remove (called with a non-empty range and an empty string)", -> - describe "when the range is contained within a single line", -> - it "removes the characters within the range", -> - range = [[3, 4], [3, 7]] - buffer.setTextInRange range, "" - - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " pivot = items.shift(), current, left = [], right = [];" - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.calls.allArgs()[0] - expect(event.oldRange).toEqual range - expect(event.newRange).toEqual [[3, 4], [3, 4]] - expect(event.oldText).toBe "var" - expect(event.newText).toBe "" - - describe "when the range spans 2 lines", -> - it "removes the characters within the range and joins the lines", -> - range = [[3, 16], [4, 4]] - buffer.setTextInRange range, "" - - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = while(items.length > 0) {" - expect(buffer.lineForRow(4)).toBe " current = items.shift();" - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.calls.allArgs()[0] - expect(event.oldRange).toEqual range - expect(event.newRange).toEqual [[3, 16], [3, 16]] - expect(event.oldText).toBe "items.shift(), current, left = [], right = [];\n " - expect(event.newText).toBe "" - - describe "when the range spans more than 2 lines", -> - it "removes the characters within the range, joining the first and last line and removing the lines in-between", -> - buffer.setTextInRange [[3, 16], [11, 9]], "" - - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = sort(Array.apply(this, arguments));" - expect(buffer.lineForRow(4)).toBe "};" - - describe "when used to replace text with other text (called with non-empty range and non-empty string)", -> - it "replaces the old text with the new text", -> - range = [[3, 16], [11, 9]] - oldText = buffer.getTextInRange(range) - - buffer.setTextInRange range, "foo\nbar" - - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = foo" - expect(buffer.lineForRow(4)).toBe "barsort(Array.apply(this, arguments));" - expect(buffer.lineForRow(5)).toBe "};" - - expect(changeHandler).toHaveBeenCalled() - [event] = changeHandler.calls.allArgs()[0] - expect(event.oldRange).toEqual range - expect(event.newRange).toEqual [[3, 16], [4, 3]] - expect(event.oldText).toBe oldText - expect(event.newText).toBe "foo\nbar" - - it "allows a change to be undone safely from an ::onDidChange callback", -> - buffer.onDidChange -> buffer.undo() - buffer.setTextInRange([[0, 0], [0, 0]], "hello") - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - describe "::setText(text)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - describe "when the buffer contains newlines", -> - it "changes the entire contents of the buffer and emits a change event", -> - lastRow = buffer.getLastRow() - expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]] - changeHandler = jasmine.createSpy('changeHandler') - buffer.onDidChange changeHandler - - newText = "I know you are.\nBut what am I?" - buffer.setText(newText) - - expect(buffer.getText()).toBe newText - expect(changeHandler).toHaveBeenCalled() - - [event] = changeHandler.calls.allArgs()[0] - expect(event.newText).toBe newText - expect(event.oldRange).toEqual expectedPreRange - expect(event.newRange).toEqual [[0, 0], [1, 14]] - - describe "with windows newlines", -> - it "changes the entire contents of the buffer", -> - buffer = new TextBuffer("first\r\nlast") - lastRow = buffer.getLastRow() - expectedPreRange = [[0, 0], [lastRow, buffer.lineForRow(lastRow).length]] - changeHandler = jasmine.createSpy('changeHandler') - buffer.onDidChange changeHandler - - newText = "new first\r\nnew last" - buffer.setText(newText) - - expect(buffer.getText()).toBe newText - expect(changeHandler).toHaveBeenCalled() - - [event] = changeHandler.calls.allArgs()[0] - expect(event.newText).toBe newText - expect(event.oldRange).toEqual expectedPreRange - expect(event.newRange).toEqual [[0, 0], [1, 8]] - - describe "::setTextViaDiff(text)", -> - beforeEach (done) -> - filePath = require.resolve('./fixtures/sample.js') - TextBuffer.load(filePath).then (result) -> - buffer = result - done() - - it "can change the entire contents of the buffer when there are no newlines", -> - buffer.setText('BUFFER CHANGE') - newText = 'DISK CHANGE' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change a buffer that contains lone carriage returns", -> - oldText = 'one\rtwo\nthree\rfour\n' - newText = 'one\rtwo and\nthree\rfour\n' - buffer.setText(oldText) - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - buffer.undo() - expect(buffer.getText()).toBe oldText - - describe "with standard newlines", -> - it "can change the entire contents of the buffer with no newline at the end", -> - newText = "I know you are.\nBut what am I?" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change the entire contents of the buffer with a newline at the end", -> - newText = "I know you are.\nBut what am I?\n" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change a few lines at the beginning in the buffer", -> - newText = buffer.getText().replace(/function/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can change a few lines in the middle of the buffer", -> - newText = buffer.getText().replace(/shift/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "can adds a newline at the end", -> - newText = buffer.getText() + '\n' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - describe "with windows newlines", -> - beforeEach -> - buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) - - it "adds a newline at the end", -> - newText = buffer.getText() + '\r\n' - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes the entire contents of the buffer with smaller content with no newline at the end", -> - newText = "I know you are.\r\nBut what am I?" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes the entire contents of the buffer with smaller content with newline at the end", -> - newText = "I know you are.\r\nBut what am I?\r\n" - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes a few lines at the beginning in the buffer", -> - newText = buffer.getText().replace(/function/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - it "changes a few lines in the middle of the buffer", -> - newText = buffer.getText().replace(/shift/g, 'omgwow') - buffer.setTextViaDiff(newText) - expect(buffer.getText()).toBe newText - - describe "::getTextInRange(range)", -> - beforeEach (done) -> - filePath = require.resolve('./fixtures/sample.js') - TextBuffer.load(filePath).then (result) -> - buffer = result - done() - - describe "when range is empty", -> - it "returns an empty string", -> - range = [[1, 1], [1, 1]] - expect(buffer.getTextInRange(range)).toBe "" - - describe "when range spans one line", -> - it "returns characters in range", -> - range = [[2, 8], [2, 13]] - expect(buffer.getTextInRange(range)).toBe "items" - - lineLength = buffer.lineForRow(2).length - range = [[2, 0], [2, lineLength]] - expect(buffer.getTextInRange(range)).toBe " if (items.length <= 1) return items;" - - describe "when range spans multiple lines", -> - it "returns characters in range (including newlines)", -> - lineLength = buffer.lineForRow(2).length - range = [[2, 0], [3, 0]] - expect(buffer.getTextInRange(range)).toBe " if (items.length <= 1) return items;\n" - - lineLength = buffer.lineForRow(2).length - range = [[2, 10], [4, 10]] - expect(buffer.getTextInRange(range)).toBe "ems.length <= 1) return items;\n var pivot = items.shift(), current, left = [], right = [];\n while(" - - describe "when the range starts before the start of the buffer", -> - it "clips the range to the start of the buffer", -> - expect(buffer.getTextInRange([[-Infinity, -Infinity], [0, Infinity]])).toBe buffer.lineForRow(0) - - describe "when the range ends after the end of the buffer", -> - it "clips the range to the end of the buffer", -> - expect(buffer.getTextInRange([[12], [13, Infinity]])).toBe buffer.lineForRow(12) - - describe "::scan(regex, fn)", -> - beforeEach -> - buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js')) - - it "calls the given function with the information about each match", -> - matches = [] - buffer.scan /current/g, (match) -> matches.push(match) - expect(matches.length).toBe 5 - - expect(matches[0].matchText).toBe 'current' - expect(matches[0].range).toEqual [[3, 31], [3, 38]] - expect(matches[0].lineText).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(matches[0].lineTextOffset).toBe 0 - expect(matches[0].leadingContextLines.length).toBe 0 - expect(matches[0].trailingContextLines.length).toBe 0 - - expect(matches[1].matchText).toBe 'current' - expect(matches[1].range).toEqual [[5, 6], [5, 13]] - expect(matches[1].lineText).toBe ' current = items.shift();' - expect(matches[1].lineTextOffset).toBe 0 - expect(matches[1].leadingContextLines.length).toBe 0 - expect(matches[1].trailingContextLines.length).toBe 0 - - it "calls the given function with the information about each match including context lines", -> - matches = [] - buffer.scan /current/g, {leadingContextLineCount: 1, trailingContextLineCount: 2}, (match) -> matches.push(match) - expect(matches.length).toBe 5 - - expect(matches[0].matchText).toBe 'current' - expect(matches[0].range).toEqual [[3, 31], [3, 38]] - expect(matches[0].lineText).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(matches[0].lineTextOffset).toBe 0 - expect(matches[0].leadingContextLines.length).toBe 1 - expect(matches[0].leadingContextLines[0]).toBe ' if (items.length <= 1) return items;' - expect(matches[0].trailingContextLines.length).toBe 2 - expect(matches[0].trailingContextLines[0]).toBe ' while(items.length > 0) {' - expect(matches[0].trailingContextLines[1]).toBe ' current = items.shift();' - - expect(matches[1].matchText).toBe 'current' - expect(matches[1].range).toEqual [[5, 6], [5, 13]] - expect(matches[1].lineText).toBe ' current = items.shift();' - expect(matches[1].lineTextOffset).toBe 0 - expect(matches[1].leadingContextLines.length).toBe 1 - expect(matches[1].leadingContextLines[0]).toBe ' while(items.length > 0) {' - expect(matches[1].trailingContextLines.length).toBe 2 - expect(matches[1].trailingContextLines[0]).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(matches[1].trailingContextLines[1]).toBe ' }' - - describe "::backwardsScan(regex, fn)", -> - beforeEach -> - buffer = TextBuffer.loadSync(require.resolve('./fixtures/sample.js')) - - it "calls the given function with the information about each match in backwards order", -> - matches = [] - buffer.backwardsScan /current/g, (match) -> matches.push(match) - expect(matches.length).toBe 5 - - expect(matches[0].matchText).toBe 'current' - expect(matches[0].range).toEqual [[6, 56], [6, 63]] - expect(matches[0].lineText).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(matches[0].lineTextOffset).toBe 0 - expect(matches[0].leadingContextLines.length).toBe 0 - expect(matches[0].trailingContextLines.length).toBe 0 - - expect(matches[1].matchText).toBe 'current' - expect(matches[1].range).toEqual [[6, 34], [6, 41]] - expect(matches[1].lineText).toBe ' current < pivot ? left.push(current) : right.push(current);' - expect(matches[1].lineTextOffset).toBe 0 - expect(matches[1].leadingContextLines.length).toBe 0 - expect(matches[1].trailingContextLines.length).toBe 0 - - describe "::scanInRange(range, regex, fn)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - describe "when given a regex with a ignore case flag", -> - it "does a case-insensitive search", -> - matches = [] - buffer.scanInRange /cuRRent/i, [[0, 0], [12, 0]], ({match, range}) -> - matches.push(match) - expect(matches.length).toBe 1 - - describe "when given a regex with no global flag", -> - it "calls the iterator with the first match for the given regex in the given range", -> - matches = [] - ranges = [] - buffer.scanInRange /cu(rr)ent/, [[4, 0], [6, 44]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe 'current' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[5, 6], [5, 13]] - - describe "when given a regex with a global flag", -> - it "calls the iterator with each match for the given regex in the given range", -> - matches = [] - ranges = [] - buffer.scanInRange /cu(rr)ent/g, [[4, 0], [6, 59]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 3 - expect(ranges.length).toBe 3 - - expect(matches[0][0]).toBe 'current' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[5, 6], [5, 13]] - - expect(matches[1][0]).toBe 'current' - expect(matches[1][1]).toBe 'rr' - expect(ranges[1]).toEqual [[6, 6], [6, 13]] - - expect(matches[2][0]).toBe 'current' - expect(matches[2][1]).toBe 'rr' - expect(ranges[2]).toEqual [[6, 34], [6, 41]] - - describe "when the last regex match exceeds the end of the range", -> - describe "when the portion of the match within the range also matches the regex", -> - it "calls the iterator with the truncated match", -> - matches = [] - ranges = [] - buffer.scanInRange /cu(r*)/g, [[4, 0], [6, 9]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 2 - expect(ranges.length).toBe 2 - - expect(matches[0][0]).toBe 'curr' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[5, 6], [5, 10]] - - expect(matches[1][0]).toBe 'cur' - expect(matches[1][1]).toBe 'r' - expect(ranges[1]).toEqual [[6, 6], [6, 9]] - - describe "when the portion of the match within the range does not matches the regex", -> - it "does not call the iterator with the truncated match", -> - matches = [] - ranges = [] - buffer.scanInRange /cu(r*)e/g, [[4, 0], [6, 9]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe 'curre' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[5, 6], [5, 11]] - - describe "when the iterator calls the 'replace' control function with a replacement string", -> - it "replaces each occurrence of the regex match with the string", -> - ranges = [] - buffer.scanInRange /cu(rr)ent/g, [[4, 0], [6, 59]], ({range, replace}) -> - ranges.push(range) - replace("foo") - - expect(ranges[0]).toEqual [[5, 6], [5, 13]] - expect(ranges[1]).toEqual [[6, 6], [6, 13]] - expect(ranges[2]).toEqual [[6, 30], [6, 37]] - - expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' - expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);' - - it "allows the match to be replaced with the empty string", -> - buffer.scanInRange /current/g, [[4, 0], [6, 59]], ({replace}) -> - replace("") - - expect(buffer.lineForRow(5)).toBe ' = items.shift();' - expect(buffer.lineForRow(6)).toBe ' < pivot ? left.push() : right.push(current);' - - describe "when the iterator calls the 'stop' control function", -> - it "stops the traversal", -> - ranges = [] - buffer.scanInRange /cu(rr)ent/g, [[4, 0], [6, 59]], ({range, stop}) -> - ranges.push(range) - stop() if ranges.length is 2 - - expect(ranges.length).toBe 2 - - it "returns the same results as a regex match on a regular string", -> - regexps = [ - /\w+/g # 1 word - /\w+\n\s*\w+/g, # 2 words separated by an newline (escape sequence) - RegExp("\\w+\n\\s*\w+", 'g'), # 2 words separated by a newline (literal) - /\w+\s+\w+/g, # 2 words separated by some whitespace - /\w+[^\w]+\w+/g, # 2 words separated by anything - /\w+\n\s*\w+\n\s*\w+/g, # 3 words separated by newlines (escape sequence) - RegExp("\\w+\n\\s*\\w+\n\\s*\\w+", 'g'), # 3 words separated by newlines (literal) - /\w+[^\w]+\w+[^\w]+\w+/g, # 3 words separated by anything - ] - - i = 0 - while i < 20 - seed = Date.now() - random = new Random(seed) - - text = buildRandomLines(random, 40) - buffer = new TextBuffer({text}) - buffer.backwardsScanChunkSize = random.intBetween(100, 1000) - - range = getRandomBufferRange(random, buffer) - .union(getRandomBufferRange(random, buffer)) - .union(getRandomBufferRange(random, buffer)) - regex = regexps[random(regexps.length)] - - expectedMatches = buffer.getTextInRange(range).match(regex) ? [] - continue unless expectedMatches.length > 0 - i++ - - forwardRanges = [] - forwardMatches = [] - buffer.scanInRange regex, range, ({range, matchText}) -> - forwardRanges.push(range) - forwardMatches.push(matchText) - expect(forwardMatches).toEqual(expectedMatches, "Seed: #{seed}") - - backwardRanges = [] - backwardMatches = [] - buffer.backwardsScanInRange regex, range, ({range, matchText}) -> - backwardRanges.push(range) - backwardMatches.push(matchText) - expect(backwardMatches).toEqual(expectedMatches.reverse(), "Seed: #{seed}") - - it "does not return empty matches at the end of the range", -> - ranges = [] - buffer.scanInRange /[ ]*/gm, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[0, 29], [0, 29]], [[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.scanInRange /[ ]*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.scanInRange /\s*/gm, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[0, 29], [1, 2]]]) - - ranges.length = 0 - buffer.scanInRange /\s*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - it "allows empty matches at the end of a range, when the range ends at column 0", -> - ranges = [] - buffer.scanInRange /^[ ]*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^[ ]*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^\s*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[9, 0], [9, 2]], [[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^\s*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.scanInRange /^\s*/gm, [[11, 0], [12, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[11, 0], [11, 2]], [[12, 0], [12, 0]]]) - - it "handles multi-line patterns", -> - matchStrings = [] - - # The '\s' character class - buffer.scan /{\s+var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A literal newline character - matchStrings.length = 0 - buffer.scan RegExp("{\n var"), ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A '\n' escape sequence - matchStrings.length = 0 - buffer.scan /{\n var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A negated character class in the middle of the pattern - matchStrings.length = 0 - buffer.scan /{[^a] var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['{\n var']) - - # A negated character class at the beginning of the pattern - matchStrings.length = 0 - buffer.scan /[^a] var/, ({matchText}) -> matchStrings.push(matchText) - expect(matchStrings).toEqual(['\n var']) - - describe "::find(regex)", -> - it "resolves with the first range that matches the given regex", (done) -> - buffer = new TextBuffer('abc\ndefghi') - buffer.find(/\wf\w*/).then (range) -> - expect(range).toEqual(Range(Point(1, 1), Point(1, 6))) - done() - - describe "::findAllSync(regex)", -> - it "returns all the ranges that match the given regex", -> - buffer = new TextBuffer('abc\ndefghi') - expect(buffer.findAllSync(/[bf]\w+/)).toEqual([ - Range(Point(0, 1), Point(0, 3)), - Range(Point(1, 2), Point(1, 6)), - ]) - - describe "::findAndMarkAllInRangeSync(markerLayer, regex, range, options)", -> - it "populates the marker index with the matching ranges", -> - buffer = new TextBuffer('abc def\nghi jkl\n') - layer = buffer.addMarkerLayer() - markers = buffer.findAndMarkAllInRangeSync(layer, /\w+/g, [[0, 1], [1, 6]], {invalidate: 'inside'}) - expect(markers.map((marker) -> marker.getRange())).toEqual([ - [[0, 1], [0, 3]], - [[0, 4], [0, 7]], - [[1, 0], [1, 3]], - [[1, 4], [1, 6]] - ]) - expect(markers[0].getInvalidationStrategy()).toBe('inside') - expect(markers[0].isExclusive()).toBe(true) - - markers = buffer.findAndMarkAllInRangeSync(layer, /abc/g, [[0, 0], [1, 0]], {invalidate: 'touch'}) - expect(markers.map((marker) -> marker.getRange())).toEqual([ - [[0, 0], [0, 3]] - ]) - expect(markers[0].getInvalidationStrategy()).toBe('touch') - expect(markers[0].isExclusive()).toBe(false) - - describe "::findWordsWithSubsequence and ::findWordsWithSubsequenceInRange", -> - it 'resolves with all words matching the given query', (done) -> - buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana') - buffer.findWordsWithSubsequence('bna', '_', 4).then (results) -> - expected = [ - { - score: 29, - matchIndices: [0, 1, 2], - positions: [{row: 0, column: 36}], - word: "bNa" - }, - { - score: 16, - matchIndices: [0, 2, 4], - positions: [{row: 0, column: 15}], - word: "ban_ana" - }, - { - score: 12, - matchIndices: [0, 2, 3], - positions: [{row: 0, column: 0}, {row: 1, column: 0}], - word: "banana" - }, - { - score: 7, - matchIndices: [0, 5, 6], - positions: [{row: 0, column: 7}], - word: "bandana" - } - ] - # JSON serialization doesn't work properly with `SubsequenceMatch` - # results, so we use another strategy to test deep equality. - for i, result of results - for prop, value in result - expect(value).toEqual(expected[i][prop]) - done() - - it 'resolves with all words matching the given query and range', (done) -> - range = {start: {column: 0, row: 0}, end: {column: 22, row: 0}} - buffer = new TextBuffer('banana bandana ban_ana bandaid band bNa\nbanana') - buffer.findWordsWithSubsequenceInRange('bna', '_', 3, range).then (results) -> - expected = [ - { - score: 16, - matchIndices: [0, 2, 4], - positions: [{row: 0, column: 15}], - word: "ban_ana" - }, - { - score: 12, - matchIndices: [0, 2, 3], - positions: [{row: 0, column: 0}], - word: "banana" - }, - { - score: 7, - matchIndices: [0, 5, 6], - positions: [{row: 0, column: 7}], - word: "bandana" - } - ] - # JSON serialization doesn't work properly with `SubsequenceMatch` - # results, so we use another strategy to test deep equality. - for i, result of results - for prop, value in result - expect(value).toEqual(expected[i][prop]) - done() - - describe "::backwardsScanInRange(range, regex, fn)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - describe "when given a regex with no global flag", -> - it "calls the iterator with the last match for the given regex in the given range", -> - matches = [] - ranges = [] - buffer.backwardsScanInRange /cu(rr)ent/, [[4, 0], [6, 44]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe 'current' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[6, 34], [6, 41]] - - describe "when given a regex with a global flag", -> - it "calls the iterator with each match for the given regex in the given range, starting with the last match", -> - matches = [] - ranges = [] - buffer.backwardsScanInRange /cu(rr)ent/g, [[4, 0], [6, 59]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 3 - expect(ranges.length).toBe 3 - - expect(matches[0][0]).toBe 'current' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[6, 34], [6, 41]] - - expect(matches[1][0]).toBe 'current' - expect(matches[1][1]).toBe 'rr' - expect(ranges[1]).toEqual [[6, 6], [6, 13]] - - expect(matches[2][0]).toBe 'current' - expect(matches[2][1]).toBe 'rr' - expect(ranges[2]).toEqual [[5, 6], [5, 13]] - - describe "when the last regex match starts at the beginning of the range", -> - it "calls the iterator with the match", -> - matches = [] - ranges = [] - buffer.scanInRange /quick/g, [[0, 4], [2, 0]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe 'quick' - expect(ranges[0]).toEqual [[0, 4], [0, 9]] - - matches = [] - ranges = [] - buffer.scanInRange /^/, [[0, 0], [2, 0]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe "" - expect(ranges[0]).toEqual [[0, 0], [0, 0]] - - describe "when the first regex match exceeds the end of the range", -> - describe "when the portion of the match within the range also matches the regex", -> - it "calls the iterator with the truncated match", -> - matches = [] - ranges = [] - buffer.backwardsScanInRange /cu(r*)/g, [[4, 0], [6, 9]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 2 - expect(ranges.length).toBe 2 - - expect(matches[0][0]).toBe 'cur' - expect(matches[0][1]).toBe 'r' - expect(ranges[0]).toEqual [[6, 6], [6, 9]] - - expect(matches[1][0]).toBe 'curr' - expect(matches[1][1]).toBe 'rr' - expect(ranges[1]).toEqual [[5, 6], [5, 10]] - - describe "when the portion of the match within the range does not matches the regex", -> - it "does not call the iterator with the truncated match", -> - matches = [] - ranges = [] - buffer.backwardsScanInRange /cu(r*)e/g, [[4, 0], [6, 9]], ({match, range}) -> - matches.push(match) - ranges.push(range) - - expect(matches.length).toBe 1 - expect(ranges.length).toBe 1 - - expect(matches[0][0]).toBe 'curre' - expect(matches[0][1]).toBe 'rr' - expect(ranges[0]).toEqual [[5, 6], [5, 11]] - - describe "when the iterator calls the 'replace' control function with a replacement string", -> - it "replaces each occurrence of the regex match with the string", -> - ranges = [] - buffer.backwardsScanInRange /cu(rr)ent/g, [[4, 0], [6, 59]], ({range, replace}) -> - ranges.push(range) - replace("foo") unless range.start.isEqual([6, 6]) - - expect(ranges[0]).toEqual [[6, 34], [6, 41]] - expect(ranges[1]).toEqual [[6, 6], [6, 13]] - expect(ranges[2]).toEqual [[5, 6], [5, 13]] - - expect(buffer.lineForRow(5)).toBe ' foo = items.shift();' - expect(buffer.lineForRow(6)).toBe ' current < pivot ? left.push(foo) : right.push(current);' - - describe "when the iterator calls the 'stop' control function", -> - it "stops the traversal", -> - ranges = [] - buffer.backwardsScanInRange /cu(rr)ent/g, [[4, 0], [6, 59]], ({range, stop}) -> - ranges.push(range) - stop() if ranges.length is 2 - - expect(ranges.length).toBe 2 - expect(ranges[0]).toEqual [[6, 34], [6, 41]] - expect(ranges[1]).toEqual [[6, 6], [6, 13]] - - describe "when called with a random range", -> - it "returns the same results as ::scanInRange, but in the opposite order", -> - for i in [1...50] - seed = Date.now() - random = new Random(seed) - - buffer.backwardsScanChunkSize = random.intBetween(1, 80) - - [startRow, endRow] = [random(buffer.getLineCount()), random(buffer.getLineCount())].sort() - startColumn = random(buffer.lineForRow(startRow).length) - endColumn = random(buffer.lineForRow(endRow).length) - range = [[startRow, startColumn], [endRow, endColumn]] - - regex = [ - /\w/g - /\w{2}/g - /\w{3}/g - /.{5}/g - ][random(4)] - - if random(2) > 0 - forwardRanges = [] - backwardRanges = [] - forwardMatches = [] - backwardMatches = [] - - buffer.scanInRange regex, range, ({range, matchText}) -> - forwardMatches.push(matchText) - forwardRanges.push(range) - - buffer.backwardsScanInRange regex, range, ({range, matchText}) -> - backwardMatches.unshift(matchText) - backwardRanges.unshift(range) - - expect(backwardRanges).toEqual(forwardRanges, "Seed: #{seed}") - expect(backwardMatches).toEqual(forwardMatches, "Seed: #{seed}") - else - referenceBuffer = new TextBuffer(text: buffer.getText()) - referenceBuffer.scanInRange regex, range, ({matchText, replace}) -> - replace(matchText + '.') - - buffer.backwardsScanInRange regex, range, ({matchText, replace}) -> - replace(matchText + '.') - - expect(buffer.getText()).toBe(referenceBuffer.getText(), "Seed: #{seed}") - - it "does not return empty matches at the end of the range", -> - ranges = [] - - buffer.backwardsScanInRange /[ ]*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /[ ]*/m, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /\s*/gm, [[1, 0], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[1, 0], [1, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /\s*/m, [[0, 29], [1, 2]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[0, 29], [1, 2]]]) - - it "allows empty matches at the end of a range, when the range ends at column 0", -> - ranges = [] - buffer.backwardsScanInRange /^[ ]*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /^[ ]*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /^\s*/gm, [[9, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]], [[9, 0], [9, 2]]]) - - ranges.length = 0 - buffer.backwardsScanInRange /^\s*/gm, [[10, 0], [10, 0]], ({range}) -> ranges.push(range) - expect(ranges).toEqual([[[10, 0], [10, 0]]]) - - describe "::characterIndexForPosition(position)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - it "returns the total number of characters that precede the given position", -> - expect(buffer.characterIndexForPosition([0, 0])).toBe 0 - expect(buffer.characterIndexForPosition([0, 1])).toBe 1 - expect(buffer.characterIndexForPosition([0, 29])).toBe 29 - expect(buffer.characterIndexForPosition([1, 0])).toBe 30 - expect(buffer.characterIndexForPosition([2, 0])).toBe 61 - expect(buffer.characterIndexForPosition([12, 2])).toBe 408 - expect(buffer.characterIndexForPosition([Infinity])).toBe 408 - - describe "when the buffer contains crlf line endings", -> - it "returns the total number of characters that precede the given position", -> - buffer.setText("line1\r\nline2\nline3\r\nline4") - expect(buffer.characterIndexForPosition([1])).toBe 7 - expect(buffer.characterIndexForPosition([2])).toBe 13 - expect(buffer.characterIndexForPosition([3])).toBe 20 - - describe "::positionForCharacterIndex(position)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - it "returns the position based on character index", -> - expect(buffer.positionForCharacterIndex(0)).toEqual [0, 0] - expect(buffer.positionForCharacterIndex(1)).toEqual [0, 1] - expect(buffer.positionForCharacterIndex(29)).toEqual [0, 29] - expect(buffer.positionForCharacterIndex(30)).toEqual [1, 0] - expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] - expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] - - describe "when the buffer contains crlf line endings", -> - it "returns the position based on character index", -> - buffer.setText("line1\r\nline2\nline3\r\nline4") - expect(buffer.positionForCharacterIndex(7)).toEqual [1, 0] - expect(buffer.positionForCharacterIndex(13)).toEqual [2, 0] - expect(buffer.positionForCharacterIndex(20)).toEqual [3, 0] - - describe "::isEmpty()", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - it "returns true for an empty buffer", -> - buffer.setText('') - expect(buffer.isEmpty()).toBeTruthy() - - it "returns false for a non-empty buffer", -> - buffer.setText('a') - expect(buffer.isEmpty()).toBeFalsy() - buffer.setText('a\nb\nc') - expect(buffer.isEmpty()).toBeFalsy() - buffer.setText('\n') - expect(buffer.isEmpty()).toBeFalsy() - - describe "::hasAstral()", -> - it "returns true for buffers containing surrogate pairs", -> - expect(new TextBuffer('hooray 😄').hasAstral()).toBeTruthy() - - it "returns false for buffers that do not contain surrogate pairs", -> - expect(new TextBuffer('nope').hasAstral()).toBeFalsy() - - describe "::onWillChange(callback)", -> - it "notifies observers before a transaction, an undo or a redo", -> - changeCount = 0 - expectedText = '' - - buffer = new TextBuffer() - checkpoint = buffer.createCheckpoint() - - buffer.onWillChange (change) -> - expect(buffer.getText()).toBe expectedText - changeCount++ - - buffer.append('a') - expect(changeCount).toBe(1) - expectedText = 'a' - - buffer.transact -> - buffer.append('b') - buffer.append('c') - expect(changeCount).toBe(2) - expectedText = 'abc' - - # Empty transactions do not cause onWillChange listeners to be called - buffer.transact -> - expect(changeCount).toBe(2) - - buffer.undo() - expect(changeCount).toBe(3) - expectedText = 'a' - - buffer.redo() - expect(changeCount).toBe(4) - expectedText = 'abc' - - buffer.revertToCheckpoint(checkpoint) - expect(changeCount).toBe(5) - - describe "::onDidChange(callback)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - it "notifies observers after a transaction, an undo or a redo", -> - textChanges = [] - buffer.onDidChange ({changes}) -> textChanges.push(changes...) - - buffer.insert([0, 0], "abc") - buffer.delete([[0, 0], [0, 1]]) - - assertChangesEqual(textChanges, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 3]] - oldText: "", - newText: "abc" - }, - { - oldRange: [[0, 0], [0, 1]], - newRange: [[0, 0], [0, 0]], - oldText: "a", - newText: "" - } - ]) - - textChanges = [] - buffer.transact -> - buffer.insert([1, 0], "v") - buffer.insert([1, 1], "x") - buffer.insert([1, 2], "y") - buffer.insert([2, 3], "zw") - buffer.delete([[2, 3], [2, 4]]) - - assertChangesEqual(textChanges, [ - { - oldRange: [[1, 0], [1, 0]], - newRange: [[1, 0], [1, 3]], - oldText: "", - newText: "vxy", - }, - { - oldRange: [[2, 3], [2, 3]], - newRange: [[2, 3], [2, 4]], - oldText: "", - newText: "w", - } - ]) - - textChanges = [] - buffer.undo() - assertChangesEqual(textChanges, [ - { - oldRange: [[1, 0], [1, 3]], - newRange: [[1, 0], [1, 0]], - oldText: "vxy", - newText: "", - }, - { - oldRange: [[2, 3], [2, 4]], - newRange: [[2, 3], [2, 3]], - oldText: "w", - newText: "", - } - ]) - - textChanges = [] - buffer.redo() - assertChangesEqual(textChanges, [ - { - oldRange: [[1, 0], [1, 0]], - newRange: [[1, 0], [1, 3]], - oldText: "", - newText: "vxy", - }, - { - oldRange: [[2, 3], [2, 3]], - newRange: [[2, 3], [2, 4]], - oldText: "", - newText: "w", - } - ]) - - textChanges = [] - buffer.transact -> - buffer.transact -> - buffer.insert([0, 0], "j") - - # we emit only one event for nested transactions - assertChangesEqual(textChanges, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 1]], - oldText: "", - newText: "j", - } - ]) - - it "doesn't notify observers after an empty transaction", -> - didChangeTextSpy = jasmine.createSpy() - buffer.onDidChange(didChangeTextSpy) - buffer.transact(->) - expect(didChangeTextSpy).not.toHaveBeenCalled() - - it "doesn't throw an error when clearing the undo stack within a transaction", -> - buffer.onDidChange(didChangeTextSpy = jasmine.createSpy()) - expect(-> buffer.transact(-> buffer.clearUndoStack())).not.toThrowError() - expect(didChangeTextSpy).not.toHaveBeenCalled() - - describe "::onDidStopChanging(callback)", -> - [delay, didStopChangingCallback] = [] - - wait = (milliseconds, callback) -> setTimeout(callback, milliseconds) - - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - delay = buffer.stoppedChangingDelay - didStopChangingCallback = jasmine.createSpy("didStopChangingCallback") - buffer.onDidStopChanging didStopChangingCallback - - it "notifies observers after a delay passes following changes", (done) -> - buffer.insert([0, 0], 'a') - expect(didStopChangingCallback).not.toHaveBeenCalled() - - wait delay / 2, -> - buffer.transact -> - buffer.transact -> - buffer.insert([0, 0], 'b') - buffer.insert([1, 0], 'c') - buffer.insert([1, 1], 'd') - expect(didStopChangingCallback).not.toHaveBeenCalled() - - wait delay / 2, -> - expect(didStopChangingCallback).not.toHaveBeenCalled() - - wait delay, -> - expect(didStopChangingCallback).toHaveBeenCalled() - assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 2]], - oldText: "", - newText: "ba", - }, - { - oldRange: [[1, 0], [1, 0]], - newRange: [[1, 0], [1, 2]], - oldText: "", - newText: "cd", - } - ]) - - didStopChangingCallback.calls.reset() - buffer.undo() - buffer.undo() - wait delay * 2, -> - expect(didStopChangingCallback).toHaveBeenCalled() - assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ - { - oldRange: [[0, 0], [0, 2]], - newRange: [[0, 0], [0, 0]], - oldText: "ba", - newText: "", - }, - { - oldRange: [[1, 0], [1, 2]], - newRange: [[1, 0], [1, 0]], - oldText: "cd", - newText: "", - }, - ]) - done() - - it "provides the correct changes when the buffer is mutated in the onDidChange callback", (done) -> - buffer.onDidChange ({changes}) -> - switch changes[0].newText - when 'a' - buffer.insert(changes[0].newRange.end, 'b') - when 'b' - buffer.insert(changes[0].newRange.end, 'c') - when 'c' - buffer.insert(changes[0].newRange.end, 'd') - - buffer.insert([0, 0], 'a') - - wait delay * 2, -> - expect(didStopChangingCallback).toHaveBeenCalled() - assertChangesEqual(didStopChangingCallback.calls.mostRecent().args[0].changes, [ - { - oldRange: [[0, 0], [0, 0]], - newRange: [[0, 0], [0, 4]], - oldText: "", - newText: "abcd", - } - ]) - done() - - describe "::append(text)", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - it "adds text to the end of the buffer", -> - buffer.setText("") - buffer.append("a") - expect(buffer.getText()).toBe "a" - buffer.append("b\nc") - expect(buffer.getText()).toBe "ab\nc" - - describe "::setLanguageMode", -> - it "destroys the previous language mode", -> - buffer = new TextBuffer() - - languageMode1 = { - alive: true, - destroy: -> @alive = false - onDidChangeHighlighting: -> {dispose: ->} - } - - languageMode2 = { - alive: true, - destroy: -> @alive = false - onDidChangeHighlighting: -> {dispose: ->} - } - - buffer.setLanguageMode(languageMode1) - expect(languageMode1.alive).toBe(true) - expect(languageMode2.alive).toBe(true) - - buffer.setLanguageMode(languageMode2) - expect(languageMode1.alive).toBe(false) - expect(languageMode2.alive).toBe(true) - - buffer.destroy() - expect(languageMode1.alive).toBe(false) - expect(languageMode2.alive).toBe(false) - - it "notifies ::onDidChangeLanguageMode observers when the language mode changes", -> - buffer = new TextBuffer() - expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true) - - events = [] - buffer.onDidChangeLanguageMode (newMode, oldMode) -> events.push({newMode: newMode, oldMode: oldMode}) - - languageMode = { - onDidChangeHighlighting: -> {dispose: ->} - } - - buffer.setLanguageMode(languageMode) - expect(buffer.getLanguageMode()).toBe(languageMode) - expect(events.length).toBe(1) - expect(events[0].newMode).toBe(languageMode) - expect(events[0].oldMode instanceof NullLanguageMode).toBe(true) - - buffer.setLanguageMode(null) - expect(buffer.getLanguageMode() instanceof NullLanguageMode).toBe(true) - expect(events.length).toBe(2) - expect(events[1].newMode).toBe(buffer.getLanguageMode()) - expect(events[1].oldMode).toBe(languageMode) - - describe "line ending support", -> - beforeEach -> - filePath = require.resolve('./fixtures/sample.js') - buffer = TextBuffer.loadSync(filePath) - - describe ".getText()", -> - it "returns the text with the corrent line endings for each row", -> - buffer.setText("a\r\nb\nc") - expect(buffer.getText()).toBe "a\r\nb\nc" - buffer.setText("a\r\nb\nc\n") - expect(buffer.getText()).toBe "a\r\nb\nc\n" - - describe "when editing a line", -> - it "preserves the existing line ending", -> - buffer.setText("a\r\nb\nc") - buffer.insert([0, 1], "1") - expect(buffer.getText()).toBe "a1\r\nb\nc" - - describe "when inserting text with multiple lines", -> - describe "when the current line has a line ending", -> - it "uses the same line ending as the line where the text is inserted", -> - buffer.setText("a\r\n") - buffer.insert([0, 1], "hello\n1\n\n2") - expect(buffer.getText()).toBe "ahello\r\n1\r\n\r\n2\r\n" - - describe "when the current line has no line ending (because it's the last line of the buffer)", -> - describe "when the buffer contains only a single line", -> - it "honors the line endings in the inserted text", -> - buffer.setText("initialtext") - buffer.append("hello\n1\r\n2\n") - expect(buffer.getText()).toBe "initialtexthello\n1\r\n2\n" - - describe "when the buffer contains a preceding line", -> - it "uses the line ending of the preceding line", -> - buffer.setText("\ninitialtext") - buffer.append("hello\n1\r\n2\n") - expect(buffer.getText()).toBe "\ninitialtexthello\n1\n2\n" - - describe "::setPreferredLineEnding(lineEnding)", -> - it "uses the given line ending when normalizing, rather than inferring one from the surrounding text", -> - buffer = new TextBuffer(text: "a \r\n") - - expect(buffer.getPreferredLineEnding()).toBe null - buffer.append(" b \n") - expect(buffer.getText()).toBe "a \r\n b \r\n" - - buffer.setPreferredLineEnding("\n") - expect(buffer.getPreferredLineEnding()).toBe "\n" - buffer.append(" c \n") - expect(buffer.getText()).toBe "a \r\n b \r\n c \n" - - buffer.setPreferredLineEnding(null) - buffer.append(" d \r\n") - expect(buffer.getText()).toBe "a \r\n b \r\n c \n d \n" - - it "persists across serialization and deserialization", (done) -> - bufferA = new TextBuffer - bufferA.setPreferredLineEnding("\r\n") - - TextBuffer.deserialize(bufferA.serialize()).then (bufferB) -> - expect(bufferB.getPreferredLineEnding()).toBe "\r\n" - done() - -assertChangesEqual = (actualChanges, expectedChanges) -> - expect(actualChanges.length).toBe(expectedChanges.length) - for actualChange, i in actualChanges - expectedChange = expectedChanges[i] - expect(actualChange.oldRange).toEqual(expectedChange.oldRange) - expect(actualChange.newRange).toEqual(expectedChange.newRange) - expect(actualChange.oldText).toEqual(expectedChange.oldText) - expect(actualChange.newText).toEqual(expectedChange.newText) From b3089d41b85aa9f8259c2f5cb1b128801edfdc3a Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 10 Oct 2025 19:24:41 -0700 Subject: [PATCH 64/64] Remove scripts and dependencies related to API doc generation --- package.json | 3 --- script/generate-docs | 20 -------------------- 2 files changed, 23 deletions(-) delete mode 100755 script/generate-docs diff --git a/package.json b/package.json index 74b267fa4d..6e5fa6e543 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "main": "./src/text-buffer", "scripts": { "prepublish": "npm run clean", - "docs": "node script/generate-docs", "clean": "rimraf lib api.json", "test": "node script/test", "ci": "npm run test && npm run bench", @@ -27,13 +26,11 @@ "electron": "^30", "jasmine": "^2.4.1", "jasmine-core": "^2.4.1", - "joanna": "github:pulsar-edit/joanna", "json-diff": "^0.3.1", "random-seed": "^0.2.0", "regression": "^1.2.1", "rimraf": "~2.2.2", "standard": "^10.0.3", - "tello": "^1.0.7", "temp": "^0.8.3", "yargs": "^6.5.0" }, diff --git a/script/generate-docs b/script/generate-docs deleted file mode 100755 index 649d481b2c..0000000000 --- a/script/generate-docs +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs') -const path = require('path') -const tello = require('tello') -const joanna = require('joanna') - -const rootDir = path.join(__dirname, '..') - -const sourceDir = 'src' -const jsFiles = [] -for (const entry of fs.readdirSync('src')) { - if (entry.endsWith('.js')) { - jsFiles.push(path.join(sourceDir, entry)) - } -} - -const jsMetadata = joanna(jsFiles) -const docs = tello.digest([jsMetadata]) -fs.writeFileSync(path.join(rootDir, 'api.json'), JSON.stringify(docs, null, 2))