├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE.md ├── README.md ├── docs └── transforms.md ├── package.json ├── script └── test ├── spec ├── buffer-layer-spec.coffee ├── file-layer-spec.coffee ├── fixtures │ └── sample.js ├── hard-tabs-transform-spec.coffee ├── lines-transform-spec.coffee ├── marker-index-spec.coffee ├── paired-characters-transform-spec.coffee ├── patch-spec.coffee ├── point-spec.coffee ├── range-spec.coffee ├── soft-wraps-transform-spec.coffee ├── spec-helper.coffee ├── spy-layer.coffee ├── string-layer.coffee ├── support │ └── jasmine.json ├── text-display-document-spec.coffee ├── text-document-spec.coffee └── transform-layer-spec.coffee └── src ├── buffer-layer.coffee ├── file-layer.coffee ├── hard-tabs-transform.coffee ├── history.coffee ├── index.coffee ├── layer.coffee ├── lines-transform.coffee ├── marker-index.coffee ├── marker-store.coffee ├── marker.coffee ├── null-layer.coffee ├── paired-characters-transform.coffee ├── patch.coffee ├── point.coffee ├── range.coffee ├── set-helpers.coffee ├── soft-wraps-transform.coffee ├── text-display-document.coffee ├── text-document.coffee ├── transform-buffer.coffee └── transform-layer.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | .DS_Store 4 | npm-debug.log 5 | *.swp 6 | .coffee 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: change 8 | 9 | node_js: 10 | - "0.12" 11 | - "iojs" 12 | 13 | branches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON('package.json') 4 | 5 | coffee: 6 | glob_to_multiple: 7 | expand: true 8 | cwd: 'src' 9 | src: ['**/*.coffee'] 10 | dest: 'lib' 11 | ext: '.js' 12 | 13 | coffeelint: 14 | options: 15 | no_empty_param_list: 16 | level: 'error' 17 | max_line_length: 18 | level: 'ignore' 19 | indentation: 20 | level: 'ignore' 21 | 22 | src: ['src/*.coffee'] 23 | test: ['spec/*.coffee'] 24 | gruntfile: ['Gruntfile.coffee'] 25 | 26 | shell: 27 | test: 28 | command: (filter) -> 29 | cmd = 'node -e "require(\'coffee-script/register\'); require(\'jasmine/bin/jasmine\')"' 30 | cmd += " -- placeholder-arg --filter=#{filter}" if filter 31 | cmd 32 | options: 33 | stdout: true 34 | stderr: true 35 | failOnError: true 36 | 37 | grunt.loadNpmTasks('grunt-contrib-coffee') 38 | grunt.loadNpmTasks('grunt-shell') 39 | grunt.loadNpmTasks('grunt-coffeelint') 40 | 41 | grunt.registerTask 'clean', -> require('rimraf').sync('lib') 42 | grunt.registerTask('lint', ['coffeelint']) 43 | grunt.registerTask('default', ['coffee', 'lint']) 44 | grunt.registerTask('test', ['coffee', 'lint', 'shell:test']) 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # text-document [![Build Status](https://travis-ci.org/atom/text-document.svg?branch=master)](https://travis-ci.org/atom/text-document) 5 | 6 | **Hey there, this library isn't being developed lately, but much of what we learned building it is being introduced into Atom incrementally. We decided it wasn't necessarily to replace this large a piece of the system at once, but it was good to get a chance to think for a while with a clean slate.** 7 | 8 | ------ 9 | 10 | This library will replace TextBuffer, TokenizedBuffer, and DisplayBuffer in Atom with API-compatible alternatives that perform better and consume vastly less memory. 11 | 12 | ## Exported Classes 13 | 14 | ### TextDocument 15 | 16 | This class will be a drop-in replacement `TextBuffer` and implement its API. We should build our tests by example (or outright copying) from TextBuffer, cleaning up their structure but leaving the API otherwise intact. 17 | 18 | Instances will house a `BufferLayer` and a `TransformLayer` based on `LinesTransform`, which they will use as primitives to expose their API. The `BufferLayer` will store a read-only portion of the file being edited in memory for fast access, referencing a temporary copy of the file on disk for content exceeding what we're willing to store. The lines transform layer stores the mapping from two dimensions (rows/columns) to one dimension (character offsets in the file). 19 | 20 | ### ScopedTextDocument 21 | 22 | This class will replace `TokenizedBuffer`, an Atom private class, implementing enough of its API to keep existing references working. We can deviate from the existing API here since this class is private, but we should have a good reason. Tests should be based on existing tests for `TokenizedBuffer`, where the APIs being tested are still relevant. 23 | 24 | Instances will house a `NestedWordLayer`, which uses Atom's existing parser to insert open and close tags for scopes into the content stream. For layers that aren't interested in syntax, these tags will ride along with the content invisibly, consuming no logical space, but scope information will be accessible to subsequent layers if they wish to make scope-based decisions. 25 | 26 | We should not replicate the `TokenizedLine` API in this layer, instead relying on a shim in Atom to wrap instances of `TextContent` and present them with the same token-based API as currently exists. Ultimately, we should aim to transition away from `TokenizedLine` in Atom and use the tagged `TextContent` stream instead. 27 | 28 | ### TextDisplayDocument 29 | 30 | This class will replace `DisplayBuffer`, another Atom private class. It will perform all transformations needed to prepare content for display, including soft wrap, folds, hard tab expansion, soft tab clipping, and paired Unicode character collapsing. Instances will contain a stacked series of transform layers to implement these operations. Again, we should default to matching the `DisplayBuffer` API where appropriate. 31 | 32 | ## Internal Structures 33 | 34 | The three public classes listed above will be implemented in terms of lower-level primitives. Each of these objects exposes an iterator-based interface that can be used to seek and read at a given location, making them optimal for streaming operations. In all layers, we maintain an *active region*, which is based on, but not necessarily identical to, the region of the file that is currently visible on screen. 35 | 36 | ### FileLayer 37 | 38 | This is the lowest layer, managing interaction with the file system. Its iterator is based on Node's built-in file handles. If the file being edited is too large to load into memory based on the active region, this layer will also manage a temporary copy of the file on disk. 39 | 40 | ### BufferLayer 41 | 42 | This layer builds on `FileLayer`, storing a portion of the file in memory based on the active region. When the active region changes, this layer flushes and loads content from the layer below accordingly. This layer can also be used above specific transform layers to cache their content. If used in this capacity, it will need to handle change events from the underlying layer. 43 | 44 | ### TransformLayer 45 | 46 | Transform layers can be instantiated with different transform implementations to implement things like tab expansion and soft wrap. They also store a region map which indexes the spatial correspondence between input and target coordinates. In addition to performing transforms in an initial streaming fashion, transform layers also transform and re-emit change events from the layer below. 47 | 48 | ### Patch 49 | 50 | This class indexes the spatial correspondence between two layers. Each transform layer uses a region map to efficiently translate positions between its input and target coordinate spaces. It is also used by the `BufferLayer` to store in-memory content. 51 | 52 | It's currently implemented as an array of *regions*, with each region having a *input extent*, *target extent*, and *content*. To find an input position corresponding to a target position or vice versa, we simply traverse the array, maintaining the total distance traversed in either dimension. To make this class efficient, this linear data structure will need to be replaced with a tree, possibly a counted B+ tree or some persistent equivalent. 53 | 54 | ## Markers 55 | 56 | Layers will also maintain a marker index. By implementing this index as a counted balanced tree, the impact of mutations on markers can be processed in logarithmic time as a function of the number of markers. We'll need to move away from emitting events on individual markers to realize the savings, however. 57 | 58 | ## Tasks 59 | 60 | The basic structure is in place, but there's still a lot to be done. 61 | 62 | * [x] Implement position translation between layers 63 | * [ ] Index position translation in `TransformLayer` using a `Patch`. The `Patch` API will need to be extended a bit to achieve this. 64 | * [ ] Implement marker API based on an efficient index 65 | * [ ] Implement `ScopedTextDocument` and create a `TextContent` data type that can intersperse scope start and end tags with strings of normal content. 66 | * [ ] Add temp file handling to `FileLayer` 67 | * [ ] Add history tracking with transactions, undo, redo, etc. 68 | 69 | ## Questions 70 | 71 | * What about using immutable data structures for our region map and marker index? That might make undo easier to implement, but it needs to be super fast. 72 | * For the future: Can we implement some sort of multi-version concurrency control to keep the content of the buffer stable for the lifetime of a transaction while performing I/O asynchronously? 73 | -------------------------------------------------------------------------------- /docs/transforms.md: -------------------------------------------------------------------------------- 1 | # Transforms 2 | 3 | Transforms are programmatically defined patches. A patch is a set of explicit instructions for translating a specific input stream to a specific output stream, a *transform* is basically a program that *computes* a patch based on the input stream. 4 | 5 | ## Transform Functions 6 | 7 | Transforms are defined by functions that are invoked repeatedly to return one or more hunks based on the input stream at a specific location. A transform functions has access to the start location in the input and the output for the hunks it is being called to compute. A transform function can also read content from the input stream. 8 | 9 | ### Transform Function Parameters: 10 | 11 | * `inputPosition` – A `Point` 12 | * `outputPosition` – A `Point` 13 | * `read` – A function that returns the next chunk of content from the input. 14 | 15 | The function returns one or more *hunks*, plain objects with an `inputExtent`, an `outputExtent`, and a `content` field. The `content` field can contain either explicit content or a `Point` representing an extent in the input for which the output is identical. We call hunks returned from the same invocation of the transform function *sibling hunks*. 16 | 17 | ### Transform Function Return Value 18 | 19 | * An `Array` of objects with the following keys: 20 | * `inputExtent` 21 | * `outputExtent` 22 | * `content` 23 | 24 | ## Hunk Invalidation 25 | 26 | The output of a transform is a function of the input. When the input changes, we update the patch to reflect the result of applying the transform to the newest input. 27 | 28 | First, we update the existing patch by splicing the new range into the old range in the input dimension. Then we determine which hunks were invalidated and need to be recomputed. Hunks are invalidated if they intersect the old range or have a sibling that does. 29 | 30 | Starting at the start of the first invalidated hunk, the transform function is reinvoked until enough hunks are returned to fill in the invalidated range. Because we always invalidate siblings, we essentially "replay" one or more transform invocations with new input. 31 | 32 | If a given transform requires buffering of earlier input to make decisions later, this dependency is captured by always restarting at the first sibling of the hunk intersecting the change. So long as the transform function consults no other state but what is available to it via its parameters, we can invalidate hunks that don't themselves intersect the change, but depend in some way on the changed content. 33 | 34 | For example, the soft wraps transform can always buffer the entirety of any line it is soft-wrapping. This means that changes on any segment of the line invalidate the entire line, allowing us to re-wrap the line. Finer grained invalidation may be possible, but we may be able to afford rewrapping everything if the soft wrap transform function is efficient. 35 | 36 | ## Ad-Hoc State Propagation 37 | 38 | Another potential invalidation tool is ad-hoc state propagation, in which a given invocation of the transform function can return an immutable state object that is passed to the next invocation of the transform function. We associate this state with each run of sibling regions, and whenever they are invalidated we cascade the invalidation to the next run of siblings if the state has changed in the new invocation. 39 | 40 | This generalizes our current approach to resuming the parser. For each invocation of the parser transform, we return the parser's stack state. If a change occurs that causes the state of the parser to be different at the end of a chunk of content, we'll invalidate the subsequent region and continue parsing. This can occur for example when inserting a quote momentarily switches all non-strings in the document to strings and vice versa. 41 | 42 | ## Cascaded Invalidations 43 | 44 | A more sophisticated possible approach could augment grouped sibling invalidation. When a hunk gets invalidated, we could expect the next hunk to exactly fill the invalidated window. If it didn't, we would invalidate the next hunk too and invoke the transform function again at the end of the last new hunk, repeating the process recursively. 45 | 46 | This could potentially save work in situations like soft wrap, because so long as the input change didn't cause a change in the wrapping structure, only a single line segment would be invalidated. We would also only invalidate line segments following the edited segment. 47 | 48 | Because soft-wrap is indentation aware, the indentation level of the line would need to be passed via ad-hoc state propagation for this approach to be viable. 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-document", 3 | "version": "0.0.0", 4 | "description": "A Toolkit for editing and and displaying text documents", 5 | "main": "./lib/index", 6 | "scripts": { 7 | "prepublish": "grunt clean lint coffee", 8 | "test": "grunt test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/atom/text-document.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/atom/text-document/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "http://github.com/atom/text-document/raw/master/LICENSE.md" 21 | } 22 | ], 23 | "dependencies": { 24 | "event-kit": "^1.0.3" 25 | }, 26 | "devDependencies": { 27 | "coffee-cache": "^0.2.0", 28 | "coffee-script": "^1.7.0", 29 | "grunt": "^0.4.1", 30 | "grunt-cli": "^0.1.8", 31 | "grunt-coffeelint": "^0.0.6", 32 | "grunt-contrib-coffee": "^0.9.0", 33 | "grunt-shell": "^1.1.2", 34 | "jasmine": "^2.2.1", 35 | "random-seed": "^0.2.0", 36 | "rimraf": "^2.2.2", 37 | "temp": "^0.8.1", 38 | "underscore": "^1.8.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("coffee-script").register(); 3 | require("jasmine/bin/jasmine"); 4 | -------------------------------------------------------------------------------- /spec/buffer-layer-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | StringLayer = require "../spec/string-layer" 3 | BufferLayer = require "../src/buffer-layer" 4 | SpyLayer = require "./spy-layer" 5 | Random = require "random-seed" 6 | {getAllIteratorValues} = require "./spec-helper" 7 | 8 | describe "BufferLayer", -> 9 | describe "::slice(start, end)", -> 10 | it "returns the content between the given start and end positions", -> 11 | inputLayer = new SpyLayer(new StringLayer("abcdefghijkl", 3)) 12 | buffer = new BufferLayer(inputLayer) 13 | 14 | expect(buffer.slice(Point(0, 1), Point(0, 3))).toBe "bc" 15 | expect(inputLayer.getRecordedReads()).toEqual ["bcd"] 16 | inputLayer.reset() 17 | 18 | expect(buffer.slice(Point(0, 2), Point(0, 4))).toBe "cd" 19 | expect(inputLayer.getRecordedReads()).toEqual ["cde"] 20 | 21 | it "returns the entire inputLayer text when no bounds are given", -> 22 | inputLayer = new SpyLayer(new StringLayer("abcdefghijkl", 3)) 23 | buffer = new BufferLayer(inputLayer) 24 | 25 | expect(buffer.slice()).toBe "abcdefghijkl" 26 | expect(inputLayer.getRecordedReads()).toEqual ["abc", "def", "ghi", "jkl", undefined] 27 | 28 | describe "iterator", -> 29 | describe "::next()", -> 30 | it "reads from the underlying layer", -> 31 | inputLayer = new SpyLayer(new StringLayer("abcdefghijkl", 3)) 32 | buffer = new BufferLayer(inputLayer) 33 | iterator = buffer.buildIterator() 34 | iterator.seek(Point(0, 3)) 35 | 36 | expect(iterator.next()).toEqual(value:"def", done: false) 37 | expect(iterator.getPosition()).toEqual(Point(0, 6)) 38 | 39 | expect(iterator.next()).toEqual(value:"ghi", done: false) 40 | expect(iterator.getPosition()).toEqual(Point(0, 9)) 41 | 42 | expect(iterator.next()).toEqual(value:"jkl", done: false) 43 | expect(iterator.getPosition()).toEqual(Point(0, 12)) 44 | 45 | expect(iterator.next()).toEqual(value: undefined, done: true) 46 | expect(iterator.getPosition()).toEqual(Point(0, 12)) 47 | 48 | expect(inputLayer.getRecordedReads()).toEqual ["def", "ghi", "jkl", undefined] 49 | inputLayer.reset() 50 | 51 | iterator.seek(Point(0, 5)) 52 | expect(iterator.next()).toEqual(value:"fgh", done: false) 53 | 54 | describe "when the buffer has an active region", -> 55 | it "caches the text within the region", -> 56 | inputLayer = new SpyLayer(new StringLayer("abcdefghijkl", 3)) 57 | buffer = new BufferLayer(inputLayer) 58 | 59 | expect(getAllIteratorValues(buffer.buildIterator())).toEqual ["abc", "def", "ghi", "jkl"] 60 | expect(inputLayer.getRecordedReads()).toEqual ["abc", "def", "ghi", "jkl", undefined] 61 | inputLayer.reset() 62 | 63 | getAllIteratorValues(buffer.buildIterator()) 64 | expect(inputLayer.getRecordedReads()).toEqual ["abc", "def", "ghi", "jkl", undefined] 65 | inputLayer.reset() 66 | 67 | buffer.setActiveRegion(Point(0, 4), Point(0, 7)) 68 | 69 | getAllIteratorValues(buffer.buildIterator()) 70 | expect(inputLayer.getRecordedReads()).toEqual ["abc", "def", "ghi", "jkl", undefined] 71 | inputLayer.reset() 72 | 73 | expect(getAllIteratorValues(buffer.buildIterator())).toEqual ["abc", "def", "ghi", "jkl"] 74 | expect(inputLayer.getRecordedReads()).toEqual ["abc", "jkl", undefined] 75 | 76 | describe "::splice(start, extent, content)", -> 77 | it "replaces the extent at the given position with the given content", -> 78 | inputLayer = new StringLayer("abcdefghijkl", 3) 79 | buffer = new BufferLayer(inputLayer) 80 | 81 | iterator = buffer.buildIterator() 82 | iterator.seek(Point(0, 2)) 83 | iterator.splice(Point(0, 3), "1234") 84 | 85 | expect(iterator.getPosition()).toEqual Point(0, 6) 86 | expect(iterator.getInputPosition()).toEqual Point(0, 5) 87 | expect(iterator.next()).toEqual {value: "fgh", done: false} 88 | 89 | expect(buffer.slice()).toBe "ab1234fghijkl" 90 | 91 | iterator.seek(Point(0, 11)) 92 | iterator.splice(Point(0, 2), "HELLO") 93 | expect(buffer.slice()).toBe "ab1234fghijHELLO" 94 | 95 | describe "randomized mutations", -> 96 | it "behaves as if it were reading and writing directly to the underlying layer", -> 97 | for i in [0..30] by 1 98 | seed = Date.now() 99 | # seed = 1426552034823 100 | random = new Random(seed) 101 | 102 | oldContent = "abcdefghijklmnopqrstuvwxyz" 103 | inputLayer = new StringLayer(oldContent) 104 | buffer = new BufferLayer(inputLayer) 105 | reference = new StringLayer(oldContent) 106 | 107 | for j in [0..10] by 1 108 | currentContent = buffer.slice() 109 | newContentLength = random(20) 110 | newContent = (oldContent[random(26)] for k in [0..newContentLength]).join("").toUpperCase() 111 | 112 | startColumn = random(currentContent.length) 113 | endColumn = random.intBetween(startColumn, currentContent.length) 114 | start = Point(0, startColumn) 115 | extent = Point(0, endColumn - startColumn) 116 | 117 | # console.log buffer.slice() 118 | # console.log "buffer.splice(#{start}, #{extent}, #{newContent})" 119 | 120 | reference.splice(start, extent, newContent) 121 | buffer.splice(start, extent, newContent) 122 | 123 | expect(buffer.slice()).toBe(reference.slice(), "Seed: #{seed}, Iteration: #{j}") 124 | return unless buffer.slice() is reference.slice() 125 | -------------------------------------------------------------------------------- /spec/file-layer-spec.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | temp = require "temp" 3 | Point = require "../src/point" 4 | FileLayer = require "../src/file-layer" 5 | 6 | describe "FileLayer", -> 7 | [layer, filePath] = [] 8 | 9 | beforeEach -> 10 | filePath = temp.openSync("file-layer-spec-").path 11 | layer = new FileLayer(filePath, 3) 12 | 13 | afterEach -> 14 | layer.destroy() 15 | 16 | describe "iteration", -> 17 | describe "::next()", -> 18 | it "reads the file in chunks of the given size", -> 19 | # α-β-γ-δ 20 | fs.writeFileSync(filePath, "\u03B1-\u03B2-\u03B3-\u03B4") 21 | 22 | iterator = layer.buildIterator() 23 | expect(iterator.getPosition()).toEqual Point.zero() 24 | 25 | expect(iterator.next()).toEqual(value: "\u03B1-\u03B2", done: false) 26 | expect(iterator.getPosition()).toEqual Point(0, 3) 27 | 28 | expect(iterator.next()).toEqual(value: "-\u03B3-", done: false) 29 | expect(iterator.getPosition()).toEqual Point(0, 6) 30 | 31 | expect(iterator.next()).toEqual(value: "\u03B4", done: false) 32 | expect(iterator.getPosition()).toEqual Point(0, 7) 33 | 34 | expect(iterator.next()).toEqual {value: undefined, done: true} 35 | expect(iterator.next()).toEqual {value: undefined, done: true} 36 | expect(iterator.getPosition()).toEqual Point(0, 7) 37 | 38 | describe "::seek(characterIndex)", -> 39 | it "moves to the correct offset in the file", -> 40 | # α-β-γ-δ 41 | fs.writeFileSync(filePath, "\u03B1-\u03B2-\u03B3-\u03B4") 42 | 43 | iterator = layer.buildIterator() 44 | iterator.seek(Point(0, 2)) 45 | expect(iterator.next()).toEqual(value: "\u03B2-\u03B3", done: false) 46 | expect(iterator.getPosition()).toEqual Point(0, 5) 47 | 48 | expect(iterator.next()).toEqual(value: "-\u03B4", done: false) 49 | expect(iterator.getPosition()).toEqual Point(0, 7) 50 | 51 | expect(iterator.next()).toEqual {value: undefined, done: true} 52 | expect(iterator.getPosition()).toEqual Point(0, 7) 53 | -------------------------------------------------------------------------------- /spec/fixtures/sample.js: -------------------------------------------------------------------------------- 1 | var quicksort = function () { 2 | var sort = function(items) { 3 | if (items.length <= 1) return items; 4 | var pivot = items.shift(), current, left = [], right = []; 5 | while(items.length > 0) { 6 | current = items.shift(); 7 | current < pivot ? left.push(current) : right.push(current); 8 | } 9 | return sort(left).concat(pivot).concat(sort(right)); 10 | }; 11 | 12 | return sort(Array.apply(this, arguments)); 13 | }; -------------------------------------------------------------------------------- /spec/hard-tabs-transform-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | HardTabsTransform = require "../src/hard-tabs-transform" 3 | StringLayer = require "../spec/string-layer" 4 | TransformLayer = require "../src/transform-layer" 5 | {clip} = TransformLayer 6 | 7 | {expectMapsSymmetrically, expectMapsToInput} = require './spec-helper' 8 | 9 | describe "HardTabsTransform", -> 10 | layer = null 11 | 12 | beforeEach -> 13 | layer = new TransformLayer(new StringLayer("\tabc\tdefg\t"), new HardTabsTransform(4)) 14 | 15 | it "expands hard tab characters to spaces based on the given tab length", -> 16 | iterator = layer.buildIterator() 17 | expect(iterator.next()).toEqual(value: "\t ", done: false) 18 | expect(iterator.getPosition()).toEqual(Point(0, 4)) 19 | expect(iterator.getInputPosition()).toEqual(Point(0, 1)) 20 | 21 | expect(iterator.next()).toEqual(value: "abc", done: false) 22 | expect(iterator.getPosition()).toEqual(Point(0, 7)) 23 | expect(iterator.getInputPosition()).toEqual(Point(0, 4)) 24 | 25 | expect(iterator.next()).toEqual(value: "\t", done: false) 26 | expect(iterator.getPosition()).toEqual(Point(0, 8)) 27 | expect(iterator.getInputPosition()).toEqual(Point(0, 5)) 28 | 29 | expect(iterator.next()).toEqual(value: "defg", done: false) 30 | expect(iterator.getPosition()).toEqual(Point(0, 12)) 31 | expect(iterator.getInputPosition()).toEqual(Point(0, 9)) 32 | 33 | expect(iterator.next()).toEqual(value: "\t ", done: false) 34 | expect(iterator.getPosition()).toEqual(Point(0, 16)) 35 | expect(iterator.getInputPosition()).toEqual(Point(0, 10)) 36 | 37 | expect(iterator.next()).toEqual {value: undefined, done: true} 38 | expect(iterator.next()).toEqual {value: undefined, done: true} 39 | expect(iterator.getPosition()).toEqual(Point(0, 16)) 40 | expect(iterator.getInputPosition()).toEqual(Point(0, 10)) 41 | 42 | it "maps target positions to input positions and vice-versa", -> 43 | expectMapsSymmetrically(layer, Point(0, 0), Point(0, 0)) 44 | expectMapsToInput(layer, Point(0, 0), Point(0, 1), clip.backward) 45 | expectMapsToInput(layer, Point(0, 0), Point(0, 2), clip.backward) 46 | expectMapsToInput(layer, Point(0, 0), Point(0, 3), clip.backward) 47 | expectMapsToInput(layer, Point(0, 1), Point(0, 1), clip.forward) 48 | expectMapsToInput(layer, Point(0, 1), Point(0, 2), clip.forward) 49 | expectMapsToInput(layer, Point(0, 1), Point(0, 3), clip.forward) 50 | expectMapsSymmetrically(layer, Point(0, 1), Point(0, 4)) 51 | -------------------------------------------------------------------------------- /spec/lines-transform-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | LinesTransform = require "../src/lines-transform" 3 | StringLayer = require "../spec/string-layer" 4 | TransformLayer = require "../src/transform-layer" 5 | {clip} = TransformLayer 6 | 7 | {expectMapsSymmetrically, expectMapsFromInput} = require "./spec-helper" 8 | 9 | describe "LinesTransform", -> 10 | layer = null 11 | 12 | beforeEach -> 13 | layer = new TransformLayer(new StringLayer("\nabc\r\ndefg\n\r"), new LinesTransform) 14 | 15 | it "breaks the input text into lines", -> 16 | iterator = layer.buildIterator() 17 | expect(iterator.next()).toEqual(value: "\n", done: false) 18 | expect(iterator.getPosition()).toEqual(Point(1, 0)) 19 | expect(iterator.getInputPosition()).toEqual(Point(0, 1)) 20 | 21 | expect(iterator.next()).toEqual(value: "abc", done: false) 22 | expect(iterator.getPosition()).toEqual(Point(1, 3)) 23 | expect(iterator.getInputPosition()).toEqual(Point(0, 4)) 24 | 25 | expect(iterator.next()).toEqual(value: "\r\n", done: false) 26 | expect(iterator.getPosition()).toEqual(Point(2, 0)) 27 | expect(iterator.getInputPosition()).toEqual(Point(0, 6)) 28 | 29 | expect(iterator.next()).toEqual(value: "defg", done: false) 30 | expect(iterator.getPosition()).toEqual(Point(2, 4)) 31 | expect(iterator.getInputPosition()).toEqual(Point(0, 10)) 32 | 33 | expect(iterator.next()).toEqual(value: "\n", done: false) 34 | expect(iterator.getPosition()).toEqual(Point(3, 0)) 35 | expect(iterator.getInputPosition()).toEqual(Point(0, 11)) 36 | 37 | expect(iterator.next()).toEqual(value: "\r", done: false) 38 | expect(iterator.getPosition()).toEqual(Point(4, 0)) 39 | expect(iterator.getInputPosition()).toEqual(Point(0, 12)) 40 | 41 | expect(iterator.next()).toEqual {value: undefined, done: true} 42 | expect(iterator.next()).toEqual {value: undefined, done: true} 43 | expect(iterator.getPosition()).toEqual(Point(4, 0)) 44 | expect(iterator.getInputPosition()).toEqual(Point(0, 12)) 45 | 46 | it "maps target positions to input positions and vice-versa", -> 47 | expectMapsSymmetrically(layer, Point(0, 0), Point(0, 0)) 48 | expectMapsSymmetrically(layer, Point(0, 1), Point(1, 0)) 49 | expectMapsSymmetrically(layer, Point(0, 2), Point(1, 1)) 50 | expectMapsSymmetrically(layer, Point(0, 3), Point(1, 2)) 51 | expectMapsSymmetrically(layer, Point(0, 4), Point(1, 3)) 52 | expectMapsFromInput(layer, Point(0, 5), Point(1, 3), clip.backward) 53 | expectMapsFromInput(layer, Point(0, 5), Point(2, 0), clip.forward) 54 | expectMapsSymmetrically(layer, Point(0, 6), Point(2, 0)) 55 | expectMapsSymmetrically(layer, Point(0, 7), Point(2, 1)) 56 | -------------------------------------------------------------------------------- /spec/marker-index-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | Range = require "../src/range" 3 | MarkerIndex = require "../src/marker-index" 4 | Random = require "random-seed" 5 | 6 | {currentSpecFailed, toEqualSet} = require "./spec-helper" 7 | 8 | describe "MarkerIndex", -> 9 | markerIndex = null 10 | 11 | beforeEach -> 12 | jasmine.addMatchers({toEqualSet}) 13 | markerIndex = new MarkerIndex 14 | 15 | describe "::getRange(id)", -> 16 | it "returns the range for the given marker id", -> 17 | markerIndex.insert("a", Point(0, 2), Point(0, 5)) 18 | markerIndex.insert("b", Point(0, 3), Point(0, 7)) 19 | markerIndex.insert("c", Point(0, 4), Point(0, 4)) 20 | markerIndex.insert("d", Point(0, 0), Point(0, 0)) 21 | markerIndex.insert("e", Point(0, 0), Point(0, 0)) 22 | markerIndex.insert("f", Point(0, 3), Point(0, 3)) 23 | markerIndex.insert("g", Point(0, 3), Point(0, 3)) 24 | 25 | expect(markerIndex.getRange("a")).toEqual Range(Point(0, 2), Point(0, 5)) 26 | expect(markerIndex.getRange("b")).toEqual Range(Point(0, 3), Point(0, 7)) 27 | expect(markerIndex.getRange("c")).toEqual Range(Point(0, 4), Point(0, 4)) 28 | expect(markerIndex.getRange("d")).toEqual Range(Point(0, 0), Point(0, 0)) 29 | expect(markerIndex.getRange("e")).toEqual Range(Point(0, 0), Point(0, 0)) 30 | expect(markerIndex.getRange("f")).toEqual Range(Point(0, 3), Point(0, 3)) 31 | expect(markerIndex.getRange("g")).toEqual Range(Point(0, 3), Point(0, 3)) 32 | 33 | markerIndex.delete("e") 34 | markerIndex.delete("c") 35 | markerIndex.delete("a") 36 | markerIndex.delete("f") 37 | 38 | expect(markerIndex.getRange("a")).toBeUndefined() 39 | expect(markerIndex.getRange("b")).toEqual Range(Point(0, 3), Point(0, 7)) 40 | expect(markerIndex.getRange("c")).toBeUndefined() 41 | expect(markerIndex.getRange("d")).toEqual Range(Point(0, 0), Point(0, 0)) 42 | expect(markerIndex.getRange("e")).toBeUndefined() 43 | expect(markerIndex.getRange("f")).toBeUndefined() 44 | expect(markerIndex.getRange("g")).toEqual Range(Point(0, 3), Point(0, 3)) 45 | 46 | describe "::findContaining(start, end)", -> 47 | it "returns the markers whose ranges contain the given range", -> 48 | markerIndex.insert("a", Point(0, 2), Point(0, 5)) 49 | markerIndex.insert("b", Point(0, 3), Point(0, 7)) 50 | markerIndex.insert("c", Point(0, 4), Point(0, 4)) 51 | markerIndex.insert("d", Point(0, 8), Point(0, 8)) 52 | 53 | # range queries 54 | expect(markerIndex.findContaining(Point(0, 1), Point(0, 3))).toEqualSet [] 55 | expect(markerIndex.findContaining(Point(0, 2), Point(0, 4))).toEqualSet ["a"] 56 | expect(markerIndex.findContaining(Point(0, 3), Point(0, 4))).toEqualSet ["a", "b"] 57 | expect(markerIndex.findContaining(Point(0, 4), Point(0, 7))).toEqualSet ["b"] 58 | expect(markerIndex.findContaining(Point(0, 4), Point(0, 8))).toEqualSet [] 59 | 60 | # point queries 61 | expect(markerIndex.findContaining(Point(0, 2))).toEqualSet ["a"] 62 | expect(markerIndex.findContaining(Point(0, 3))).toEqualSet ["a", "b"] 63 | expect(markerIndex.findContaining(Point(0, 5))).toEqualSet ["a", "b"] 64 | expect(markerIndex.findContaining(Point(0, 7))).toEqualSet ["b"] 65 | expect(markerIndex.findContaining(Point(0, 4))).toEqualSet ["a", "b", "c"] 66 | expect(markerIndex.findContaining(Point(0, 8))).toEqualSet ["d"] 67 | 68 | describe "::findContainedIn(start, end)", -> 69 | it "returns the markers whose ranges are contained in the given range", -> 70 | markerIndex.insert("a", Point(0, 2), Point(0, 5)) 71 | markerIndex.insert("b", Point(0, 3), Point(0, 7)) 72 | markerIndex.insert("c", Point(0, 4), Point(0, 4)) 73 | 74 | # range queries 75 | expect(markerIndex.findContainedIn(Point(0, 1), Point(0, 3))).toEqualSet [] 76 | expect(markerIndex.findContainedIn(Point(0, 1), Point(0, 5))).toEqualSet ["a", "c"] 77 | expect(markerIndex.findContainedIn(Point(0, 2), Point(0, 5))).toEqualSet ["a", "c"] 78 | expect(markerIndex.findContainedIn(Point(0, 2), Point(0, 8))).toEqualSet ["a", "b", "c"] 79 | expect(markerIndex.findContainedIn(Point(0, 3), Point(0, 8))).toEqualSet ["b", "c"] 80 | expect(markerIndex.findContainedIn(Point(0, 5), Point(0, 8))).toEqualSet [] 81 | 82 | # point queries 83 | expect(markerIndex.findContainedIn(Point(0, 4))).toEqualSet ["c"] 84 | expect(markerIndex.findContainedIn(Point(0, 5))).toEqualSet [] 85 | 86 | describe "::findIntersecting(start, end)", -> 87 | it "returns the markers whose ranges intersect the given range", -> 88 | markerIndex.insert("a", Point(0, 2), Point(0, 5)) 89 | markerIndex.insert("b", Point(0, 3), Point(0, 7)) 90 | 91 | # range queries 92 | expect(markerIndex.findIntersecting(Point(0, 0), Point(0, 1))).toEqualSet [] 93 | expect(markerIndex.findIntersecting(Point(0, 1), Point(0, 2))).toEqualSet ["a"] 94 | expect(markerIndex.findIntersecting(Point(0, 1), Point(0, 3))).toEqualSet ["a", "b"] 95 | expect(markerIndex.findIntersecting(Point(0, 3), Point(0, 4))).toEqualSet ["a", "b"] 96 | expect(markerIndex.findIntersecting(Point(0, 5), Point(0, 6))).toEqualSet ["a", "b"] 97 | expect(markerIndex.findIntersecting(Point(0, 6), Point(0, 8))).toEqualSet ["b"] 98 | expect(markerIndex.findIntersecting(Point(0, 8), Point(0, 9))).toEqualSet [] 99 | 100 | # point queries 101 | expect(markerIndex.findIntersecting(Point(0, 2))).toEqualSet ["a"] 102 | expect(markerIndex.findIntersecting(Point(0, 3))).toEqualSet ["a", "b"] 103 | expect(markerIndex.findIntersecting(Point(0, 4))).toEqualSet ["a", "b"] 104 | expect(markerIndex.findIntersecting(Point(0, 5))).toEqualSet ["a", "b"] 105 | expect(markerIndex.findIntersecting(Point(0, 7))).toEqualSet ["b"] 106 | 107 | describe "::findStartingIn(start, end)", -> 108 | it "returns markers ending at the given point", -> 109 | markerIndex.insert("a", Point(0, 0), Point(0, 0)) 110 | markerIndex.insert("b", Point(0, 2), Point(0, 5)) 111 | markerIndex.insert("c", Point(0, 2), Point(0, 7)) 112 | markerIndex.insert("d", Point(0, 4), Point(0, 8)) 113 | 114 | # range queries 115 | expect(markerIndex.findStartingIn(Point(0, 0), Point(0, 0))).toEqualSet ["a"] 116 | expect(markerIndex.findStartingIn(Point(0, 0), Point(0, 1))).toEqualSet ["a"] 117 | expect(markerIndex.findStartingIn(Point(0, 1), Point(0, 2))).toEqualSet ["b", "c"] 118 | expect(markerIndex.findStartingIn(Point(0, 2), Point(0, 3))).toEqualSet ["b", "c"] 119 | expect(markerIndex.findStartingIn(Point(0, 2), Point(0, 4))).toEqualSet ["b", "c", "d"] 120 | expect(markerIndex.findStartingIn(Point(0, 4), Point(0, 5))).toEqualSet ["d"] 121 | expect(markerIndex.findStartingIn(Point(0, 3), Point(0, 5))).toEqualSet ["d"] 122 | expect(markerIndex.findStartingIn(Point(0, 6), Point(0, 8))).toEqualSet [] 123 | 124 | # point queries 125 | expect(markerIndex.findStartingIn(Point(0, 1))).toEqualSet [] 126 | expect(markerIndex.findStartingIn(Point(0, 2))).toEqualSet ["b", "c"] 127 | expect(markerIndex.findStartingIn(Point(0, 4))).toEqualSet ["d"] 128 | 129 | describe "::findEndingIn(start, end)", -> 130 | it "returns markers ending at the given point", -> 131 | markerIndex.insert("a", Point(0, 2), Point(0, 5)) 132 | markerIndex.insert("b", Point(0, 3), Point(0, 7)) 133 | markerIndex.insert("c", Point(0, 4), Point(0, 7)) 134 | 135 | # range queries 136 | expect(markerIndex.findEndingIn(Point(0, 0), Point(0, 4))).toEqualSet [] 137 | expect(markerIndex.findEndingIn(Point(0, 0), Point(0, 5))).toEqualSet ["a"] 138 | expect(markerIndex.findEndingIn(Point(0, 5), Point(0, 6))).toEqualSet ["a"] 139 | expect(markerIndex.findEndingIn(Point(0, 2), Point(0, 6))).toEqualSet ["a"] 140 | expect(markerIndex.findEndingIn(Point(0, 1), Point(0, 7))).toEqualSet ["a", "b", "c"] 141 | expect(markerIndex.findEndingIn(Point(0, 1), Point(0, 8))).toEqualSet ["a", "b", "c"] 142 | 143 | # point queries 144 | expect(markerIndex.findEndingIn(Point(0, 4))).toEqualSet [] 145 | expect(markerIndex.findEndingIn(Point(0, 5))).toEqualSet ["a"] 146 | expect(markerIndex.findEndingIn(Point(0, 7))).toEqualSet ["b", "c"] 147 | 148 | describe "::splice(position, oldExtent, newExtent)", -> 149 | describe "when the change has a non-empty old extent and new extent", -> 150 | it "updates markers based on the change", -> 151 | markerIndex.insert("preceding", Point(0, 3), Point(0, 4)) 152 | markerIndex.insert("ending-at-start", Point(0, 3), Point(0, 5)) 153 | markerIndex.insert("overlapping-start", Point(0, 4), Point(0, 6)) 154 | markerIndex.insert("starting-at-start", Point(0, 5), Point(0, 7)) 155 | markerIndex.insert("within", Point(0, 6), Point(0, 7)) 156 | markerIndex.insert("surrounding", Point(0, 4), Point(0, 9)) 157 | markerIndex.insert("ending-at-end", Point(0, 6), Point(0, 8)) 158 | markerIndex.insert("overlapping-end", Point(0, 6), Point(0, 9)) 159 | markerIndex.insert("starting-at-end", Point(0, 8), Point(0, 10)) 160 | markerIndex.insert("following", Point(0, 9), Point(0, 10)) 161 | 162 | markerIndex.splice(Point(0, 5), Point(0, 3), Point(0, 4)) 163 | 164 | # Markers that preceded the change do not move. 165 | expect(markerIndex.getRange("preceding")).toEqual Range(Point(0, 3), Point(0, 4)) 166 | 167 | # Markers that ended at the start of the change do not move. 168 | expect(markerIndex.getRange("ending-at-start")).toEqual Range(Point(0, 3), Point(0, 5)) 169 | 170 | # Markers that overlapped the start of the change maintain their start 171 | # position, and now end at the end of the change. 172 | expect(markerIndex.getRange("overlapping-start")).toEqual Range(Point(0, 4), Point(0, 9)) 173 | 174 | # Markers that start at the start of the change maintain their start 175 | # position. 176 | expect(markerIndex.getRange("starting-at-start")).toEqual Range(Point(0, 5), Point(0, 9)) 177 | 178 | # Markers that were within the change become points at the end of the 179 | # change. 180 | expect(markerIndex.getRange("within")).toEqual Range(Point(0, 9), Point(0, 9)) 181 | 182 | # Markers that surrounded the change maintain their start position and 183 | # their logical end position. 184 | expect(markerIndex.getRange("surrounding")).toEqual Range(Point(0, 4), Point(0, 10)) 185 | 186 | # Markers that end at the end of the change maintain their logical end 187 | # position. 188 | expect(markerIndex.getRange("ending-at-end")).toEqual Range(Point(0, 9), Point(0, 9)) 189 | 190 | # Markers that overlapped the end of the change now start at the end of 191 | # the change, and maintain their logical end position. 192 | expect(markerIndex.getRange("overlapping-end")).toEqual Range(Point(0, 9), Point(0, 10)) 193 | 194 | # Markers that start at the end of the change maintain their logical 195 | # start and end positions. 196 | expect(markerIndex.getRange("starting-at-end")).toEqual Range(Point(0, 9), Point(0, 11)) 197 | 198 | # Markers that followed the change maintain their logical start and end 199 | # positions. 200 | expect(markerIndex.getRange("following")).toEqual Range(Point(0, 10), Point(0, 11)) 201 | 202 | describe "when the change has an empty old extent", -> 203 | describe "when there is no marker boundary at the splice location", -> 204 | it "treats the change as being inside markers that it intersects", -> 205 | markerIndex.insert("surrounds-point", Point(0, 3), Point(0, 8)) 206 | 207 | markerIndex.splice(Point(0, 5), Point(0, 0), Point(0, 4)) 208 | 209 | expect(markerIndex.getRange("surrounds-point")).toEqual Range(Point(0, 3), Point(0, 12)) 210 | 211 | describe "when a non-empty marker starts or ends at the splice position", -> 212 | it "treats the change as being inside markers that it intersects unless they are exclusive", -> 213 | markerIndex.insert("starts-at-point", Point(0, 5), Point(0, 8)) 214 | markerIndex.insert("ends-at-point", Point(0, 3), Point(0, 5)) 215 | 216 | markerIndex.insert("starts-at-point-exclusive", Point(0, 5), Point(0, 8)) 217 | markerIndex.insert("ends-at-point-exclusive", Point(0, 3), Point(0, 5)) 218 | markerIndex.setExclusive("starts-at-point-exclusive", true) 219 | markerIndex.setExclusive("ends-at-point-exclusive", true) 220 | 221 | expect(markerIndex.isExclusive("starts-at-point")).toBe false 222 | expect(markerIndex.isExclusive("starts-at-point-exclusive")).toBe true 223 | 224 | markerIndex.splice(Point(0, 5), Point(0, 0), Point(0, 4)) 225 | 226 | expect(markerIndex.getRange("starts-at-point")).toEqual Range(Point(0, 5), Point(0, 12)) 227 | expect(markerIndex.getRange("ends-at-point")).toEqual Range(Point(0, 3), Point(0, 9)) 228 | expect(markerIndex.getRange("starts-at-point-exclusive")).toEqual Range(Point(0, 9), Point(0, 12)) 229 | expect(markerIndex.getRange("ends-at-point-exclusive")).toEqual Range(Point(0, 3), Point(0, 5)) 230 | 231 | describe "when there is an empty marker at the splice position", -> 232 | it "treats the change as being inside markers that it intersects", -> 233 | markerIndex.insert("starts-at-point", Point(0, 5), Point(0, 8)) 234 | markerIndex.insert("ends-at-point", Point(0, 3), Point(0, 5)) 235 | markerIndex.insert("at-point-inclusive", Point(0, 5), Point(0, 5)) 236 | markerIndex.insert("at-point-exclusive", Point(0, 5), Point(0, 5)) 237 | markerIndex.setExclusive("at-point-exclusive", true) 238 | 239 | markerIndex.splice(Point(0, 5), Point(0, 0), Point(0, 4)) 240 | 241 | expect(markerIndex.getRange("starts-at-point")).toEqual Range(Point(0, 5), Point(0, 12)) 242 | expect(markerIndex.getRange("ends-at-point")).toEqual Range(Point(0, 3), Point(0, 9)) 243 | expect(markerIndex.getRange("at-point-inclusive")).toEqual Range(Point(0, 5), Point(0, 9)) 244 | expect(markerIndex.getRange("at-point-exclusive")).toEqual Range(Point(0, 9), Point(0, 9)) 245 | 246 | describe "when the change spans multiple rows", -> 247 | it "updates markers based on the change", -> 248 | markerIndex.insert("a", Point(0, 6), Point(0, 9)) 249 | 250 | markerIndex.splice(Point(0, 1), Point(0, 0), Point(1, 3)) 251 | expect(markerIndex.getRange("a")).toEqual Range(Point(1, 8), Point(1, 11)) 252 | 253 | markerIndex.splice(Point(0, 1), Point(1, 3), Point(0, 0)) 254 | expect(markerIndex.getRange("a")).toEqual Range(Point(0, 6), Point(0, 9)) 255 | 256 | markerIndex.splice(Point(0, 5), Point(0, 3), Point(1, 3)) 257 | expect(markerIndex.getRange("a")).toEqual Range(Point(1, 3), Point(1, 4)) 258 | 259 | describe "::dump()", -> 260 | it "returns an object containing each marker's range and exclusivity", -> 261 | markerIndex.insert("a", Point(0, 2), Point(0, 5)) 262 | markerIndex.insert("b", Point(0, 3), Point(0, 7)) 263 | markerIndex.insert("c", Point(0, 4), Point(0, 4)) 264 | markerIndex.insert("d", Point(0, 7), Point(0, 8)) 265 | markerIndex.setExclusive("d", true) 266 | 267 | expect(markerIndex.dump()).toEqual { 268 | "a": {range: Range(Point(0, 2), Point(0, 5)), isExclusive: false} 269 | "b": {range: Range(Point(0, 3), Point(0, 7)), isExclusive: false} 270 | "c": {range: Range(Point(0, 4), Point(0, 4)), isExclusive: false} 271 | "d": {range: Range(Point(0, 7), Point(0, 8)), isExclusive: true} 272 | } 273 | 274 | describe "::load(snapshot)", -> 275 | it "clears its contents and inserts the markers described by the given snapshot", -> 276 | markerIndex.insert("x", Point(0, 1), Point(0, 2)) 277 | 278 | markerIndex.load({ 279 | "a": {range: Range(Point(0, 2), Point(0, 5)), isExclusive: false} 280 | "b": {range: Range(Point(0, 3), Point(0, 7)), isExclusive: false} 281 | "c": {range: Range(Point(0, 4), Point(0, 4)), isExclusive: false} 282 | "d": {range: Range(Point(0, 7), Point(0, 8)), isExclusive: true} 283 | }) 284 | 285 | expect(markerIndex.getRange("a")).toEqual Range(Point(0, 2), Point(0, 5)) 286 | expect(markerIndex.getRange("b")).toEqual Range(Point(0, 3), Point(0, 7)) 287 | expect(markerIndex.getRange("c")).toEqual Range(Point(0, 4), Point(0, 4)) 288 | expect(markerIndex.getRange("d")).toEqual Range(Point(0, 7), Point(0, 8)) 289 | expect(markerIndex.isExclusive("c")).toBe false 290 | expect(markerIndex.isExclusive("d")).toBe true 291 | 292 | describe "randomized mutations", -> 293 | [seed, random, markers, idCounter] = [] 294 | 295 | it "maintains data structure invariants and returns correct query results", -> 296 | for i in [1..10] 297 | seed = Date.now() # paste the failing seed here to reproduce if there are failures 298 | random = new Random(seed) 299 | markers = [] 300 | idCounter = 1 301 | markerIndex = new MarkerIndex 302 | 303 | for j in [1..50] 304 | # 60% insert, 20% splice, 20% delete 305 | 306 | if markers.length is 0 or random(10) > 4 307 | id = idCounter++ 308 | [start, end] = getRange() 309 | # console.log "#{j}: insert(#{id}, #{start}, #{end})" 310 | markerIndex.insert(id, start, end) 311 | markers.push({id, start, end}) 312 | else if random(10) > 2 313 | [start, oldExtent, newExtent] = getSplice() 314 | # console.log "#{j}: splice(#{start}, #{oldExtent}, #{newExtent})" 315 | markerIndex.splice(start, oldExtent, newExtent) 316 | spliceMarkers(start, oldExtent, newExtent) 317 | else 318 | [{id}] = markers.splice(random(markers.length - 1), 1) 319 | # console.log "#{j}: delete(#{id})" 320 | markerIndex.delete(id) 321 | 322 | # console.log markerIndex.rootNode.toString() 323 | 324 | for {id, start, end} in markers 325 | expect(markerIndex.getStart(id)).toEqual start, "(Marker #{id} start; Seed: #{seed})" 326 | expect(markerIndex.getEnd(id)).toEqual end, "(Marker #{id} end; Seed: #{seed})" 327 | return if currentSpecFailed() 328 | 329 | for k in [1..10] 330 | [queryStart, queryEnd] = getRange() 331 | # console.log "#{k}: findContaining(#{queryStart}, #{queryEnd})" 332 | expect(markerIndex.findContaining(queryStart, queryEnd)).toEqualSet(getExpectedContaining(queryStart, queryEnd), "(Seed: #{seed})") 333 | return if currentSpecFailed() 334 | 335 | getSplice = -> 336 | start = Point(random(100), random(100)) 337 | oldExtent = Point(random(100 - start.row), random(100)) 338 | newExtent = Point(random(100 - start.row), random(100)) 339 | [start, oldExtent, newExtent] 340 | 341 | spliceMarkers = (spliceStart, oldExtent, newExtent) -> 342 | spliceOldEnd = spliceStart.traverse(oldExtent) 343 | spliceNewEnd = spliceStart.traverse(newExtent) 344 | 345 | shiftBySplice = (point) -> 346 | spliceNewEnd.traverse(point.traversalFrom(spliceOldEnd)) 347 | 348 | for marker in markers 349 | if spliceStart.compare(marker.start) < 0 350 | 351 | # replacing text before the marker or inserting at the start of the marker 352 | if spliceOldEnd.compare(marker.start) <= 0 353 | marker.start = shiftBySplice(marker.start) 354 | marker.end = shiftBySplice(marker.end) 355 | 356 | # replacing text that overlaps the start of the marker 357 | else if spliceOldEnd.compare(marker.end) < 0 358 | marker.start = spliceNewEnd 359 | marker.end = shiftBySplice(marker.end) 360 | 361 | # replacing text surrounding the marker 362 | else 363 | marker.start = spliceNewEnd 364 | marker.end = spliceNewEnd 365 | 366 | else if spliceStart.isEqual(marker.start) and spliceStart.compare(marker.end) < 0 367 | 368 | # replacing text at the start of the marker, within the marker 369 | if spliceOldEnd.compare(marker.end) < 0 370 | marker.end = shiftBySplice(marker.end) 371 | 372 | # replacing text at the start of the marker, longer than the marker 373 | else 374 | marker.end = spliceNewEnd 375 | 376 | else if spliceStart.compare(marker.end) < 0 377 | 378 | # replacing text within the marker 379 | if spliceOldEnd.compare(marker.end) <= 0 380 | marker.end = shiftBySplice(marker.end) 381 | 382 | # replacing text that overlaps the end of the marker 383 | else if spliceOldEnd.compare(marker.end) > 0 384 | marker.end = spliceNewEnd 385 | 386 | else if spliceStart.compare(marker.end) is 0 387 | 388 | # inserting text at the end of the marker 389 | if spliceOldEnd.isEqual(marker.end) 390 | marker.end = spliceNewEnd 391 | 392 | getRange = -> 393 | start = Point(random(100), random(100)) 394 | endRow = random.intBetween(start.row, 100) 395 | if endRow is start.row 396 | endColumn = random.intBetween(start.column, 100) 397 | else 398 | endColumn = random.intBetween(0, 100) 399 | end = Point(endRow, endColumn) 400 | [start, end] 401 | 402 | getExpectedContaining = (start, end) -> 403 | expected = [] 404 | for marker in markers 405 | if marker.start.compare(start) <= 0 and end.compare(marker.end) <= 0 406 | expected.push(marker.id) 407 | expected 408 | -------------------------------------------------------------------------------- /spec/paired-characters-transform-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | PairedCharactersTransform = require "../src/paired-characters-transform" 3 | StringLayer = require "../spec/string-layer" 4 | TransformLayer = require "../src/transform-layer" 5 | 6 | {expectMapsSymmetrically} = require "./spec-helper" 7 | 8 | describe "PairedCharactersTransform", -> 9 | layer = null 10 | 11 | beforeEach -> 12 | layer = new TransformLayer( 13 | new StringLayer("a\uD835\uDF97b\uD835\uDF97c"), 14 | new PairedCharactersTransform 15 | ) 16 | 17 | it "replaces paired characters with single characters", -> 18 | iterator = layer.buildIterator() 19 | 20 | expect(iterator.next()).toEqual(value: "a", done: false) 21 | expect(iterator.getPosition()).toEqual(Point(0, 1)) 22 | expect(iterator.getInputPosition()).toEqual(Point(0, 1)) 23 | 24 | expect(iterator.next()).toEqual(value: "\uD835\uDF97", done: false) 25 | expect(iterator.getPosition()).toEqual(Point(0, 2)) 26 | expect(iterator.getInputPosition()).toEqual(Point(0, 3)) 27 | 28 | expect(iterator.next()).toEqual(value: "b", done: false) 29 | expect(iterator.getPosition()).toEqual(Point(0, 3)) 30 | expect(iterator.getInputPosition()).toEqual(Point(0, 4)) 31 | 32 | expect(iterator.next()).toEqual(value: "\uD835\uDF97", done: false) 33 | expect(iterator.getPosition()).toEqual(Point(0, 4)) 34 | expect(iterator.getInputPosition()).toEqual(Point(0, 6)) 35 | 36 | expect(iterator.next()).toEqual(value: "c", done: false) 37 | expect(iterator.getPosition()).toEqual(Point(0, 5)) 38 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 39 | 40 | expect(iterator.next()).toEqual {value: undefined, done: true} 41 | expect(iterator.getPosition()).toEqual(Point(0, 5)) 42 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 43 | 44 | 45 | it "maps target positions to input positions and vice-versa", -> 46 | expectMapsSymmetrically(layer, Point(0, 0), Point(0, 0)) 47 | expectMapsSymmetrically(layer, Point(0, 1), Point(0, 1)) 48 | expectMapsSymmetrically(layer, Point(0, 3), Point(0, 2)) 49 | expectMapsSymmetrically(layer, Point(0, 4), Point(0, 3)) 50 | expectMapsSymmetrically(layer, Point(0, 6), Point(0, 4)) 51 | -------------------------------------------------------------------------------- /spec/patch-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | Patch = require "../src/patch" 3 | 4 | describe "Patch", -> 5 | patch = null 6 | 7 | beforeEach -> 8 | patch = new Patch 9 | 10 | describe "iterator", -> 11 | iterator = null 12 | 13 | it "terminates after traversing to infinity when the patch is empty", -> 14 | iterator = patch.buildIterator() 15 | expect(iterator.getPosition()).toEqual(Point.zero()) 16 | 17 | expect(iterator.next()).toEqual(value: null, done: false) 18 | expect(iterator.getPosition()).toEqual(Point.infinity()) 19 | expect(iterator.getInputPosition()).toEqual(Point.infinity()) 20 | 21 | expect(iterator.next()).toEqual(value: null, done: true) 22 | expect(iterator.getPosition()).toEqual(Point.infinity()) 23 | expect(iterator.getInputPosition()).toEqual(Point.infinity()) 24 | 25 | expectHunks = (hunks...) -> 26 | iterator.seek(Point.zero()) 27 | for [value, position, inputPosition] in hunks 28 | expect(iterator.next()).toEqual {value, done: false} 29 | expect(iterator.getPosition()).toEqual position 30 | expect(iterator.getInputPosition()).toEqual inputPosition 31 | expect(iterator.next()).toEqual {value: null, done: true} 32 | 33 | describe "::splice(extent, content)", -> 34 | describe "splicing with a positive delta", -> 35 | beforeEach -> 36 | iterator = patch.buildIterator() 37 | iterator.seek(Point(0, 4)) 38 | iterator.splice(Point(0, 3), "abcde") 39 | 40 | it "inserts new content into the map", -> 41 | expect(iterator.getPosition()).toEqual(Point(0, 9)) 42 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 43 | 44 | expectHunks( 45 | [null, Point(0, 4), Point(0, 4)] 46 | ["abcde", Point(0, 9), Point(0, 7)] 47 | [null, Point.infinity(), Point.infinity()] 48 | ) 49 | 50 | iterator.seek(Point(0, 8)) 51 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 52 | 53 | expect(iterator.next()).toEqual(value: "e", done: false) 54 | expect(iterator.getPosition()).toEqual(Point(0, 9)) 55 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 56 | 57 | it "can apply a second splice that precedes the existing splice", -> 58 | iterator.seek(Point(0, 1)) 59 | iterator.splice(Point(0, 2), "fgh") 60 | expect(iterator.getPosition()).toEqual(Point(0, 4)) 61 | expect(iterator.getInputPosition()).toEqual(Point(0, 3)) 62 | 63 | expectHunks( 64 | [null, Point(0, 1), Point(0, 1)] 65 | ["fgh", Point(0, 4), Point(0, 3)] 66 | [null, Point(0, 5), Point(0, 4)] 67 | ["abcde", Point(0, 10), Point(0, 7)] 68 | [null, Point.infinity(), Point.infinity()] 69 | ) 70 | 71 | it "can apply a second splice that ends at the beginning of the existing splice", -> 72 | iterator.seek(Point(0, 2)) 73 | iterator.splice(Point(0, 2), "fgh") 74 | expect(iterator.getPosition()).toEqual(Point(0, 5)) 75 | expect(iterator.getInputPosition()).toEqual(Point(0, 5)) 76 | 77 | expectHunks( 78 | [null, Point(0, 2), Point(0, 2)] 79 | ["fghabcde", Point(0, 10), Point(0, 7)] 80 | [null, Point.infinity(), Point.infinity()] 81 | ) 82 | 83 | it "can apply a second splice that spans the beginning of the existing splice", -> 84 | iterator.seek(Point(0, 3)) 85 | iterator.splice(Point(0, 2), "fghi") 86 | expect(iterator.getPosition()).toEqual(Point(0, 7)) 87 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 88 | 89 | expectHunks( 90 | [null, Point(0, 3), Point(0, 3)] 91 | ["fghibcde", Point(0, 11), Point(0, 7)] 92 | [null, Point.infinity(), Point.infinity()] 93 | ) 94 | 95 | it "can apply a second splice that starts at the beginning of the existing splice", -> 96 | iterator.seek(Point(0, 4)) 97 | iterator.splice(Point(0, 2), "fghi") 98 | expect(iterator.getPosition()).toEqual(Point(0, 8)) 99 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 100 | 101 | expectHunks( 102 | [null, Point(0, 4), Point(0, 4)] 103 | ["fghicde", Point(0, 11), Point(0, 7)] 104 | [null, Point.infinity(), Point.infinity()] 105 | ) 106 | 107 | it "can apply a second splice that is contained within the existing splice", -> 108 | iterator.seek(Point(0, 6)) 109 | iterator.splice(Point(0, 2), "fghi") 110 | expect(iterator.getPosition()).toEqual(Point(0, 10)) 111 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 112 | 113 | expectHunks( 114 | [null, Point(0, 4), Point(0, 4)] 115 | ["abfghie", Point(0, 11), Point(0, 7)] 116 | [null, Point.infinity(), Point.infinity()] 117 | ) 118 | 119 | it "can apply a second splice that encompasses the existing splice", -> 120 | iterator.seek(Point(0, 2)) 121 | iterator.splice(Point(0, 8), "fghijklmno") 122 | expect(iterator.getPosition()).toEqual(Point(0, 12)) 123 | expect(iterator.getInputPosition()).toEqual(Point(0, 8)) 124 | 125 | expectHunks( 126 | [null, Point(0, 2), Point(0, 2)] 127 | ["fghijklmno", Point(0, 12), Point(0, 8)] 128 | [null, Point.infinity(), Point.infinity()] 129 | ) 130 | 131 | it "can apply a second splice that ends at the end of the existing splice", -> 132 | iterator.seek(Point(0, 6)) 133 | iterator.splice(Point(0, 3), "fghij") 134 | expect(iterator.getPosition()).toEqual(Point(0, 11)) 135 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 136 | 137 | expectHunks( 138 | [null, Point(0, 4), Point(0, 4)] 139 | ["abfghij", Point(0, 11), Point(0, 7)] 140 | [null, Point.infinity(), Point.infinity()] 141 | ) 142 | 143 | it "can apply a second splice that spans the end of the existing splice", -> 144 | iterator.seek(Point(0, 8)) 145 | iterator.splice(Point(0, 4), "fghijk") 146 | expect(iterator.getPosition()).toEqual(Point(0, 14)) 147 | expect(iterator.getInputPosition()).toEqual(Point(0, 10)) 148 | 149 | expectHunks( 150 | [null, Point(0, 4), Point(0, 4)] 151 | ["abcdfghijk", Point(0, 14), Point(0, 10)] 152 | [null, Point.infinity(), Point.infinity()] 153 | ) 154 | 155 | it "can apply a second splice that follows the existing splice", -> 156 | iterator.seek(Point(0, 12)) 157 | iterator.splice(Point(0, 3), "fghij") 158 | expect(iterator.getPosition()).toEqual(Point(0, 17)) 159 | expect(iterator.getInputPosition()).toEqual(Point(0, 13)) 160 | 161 | expectHunks( 162 | [null, Point(0, 4), Point(0, 4)] 163 | ["abcde", Point(0, 9), Point(0, 7)] 164 | [null, Point(0, 12), Point(0, 10)] 165 | ["fghij", Point(0, 17), Point(0, 13)] 166 | [null, Point.infinity(), Point.infinity()] 167 | ) 168 | 169 | describe "splicing with a negative delta", -> 170 | iterator = null 171 | 172 | beforeEach -> 173 | iterator = patch.buildIterator() 174 | iterator.seek(Point(0, 2)) 175 | iterator.splice(Point(0, 5), "abc") 176 | 177 | it "inserts new content into the map", -> 178 | expect(iterator.getPosition()).toEqual(Point(0, 5)) 179 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 180 | 181 | expectHunks( 182 | [null, Point(0, 2), Point(0, 2)] 183 | ["abc", Point(0, 5), Point(0, 7)] 184 | [null, Point.infinity(), Point.infinity()] 185 | ) 186 | 187 | iterator.seek(Point(0, 5)) 188 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 189 | 190 | iterator.seek(Point(0, 3)) 191 | expect(iterator.getInputPosition()).toEqual(Point(0, 3)) 192 | 193 | expect(iterator.next()).toEqual(value: "bc", done: false) 194 | expect(iterator.getPosition()).toEqual(Point(0, 5)) 195 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 196 | 197 | it "can apply a second splice that is contained within the existing splice", -> 198 | iterator.seek(Point(0, 3)) 199 | iterator.splice(Point(0, 1), "") 200 | expect(iterator.getPosition()).toEqual(Point(0, 3)) 201 | expect(iterator.getInputPosition()).toEqual(Point(0, 3)) 202 | 203 | expectHunks( 204 | [null, Point(0, 2), Point(0, 2)] 205 | ["ac", Point(0, 4), Point(0, 7)] 206 | [null, Point.infinity(), Point.infinity()] 207 | ) 208 | 209 | describe "::seek(position)", -> 210 | it "stops at the first hunk that starts at or contains the given position", -> 211 | iterator = patch.buildIterator() 212 | iterator.seek(Point(0, 2)) 213 | iterator.splice(Point(0, 5), "") 214 | 215 | iterator.seek(Point.zero()) 216 | iterator.seek(Point(0, 2)) 217 | expect(iterator.getInputPosition()).toEqual(Point(0, 2)) 218 | 219 | expect(iterator.next()).toEqual {value: "", done: false} 220 | expect(iterator.getPosition()).toEqual(Point(0, 2)) 221 | expect(iterator.getInputPosition()).toEqual(Point(0, 7)) 222 | -------------------------------------------------------------------------------- /spec/point-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | 3 | describe "Point", -> 4 | describe "::fromObject(object, copy)", -> 5 | it "returns a new Point if object is point-compatible array ", -> 6 | expect(Point.fromObject([1, 3])).toEqual Point(1, 3) 7 | expect(Point.fromObject([Infinity, Infinity])).toEqual Point.infinity() 8 | 9 | it "returns the copy of object if it is an instanceof Point", -> 10 | origin = Point(0, 0) 11 | expect(Point.fromObject(origin, false) is origin).toBe true 12 | expect(Point.fromObject(origin, true) is origin).toBe false 13 | 14 | describe "::copy()", -> 15 | it "returns a copy of the object", -> 16 | expect(Point(3, 4).copy()).toEqual Point(3, 4) 17 | expect(Point.zero().copy()).toEqual [0, 0] 18 | 19 | describe "::negate()", -> 20 | it "returns a new point with row and column negated", -> 21 | expect(Point(3, 4).negate()).toEqual Point(-3, -4) 22 | expect(Point.zero().negate()).toEqual [0, 0] 23 | 24 | describe "::freeze()", -> 25 | it "makes the Point object immutable", -> 26 | expect(Object.isFrozen(Point(3, 4).freeze())).toBe true 27 | expect(Object.isFrozen(Point.zero().freeze())).toBe true 28 | 29 | describe "::compare(other)", -> 30 | it "returns -1 for <, 0 for =, 1 for > comparisions", -> 31 | expect(Point(2, 3).compare(Point(2, 6))).toBe -1 32 | expect(Point(2, 3).compare(Point(3, 4))).toBe -1 33 | expect(Point(1, 1).compare(Point(1, 1))).toBe 0 34 | expect(Point(2, 3).compare(Point(2, 0))).toBe 1 35 | expect(Point(2, 3).compare(Point(1, 3))).toBe 1 36 | 37 | expect(Point(2, 3).compare([2, 6])).toBe -1 38 | expect(Point(2, 3).compare([3, 4])).toBe -1 39 | expect(Point(1, 1).compare([1, 1])).toBe 0 40 | expect(Point(2, 3).compare([2, 0])).toBe 1 41 | expect(Point(2, 3).compare([1, 3])).toBe 1 42 | 43 | describe "::isLessThan(other)", -> 44 | it "returns a boolean indicating whether a point precedes the given Point ", -> 45 | expect(Point(2, 3).isLessThan(Point(2, 5))).toBe true 46 | expect(Point(2, 3).isLessThan(Point(3, 4))).toBe true 47 | expect(Point(2, 3).isLessThan(Point(2, 3))).toBe false 48 | expect(Point(2, 3).isLessThan(Point(2, 1))).toBe false 49 | expect(Point(2, 3).isLessThan(Point(1, 2))).toBe false 50 | 51 | expect(Point(2, 3).isLessThan([2, 5])).toBe true 52 | expect(Point(2, 3).isLessThan([3, 4])).toBe true 53 | expect(Point(2, 3).isLessThan([2, 3])).toBe false 54 | expect(Point(2, 3).isLessThan([2, 1])).toBe false 55 | expect(Point(2, 3).isLessThan([1, 2])).toBe false 56 | 57 | describe "::isLessThanOrEqual(other)", -> 58 | it "returns a boolean indicating whether a point precedes or equal the given Point ", -> 59 | expect(Point(2, 3).isLessThanOrEqual(Point(2, 5))).toBe true 60 | expect(Point(2, 3).isLessThanOrEqual(Point(3, 4))).toBe true 61 | expect(Point(2, 3).isLessThanOrEqual(Point(2, 3))).toBe true 62 | expect(Point(2, 3).isLessThanOrEqual(Point(2, 1))).toBe false 63 | expect(Point(2, 3).isLessThanOrEqual(Point(1, 2))).toBe false 64 | 65 | expect(Point(2, 3).isLessThanOrEqual([2, 5])).toBe true 66 | expect(Point(2, 3).isLessThanOrEqual([3, 4])).toBe true 67 | expect(Point(2, 3).isLessThanOrEqual([2, 3])).toBe true 68 | expect(Point(2, 3).isLessThanOrEqual([2, 1])).toBe false 69 | expect(Point(2, 3).isLessThanOrEqual([1, 2])).toBe false 70 | 71 | describe "::isGreaterThan(other)", -> 72 | it "returns a boolean indicating whether a point follows the given Point ", -> 73 | expect(Point(2, 3).isGreaterThan(Point(2, 5))).toBe false 74 | expect(Point(2, 3).isGreaterThan(Point(3, 4))).toBe false 75 | expect(Point(2, 3).isGreaterThan(Point(2, 3))).toBe false 76 | expect(Point(2, 3).isGreaterThan(Point(2, 1))).toBe true 77 | expect(Point(2, 3).isGreaterThan(Point(1, 2))).toBe true 78 | 79 | expect(Point(2, 3).isGreaterThan([2, 5])).toBe false 80 | expect(Point(2, 3).isGreaterThan([3, 4])).toBe false 81 | expect(Point(2, 3).isGreaterThan([2, 3])).toBe false 82 | expect(Point(2, 3).isGreaterThan([2, 1])).toBe true 83 | expect(Point(2, 3).isGreaterThan([1, 2])).toBe true 84 | 85 | describe "::isGreaterThanOrEqual(other)", -> 86 | it "returns a boolean indicating whether a point follows or equal the given Point ", -> 87 | expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 5))).toBe false 88 | expect(Point(2, 3).isGreaterThanOrEqual(Point(3, 4))).toBe false 89 | expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 3))).toBe true 90 | expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 1))).toBe true 91 | expect(Point(2, 3).isGreaterThanOrEqual(Point(1, 2))).toBe true 92 | 93 | expect(Point(2, 3).isGreaterThanOrEqual([2, 5])).toBe false 94 | expect(Point(2, 3).isGreaterThanOrEqual([3, 4])).toBe false 95 | expect(Point(2, 3).isGreaterThanOrEqual([2, 3])).toBe true 96 | expect(Point(2, 3).isGreaterThanOrEqual([2, 1])).toBe true 97 | expect(Point(2, 3).isGreaterThanOrEqual([1, 2])).toBe true 98 | 99 | describe "::isEqual()", -> 100 | it "returns if whether two points are equal", -> 101 | expect(Point(1, 1).isEqual(Point(1, 1))).toBe true 102 | expect(Point(1, 1).isEqual([1, 1])).toBe true 103 | expect(Point(1, 2).isEqual(Point(3, 3))).toBe false 104 | expect(Point(1, 2).isEqual([3, 3])).toBe false 105 | 106 | describe "::isPositive()", -> 107 | it "returns true if the point represents a forward traversal", -> 108 | expect(Point(-1, -1).isPositive()).toBe false 109 | expect(Point(-1, 0).isPositive()).toBe false 110 | expect(Point(-1, Infinity).isPositive()).toBe false 111 | expect(Point(0, 0).isPositive()).toBe false 112 | 113 | expect(Point(0, 1).isPositive()).toBe true 114 | expect(Point(5, 0).isPositive()).toBe true 115 | expect(Point(5, -1).isPositive()).toBe true 116 | 117 | describe "::isZero()", -> 118 | it "returns true if the point is zero", -> 119 | expect(Point(1, 1).isZero()).toBe false 120 | expect(Point(0, 1).isZero()).toBe false 121 | expect(Point(1, 0).isZero()).toBe false 122 | expect(Point(0, 0).isZero()).toBe true 123 | 124 | describe "::min(a, b)", -> 125 | it "returns the minimum of two points", -> 126 | expect(Point.min(Point(3, 4), Point(1, 1))).toEqual Point(1, 1) 127 | expect(Point.min(Point(1, 2), Point(5, 6))).toEqual Point(1, 2) 128 | expect(Point.min([3, 4], [1, 1])).toEqual [1, 1] 129 | expect(Point.min([1, 2], [5, 6])).toEqual [1, 2] 130 | 131 | describe "::max(a, b)", -> 132 | it "returns the minimum of two points", -> 133 | expect(Point.max(Point(3, 4), Point(1, 1))).toEqual Point(3, 4) 134 | expect(Point.max(Point(1, 2), Point(5, 6))).toEqual Point(5, 6) 135 | expect(Point.max([3, 4], [1, 1])).toEqual [3, 4] 136 | expect(Point.max([1, 2], [5, 6])).toEqual [5, 6] 137 | 138 | describe "::sanitizeNegatives()", -> 139 | it "returns the point so that it has valid buffer coordinates", -> 140 | expect(Point(-1, -1).sanitizeNegatives()).toEqual Point(0, 0) 141 | expect(Point(-1, 0).sanitizeNegatives()).toEqual Point(0, 0) 142 | expect(Point(-1, Infinity).sanitizeNegatives()).toEqual Point(0, 0) 143 | 144 | expect(Point(5, -1).sanitizeNegatives()).toEqual Point(5, 0) 145 | expect(Point(5, -Infinity).sanitizeNegatives()).toEqual Point(5, 0) 146 | expect(Point(5, 5).sanitizeNegatives()).toEqual Point(5, 5) 147 | 148 | describe "::translate(delta)", -> 149 | it "returns a new point by adding corresponding coordinates", -> 150 | expect(Point(1, 1).translate(Point(2, 3))).toEqual Point(3, 4) 151 | expect(Point.infinity().translate(Point(2, 3))).toEqual Point.infinity() 152 | 153 | expect(Point.zero().translate([5, 6])).toEqual [5, 6] 154 | expect(Point(1, 1).translate([3, 4])).toEqual [4, 5] 155 | 156 | describe "::traverse(delta)", -> 157 | it "returns a new point by traversing given rows and columns", -> 158 | expect(Point(2, 3).traverse(Point(0, 3))).toEqual Point(2, 6) 159 | expect(Point(2, 3).traverse([0, 3])).toEqual [2, 6] 160 | 161 | expect(Point(1, 3).traverse(Point(4, 2))).toEqual [5, 2] 162 | expect(Point(1, 3).traverse([5, 4])).toEqual [6, 4] 163 | 164 | describe "::traversalFrom(other)", -> 165 | it "returns a point that other has to traverse to get to given point", -> 166 | expect(Point(2, 5).traversalFrom(Point(2, 3))).toEqual Point(0, 2) 167 | expect(Point(2, 3).traversalFrom(Point(2, 5))).toEqual Point(0, -2) 168 | expect(Point(2, 3).traversalFrom(Point(2, 3))).toEqual Point(0, 0) 169 | 170 | expect(Point(3, 4).traversalFrom(Point(2, 3))).toEqual Point(1, 4) 171 | expect(Point(2, 3).traversalFrom(Point(3, 5))).toEqual Point(-1, 3) 172 | 173 | expect(Point(2, 5).traversalFrom([2, 3])).toEqual [0, 2] 174 | expect(Point(2, 3).traversalFrom([2, 5])).toEqual [0, -2] 175 | expect(Point(2, 3).traversalFrom([2, 3])).toEqual [0, 0] 176 | 177 | expect(Point(3, 4).traversalFrom([2, 3])).toEqual [1, 4] 178 | expect(Point(2, 3).traversalFrom([3, 5])).toEqual [-1, 3] 179 | 180 | describe "::toArray()", -> 181 | it "returns an array of row and column", -> 182 | expect(Point(1, 3).toArray()).toEqual [1, 3] 183 | expect(Point.zero().toArray()).toEqual [0, 0] 184 | expect(Point.infinity().toArray()).toEqual [Infinity, Infinity] 185 | 186 | describe "::serialize()", -> 187 | it "returns an array of row and column", -> 188 | expect(Point(1, 3).serialize()).toEqual [1, 3] 189 | expect(Point.zero().serialize()).toEqual [0, 0] 190 | expect(Point.infinity().serialize()).toEqual [Infinity, Infinity] 191 | 192 | describe "::toString()", -> 193 | it "returns string representation of Point", -> 194 | expect(Point(4, 5).toString()).toBe "(4, 5)" 195 | expect(Point.zero().toString()).toBe "(0, 0)" 196 | expect(Point.infinity().toString()).toBe "(Infinity, Infinity)" 197 | -------------------------------------------------------------------------------- /spec/range-spec.coffee: -------------------------------------------------------------------------------- 1 | Range = require "../src/range" 2 | Point = require "../src/point" 3 | 4 | describe "Range", -> 5 | describe "::copy()", -> 6 | it "returns a copy of the given range", -> 7 | expect(Range([1, 3], [3, 4]).copy()).toEqual Range([1, 3], [3, 4]) 8 | expect(Range([1, 3], [2, 3]).copy()).toEqual [[1, 3], [2, 3]] 9 | 10 | describe "::negate()", -> 11 | it "should negate the start and end points", -> 12 | expect(Range([ 0, 0], [ 0, 0]).negate()).toEqual [[ 0, 0], [ 0, 0]] 13 | expect(Range([ 1, 2], [ 3, 4]).negate()).toEqual [[-1, -2], [-3, -4]] 14 | expect(Range([-1, -2], [-3, -4]).negate()).toEqual [[ 1, 2], [ 3, 4]] 15 | expect(Range([-1, 2], [ 3, -4]).negate()).toEqual [[ 1, -2], [-3, 4]] 16 | 17 | describe "::reverse()", -> 18 | it "returns a range with reversed start and end points", -> 19 | expect(Range(Point(0, 2), Point(3, 4)).reverse()).toEqual [[3, 4], [0, 2]] 20 | expect(Range([3, 4], [2, 3]).reverse()).toEqual [[2, 3], [3, 4]] 21 | 22 | describe "::isEmpty()", -> 23 | it "returns whether if start is equal to end", -> 24 | expect(Range([0, 0], [0, 0]).isEmpty()).toBe true 25 | expect(Range([2, 3], [4, 5]).isEmpty()).toBe false 26 | 27 | describe "::isSingleLine()", -> 28 | it "returns whether start row is equal to end row", -> 29 | expect(Range([2, 3], [3, 4]).isSingleLine()).toBe false 30 | expect(Range([1, 2], [1, 5]).isSingleLine()).toBe true 31 | 32 | describe "::getRowCount()", -> 33 | it "returns total number of rows in the given range", -> 34 | expect(Range([2, 3], [4, 4]).getRowCount()).toBe 3 35 | expect(Range([2, 4], [2, 6]).getRowCount()).toBe 1 36 | expect(Range([2, 4], [2, 1]).getRowCount()).toBe 1 37 | 38 | expect(Range([2, 5], [0, 5]).getRowCount()).toBe -1 #incorrect due to impl 39 | 40 | describe "::getRows()", -> 41 | it "returns the rows from start.row to end.row", -> 42 | expect(Range([2, 5], [7, 6]).getRows()).toEqual [2..7] 43 | expect(Range([2, 5], [2, 9]).getRows()).toEqual [2] 44 | expect(Range([5, 6], [0, 4]).getRows()).toEqual [5..0] 45 | 46 | describe "::freeze()", -> 47 | it "makes the range object immutable", -> 48 | expect(Object.isFrozen(Range([2, 4], [3, 5]).freeze())).toBe true 49 | expect(Object.isFrozen(Range([0, 0], [0, 0]).freeze())).toBe true 50 | 51 | describe "::union(otherRange)", -> 52 | it "returns a new range that contains this range and the given range", -> 53 | expect(Range([2, 3], [3, 3]).union(Range([3, 5], [4, 6]))).toEqual [[2, 3], [4, 6]] 54 | expect(Range([2, 4], [3, 5]).union([[3, 0], [4, 5]])).toEqual [[2, 4], [4, 5]] 55 | expect(Range([2, 4], [3, 4]).union([[1, 0], [2, 7]])).toEqual [[1, 0], [3, 4]] 56 | expect(Range([2, 4], [3, 4]).union([[1, 0], [4, 5]])).toEqual [[1, 0], [4, 5]] 57 | expect(Range([2, 4], [3, 4]).union([[1, 0], [2, 2]])).toEqual [[1, 0], [3, 4]] 58 | 59 | expect(Range([4, 3], [2, 3]).union([[2, 2], [0, 0]])).toEqual [[2, 2], [2, 3]] 60 | 61 | describe "::translate(startDelta, [endDelta])", -> 62 | it "translate start by startDelta and end by endDelta and returns the range", -> 63 | expect(Range([2, 3], [4, 5]).translate([1, 1])).toEqual [[3, 4], [5, 6]] 64 | expect(Range([1, 1], [3, 3]).translate([1, 1], [2, 2])).toEqual [[2, 2], [5, 5]] 65 | 66 | describe "::traverse(delta)", -> 67 | it "traverse start & end by delta and returns the range", -> 68 | expect(Range([1, 1], [3, 3]).traverse([1, 1])).toEqual [[2, 1], [4, 1]] 69 | expect(Range([2, 2], [2, 6]).traverse([0, 3])).toEqual [[2, 5], [2, 9]] 70 | 71 | describe "::compare(other)", -> 72 | it "returns -1 for <, 0 for =, 1 for > comparisions", -> 73 | expect(Range([1, 2], [2, 3]).compare([[1, 3], [4, 5]])).toBe -1 74 | expect(Range([3, 2], [3, 4]).compare([[1, 3], [4, 5]])).toBe 1 75 | expect(Range([1, 2], [2, 3]).compare([[1, 2], [4, 5]])).toBe 1 76 | expect(Range([1, 2], [2, 3]).compare([[1, 2], [1, 0]])).toBe -1 77 | expect(Range([1, 2], [2, 3]).compare([[1, 2], [2, 3]])).toBe 0 78 | 79 | expect(Range([2, 3], [1, 2]).compare([[4, 5], [1, 3]])).toBe -1 80 | expect(Range([3, 4], [3, 2]).compare([[4, 5], [1, 3]])).toBe -1 81 | expect(Range([2, 3], [1, 2]).compare([[4, 5], [1, 2]])).toBe -1 82 | expect(Range([2, 3], [1, 2]).compare([[1, 0], [1, 2]])).toBe 1 83 | 84 | describe "::isEqual(otherRange)", -> 85 | it "returns whether otherRange is equal to the given range", -> 86 | expect(Range([1, 2], [3, 4]).isEqual([[1, 2], [3, 4]])).toBe true 87 | expect(Range([1, 2], [3, 4]).isEqual([[1, 2], [3, 3]])).toBe false 88 | 89 | describe "::coversSameRows(otherRange)", -> 90 | it "returns whether start.row and end.row for given range and otherRange are equal", -> 91 | expect(Range([1, 2], [4, 5]).coversSameRows([[1, 3], [4, 7]])).toBe true 92 | expect(Range([1, 2], [4, 5]).coversSameRows([[2, 3], [4, 7]])).toBe false 93 | expect(Range([1, 2], [4, 5]).coversSameRows([[1, 3], [3, 7]])).toBe false 94 | 95 | describe "::intersectsWith(other, [exclusive])", -> 96 | intersectsWith = (range1, range2, exclusive) -> 97 | range1 = Range.fromObject(range1) 98 | range1.intersectsWith(range2, exclusive) 99 | 100 | describe "when the exclusive argument is false (the default)", -> 101 | it "returns true if the ranges intersect, exclusive of their endpoints", -> 102 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]])).toBe false 103 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]])).toBe true 104 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]])).toBe true 105 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]])).toBe true 106 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]])).toBe true 107 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]])).toBe true 108 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]])).toBe false 109 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false 110 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false 111 | 112 | describe "when the exclusive argument is true", -> 113 | it "returns true if the ranges intersect, exclusive of their endpoints", -> 114 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]], true)).toBe false 115 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]], true)).toBe false 116 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]], true)).toBe true 117 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]], true)).toBe false 118 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]], true)).toBe true 119 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]], true)).toBe true 120 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]], true)).toBe false 121 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false 122 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false 123 | 124 | describe "::intersectsRowRange(startRow, endRow)", -> 125 | it "returns whether there is a row in given range that lies between startRow and endRow", -> 126 | expect(Range([1, 2], [3, 5]).intersectsRowRange( 2, 4)).toBe true 127 | expect(Range([1, 2], [3, 5]).intersectsRowRange( 4, 2)).toBe true 128 | expect(Range([1, 2], [3, 5]).intersectsRowRange( 0, 2)).toBe true 129 | expect(Range([1, 2], [3, 5]).intersectsRowRange( 4, 6)).toBe false 130 | expect(Range([1, 2], [3, 5]).intersectsRowRange( 6, 4)).toBe false 131 | expect(Range([1, 2], [3, 5]).intersectsRowRange(-2, -4)).toBe false 132 | 133 | describe "::getExtent()", -> 134 | it "returns a point which start has to traverse to reach end", -> 135 | expect(Range([2, 2], [4, 5]).getExtent()).toEqual [2, 5] 136 | expect(Range([2, 2], [2, 5]).getExtent()).toEqual [0, 3] 137 | 138 | describe "::containsPoint(point)", -> 139 | describe "when the 'exclusive' option is true", -> 140 | it "returns true if the given point is strictly between the range's endpoints", -> 141 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 1], true)).toBe false 142 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 2], true)).toBe true 143 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 4], true)).toBe false 144 | 145 | describe "when the 'exclusive' option is false or missing", -> 146 | it "returns true if the given point is between the range's endpoints", -> 147 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 0])).toBe false 148 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 1])).toBe true 149 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 4])).toBe true 150 | expect(Range(Point(0, 1), Point(0, 4)).containsPoint([0, 5])).toBe false 151 | 152 | describe "::containsRange(otherRange)", -> 153 | describe "when the 'exclusive' option is true", -> 154 | it "returns true if the otherRange is strictly between the range's endpoints", -> 155 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[0, 1], [1, 3]], true)).toBe false 156 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[0, 2], [1, 3]], true)).toBe true 157 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[1, 4], [1, 9]], true)).toBe false 158 | 159 | describe "when the 'exclusive' option is false or missing", -> 160 | it "returns true if the otherRange is between the range's endpoints", -> 161 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[0, 0], [1, 2]])).toBe false 162 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[0, 1], [1, 4]])).toBe true 163 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[1, 4], [1, 4]])).toBe true 164 | expect(Range(Point(0, 1), Point(1, 4)).containsRange([[0, 5], [1, 6]])).toBe false 165 | 166 | describe "::deserialize(array)", -> 167 | it "coverts the result of Range.serialize back to a range", -> 168 | expect(Range.deserialize([[1, 2], [3, 4]])).toEqual [[1, 2], [3, 4]] 169 | expect(Range.deserialize(Range([1, 2], [3, 4]).serialize())).toEqual [[1, 2], [3, 4]] 170 | 171 | describe "::serialize()", -> 172 | it "converts the range into range-compatible array", -> 173 | expect(Range([1, 3], [3, 4]).serialize()).toEqual [[1, 3], [3, 4]] 174 | expect(Range([1, 3], [1, 3]).serialize()).toEqual [[1, 3], [1, 3]] 175 | 176 | describe "::toString()", -> 177 | it "returns the string representation of range", -> 178 | expect(Range([1, 3], [3, 4]).toString()).toBe "((1, 3), (3, 4))" 179 | expect(Range([4, 3], [2, 4]).toString()).toBe "((4, 3), (2, 4))" 180 | expect(Range([0, 0], [0, 0]).toString()).toBe "((0, 0), (0, 0))" 181 | -------------------------------------------------------------------------------- /spec/soft-wraps-transform-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | SoftWrapsTransform = require "../src/soft-wraps-transform" 3 | StringLayer = require "../spec/string-layer" 4 | TransformLayer = require "../src/transform-layer" 5 | 6 | {expectMapsSymmetrically} = require "./spec-helper" 7 | 8 | describe "SoftWrapsTransform", -> 9 | it "inserts a line-break at the end of the last whitespace sequence that starts before the max column", -> 10 | layer = new TransformLayer( 11 | new StringLayer("abc def ghi jklmno\tpqr"), 12 | new SoftWrapsTransform(10) 13 | ) 14 | 15 | iterator = layer.buildIterator() 16 | expect(iterator.next()).toEqual(value: "abc def ", done: false) 17 | expect(iterator.getPosition()).toEqual(Point(1, 0)) 18 | expect(iterator.getInputPosition()).toEqual(Point(0, 8)) 19 | 20 | expect(iterator.next()).toEqual(value: "ghi jklmno\t", done: false) 21 | expect(iterator.getPosition()).toEqual(Point(2, 0)) 22 | expect(iterator.getInputPosition()).toEqual(Point(0, 19)) 23 | 24 | expect(iterator.next()).toEqual(value: "pqr", done: false) 25 | expect(iterator.getPosition()).toEqual(Point(2, 3)) 26 | expect(iterator.getInputPosition()).toEqual(Point(0, 22)) 27 | 28 | expect(iterator.next()).toEqual {value: undefined, done: true} 29 | expect(iterator.next()).toEqual {value: undefined, done: true} 30 | expect(iterator.getPosition()).toEqual(Point(2, 3)) 31 | expect(iterator.getInputPosition()).toEqual(Point(0, 22)) 32 | 33 | iterator.seek(Point(0, 4)) 34 | expect(iterator.getInputPosition()).toEqual(Point(0, 4)) 35 | 36 | expect(iterator.next()).toEqual(value: "def ", done: false) 37 | expect(iterator.getPosition()).toEqual(Point(1, 0)) 38 | expect(iterator.getInputPosition()).toEqual(Point(0, 8)) 39 | 40 | it "breaks lines within words if there is no whitespace starting before the max column", -> 41 | layer = new TransformLayer( 42 | new StringLayer("abcdefghijkl"), 43 | new SoftWrapsTransform(5) 44 | ) 45 | 46 | iterator = layer.buildIterator() 47 | expect(iterator.next()).toEqual(value: "abcde", done: false) 48 | expect(iterator.getPosition()).toEqual(Point(1, 0)) 49 | expect(iterator.getInputPosition()).toEqual(Point(0, 5)) 50 | 51 | expect(iterator.next()).toEqual(value: "fghij", done: false) 52 | expect(iterator.getPosition()).toEqual(Point(2, 0)) 53 | expect(iterator.getInputPosition()).toEqual(Point(0, 10)) 54 | 55 | expect(iterator.next()).toEqual(value: "kl", done: false) 56 | expect(iterator.getPosition()).toEqual(Point(2, 2)) 57 | expect(iterator.getInputPosition()).toEqual(Point(0, 12)) 58 | 59 | expect(iterator.next()).toEqual {value: undefined, done: true} 60 | expect(iterator.next()).toEqual {value: undefined, done: true} 61 | expect(iterator.getPosition()).toEqual(Point(2, 2)) 62 | expect(iterator.getInputPosition()).toEqual(Point(0, 12)) 63 | 64 | it "reads from the input layer until it reads a newline or it exceeds the max column", -> 65 | layer = new TransformLayer( 66 | new StringLayer("abc defghijkl", 5), 67 | new SoftWrapsTransform(10) 68 | ) 69 | 70 | iterator = layer.buildIterator() 71 | expect(iterator.next()).toEqual(value: "abc ", done: false) 72 | expect(iterator.getPosition()).toEqual(Point(1, 0)) 73 | expect(iterator.getInputPosition()).toEqual(Point(0, 4)) 74 | 75 | expect(iterator.next()).toEqual(value: "defghijkl", done: false) 76 | expect(iterator.getPosition()).toEqual(Point(1, 9)) 77 | expect(iterator.getInputPosition()).toEqual(Point(0, 13)) 78 | 79 | it "maps target positions to input positions and vice-versa", -> 80 | layer = new TransformLayer( 81 | new StringLayer("abcdefghijkl"), 82 | new SoftWrapsTransform(5) 83 | ) 84 | 85 | expectMapsSymmetrically(layer, Point(0, 0), Point(0, 0)) 86 | expectMapsSymmetrically(layer, Point(0, 1), Point(0, 1)) 87 | expectMapsSymmetrically(layer, Point(0, 5), Point(1, 0)) 88 | expectMapsSymmetrically(layer, Point(0, 6), Point(1, 1)) 89 | expectMapsSymmetrically(layer, Point(0, 10), Point(2, 0)) 90 | -------------------------------------------------------------------------------- /spec/spec-helper.coffee: -------------------------------------------------------------------------------- 1 | require 'coffee-cache' 2 | 3 | Marker = require "../src/marker" 4 | Marker::jasmineToString = -> @toString() 5 | 6 | currentSpecResult = null 7 | jasmine.getEnv().addReporter specStarted: (result) -> currentSpecResult = result 8 | currentSpecFailed = -> currentSpecResult.failedExpectations.length > 0 9 | 10 | beforeEach -> jasmine.addCustomEqualityTester (a, b) -> a?.isEqual?(b) 11 | 12 | expectMapsToInput = (layer, inputPosition, position, clip) -> 13 | expect(layer.toInputPosition(position, clip)).toEqual(inputPosition) 14 | 15 | expectMapsFromInput = (layer, inputPosition, position, clip) -> 16 | expect(layer.fromInputPosition(inputPosition, clip)).toEqual(position) 17 | 18 | expectMapsSymmetrically = (layer, inputPosition, position) -> 19 | expectMapsToInput(layer, inputPosition, position) 20 | expectMapsFromInput(layer, inputPosition, position) 21 | 22 | getAllIteratorValues = (iterator) -> 23 | values = [] 24 | until (next = iterator.next()).done 25 | values.push(next.value) 26 | values 27 | 28 | toEqualSet = -> 29 | compare: (actualSet, expectedItems, customMessage) -> 30 | result = {pass: true, message: ""} 31 | expectedSet = new Set(expectedItems) 32 | 33 | expectedSet.forEach (item) -> 34 | unless actualSet.has(item) 35 | result.pass = false 36 | result.message = "Expected set #{formatSet(actualSet)} to have item #{item}." 37 | 38 | actualSet.forEach (item) -> 39 | unless expectedSet.has(item) 40 | result.pass = false 41 | result.message = "Expected set #{formatSet(actualSet)} not to have item #{item}." 42 | 43 | result.message += " " + customMessage if customMessage? 44 | result 45 | 46 | formatSet = (set) -> 47 | "(#{setToArray(set).join(' ')})" 48 | 49 | setToArray = (set) -> 50 | items = [] 51 | set.forEach (item) -> items.push(item) 52 | items.sort() 53 | 54 | module.exports = { 55 | currentSpecFailed, expectMapsToInput, expectMapsFromInput, 56 | expectMapsSymmetrically, toEqualSet, getAllIteratorValues 57 | } 58 | -------------------------------------------------------------------------------- /spec/spy-layer.coffee: -------------------------------------------------------------------------------- 1 | Layer = require "../src/layer" 2 | Point = require "../src/point" 3 | 4 | module.exports = 5 | class SpyLayer extends Layer 6 | constructor: (@inputLayer) -> 7 | super 8 | @reset() 9 | 10 | buildIterator: -> 11 | new SpyLayerIterator(this, @inputLayer.buildIterator()) 12 | 13 | reset: -> @recordedReads = [] 14 | getRecordedReads: -> @recordedReads 15 | 16 | class SpyLayerIterator 17 | constructor: (@layer, @iterator) -> 18 | 19 | next: -> 20 | next = @iterator.next() 21 | @layer.recordedReads.push(next.value) 22 | next 23 | 24 | seek: (position) -> @iterator.seek(position) 25 | getPosition: -> @iterator.getPosition() 26 | -------------------------------------------------------------------------------- /spec/string-layer.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | Layer = require "../src/layer" 3 | 4 | module.exports = 5 | class StringLayer extends Layer 6 | constructor: (@content, @chunkSize=@content.length) -> 7 | super 8 | 9 | buildIterator: -> 10 | new StringLayerIterator(this) 11 | 12 | class StringLayerIterator 13 | constructor: (@layer) -> 14 | @position = Point.zero() 15 | 16 | next: -> 17 | if @position.column >= @layer.content.length 18 | return {value: undefined, done: true} 19 | else 20 | startColumn = @position.column 21 | @position = Point(0, Math.min(startColumn + @layer.chunkSize, @layer.content.length)) 22 | {value: @layer.content.slice(startColumn, @position.column), done: false} 23 | 24 | seek: (@position) -> 25 | @assertValidPosition(@position) 26 | 27 | getPosition: -> 28 | @position.copy() 29 | 30 | splice: (oldExtent, content) -> 31 | @assertValidPosition(@position.traverse(oldExtent)) 32 | 33 | @layer.emitter.emit("will-change", {@position, oldExtent}) 34 | 35 | @layer.content = 36 | @layer.content.substring(0, @position.column) + 37 | content + 38 | @layer.content.substring(@position.column + oldExtent.column) 39 | 40 | change = Object.freeze({@position, oldExtent, newExtent: Point(0, content.length)}) 41 | @position = @position.traverse(Point(0, content.length)) 42 | @layer.emitter.emit("did-change", change) 43 | 44 | assertValidPosition: (position) -> 45 | unless position.row is 0 and 0 <= position.column <= @layer.content.length 46 | throw new Error("Invalid position #{position}") 47 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*spec.coffee" 5 | ], 6 | "helpers": [ 7 | "spec-helper.coffee" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/text-display-document-spec.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | path = require "path" 3 | Point = require "../src/point" 4 | TextDocument = require "../src/text-document" 5 | TextDisplayDocument = require "../src/text-display-document" 6 | 7 | describe "TextDisplayDocument", -> 8 | [document, displayDocument] = [] 9 | 10 | beforeEach -> 11 | document = new TextDocument 12 | 13 | describe "::tokenizedLinesForScreenRows(start, end)", -> 14 | it "accounts for hard tabs and soft wraps", -> 15 | document.setText(""" 16 | abcd\tefg 17 | 18 | hij 19 | """) 20 | 21 | displayDocument = new TextDisplayDocument(document, 22 | tabLength: 4 23 | softWrapColumn: 10 24 | ) 25 | 26 | tokenizedLines = displayDocument.tokenizedLinesForScreenRows(0, Infinity) 27 | expect(tokenizedLines.map (line) -> line.text).toEqual [ 28 | "abcd\t " 29 | "efg\n" 30 | "\n" 31 | "hij" 32 | ] 33 | 34 | describe "position translation", -> 35 | beforeEach -> 36 | document.setText(fs.readFileSync(path.join(__dirname, "fixtures", "sample.js"), 'utf8')) 37 | 38 | displayDocument = new TextDisplayDocument(document, 39 | tabLength: 4 40 | softWrapColumn: 50 41 | ) 42 | 43 | describe "with soft wrapping", -> 44 | it "translates positions accounting for wrapped lines", -> 45 | # before any wrapped lines, within a line 46 | expect(displayDocument.screenPositionForBufferPosition([0, 5])).toEqual(Point(0, 5)) 47 | expect(displayDocument.bufferPositionForScreenPosition([0, 5])).toEqual(Point(0, 5)) 48 | 49 | # before any wrapped lines, at the end of line 50 | expect(displayDocument.screenPositionForBufferPosition([0, 29])).toEqual(Point(0, 29)) 51 | expect(displayDocument.bufferPositionForScreenPosition([0, 29])).toEqual(Point(0, 29)) 52 | 53 | # before any wrapped lines, past the end of the line 54 | expect(displayDocument.screenPositionForBufferPosition([0, 31])).toEqual(Point(0, 29)) 55 | expect(displayDocument.bufferPositionForScreenPosition([0, 31])).toEqual(Point(0, 29)) 56 | 57 | # on a wrapped line, at the wrap column 58 | expect(displayDocument.screenPositionForBufferPosition([3, 50])).toEqual(Point(3, 50)) 59 | expect(displayDocument.bufferPositionForScreenPosition([3, 50])).toEqual(Point(3, 50)) 60 | 61 | # on a wrapped line, past the wrap column 62 | expect(displayDocument.screenPositionForBufferPosition([3, 51])).toEqual(Point(4, 0)) 63 | expect(displayDocument.bufferPositionForScreenPosition([3, 51])).toEqual(Point(3, 50)) 64 | expect(displayDocument.bufferPositionForScreenPosition([3, 55])).toEqual(Point(3, 50)) 65 | expect(displayDocument.screenPositionForBufferPosition([3, 62])).toEqual(Point(4, 11)) 66 | expect(displayDocument.bufferPositionForScreenPosition([4, 11])).toEqual(Point(3, 62)) 67 | 68 | # after a wrapped line 69 | expect(displayDocument.screenPositionForBufferPosition([4, 5])).toEqual(Point(5, 5)) 70 | expect(displayDocument.bufferPositionForScreenPosition([5, 5])).toEqual(Point(4, 5)) 71 | 72 | # invalid screen positions 73 | expect(displayDocument.bufferPositionForScreenPosition([-5, -5])).toEqual(Point(0, 0)) 74 | expect(displayDocument.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual(Point(12, 2)) 75 | expect(displayDocument.bufferPositionForScreenPosition([3, -5])).toEqual(Point(3, 0)) 76 | expect(displayDocument.bufferPositionForScreenPosition([3, Infinity])).toEqual(Point(3, 50)) 77 | -------------------------------------------------------------------------------- /spec/transform-layer-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require "../src/point" 2 | TransformLayer = require "../src/transform-layer" 3 | LinesTransform = require "../src/lines-transform" 4 | StringLayer = require "../spec/string-layer" 5 | 6 | describe "TransformLayer", -> 7 | describe "::getLines()", -> 8 | it "returns the content as an array of lines", -> 9 | stringLayer = new StringLayer("\nabc\ndefg\n") 10 | layer = new TransformLayer(stringLayer, new LinesTransform) 11 | 12 | expect(layer.getLines()).toEqual [ 13 | "\n" 14 | "abc\n" 15 | "defg\n" 16 | "" 17 | ] 18 | 19 | describe "::slice(start, end)", -> 20 | it "returns the content between the start and end points", -> 21 | stringLayer = new StringLayer("\nabc\ndefg\n") 22 | layer = new TransformLayer(stringLayer, new LinesTransform) 23 | 24 | expect(layer.slice(Point(0, 0), Point(1, 0))).toBe "\n" 25 | expect(layer.slice(Point(0, 0), Point(1, 1))).toBe "\na" 26 | expect(layer.slice(Point(1, 1), Point(2, 1))).toBe "bc\nd" 27 | expect(layer.slice(Point(1, 0), Point(2, 0))).toBe "abc\n" 28 | 29 | describe "::splice(start, extent, content)", -> 30 | it "splices into the underlying layer with a translated start and extent", -> 31 | stringLayer = new StringLayer("\nabc\ndefg\n") 32 | layer = new TransformLayer(stringLayer, new LinesTransform) 33 | 34 | newExtent = layer.splice(Point(1, 2), Point(1, 2), "xyz") 35 | expect(newExtent).toEqual Point(0, 3) 36 | expect(layer.getLines()).toEqual [ 37 | "\n" 38 | "abxyzfg\n", 39 | "" 40 | ] 41 | 42 | newExtent = layer.splice(Point(1, 2), Point(0, 0), "123\n4") 43 | expect(newExtent).toEqual Point(1, 1) 44 | expect(layer.getLines()).toEqual [ 45 | "\n" 46 | "ab123\n" 47 | "4xyzfg\n", 48 | "" 49 | ] 50 | 51 | describe "when the input layer's content changes", -> 52 | it "emits an event and returns content based on the new input content", -> 53 | stringLayer = new StringLayer("\nabc\ndefg\n") 54 | layer = new TransformLayer(stringLayer, new LinesTransform) 55 | 56 | events = [] 57 | layer.onDidChange (event) -> events.push(event) 58 | 59 | stringLayer.splice(Point(0, "\nabc\nd".length), Point(0, 1), "x\nyz") 60 | 61 | expect(layer.getLines()).toEqual [ 62 | "\n" 63 | "abc\n" 64 | "dx\n" 65 | "yzfg\n" 66 | "" 67 | ] 68 | 69 | expect(events).toEqual([{ 70 | position: Point(2, 1) 71 | oldExtent: Point(0, 1) 72 | newExtent: Point(1, 2) 73 | }]) 74 | -------------------------------------------------------------------------------- /src/buffer-layer.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | Layer = require "./layer" 3 | Patch = require "./patch" 4 | 5 | module.exports = 6 | class BufferLayer extends Layer 7 | constructor: (@inputLayer) -> 8 | super 9 | @patch = new Patch 10 | @activeRegionStart = null 11 | @activeRegionEnd = null 12 | 13 | buildIterator: -> 14 | new BufferLayerIterator(this, @inputLayer.buildIterator(), @patch.buildIterator()) 15 | 16 | setActiveRegion: (@activeRegionStart, @activeRegionEnd) -> 17 | 18 | contentOverlapsActiveRegion: ({column}, content) -> 19 | (@activeRegionStart? and @activeRegionEnd?) and 20 | (column + content.length >= @activeRegionStart.column) and 21 | (column <= @activeRegionEnd.column) 22 | 23 | class BufferLayerIterator 24 | constructor: (@layer, @inputIterator, @patchIterator) -> 25 | @position = Point.zero() 26 | @inputPosition = Point.zero() 27 | 28 | next: -> 29 | comparison = @patchIterator.getPosition().compare(@position) 30 | if comparison <= 0 31 | @patchIterator.seek(@position) if comparison < 0 32 | next = @patchIterator.next() 33 | if next.value? 34 | @position = @patchIterator.getPosition() 35 | @inputPosition = @patchIterator.getInputPosition() 36 | return {value: next.value, done: next.done} 37 | 38 | @inputIterator.seek(@inputPosition) 39 | next = @inputIterator.next() 40 | nextInputPosition = @inputIterator.getPosition() 41 | 42 | inputOvershoot = @inputIterator.getPosition().traversalFrom(@patchIterator.getInputPosition()) 43 | if inputOvershoot.compare(Point.zero()) > 0 44 | next.value = next.value.substring(0, next.value.length - inputOvershoot.column) 45 | nextPosition = @patchIterator.getPosition() 46 | else 47 | nextPosition = @position.traverse(nextInputPosition.traversalFrom(@inputPosition)) 48 | 49 | if next.value? and @layer.contentOverlapsActiveRegion(@position, next.value) 50 | @patchIterator.seek(@position) 51 | extent = Point(0, next.value.length ? 0) 52 | @patchIterator.splice(extent, next.value) 53 | 54 | @inputPosition = nextInputPosition 55 | @position = nextPosition 56 | next 57 | 58 | seek: (@position) -> 59 | @patchIterator.seek(@position) 60 | @inputPosition = @patchIterator.getInputPosition() 61 | @inputIterator.seek(@inputPosition) 62 | 63 | getPosition: -> 64 | @position.copy() 65 | 66 | getInputPosition: -> 67 | @inputPosition.copy() 68 | 69 | splice: (extent, content) -> 70 | @patchIterator.splice(extent, content) 71 | @position = @patchIterator.getPosition() 72 | @inputPosition = @patchIterator.getInputPosition() 73 | @inputIterator.seek(@inputPosition) 74 | -------------------------------------------------------------------------------- /src/file-layer.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | Point = require "./point" 3 | 4 | module.exports = 5 | class FileLayer 6 | constructor: (path, @chunkSize) -> 7 | @buffer = new Buffer(@chunkSize * 4) 8 | @fd = fs.openSync(path, 'r') 9 | 10 | destroy: -> 11 | fs.close(@fd) 12 | 13 | buildIterator: -> 14 | new FileLayerIterator(this) 15 | 16 | getChunk: (byteOffset) -> 17 | bytesRead = fs.readSync(@fd, @buffer, 0, @buffer.length, byteOffset) 18 | if bytesRead > 0 19 | @buffer.toString('utf8', 0, bytesRead).substr(0, @chunkSize) 20 | else 21 | null 22 | 23 | class FileLayerIterator 24 | constructor: (@store) -> 25 | @bytePosition = 0 26 | @position = Point.zero() 27 | 28 | next: -> 29 | if chunk = @store.getChunk(@bytePosition) 30 | @position.column += chunk.length 31 | @bytePosition += Buffer.byteLength(chunk) 32 | {value: chunk, done: false} 33 | else 34 | {value: undefined, done: true} 35 | 36 | seek: (position) -> 37 | if @position.column > position.column 38 | @bytePosition = 0 39 | @position.column = 0 40 | 41 | until @position.column is position.column 42 | if chunk = @store.getChunk(@bytePosition) 43 | chunk = chunk.substring(0, position.column - @position.column) 44 | @bytePosition += Buffer.byteLength(chunk) 45 | @position.column += chunk.length 46 | else 47 | break 48 | 49 | getPosition: -> 50 | @position.copy() 51 | -------------------------------------------------------------------------------- /src/hard-tabs-transform.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class HardTabsTransform 3 | constructor: (@tabLength) -> 4 | 5 | operate: ({read, transform, getPosition, clipping}) -> 6 | if (input = read())? 7 | switch (i = input.indexOf("\t")) 8 | when -1 9 | transform(input.length) 10 | when 0 11 | transform(1, @tabStringForColumn(getPosition().column), null, clipping.open) 12 | else 13 | transform(i) 14 | transform(1, @tabStringForColumn(getPosition().column), null, clipping.open) 15 | 16 | tabStringForColumn: (column) -> 17 | length = @tabLength - (column % @tabLength) 18 | result = "\t" 19 | result += " " for i in [1...length] by 1 20 | result 21 | -------------------------------------------------------------------------------- /src/history.coffee: -------------------------------------------------------------------------------- 1 | class Checkpoint 2 | counter = 1 3 | 4 | constructor: (metadata, internal) -> 5 | @id = counter++ 6 | @internal = internal ? false 7 | @metadata = metadata 8 | 9 | module.exports = 10 | class History 11 | constructor: -> 12 | @undoStack = [] 13 | @redoStack = [] 14 | 15 | createCheckpoint: (metadata) -> 16 | checkpoint = new Checkpoint(metadata) 17 | @undoStack.push(checkpoint) 18 | checkpoint 19 | 20 | groupChangesSinceCheckpoint: (checkpoint) -> 21 | return false if @undoStack.lastIndexOf(checkpoint) is -1 22 | for entry, i in @undoStack by -1 23 | break if entry is checkpoint 24 | @undoStack.splice(i, 1) if entry instanceof Checkpoint 25 | true 26 | 27 | applyCheckpointGroupingInterval: (checkpoint, groupingInterval) -> 28 | return if groupingInterval is 0 29 | 30 | now = Date.now() 31 | 32 | groupedCheckpoint = null 33 | checkpointIndex = @undoStack.lastIndexOf(checkpoint) 34 | 35 | for i in [checkpointIndex - 1..0] by -1 36 | entry = @undoStack[i] 37 | if entry instanceof Checkpoint 38 | if (entry.timestamp + Math.min(entry.groupingInterval, groupingInterval)) >= now 39 | @undoStack.splice(checkpointIndex, 1) 40 | groupedCheckpoint = entry 41 | else 42 | groupedCheckpoint = checkpoint 43 | break 44 | 45 | groupedCheckpoint.timestamp = now 46 | groupedCheckpoint.groupingInterval = groupingInterval 47 | 48 | pushChange: (change) -> 49 | @undoStack.push(change) 50 | @redoStack.length = 0 51 | 52 | popUndoStack: (metadata) -> 53 | if (checkpointIndex = @getBoundaryCheckpointIndex(@undoStack))? 54 | @redoStack.push(new Checkpoint(metadata, true)) 55 | result = @popChanges(@undoStack, @redoStack, checkpointIndex) 56 | result.changes = result.changes.map(@invertChange) 57 | result 58 | 59 | popRedoStack: (metadata) -> 60 | if (checkpointIndex = @getBoundaryCheckpointIndex(@redoStack))? 61 | @undoStack.push(new Checkpoint(metadata, true)) 62 | @popChanges(@redoStack, @undoStack, checkpointIndex) 63 | 64 | truncateUndoStack: (checkpoint) -> 65 | checkpointIndex = @undoStack.lastIndexOf(checkpoint) 66 | return false if checkpointIndex is -1 67 | 68 | invertedChanges = [] 69 | while entry = @undoStack.pop() 70 | if entry instanceof Checkpoint 71 | break if entry is checkpoint 72 | else 73 | invertedChanges.push(@invertChange(entry)) 74 | invertedChanges 75 | 76 | ### 77 | Section: Private 78 | ### 79 | 80 | getBoundaryCheckpointIndex: (stack) -> 81 | result = null 82 | hasSeenChanges = false 83 | for entry, i in stack by -1 84 | if entry instanceof Checkpoint 85 | result = i if hasSeenChanges 86 | else 87 | hasSeenChanges = true 88 | break if result? 89 | result 90 | 91 | popChanges: (fromStack, toStack, checkpointIndex) -> 92 | changes = [] 93 | metadata = fromStack[checkpointIndex].metadata 94 | for entry in fromStack.splice(checkpointIndex) by -1 95 | isCheckpoint = entry instanceof Checkpoint 96 | toStack.push(entry) unless isCheckpoint and entry.internal 97 | changes.push(entry) unless isCheckpoint 98 | {changes, metadata} 99 | 100 | invertChange: ({oldRange, newRange, oldText, newText}) -> 101 | Object.freeze({ 102 | oldRange: newRange 103 | newRange: oldRange 104 | oldText: newText 105 | newText: oldText 106 | }) 107 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | TextDocument = require "./text-document" 2 | TextDisplayDocument = require "./text-display-document" 3 | 4 | module.exports = {TextDocument, TextDisplayDocument} 5 | -------------------------------------------------------------------------------- /src/layer.coffee: -------------------------------------------------------------------------------- 1 | {Emitter} = require "event-kit" 2 | Point = require "./point" 3 | 4 | module.exports = 5 | class Layer 6 | constructor: -> 7 | @emitter = new Emitter 8 | 9 | onWillChange: (fn) -> 10 | @emitter.on("will-change", fn) 11 | 12 | onDidChange: (fn) -> 13 | @emitter.on("did-change", fn) 14 | 15 | getExtent: -> 16 | iterator = @buildIterator() 17 | loop 18 | break if iterator.next().done 19 | iterator.getPosition() 20 | 21 | getLines: -> 22 | result = [] 23 | currentLine = "" 24 | iterator = @buildIterator() 25 | loop 26 | {value, done} = iterator.next() 27 | break if done 28 | currentLine += value 29 | if iterator.getPosition().column is 0 30 | result.push(currentLine) 31 | currentLine = "" 32 | result.push(currentLine) 33 | result 34 | 35 | slice: (start = Point.zero(), end = Point.infinity()) -> 36 | result = "" 37 | iterator = @buildIterator() 38 | 39 | lastPosition = start 40 | iterator.seek(start) 41 | 42 | loop 43 | {value, done} = iterator.next() 44 | break if done 45 | if iterator.getPosition().compare(end) <= 0 46 | result += value 47 | else 48 | result += value.slice(0, end.traversalFrom(lastPosition).column) 49 | break 50 | lastPosition = iterator.getPosition() 51 | result 52 | 53 | splice: (start, extent, content) -> 54 | iterator = @buildIterator() 55 | iterator.seek(start) 56 | iterator.splice(extent, content) 57 | iterator.getPosition().traversalFrom(start) 58 | -------------------------------------------------------------------------------- /src/lines-transform.coffee: -------------------------------------------------------------------------------- 1 | Point = require './point' 2 | 3 | LineBreak = /\r?\n|\r/ 4 | 5 | module.exports = 6 | class LinesTransform 7 | operate: ({read, transform, clipping}) -> 8 | if input = read() 9 | if match = input.match(LineBreak) 10 | if match.index is 0 11 | transform(match[0].length, match[0], Point(1, 0)) 12 | else 13 | transform(match.index) 14 | transform(match[0].length, match[0], Point(1, 0), clipping.open) 15 | else 16 | transform(input.length) 17 | -------------------------------------------------------------------------------- /src/marker-index.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | Range = require "./range" 3 | {addSet, subtractSet, intersectSet, setEqual} = require "./set-helpers" 4 | 5 | BRANCHING_THRESHOLD = 3 6 | 7 | class Node 8 | constructor: (@children) -> 9 | @ids = new Set 10 | @extent = Point.zero() 11 | for child in @children 12 | @extent = @extent.traverse(child.extent) 13 | addSet(@ids, child.ids) 14 | 15 | insert: (ids, start, end) -> 16 | rangeIsEmpty = start.compare(end) is 0 17 | childEnd = Point.zero() 18 | i = 0 19 | while i < @children.length 20 | child = @children[i++] 21 | childStart = childEnd 22 | childEnd = childStart.traverse(child.extent) 23 | 24 | switch childEnd.compare(start) 25 | when -1 then childPrecedesRange = true 26 | when 1 then childPrecedesRange = false 27 | when 0 28 | if child.hasEmptyRightmostLeaf() 29 | childPrecedesRange = false 30 | else 31 | childPrecedesRange = true 32 | if rangeIsEmpty 33 | ids = new Set(ids) 34 | child.findContaining(child.extent, ids) 35 | continue if childPrecedesRange 36 | 37 | switch childStart.compare(end) 38 | when -1 then childFollowsRange = false 39 | when 1 then childFollowsRange = true 40 | when 0 then childFollowsRange = not (child.hasEmptyLeftmostLeaf() or rangeIsEmpty) 41 | break if childFollowsRange 42 | 43 | relativeStart = Point.max(Point.zero(), start.traversalFrom(childStart)) 44 | relativeEnd = Point.min(child.extent, end.traversalFrom(childStart)) 45 | if newChildren = child.insert(ids, relativeStart, relativeEnd) 46 | @children.splice(i - 1, 1, newChildren...) 47 | i += newChildren.length - 1 48 | break if rangeIsEmpty 49 | 50 | if newNodes = @splitIfNeeded() 51 | newNodes 52 | else 53 | addSet(@ids, ids) 54 | return 55 | 56 | delete: (id) -> 57 | return unless @ids.delete(id) 58 | i = 0 59 | while i < @children.length 60 | @children[i].delete(id) 61 | i++ unless @mergeChildrenIfNeeded(i - 1) 62 | 63 | splice: (position, oldExtent, newExtent, exclusiveIds, precedingIds) -> 64 | oldRangeIsEmpty = oldExtent.isZero() 65 | spliceOldEnd = position.traverse(oldExtent) 66 | spliceNewEnd = position.traverse(newExtent) 67 | extentAfterChange = @extent.traversalFrom(spliceOldEnd) 68 | @extent = spliceNewEnd.traverse(Point.max(Point.zero(), extentAfterChange)) 69 | 70 | if position.isZero() and oldExtent.isZero() 71 | precedingIds?.forEach (id) => 72 | unless exclusiveIds.has(id) 73 | @ids.add(id) 74 | 75 | i = 0 76 | childEnd = Point.zero() 77 | while i < @children.length 78 | child = @children[i] 79 | childStart = childEnd 80 | childEnd = childStart.traverse(child.extent) 81 | 82 | switch childEnd.compare(position) 83 | when -1 then childPrecedesRange = true 84 | when 0 then childPrecedesRange = not (child.hasEmptyRightmostLeaf() and oldRangeIsEmpty) 85 | when 1 then childPrecedesRange = false 86 | 87 | unless childPrecedesRange 88 | if remainderToDelete? 89 | if remainderToDelete.isPositive() 90 | previousExtent = child.extent 91 | child.splice(Point.zero(), remainderToDelete, Point.zero()) 92 | remainderToDelete = remainderToDelete.traversalFrom(previousExtent) 93 | childEnd = childStart.traverse(child.extent) 94 | else 95 | relativeStart = position.traversalFrom(childStart) 96 | if splitNodes = child.splice(relativeStart, oldExtent, newExtent, exclusiveIds, precedingIds) 97 | @children.splice(i, 1, splitNodes...) 98 | remainderToDelete = spliceOldEnd.traversalFrom(childEnd) 99 | childEnd = childStart.traverse(child.extent) 100 | 101 | i++ unless @mergeChildrenIfNeeded(i - 1) 102 | precedingIds = child.ids 103 | 104 | @splitIfNeeded() 105 | 106 | getStart: (id) -> 107 | return unless @ids.has(id) 108 | childEnd = Point.zero() 109 | for child in @children 110 | childStart = childEnd 111 | childEnd = childStart.traverse(child.extent) 112 | if startRelativeToChild = child.getStart(id) 113 | return childStart.traverse(startRelativeToChild) 114 | return 115 | 116 | getEnd: (id) -> 117 | return unless @ids.has(id) 118 | childEnd = Point.zero() 119 | for child in @children 120 | childStart = childEnd 121 | childEnd = childStart.traverse(child.extent) 122 | if endRelativeToChild = child.getEnd(id) 123 | end = childStart.traverse(endRelativeToChild) 124 | else if end? 125 | break 126 | end 127 | 128 | dump: (offset, snapshot) -> 129 | childEnd = offset 130 | for child in @children 131 | childStart = childEnd 132 | childEnd = childStart.traverse(child.extent) 133 | child.dump(childStart, snapshot) 134 | 135 | findContaining: (point, set) -> 136 | childEnd = Point.zero() 137 | for child in @children 138 | childStart = childEnd 139 | childEnd = childStart.traverse(child.extent) 140 | continue if childEnd.compare(point) < 0 141 | break if childStart.compare(point) > 0 142 | child.findContaining(point.traversalFrom(childStart), set) 143 | 144 | findIntersecting: (start, end, set) -> 145 | if start.isZero() and end.compare(@extent) is 0 146 | addSet(set, @ids) 147 | return 148 | 149 | childEnd = Point.zero() 150 | for child in @children 151 | childStart = childEnd 152 | childEnd = childStart.traverse(child.extent) 153 | continue if childEnd.compare(start) < 0 154 | break if childStart.compare(end) > 0 155 | child.findIntersecting( 156 | Point.max(Point.zero(), start.traversalFrom(childStart)), 157 | Point.min(child.extent, end.traversalFrom(childStart)), 158 | set 159 | ) 160 | 161 | hasEmptyRightmostLeaf: -> 162 | @children[@children.length - 1].hasEmptyRightmostLeaf() 163 | 164 | hasEmptyLeftmostLeaf: -> 165 | @children[0].hasEmptyLeftmostLeaf() 166 | 167 | shouldMergeWith: (other) -> 168 | childCount = @children.length + other.children.length 169 | if @children[@children.length - 1].shouldMergeWith(other.children[0]) 170 | childCount-- 171 | childCount <= BRANCHING_THRESHOLD 172 | 173 | merge: (other) -> 174 | children = @children.concat(other.children) 175 | joinIndex = @children.length - 1 176 | if children[joinIndex].shouldMergeWith(children[joinIndex + 1]) 177 | children.splice(joinIndex, 2, children[joinIndex].merge(children[joinIndex + 1])) 178 | new Node(children) 179 | 180 | splitIfNeeded: -> 181 | if (branchingRatio = @children.length / BRANCHING_THRESHOLD) > 1 182 | splitIndex = Math.ceil(branchingRatio) 183 | [new Node(@children.slice(0, splitIndex)), new Node(@children.slice(splitIndex))] 184 | 185 | mergeChildrenIfNeeded: (i) -> 186 | if @children[i]?.shouldMergeWith(@children[i + 1]) 187 | @children.splice(i, 2, @children[i].merge(@children[i + 1])) 188 | true 189 | else 190 | false 191 | 192 | toString: (indentLevel=0) -> 193 | indent = "" 194 | indent += " " for i in [0...indentLevel] by 1 195 | 196 | ids = [] 197 | values = @ids.values() 198 | until (next = values.next()).done 199 | ids.push(next.value) 200 | 201 | """ 202 | #{indent}Node #{@extent} (#{ids.join(" ")}) 203 | #{@children.map((c) -> c.toString(indentLevel + 2)).join("\n")} 204 | """ 205 | 206 | class Leaf 207 | constructor: (@extent, @ids) -> 208 | 209 | insert: (ids, start, end) -> 210 | # If the given range matches the start and end of this leaf exactly, add 211 | # the given id to this leaf. Otherwise, split this leaf into up to 3 leaves, 212 | # adding the id to the portion of this leaf that intersects the given range. 213 | if start.isZero() and end.compare(@extent) is 0 214 | addSet(@ids, ids) 215 | return 216 | else 217 | newIds = new Set(@ids) 218 | addSet(newIds, ids) 219 | newLeaves = [] 220 | newLeaves.push(new Leaf(start, new Set(@ids))) if start.isPositive() 221 | newLeaves.push(new Leaf(end.traversalFrom(start), newIds)) 222 | newLeaves.push(new Leaf(@extent.traversalFrom(end), new Set(@ids))) if @extent.compare(end) > 0 223 | newLeaves 224 | 225 | delete: (id) -> 226 | @ids.delete(id) 227 | 228 | splice: (position, spliceOldExtent, spliceNewExtent, exclusiveIds, precedingIds) -> 229 | if position.isZero() and spliceOldExtent.isZero() 230 | boundaryIds = new Set 231 | addSet(boundaryIds, precedingIds) 232 | addSet(boundaryIds, @ids) 233 | subtractSet(boundaryIds, exclusiveIds) 234 | [new Leaf(spliceNewExtent, boundaryIds), this] 235 | else 236 | spliceOldEnd = position.traverse(spliceOldExtent) 237 | spliceNewEnd = position.traverse(spliceNewExtent) 238 | extentAfterChange = @extent.traversalFrom(spliceOldEnd) 239 | @extent = spliceNewEnd.traverse(Point.max(Point.zero(), extentAfterChange)) 240 | return 241 | 242 | getStart: (id) -> 243 | Point.zero() if @ids.has(id) 244 | 245 | getEnd: (id) -> 246 | @extent if @ids.has(id) 247 | 248 | dump: (offset, snapshot) -> 249 | end = offset.traverse(@extent) 250 | @ids.forEach (id) -> 251 | if snapshot[id].range? 252 | snapshot[id].range.end = end 253 | else 254 | snapshot[id].range = Range(offset, end) 255 | 256 | findContaining: (point, set) -> 257 | addSet(set, @ids) 258 | 259 | findIntersecting: (start, end, set) -> 260 | addSet(set, @ids) 261 | 262 | hasEmptyRightmostLeaf: -> 263 | @extent.isZero() 264 | 265 | hasEmptyLeftmostLeaf: -> 266 | @extent.isZero() 267 | 268 | shouldMergeWith: (other) -> 269 | setEqual(@ids, other.ids) or @extent.isZero() and other.extent.isZero() 270 | 271 | merge: (other) -> 272 | ids = new Set(@ids) 273 | other.ids.forEach (id) -> ids.add(id) 274 | new Leaf(@extent.traverse(other.extent), ids) 275 | 276 | toString: (indentLevel=0) -> 277 | indent = "" 278 | indent += " " for i in [0...indentLevel] by 1 279 | 280 | ids = [] 281 | values = @ids.values() 282 | until (next = values.next()).done 283 | ids.push(next.value) 284 | 285 | "#{indent}Leaf #{@extent} (#{ids.join(" ")})" 286 | 287 | module.exports = 288 | class MarkerIndex 289 | constructor: -> 290 | @clear() 291 | 292 | insert: (id, start, end) -> 293 | if splitNodes = @rootNode.insert(new Set().add(id), start, end) 294 | @rootNode = new Node(splitNodes) 295 | 296 | delete: (id) -> 297 | @rootNode.delete(id) 298 | @condenseIfNeeded() 299 | 300 | splice: (position, oldExtent, newExtent) -> 301 | if splitNodes = @rootNode.splice(position, oldExtent, newExtent, @exclusiveIds, new Set) 302 | @rootNode = new Node(splitNodes) 303 | @condenseIfNeeded() 304 | 305 | isExclusive: (id) -> 306 | @exclusiveIds.has(id) 307 | 308 | setExclusive: (id, isExclusive) -> 309 | if isExclusive 310 | @exclusiveIds.add(id) 311 | else 312 | @exclusiveIds.delete(id) 313 | 314 | getRange: (id) -> 315 | if start = @getStart(id) 316 | Range(start, @getEnd(id)) 317 | 318 | getStart: (id) -> 319 | @rootNode.getStart(id) 320 | 321 | getEnd: (id) -> 322 | @rootNode.getEnd(id) 323 | 324 | findContaining: (start, end) -> 325 | containing = new Set 326 | @rootNode.findContaining(start, containing) 327 | if end? and end.compare(start) isnt 0 328 | containingEnd = new Set 329 | @rootNode.findContaining(end, containingEnd) 330 | containing.forEach (id) -> containing.delete(id) unless containingEnd.has(id) 331 | containing 332 | 333 | findContainedIn: (start, end = start) -> 334 | result = @findStartingIn(start, end) 335 | subtractSet(result, @findIntersecting(end.traverse(Point(0, 1)))) 336 | result 337 | 338 | findIntersecting: (start, end = start) -> 339 | intersecting = new Set 340 | @rootNode.findIntersecting(start, end, intersecting) 341 | intersecting 342 | 343 | findStartingIn: (start, end = start) -> 344 | result = @findIntersecting(start, end) 345 | if start.isPositive() 346 | if start.column is 0 347 | previousPoint = Point(start.row - 1, Infinity) 348 | else 349 | previousPoint = Point(start.row, start.column - 1) 350 | subtractSet(result, @findIntersecting(previousPoint)) 351 | result 352 | 353 | findEndingIn: (start, end = start) -> 354 | result = @findIntersecting(start, end) 355 | subtractSet(result, @findIntersecting(end.traverse(Point(0, 1)))) 356 | result 357 | 358 | clear: -> 359 | @exclusiveIds = new Set 360 | @rootNode = new Leaf(Point.infinity(), new Set) 361 | 362 | dump: -> 363 | snapshot = {} 364 | @rootNode.ids.forEach (id) => 365 | snapshot[id] = {range: null, isExclusive: @exclusiveIds.has(id)} 366 | @rootNode.dump(Point.zero(), snapshot) 367 | snapshot 368 | 369 | load: (snapshot) -> 370 | @clear() 371 | for id, {range: {start, end}, isExclusive} of snapshot 372 | @insert(id, start, end) 373 | @setExclusive(id, isExclusive) 374 | 375 | ### 376 | Section: Private 377 | ### 378 | 379 | condenseIfNeeded: -> 380 | while @rootNode.children?.length is 1 381 | @rootNode = @rootNode.children[0] 382 | -------------------------------------------------------------------------------- /src/marker-store.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | Range = require "./range" 3 | Marker = require "./marker" 4 | MarkerIndex = require "./marker-index" 5 | {intersectSet} = require "./set-helpers" 6 | 7 | module.exports = 8 | class MarkerStore 9 | constructor: (@delegate) -> 10 | @index = new MarkerIndex 11 | @markersById = {} 12 | @nextMarkerId = 0 13 | 14 | ### 15 | Section: TextDocument API 16 | ### 17 | 18 | getMarker: (id) -> 19 | @markersById[id] 20 | 21 | getMarkers: -> 22 | marker for id, marker of @markersById 23 | 24 | findMarkers: (params) -> 25 | markerIds = new Set(Object.keys(@markersById)) 26 | 27 | if params.startPosition? 28 | point = Point.fromObject(params.startPosition) 29 | intersectSet(markerIds, @index.findStartingIn(point)) 30 | delete params.startPosition 31 | 32 | if params.endPosition? 33 | point = Point.fromObject(params.endPosition) 34 | intersectSet(markerIds, @index.findEndingIn(point)) 35 | delete params.endPosition 36 | 37 | if params.containsPoint? 38 | point = Point.fromObject(params.containsPoint) 39 | intersectSet(markerIds, @index.findContaining(point)) 40 | delete params.containsPoint 41 | 42 | if params.containsRange? 43 | {start, end} = Range.fromObject(params.containsRange) 44 | intersectSet(markerIds, @index.findContaining(start, end)) 45 | delete params.containsRange 46 | 47 | if params.intersectsRange? 48 | {start, end} = Range.fromObject(params.intersectsRange) 49 | intersectSet(markerIds, @index.findIntersecting(start, end)) 50 | delete params.intersectsRange 51 | 52 | if params.startRow? 53 | row = params.startRow 54 | intersectSet(markerIds, @index.findStartingIn(Point(row, 0), Point(row, Infinity))) 55 | delete params.startRow 56 | 57 | if params.endRow? 58 | row = params.endRow 59 | intersectSet(markerIds, @index.findEndingIn(Point(row, 0), Point(row, Infinity))) 60 | delete params.endRow 61 | 62 | if params.intersectsRow? 63 | row = params.intersectsRow 64 | intersectSet(markerIds, @index.findIntersecting(Point(row, 0), Point(row, Infinity))) 65 | delete params.intersectsRow 66 | 67 | if params.intersectsRowRange? 68 | [startRow, endRow] = params.intersectsRowRange 69 | intersectSet(markerIds, @index.findIntersecting(Point(startRow, 0), Point(endRow, Infinity))) 70 | delete params.intersectsRowRange 71 | 72 | if params.containedInRange? 73 | {start, end} = Range.fromObject(params.containedInRange) 74 | intersectSet(markerIds, @index.findContainedIn(start, end)) 75 | delete params.containedInRange 76 | 77 | result = [] 78 | for id, marker of @markersById 79 | result.push(marker) if markerIds.has(id) and marker.matchesParams(params) 80 | result.sort (marker1, marker2) -> marker1.compare(marker2) 81 | 82 | markRange: (range, options={}) -> 83 | range = Range.fromObject(range) 84 | marker = new Marker(String(@nextMarkerId++), this, range, options) 85 | @markersById[marker.id] = marker 86 | @index.insert(marker.id, range.start, range.end) 87 | if marker.invalidationStrategy is 'inside' 88 | @index.setExclusive(marker.id, true) 89 | @delegate.markerCreated(marker) 90 | marker 91 | 92 | markPosition: (position, options) -> 93 | properties = {} 94 | properties[key] = value for key, value of options 95 | properties.tailed = false 96 | @markRange(Range(position, position), properties) 97 | 98 | splice: (start, oldExtent, newExtent) -> 99 | end = start.traverse(oldExtent) 100 | 101 | intersecting = @index.findIntersecting(start, end) 102 | endingAt = @index.findEndingIn(start) 103 | startingAt = @index.findStartingIn(end) 104 | startingIn = @index.findStartingIn(start.traverse(Point(0, 1)), end.traverse(Point(0, -1))) 105 | endingIn = @index.findEndingIn(start.traverse(Point(0, 1)), end.traverse(Point(0, -1))) 106 | 107 | for id, marker of @markersById 108 | switch marker.invalidationStrategy 109 | when 'touch' 110 | invalid = intersecting.has(id) 111 | when 'inside' 112 | invalid = intersecting.has(id) and not (startingAt.has(id) or endingAt.has(id)) 113 | when 'overlap' 114 | invalid = startingIn.has(id) or endingIn.has(id) 115 | when 'surround' 116 | invalid = startingIn.has(id) and endingIn.has(id) 117 | when 'never' 118 | invalid = false 119 | 120 | marker.valid = not invalid 121 | 122 | @index.splice(start, oldExtent, newExtent) 123 | 124 | restoreFromSnapshot: (snapshots) -> 125 | for id, marker of @markersById 126 | if snapshot = snapshots[id] 127 | marker.update(snapshot, true) 128 | 129 | emitChangeEvents: -> 130 | for id, marker of @markersById 131 | marker.emitChangeEvent(marker.getRange(), true, false) 132 | 133 | createSnapshot: -> 134 | markerSnapshots = @index.dump() 135 | for id, marker of @markersById 136 | snapshot = markerSnapshots[id] 137 | delete snapshot.isExclusive 138 | snapshot.reversed = marker.isReversed() 139 | snapshot.tailed = marker.hasTail() 140 | snapshot.invalidate = marker.invalidationStrategy 141 | snapshot.valid = marker.isValid() 142 | snapshot.properties = marker.properties 143 | markerSnapshots 144 | 145 | ### 146 | Section: Marker API 147 | ### 148 | 149 | destroyMarker: (id) -> 150 | delete @markersById[id] 151 | @index.delete(id) 152 | 153 | getMarkerRange: (id) -> 154 | @index.getRange(id) 155 | 156 | getMarkerStartPosition: (id) -> 157 | @index.getStart(id) 158 | 159 | getMarkerEndPosition: (id) -> 160 | @index.getEnd(id) 161 | 162 | setMarkerRange: (id, range) -> 163 | @index.delete(id) 164 | {start, end} = Range.fromObject(range) 165 | @index.insert(id, @delegate.clipPosition(start), @delegate.clipPosition(end)) 166 | 167 | setMarkerHasTail: (id, hasTail) -> 168 | @index.setExclusive(id, not hasTail) 169 | -------------------------------------------------------------------------------- /src/marker.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | Range = require "./range" 3 | {Emitter} = require "event-kit" 4 | 5 | module.exports = 6 | class Marker 7 | constructor: (@id, @store, range, @properties) -> 8 | @emitter = new Emitter 9 | @valid = true 10 | 11 | @tailed = @properties.tailed ? true 12 | delete @properties.tailed 13 | 14 | @reversed = @properties.reversed ? false 15 | delete @properties.reversed 16 | 17 | @invalidationStrategy = @properties.invalidate ? 'overlap' 18 | delete @properties.invalidate 19 | 20 | @store.setMarkerHasTail(@id, @tailed) 21 | @previousEventState = @getEventState(range) 22 | 23 | getRange: -> 24 | @store.getMarkerRange(@id) 25 | 26 | getHeadPosition: -> 27 | if @reversed 28 | @store.getMarkerStartPosition(@id) 29 | else 30 | @store.getMarkerEndPosition(@id) 31 | 32 | getTailPosition: -> 33 | if @reversed 34 | @store.getMarkerEndPosition(@id) 35 | else 36 | @store.getMarkerStartPosition(@id) 37 | 38 | setRange: (range, properties) -> 39 | if properties?.reversed? 40 | reversed = properties.reversed 41 | delete properties.reversed 42 | @update({range: Range.fromObject(range), tailed: true, reversed, properties}) 43 | 44 | setHeadPosition: (position, properties) -> 45 | @update({headPosition: Point.fromObject(position), properties}) 46 | 47 | setTailPosition: (position, properties) -> 48 | @update({tailPosition: Point.fromObject(position), properties}) 49 | 50 | clearTail: -> 51 | @update({tailed: false}) 52 | 53 | plantTail: -> 54 | @update({tailed: true}) 55 | 56 | getInvalidationStrategy: -> @invalidationStrategy 57 | 58 | getProperties: -> @properties 59 | 60 | setProperties: (newProperties) -> 61 | for key, value of newProperties 62 | @properties[key] = value 63 | 64 | update: ({reversed, tailed, valid, headPosition, tailPosition, range, properties}, textChanged=false) -> 65 | changed = propertiesChanged = false 66 | 67 | wasTailed = @tailed 68 | newRange = oldRange = @getRange() 69 | if @reversed 70 | oldHeadPosition = oldRange.start 71 | oldTailPosition = oldRange.end 72 | else 73 | oldHeadPosition = oldRange.end 74 | oldTailPosition = oldRange.start 75 | 76 | if reversed? and reversed isnt @reversed 77 | @reversed = reversed 78 | changed = true 79 | 80 | if valid? and valid isnt @valid 81 | @valid = valid 82 | changed = true 83 | 84 | if tailed? and tailed isnt @tailed 85 | @tailed = tailed 86 | changed = true 87 | unless @tailed 88 | @reversed = false 89 | newRange = Range(oldHeadPosition, oldHeadPosition) 90 | 91 | if properties? and not @matchesParams(properties) 92 | @setProperties(properties) 93 | changed = true 94 | propertiesChanged = true 95 | 96 | if range? 97 | newRange = range 98 | 99 | if headPosition? and not headPosition.isEqual(oldHeadPosition) 100 | changed = true 101 | if not @tailed 102 | newRange = Range(headPosition, headPosition) 103 | else if headPosition.compare(oldTailPosition) < 0 104 | @reversed = true 105 | newRange = Range(headPosition, oldTailPosition) 106 | else 107 | @reversed = false 108 | newRange = Range(oldTailPosition, headPosition) 109 | 110 | if tailPosition? and not tailPosition.isEqual(oldTailPosition) 111 | changed = true 112 | @tailed = true 113 | if tailPosition.compare(oldHeadPosition) < 0 114 | @reversed = false 115 | newRange = Range(tailPosition, oldHeadPosition) 116 | else 117 | @reversed = true 118 | newRange = Range(oldHeadPosition, tailPosition) 119 | changed = true 120 | 121 | unless newRange.isEqual(oldRange) 122 | @store.setMarkerRange(@id, newRange) 123 | unless @tailed is wasTailed 124 | @store.setMarkerHasTail(@id, @tailed) 125 | @emitChangeEvent(newRange, textChanged, propertiesChanged) 126 | changed 127 | 128 | emitChangeEvent: (currentRange, textChanged, propertiesChanged) -> 129 | oldState = @previousEventState 130 | newState = @previousEventState = @getEventState(currentRange) 131 | 132 | return unless propertiesChanged or 133 | oldState.valid isnt newState.valid or 134 | oldState.tailed isnt newState.tailed or 135 | oldState.headPosition.compare(newState.headPosition) isnt 0 or 136 | oldState.tailPosition.compare(newState.tailPosition) isnt 0 137 | 138 | @emitter.emit("did-change", { 139 | wasValid: oldState.valid, isValid: newState.valid 140 | hadTail: oldState.tailed, hasTail: newState.tailed 141 | oldProperties: oldState.properties, newProperties: newState.properties 142 | oldHeadPosition: oldState.headPosition, newHeadPosition: newState.headPosition 143 | oldTailPosition: oldState.tailPosition, newTailPosition: newState.tailPosition 144 | textChanged: textChanged 145 | }) 146 | 147 | isValid: -> @valid 148 | 149 | matchesParams: (params) -> 150 | for key, value of params 151 | if key is 'invalidate' 152 | return false unless @invalidationStrategy is value 153 | else 154 | return false unless @properties[key] is value 155 | true 156 | 157 | compare: (other) -> 158 | @getRange().compare(other.getRange()) 159 | 160 | isReversed: -> @reversed 161 | 162 | hasTail: -> @tailed 163 | 164 | destroy: -> 165 | @store.destroyMarker(@id) 166 | @emitter.emit("did-destroy") 167 | 168 | copy: (options) -> 169 | properties = clone(@properties) 170 | properties[key] = value for key, value of options 171 | @store.markRange(@getRange(), options) 172 | 173 | ### 174 | Section: Event Subscription 175 | ### 176 | 177 | onDidDestroy: (callback) -> 178 | @emitter.on("did-destroy", callback) 179 | 180 | onDidChange: (callback) -> 181 | @emitter.on("did-change", callback) 182 | 183 | toString: -> 184 | "[Marker #{@id}, #{@getRange()}]" 185 | 186 | ### 187 | Section: Private 188 | ### 189 | 190 | getEventState: (range) -> 191 | { 192 | headPosition: (if @reversed then range.start else range.end) 193 | tailPosition: (if @reversed then range.end else range.start) 194 | properties: clone(@properties) 195 | tailed: @tailed 196 | valid: true 197 | } 198 | 199 | clone = (object) -> 200 | result = {} 201 | result[key] = value for key, value of object 202 | result 203 | -------------------------------------------------------------------------------- /src/null-layer.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | 3 | module.exports = 4 | class NullLayer 5 | buildIterator: -> 6 | new NullLayerIterator 7 | 8 | class NullLayerIterator 9 | next: -> {value: null, done: true} 10 | seek: -> 11 | getPosition: -> Point.zero() 12 | -------------------------------------------------------------------------------- /src/paired-characters-transform.coffee: -------------------------------------------------------------------------------- 1 | Point = require './point' 2 | 3 | module.exports = 4 | class PairedCharactersTransform 5 | operate: ({read, transform}) -> 6 | if input = read() 7 | for i in [0...input.length - 1] by 1 8 | if isPairedCharacter(input.charCodeAt(i), input.charCodeAt(i + 1)) 9 | transform(i) 10 | transform(2, input.substring(i, i + 2), Point(0, 1)) 11 | return 12 | 13 | transform(input.length) 14 | 15 | # Is the character at the given index the start of high/low surrogate pair 16 | # a variation sequence, or a combined character? 17 | # 18 | # * `string` The {String} to check for a surrogate pair, variation sequence, 19 | # or combined character. 20 | # * `index` The {Number} index to look for a surrogate pair, variation 21 | # sequence, or combined character. 22 | # 23 | # Return a {Boolean}. 24 | isPairedCharacter = (charCodeA, charCodeB) -> 25 | isSurrogatePair(charCodeA, charCodeB) or 26 | isVariationSequence(charCodeA, charCodeB) or 27 | isCombinedCharacter(charCodeA, charCodeB) 28 | 29 | # Are the given character codes a high/low surrogate pair? 30 | # 31 | # * `charCodeA` The first character code {Number}. 32 | # * `charCode2` The second character code {Number}. 33 | # 34 | # Return a {Boolean}. 35 | isSurrogatePair = (charCodeA, charCodeB) -> 36 | isHighSurrogate(charCodeA) and isLowSurrogate(charCodeB) 37 | 38 | # Are the given character codes a variation sequence? 39 | # 40 | # * `charCodeA` The first character code {Number}. 41 | # * `charCode2` The second character code {Number}. 42 | # 43 | # Return a {Boolean}. 44 | isVariationSequence = (charCodeA, charCodeB) -> 45 | not isVariationSelector(charCodeA) and isVariationSelector(charCodeB) 46 | 47 | # Are the given character codes a combined character pair? 48 | # 49 | # * `charCodeA` The first character code {Number}. 50 | # * `charCode2` The second character code {Number}. 51 | # 52 | # Return a {Boolean}. 53 | isCombinedCharacter = (charCodeA, charCodeB) -> 54 | not isCombiningCharacter(charCodeA) and isCombiningCharacter(charCodeB) 55 | 56 | isHighSurrogate = (charCode) -> 57 | 0xD800 <= charCode <= 0xDBFF 58 | 59 | isLowSurrogate = (charCode) -> 60 | 0xDC00 <= charCode <= 0xDFFF 61 | 62 | isVariationSelector = (charCode) -> 63 | 0xFE00 <= charCode <= 0xFE0F 64 | 65 | isCombiningCharacter = (charCode) -> 66 | 0x0300 <= charCode <= 0x036F or 67 | 0x1AB0 <= charCode <= 0x1AFF or 68 | 0x1DC0 <= charCode <= 0x1DFF or 69 | 0x20D0 <= charCode <= 0x20FF or 70 | 0xFE20 <= charCode <= 0xFE2F 71 | -------------------------------------------------------------------------------- /src/patch.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | 3 | module.exports = 4 | class Patch 5 | constructor: -> 6 | @hunks = [{ 7 | content: null 8 | extent: Point.infinity() 9 | inputExtent: Point.infinity() 10 | }] 11 | 12 | buildIterator: -> 13 | new PatchIterator(this) 14 | 15 | class PatchIterator 16 | constructor: (@patch) -> 17 | @seek(Point.zero()) 18 | 19 | seek: (@position) -> 20 | position = Point.zero() 21 | inputPosition = Point.zero() 22 | 23 | for hunk, index in @patch.hunks 24 | nextPosition = position.traverse(hunk.extent) 25 | nextInputPosition = inputPosition.traverse(hunk.inputExtent) 26 | 27 | if nextPosition.compare(@position) > 0 or position.compare(@position) is 0 28 | @index = index 29 | @hunkOffset = @position.traversalFrom(position) 30 | @inputPosition = Point.min(inputPosition.traverse(@hunkOffset), nextInputPosition) 31 | return 32 | 33 | position = nextPosition 34 | inputPosition = nextInputPosition 35 | 36 | # This shouldn't happen because the last hunk's extent is infinite. 37 | throw new Error("No hunk found for position #{@position}") 38 | 39 | next: -> 40 | if hunk = @patch.hunks[@index] 41 | value = hunk.content?.slice(@hunkOffset.column) ? null 42 | 43 | remainingExtent = hunk.extent.traversalFrom(@hunkOffset) 44 | remainingInputExtent = hunk.inputExtent.traversalFrom(@hunkOffset) 45 | 46 | @position = @position.traverse(remainingExtent) 47 | if remainingInputExtent.isPositive() 48 | @inputPosition = @inputPosition.traverse(remainingInputExtent) 49 | 50 | @index++ 51 | @hunkOffset = Point.zero() 52 | {value, done: false} 53 | else 54 | {value: null, done: true} 55 | 56 | splice: (oldExtent, newContent) -> 57 | newHunks = [] 58 | startIndex = @index 59 | startPosition = @position 60 | startInputPosition = @inputPosition 61 | 62 | unless @hunkOffset.isZero() 63 | hunkToSplit = @patch.hunks[@index] 64 | newHunks.push({ 65 | extent: @hunkOffset 66 | inputExtent: Point.min(@hunkOffset, hunkToSplit.inputExtent) 67 | content: hunkToSplit.content?.substring(0, @hunkOffset.column) ? null 68 | }) 69 | 70 | @seek(@position.traverse(oldExtent)) 71 | 72 | inputExtent = @inputPosition.traversalFrom(startInputPosition) 73 | newExtent = Point(0, newContent.length) 74 | newHunks.push({ 75 | extent: newExtent 76 | inputExtent: inputExtent 77 | content: newContent 78 | }) 79 | 80 | hunkToSplit = @patch.hunks[@index] 81 | newHunks.push({ 82 | extent: hunkToSplit.extent.traversalFrom(@hunkOffset) 83 | inputExtent: Point.max(Point.zero(), hunkToSplit.inputExtent.traversalFrom(@hunkOffset)) 84 | content: hunkToSplit.content?.slice(@hunkOffset.column) 85 | }) 86 | 87 | spliceHunks = [] 88 | lastHunk = null 89 | for hunk in newHunks 90 | if lastHunk?.content? and hunk.content? 91 | lastHunk.content += hunk.content 92 | lastHunk.inputExtent = lastHunk.inputExtent.traverse(hunk.inputExtent) 93 | lastHunk.extent = lastHunk.extent.traverse(hunk.extent) 94 | else 95 | spliceHunks.push(hunk) 96 | lastHunk = hunk 97 | 98 | @patch.hunks.splice(startIndex, @index - startIndex + 1, spliceHunks...) 99 | 100 | @seek(startPosition.traverse(newExtent)) 101 | 102 | getPosition: -> 103 | @position.copy() 104 | 105 | getInputPosition: -> 106 | @inputPosition.copy() 107 | -------------------------------------------------------------------------------- /src/point.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | class Point 3 | @zero: -> 4 | new Point(0, 0) 5 | 6 | @infinity: -> 7 | new Point(Infinity, Infinity) 8 | 9 | @min: (left, right) -> 10 | left = Point.fromObject(left) 11 | right = Point.fromObject(right) 12 | if left.compare(right) <= 0 13 | left 14 | else 15 | right 16 | 17 | @max: (left, right) -> 18 | left = Point.fromObject(left) 19 | right = Point.fromObject(right) 20 | if left.compare(right) >= 0 21 | left 22 | else 23 | right 24 | 25 | @fromObject: (object, copy) -> 26 | if object instanceof Point 27 | if copy then object.copy() else object 28 | else 29 | if Array.isArray(object) 30 | [row, column] = object 31 | else 32 | { row, column } = object 33 | 34 | new Point(row, column) 35 | 36 | constructor: (row, column) -> 37 | unless this instanceof Point 38 | return new Point(row, column) 39 | @row = row 40 | @column = column 41 | 42 | compare: (other) -> 43 | other = Point.fromObject(other) 44 | if @row > other.row 45 | 1 46 | else if @row < other.row 47 | -1 48 | else if @column > other.column 49 | 1 50 | else if @column < other.column 51 | -1 52 | else 53 | 0 54 | 55 | isEqual: (other) -> 56 | @compare(other) is 0 57 | 58 | isLessThan: (other) -> 59 | @compare(other) is -1 60 | 61 | isLessThanOrEqual: (other) -> 62 | cmp = @compare(other) 63 | cmp is -1 or cmp is 0 64 | 65 | isGreaterThan: (other) -> 66 | @compare(other) is 1 67 | 68 | isGreaterThanOrEqual: (other) -> 69 | cmp = @compare(other) 70 | cmp is 1 or cmp is 0 71 | 72 | copy: -> 73 | new Point(@row, @column) 74 | 75 | negate: -> 76 | new Point(-@row, -@column) 77 | 78 | freeze: -> 79 | Object.freeze(this) 80 | 81 | isZero: -> 82 | @row is 0 and @column is 0 83 | 84 | isPositive: -> 85 | if @row > 0 86 | true 87 | else if @row < 0 88 | false 89 | else 90 | @column > 0 91 | 92 | sanitizeNegatives: -> 93 | if @row < 0 94 | new Point(0, 0) 95 | else if @column < 0 96 | new Point(@row, 0) 97 | else 98 | @copy() 99 | 100 | translate: (delta) -> 101 | delta = Point.fromObject(delta) 102 | new Point(@row + delta.row, @column + delta.column) 103 | 104 | traverse: (delta) -> 105 | delta = Point.fromObject(delta) 106 | if delta.row is 0 107 | new Point(@row, @column + delta.column) 108 | else 109 | new Point(@row + delta.row, delta.column) 110 | 111 | traversalFrom: (other) -> 112 | other = Point.fromObject(other) 113 | if @row is other.row 114 | if @column is Infinity and other.column is Infinity 115 | new Point(0, 0) 116 | else 117 | new Point(0, @column - other.column) 118 | else 119 | new Point(@row - other.row, @column) 120 | 121 | toArray: -> 122 | [@row, @column] 123 | 124 | serialize: -> 125 | @toArray() 126 | 127 | toString: -> 128 | "(#{@row}, #{@column})" 129 | -------------------------------------------------------------------------------- /src/range.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | 3 | module.exports = 4 | class Range 5 | @fromObject: (object) -> 6 | if object instanceof Range 7 | object 8 | else 9 | if Array.isArray(object) 10 | [start, end] = object 11 | else 12 | {start, end} = object 13 | new Range(start, end) 14 | 15 | constructor: (start, end) -> 16 | unless this instanceof Range 17 | return new Range(start, end) 18 | @start = Point.fromObject(start) 19 | @end = Point.fromObject(end) 20 | 21 | copy: -> 22 | new Range(@start, @end) 23 | 24 | negate: -> 25 | new Range(@start.negate(), @end.negate()) 26 | 27 | reverse: -> 28 | new Range(@end, @start) 29 | 30 | isEmpty: -> 31 | @start.compare(@end) is 0 32 | 33 | isSingleLine: -> 34 | @start.row is @end.row 35 | 36 | getRowCount: -> 37 | @end.row - @start.row + 1 38 | 39 | getRows: -> 40 | [@start.row..@end.row] 41 | 42 | freeze: -> 43 | @start.freeze() 44 | @end.freeze() 45 | Object.freeze(this) 46 | 47 | union: (other) -> 48 | other = Range.fromObject(other) 49 | Range(Point.min(@start, other.start), Point.max(@end, other.end)) 50 | 51 | translate: (startDelta, endDelta=startDelta) -> 52 | startDelta = Point.fromObject(startDelta) 53 | endDelta = Point.fromObject(endDelta) 54 | Range(@start.translate(startDelta), @end.translate(endDelta)) 55 | 56 | traverse: (delta) -> 57 | delta = Point.fromObject(delta) 58 | Range(@start.traverse(delta), @end.traverse(delta)) 59 | 60 | compare: (other) -> 61 | other = Range.fromObject(other) 62 | if value = @start.compare(other.start) 63 | value 64 | else 65 | other.end.compare(@end) 66 | 67 | isEqual: (other) -> 68 | other = Range.fromObject(other) 69 | @start.isEqual(other.start) and @end.isEqual(other.end) 70 | 71 | coversSameRows: (other) -> 72 | other = Range.fromObject(other) 73 | @start.row is other.start.row and @end.row is other.end.row 74 | 75 | intersectsWith: (other, exclusive) -> 76 | other = Range.fromObject(other) 77 | if exclusive 78 | @end.isGreaterThan(other.start) and @start.isLessThan(other.end) 79 | else 80 | @end.isGreaterThanOrEqual(other.start) and @start.isLessThanOrEqual(other.end) 81 | 82 | intersectsRow: (row) -> 83 | @start.row <= row <= @end.row or @start.row >= row >= @end.row 84 | 85 | intersectsRowRange: (startRow, endRow) -> 86 | [startRow, endRow] = [endRow, startRow] if startRow > endRow 87 | @end.row >= startRow and @start.row <= endRow 88 | 89 | getExtent: -> 90 | @end.traversalFrom(@start) 91 | 92 | containsPoint: (point, exclusive) -> 93 | point = Point.fromObject(point) 94 | if exclusive 95 | @start.compare(point) < 0 and point.compare(@end) < 0 96 | else 97 | @start.compare(point) <= 0 and point.compare(@end) <= 0 98 | 99 | containsRange: (other, exclusive) -> 100 | other = Range.fromObject(other) 101 | if exclusive 102 | @start.isLessThan(other.start) and @end.isGreaterThan(other.end) 103 | else 104 | @start.isLessThanOrEqual(other.start) and @end.isGreaterThanOrEqual(other.end) 105 | 106 | @deserialize: (array)-> 107 | Range.fromObject(array) 108 | 109 | serialize: -> 110 | [@start.serialize(), @end.serialize()] 111 | 112 | toString: -> 113 | "(#{@start}, #{@end})" 114 | -------------------------------------------------------------------------------- /src/set-helpers.coffee: -------------------------------------------------------------------------------- 1 | setEqual = (a, b) -> 2 | return false unless a.size is b.size 3 | iterator = a.values() 4 | until (next = iterator.next()).done 5 | return false unless b.has(next.value) 6 | true 7 | 8 | subtractSet = (set, valuesToRemove) -> 9 | valuesToRemove.forEach (value) -> set.delete(value) 10 | 11 | addSet = (set, valuesToAdd) -> 12 | valuesToAdd.forEach (value) -> set.add(value) 13 | 14 | intersectSet = (set, other) -> 15 | set.forEach (value) -> set.delete(value) unless other.has(value) 16 | 17 | module.exports = {setEqual, subtractSet, addSet, intersectSet} 18 | -------------------------------------------------------------------------------- /src/soft-wraps-transform.coffee: -------------------------------------------------------------------------------- 1 | Point = require './point' 2 | WhitespaceRegExp = /\s/ 3 | 4 | module.exports = 5 | class SoftWrapsTransform 6 | constructor: (@maxColumn) -> 7 | 8 | operate: ({read, transform, getPosition}) -> 9 | {column} = getPosition() 10 | startColumn = column 11 | lastWhitespaceColumn = null 12 | output = "" 13 | 14 | while (input = read())? 15 | lastOutputLength = output.length 16 | output += input 17 | 18 | for i in [0...input.length] by 1 19 | if input[i] is "\n" 20 | transform(lastOutputLength + i + 1) 21 | return 22 | 23 | if WhitespaceRegExp.test(input[i]) 24 | lastWhitespaceColumn = column 25 | else if column >= @maxColumn 26 | if lastWhitespaceColumn? 27 | output = output.substring(0, lastWhitespaceColumn - startColumn + 1) 28 | else 29 | output = output.substring(0, lastOutputLength + i) 30 | 31 | transform(output.length, output, Point(1, 0)) 32 | return 33 | 34 | column++ 35 | 36 | if output.length > 0 37 | transform(output.length) 38 | -------------------------------------------------------------------------------- /src/text-display-document.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | PairedCharactersTransform = require "./paired-characters-transform" 3 | HardTabsTransform = require "./hard-tabs-transform" 4 | SoftWrapsTransform = require "./soft-wraps-transform" 5 | TransformLayer = require "./transform-layer" 6 | {clip} = TransformLayer 7 | 8 | module.exports = 9 | class TextDisplayDocument 10 | constructor: (@textDocument, {tabLength, softWrapColumn}={}) -> 11 | transforms = [ 12 | new HardTabsTransform(tabLength) 13 | new SoftWrapsTransform(softWrapColumn) 14 | new PairedCharactersTransform() 15 | ] 16 | 17 | @layersByIndex = [] 18 | inputLayer = @textDocument.linesLayer 19 | for transform in transforms 20 | layer = new TransformLayer(inputLayer, transform) 21 | @layersByIndex.push(layer) 22 | inputLayer = layer 23 | 24 | tokenizedLinesForScreenRows: (start, end) -> 25 | topLayer = @layersByIndex[@layersByIndex.length - 1] 26 | for lineText in topLayer.getLines() 27 | {text: lineText} 28 | 29 | screenPositionForBufferPosition: (position) -> 30 | position = @textDocument.clipPosition(position) 31 | for layer in @layersByIndex 32 | position = layer.fromInputPosition(position, clip.backward) 33 | position 34 | 35 | bufferPositionForScreenPosition: (position) -> 36 | for layer in @layersByIndex by -1 37 | position = layer.toInputPosition(position, clip.backward) 38 | @textDocument.clipPosition(position) 39 | -------------------------------------------------------------------------------- /src/text-document.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | {Emitter} = require "event-kit" 3 | Point = require "./point" 4 | Range = require "./range" 5 | MarkerStore = require "./marker-store" 6 | NullLayer = require "./null-layer" 7 | BufferLayer = require "./buffer-layer" 8 | TransformLayer = require "./transform-layer" 9 | LinesTransform = require "./lines-transform" 10 | History = require "./history" 11 | 12 | TransactionAborted = Symbol("transaction aborted") 13 | 14 | LineEnding = /[\r\n]*$/ 15 | 16 | module.exports = 17 | class TextDocument 18 | constructor: (options) -> 19 | @transactCallDepth = 0 20 | @history = new History 21 | @markerStore = new MarkerStore(this) 22 | @emitter = new Emitter 23 | @refcount = 1 24 | @destroyed = false 25 | @encoding = 'utf8' 26 | @bufferLayer = new BufferLayer(new NullLayer) 27 | @linesLayer = new TransformLayer(@bufferLayer, new LinesTransform) 28 | if typeof options is 'string' 29 | @setText(options) 30 | else if options?.filePath? 31 | @setPath(options.filePath) 32 | @load() if options.load 33 | 34 | ### 35 | Section: Lifecycle 36 | ### 37 | 38 | destroy: -> 39 | @destroyed = true 40 | @emitter.emit "did-destroy" 41 | 42 | retain: -> 43 | @refcount++ 44 | 45 | release: -> 46 | @refcount-- 47 | @destroy() if @refcount is 0 48 | 49 | isAlive: -> 50 | not @destroyed 51 | 52 | isDestroyed: -> 53 | @destroyed 54 | 55 | ### 56 | Section: Event Subscription 57 | ### 58 | 59 | onDidChange: (callback) -> 60 | @emitter.on("did-change", callback) 61 | 62 | onWillChange: (callback) -> 63 | @emitter.on("will-change", callback) 64 | 65 | onDidDestroy: (callback) -> 66 | @emitter.on("did-destroy", callback) 67 | 68 | onWillThrowWatchError: (callback) -> 69 | @emitter.on("will-throw-watch-error", callback) 70 | 71 | onDidSave: (callback) -> 72 | @emitter.on("did-save", callback) 73 | 74 | onDidChangePath: (callback) -> 75 | @emitter.on("did-change-path", callback) 76 | 77 | preemptDidChange: (callback) -> 78 | @emitter.preempt("did-change", callback) 79 | 80 | onDidUpdateMarkers: (callback) -> 81 | @emitter.on("did-update-markers", callback) 82 | 83 | onDidChangeEncoding: (callback) -> 84 | @emitter.on("did-change-encoding", callback) 85 | 86 | onDidStopChanging: (callback) -> 87 | @emitter.on("did-stop-changing", callback) 88 | 89 | onDidConflict: (callback) -> 90 | @emitter.on("did-conflict", callback) 91 | 92 | onDidChangeModified: (callback) -> 93 | @emitter.on("did-change-modified", callback) 94 | 95 | onWillReload: (callback) -> 96 | @emitter.on("will-reload", callback) 97 | 98 | onDidReload: (callback) -> 99 | @emitter.on("did-reload", callback) 100 | 101 | onWillSave: (callback) -> 102 | @emitter.on("will-save", callback) 103 | 104 | onDidCreateMarker: (callback) -> 105 | @emitter.on("did-create-marker", callback) 106 | 107 | ### 108 | Section: File Details 109 | ### 110 | 111 | getPath: -> @path 112 | 113 | getUri: -> @path 114 | 115 | setPath: (@path) -> 116 | @loaded = false 117 | 118 | load: -> 119 | new Promise (resolve) => 120 | fs.readFile @path, @encoding, (err, contents) => 121 | @loaded = true 122 | @setText(contents) if contents 123 | @emitter.emit("did-load") 124 | resolve(this) 125 | 126 | isModified: -> 127 | false 128 | 129 | setEncoding: (@encoding) -> 130 | 131 | getEncoding: -> @encoding 132 | 133 | ### 134 | Section: Reading Text 135 | ### 136 | 137 | getText: -> 138 | @linesLayer.slice() 139 | 140 | getTextInRange: (range) -> 141 | range = Range.fromObject(range) 142 | @linesLayer.slice(range.start, range.end) 143 | 144 | setText: (text) -> 145 | @bufferLayer.splice(Point.zero(), @bufferLayer.getExtent(), text) 146 | 147 | setTextInRange: (oldRange, newText) -> 148 | unless @transactCallDepth > 0 149 | return @transact => @setTextInRange(oldRange, newText) 150 | oldRange = Range.fromObject(oldRange) 151 | oldRange.start = @clipPosition(oldRange.start) 152 | oldRange.end = @clipPosition(oldRange.end) 153 | oldText = @getTextInRange(oldRange) 154 | @applyChange({oldRange, oldText, newText}) 155 | 156 | append: (text) -> 157 | @insert(@getEndPosition(), text) 158 | 159 | insert: (position, text) -> 160 | @setTextInRange(Range(position, position), text) 161 | 162 | delete: (range) -> 163 | @setTextInRange(range, "") 164 | 165 | lineForRow: (row) -> 166 | @linesLayer 167 | .slice(Point(row, 0), Point(row + 1, 0)) 168 | .replace(LineEnding, "") 169 | 170 | lineEndingForRow: (row) -> 171 | @linesLayer 172 | .slice(Point(row, 0), Point(row + 1, 0)) 173 | .match(LineEnding)[0] 174 | 175 | isEmpty: -> 176 | @bufferLayer.getExtent().isZero() 177 | 178 | previousNonBlankRow: -> 0 179 | 180 | nextNonBlankRow: -> 0 181 | 182 | isRowBlank: -> false 183 | 184 | ### 185 | Section: Markers 186 | ### 187 | 188 | getMarker: (id) -> @markerStore.getMarker(id) 189 | getMarkers: -> @markerStore.getMarkers() 190 | findMarkers: (params) -> @markerStore.findMarkers(params) 191 | markRange: (range, options) -> @markerStore.markRange(range, options) 192 | markPosition: (position, options) -> @markerStore.markPosition(position, options) 193 | 194 | ### 195 | Section: Buffer Range Details 196 | ### 197 | 198 | getRange: -> 199 | Range(Point.zero(), @getEndPosition()) 200 | 201 | rangeForRow: (row, includeNewline) -> 202 | if includeNewline 203 | Range(Point(row, 0), @clipPosition(Point(row + 1, 0))) 204 | else 205 | Range(Point(row, 0), @clipPosition(Point(row, Infinity))) 206 | 207 | getLineCount: -> 208 | @getEndPosition().row + 1 209 | 210 | getLastRow: -> 211 | @getEndPosition().row 212 | 213 | getEndPosition: -> 214 | @linesLayer.getExtent() 215 | 216 | clipPosition: (position) -> 217 | position = Point.fromObject(position) 218 | @linesLayer.clipPosition(position) 219 | 220 | positionForCharacterIndex: (index) -> 221 | @linesLayer.fromInputPosition(new Point(0, index)) 222 | 223 | characterIndexForPosition: (position) -> 224 | @linesLayer.toInputPosition(Point.fromObject(position)).column 225 | 226 | ### 227 | Section: History 228 | ### 229 | 230 | undo: -> 231 | if poppedEntries = @history.popUndoStack(@markerStore.createSnapshot()) 232 | @applyChange(change, true) for change in poppedEntries.changes 233 | @markerStore.restoreFromSnapshot(poppedEntries.metadata) 234 | @emitter.emit("did-update-markers") 235 | 236 | redo: -> 237 | if poppedEntries = @history.popRedoStack(@markerStore.createSnapshot()) 238 | @applyChange(change, true) for change in poppedEntries.changes 239 | @markerStore.restoreFromSnapshot(poppedEntries.metadata) 240 | @emitter.emit("did-update-markers") 241 | 242 | transact: (groupingInterval, fn) -> 243 | if typeof groupingInterval is 'function' 244 | fn = groupingInterval 245 | groupingInterval = 0 246 | 247 | checkpoint = @history.createCheckpoint(@markerStore.createSnapshot()) 248 | 249 | try 250 | @transactCallDepth++ 251 | result = fn() 252 | catch exception 253 | @revertToCheckpoint(checkpoint) 254 | throw exception unless exception is TransactionAborted 255 | return 256 | finally 257 | @transactCallDepth-- 258 | 259 | @history.groupChangesSinceCheckpoint(checkpoint) 260 | @history.applyCheckpointGroupingInterval(checkpoint, groupingInterval) 261 | 262 | @markerStore.emitChangeEvents() 263 | @emitter.emit("did-update-markers") 264 | result 265 | 266 | abortTransaction: -> 267 | throw TransactionAborted 268 | 269 | createCheckpoint: -> 270 | @history.createCheckpoint() 271 | 272 | groupChangesSinceCheckpoint: (checkpoint) -> 273 | @history.groupChangesSinceCheckpoint(checkpoint) 274 | 275 | revertToCheckpoint: (checkpoint) -> 276 | if changesToUndo = @history.truncateUndoStack(checkpoint) 277 | @applyChange(change, true) for change in changesToUndo 278 | true 279 | else 280 | false 281 | 282 | ### 283 | Section: Private 284 | ### 285 | 286 | markerCreated: (marker) -> 287 | @emitter.emit("did-create-marker", marker) 288 | 289 | applyChange: (change, skipUndo) -> 290 | @emitter.emit("will-change", change) 291 | {oldRange, newText} = change 292 | start = oldRange.start 293 | oldExtent = oldRange.getExtent() 294 | 295 | newExtent = @linesLayer.splice(oldRange.start, oldRange.getExtent(), newText) 296 | @markerStore.splice(oldRange.start, oldExtent, newExtent) 297 | 298 | change.newRange ?= Range(start, start.traverse(newExtent)) 299 | Object.freeze(change) 300 | 301 | @history.pushChange(change) unless skipUndo 302 | @emitter.emit("did-change", change) 303 | 304 | change.newRange 305 | -------------------------------------------------------------------------------- /src/transform-buffer.coffee: -------------------------------------------------------------------------------- 1 | Point = require "./point" 2 | 3 | CLIPPING__OPEN_INTERVAL = Symbol('clipping (open interval)') 4 | 5 | module.exports = 6 | class TransformBuffer 7 | constructor: (@transformer, @inputIterator) -> 8 | @reset(Point.zero(), Point.zero()) 9 | @transformContext = { 10 | clipping: open: CLIPPING__OPEN_INTERVAL 11 | read: @read.bind(this) 12 | getPosition: @getPosition.bind(this) 13 | transform: @transform.bind(this) 14 | } 15 | 16 | next: -> 17 | @inputIndex = 0 18 | @transformer.operate(@transformContext) unless @outputs.length > 0 19 | @outputs.shift() 20 | 21 | reset: (position, inputPosition) -> 22 | @position = position.copy() 23 | @inputPosition = inputPosition.copy() 24 | @outputs = [] 25 | @inputs = [] 26 | @inputIndex = 0 27 | 28 | read: -> 29 | if input = @inputs[@inputIndex] 30 | content = input.content 31 | else 32 | content = @inputIterator.next().value 33 | @inputs.push( 34 | content: content 35 | inputPosition: @inputIterator.getPosition() 36 | ) 37 | @inputIndex++ 38 | content 39 | 40 | getPosition: -> 41 | @position.copy() 42 | 43 | transform: (consumedCount, producedContent, producedExtent, clipping) -> 44 | if producedContent? 45 | @consume(consumedCount) 46 | producedExtent ?= Point(0, producedContent.length) 47 | @produce(producedContent, producedExtent, clipping) 48 | else 49 | startInputPosition = @inputPosition.copy() 50 | consumedContent = @consume(consumedCount) 51 | consumedExtent = @inputPosition.traversalFrom(startInputPosition) 52 | @produce(consumedContent, consumedExtent, clipping) 53 | 54 | consume: (count) -> 55 | consumedContent = "" 56 | while count > 0 57 | if count >= @inputs[0].content.length 58 | {content, @inputPosition} = @inputs.shift() 59 | consumedContent += content 60 | count -= content.length 61 | @inputIndex-- 62 | else 63 | consumedContent += @inputs[0].content.substring(0, count) 64 | @inputs[0].content = @inputs[0].content.substring(count) 65 | @inputPosition.column += count 66 | count = 0 67 | consumedContent 68 | 69 | produce: (content, extent, clipping) -> 70 | @position = @position.traverse(extent) 71 | @outputs.push( 72 | content: content 73 | position: @position.copy() 74 | inputPosition: @inputPosition.copy() 75 | clipping: clipping 76 | ) 77 | -------------------------------------------------------------------------------- /src/transform-layer.coffee: -------------------------------------------------------------------------------- 1 | Layer = require "./layer" 2 | Point = require "./point" 3 | TransformBuffer = require './transform-buffer' 4 | 5 | CLIP_FORWARD = Symbol('clip forward') 6 | CLIP_BACKWARD = Symbol('clip backward') 7 | 8 | module.exports = 9 | class TransformLayer extends Layer 10 | @clip: 11 | forward: CLIP_FORWARD 12 | backward: CLIP_BACKWARD 13 | 14 | pendingChangeOldExtent: null 15 | 16 | constructor: (@inputLayer, @transformer) -> 17 | super 18 | @inputLayer.onWillChange(@inputLayerWillChange) 19 | @inputLayer.onDidChange(@inputLayerDidChange) 20 | 21 | buildIterator: -> 22 | new TransformLayerIterator(this, @inputLayer.buildIterator()) 23 | 24 | inputLayerWillChange: ({position, oldExtent}) => 25 | iterator = @buildIterator() 26 | iterator.seekToInputPosition(position) 27 | startPosition = iterator.getPosition() 28 | iterator.seekToInputPosition(position.traverse(oldExtent)) 29 | @pendingChangeOldExtent = iterator.getPosition().traversalFrom(startPosition) 30 | 31 | inputLayerDidChange: ({position, newExtent}) => 32 | iterator = @buildIterator() 33 | iterator.seekToInputPosition(position) 34 | startPosition = iterator.getPosition() 35 | iterator.seekToInputPosition(position.traverse(newExtent)) 36 | 37 | oldExtent = @pendingChangeOldExtent 38 | newExtent = iterator.getPosition().traversalFrom(startPosition) 39 | @pendingChangeOldExtent = null 40 | 41 | @emitter.emit "did-change", {position: startPosition, oldExtent, newExtent} 42 | 43 | clipPosition: (position, clip) -> 44 | iterator = @buildIterator() 45 | iterator.seek(position, clip) 46 | iterator.getPosition() 47 | 48 | toInputPosition: (position, clip) -> 49 | iterator = @buildIterator() 50 | iterator.seek(position, clip) 51 | iterator.getInputPosition() 52 | 53 | fromInputPosition: (inputPosition, clip) -> 54 | iterator = @buildIterator() 55 | iterator.seekToInputPosition(inputPosition, clip) 56 | iterator.getPosition() 57 | 58 | class TransformLayerIterator 59 | clipping: undefined 60 | 61 | constructor: (@layer, @inputIterator) -> 62 | @position = Point.zero() 63 | @inputPosition = Point.zero() 64 | @transformBuffer = new TransformBuffer(@layer.transformer, @inputIterator) 65 | 66 | next: -> 67 | if next = @transformBuffer.next() 68 | {content, @position, @inputPosition, @clipping} = next 69 | {value: content, done: false} 70 | else 71 | {value: undefined, done: true} 72 | 73 | seek: (position, clip=CLIP_BACKWARD) -> 74 | @position = Point.zero() 75 | @inputPosition = Point.zero() 76 | @inputIterator.seek(@inputPosition) 77 | @transformBuffer.reset(@position, @inputPosition) 78 | position = Point.fromObject(position).sanitizeNegatives() 79 | return if position.isZero() 80 | 81 | done = overshot = false 82 | until done or overshot 83 | lastPosition = @position 84 | lastInputPosition = @inputPosition 85 | {done} = @next() 86 | switch @position.compare(position) 87 | when 0 then done = true 88 | when 1 then overshot = true 89 | 90 | if overshot 91 | if @clipping? 92 | if clip is CLIP_BACKWARD 93 | @position = lastPosition 94 | @inputPosition = lastInputPosition 95 | else 96 | @position = position 97 | overshoot = position.traversalFrom(lastPosition) 98 | inputPositionWithOvershoot = lastInputPosition.traverse(overshoot) 99 | if inputPositionWithOvershoot.compare(@inputPosition) >= 0 100 | if clip is CLIP_BACKWARD 101 | @inputPosition = @inputPosition.traverse(Point(0, -1)) 102 | else 103 | @inputPosition = inputPositionWithOvershoot 104 | 105 | @inputIterator.seek(@inputPosition) 106 | @transformBuffer.reset(@position, @inputPosition) 107 | 108 | seekToInputPosition: (inputPosition, clip = CLIP_BACKWARD) -> 109 | @position = Point.zero() 110 | @inputPosition = Point.zero() 111 | @inputIterator.seek(@inputPosition) 112 | @transformBuffer.reset(@position, @inputPosition) 113 | inputPosition = Point.fromObject(inputPosition).sanitizeNegatives() 114 | return if inputPosition.isZero() 115 | 116 | done = overshot = false 117 | until done or overshot 118 | lastPosition = @position 119 | lastInputPosition = @inputPosition 120 | {done} = @next() 121 | switch @inputPosition.compare(inputPosition) 122 | when 0 then done = true 123 | when 1 then overshot = true 124 | 125 | if overshot 126 | if @clipping? 127 | if clip is CLIP_BACKWARD 128 | @position = lastPosition 129 | @inputPosition = lastInputPosition 130 | else 131 | @inputPosition = inputPosition 132 | overshoot = inputPosition.traversalFrom(lastInputPosition) 133 | positionWithOvershoot = lastPosition.traverse(overshoot) 134 | if positionWithOvershoot.compare(@position) >= 0 135 | if clip is CLIP_BACKWARD 136 | @position = @position.traverse(Point(0, -1)) 137 | else 138 | @position = positionWithOvershoot 139 | 140 | @inputIterator.seek(@inputPosition) 141 | @transformBuffer.reset(@position, @inputPosition) 142 | 143 | splice: (extent, content) -> 144 | startPosition = @getPosition() 145 | inputStartPosition = @getInputPosition() 146 | @seek(@getPosition().traverse(extent)) 147 | inputExtent = @getInputPosition().traversalFrom(inputStartPosition) 148 | @seekToInputPosition(inputStartPosition) 149 | @inputIterator.splice(inputExtent, content) 150 | @seekToInputPosition(@inputIterator.getPosition()) 151 | 152 | getPosition: -> 153 | @position.copy() 154 | 155 | getInputPosition: -> 156 | @inputPosition.copy() 157 | --------------------------------------------------------------------------------