├── .coffeelintignore ├── spec ├── helpers │ ├── coffee.js │ ├── sample-text.js │ ├── random.js │ ├── set.js │ └── test-language-mode.js ├── .eslintrc ├── fixtures │ ├── win1251.txt │ └── sample.js ├── support │ ├── runner │ │ ├── index.html │ │ ├── package.json │ │ ├── renderer.js │ │ └── main.js │ └── jasmine.json ├── text-buffer-spec.js ├── range-spec.coffee ├── point-spec.coffee ├── marker-layer-spec.coffee └── display-marker-layer-spec.coffee ├── src ├── constants.js ├── null-language-mode.js ├── set-helpers.coffee ├── is-character-pair.coffee ├── point-helpers.coffee ├── helpers.js ├── point.coffee ├── range.coffee ├── default-history-provider.coffee ├── screen-line-builder.js ├── marker.coffee ├── marker-layer.coffee ├── display-marker.coffee └── display-marker-layer.coffee ├── .babelrc ├── benchmarks ├── index.js ├── mutation.js ├── construction.js └── helpers.js ├── .gitignore ├── .npmignore ├── .gitattributes ├── README.md ├── .github └── workflows │ └── ci.yml ├── coffeelint.json ├── script ├── generate-docs └── test ├── LICENSE.md └── package.json /.coffeelintignore: -------------------------------------------------------------------------------- 1 | spec/fixtures 2 | -------------------------------------------------------------------------------- /spec/helpers/coffee.js: -------------------------------------------------------------------------------- 1 | require('coffee-cache') 2 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | exports.MAX_BUILT_IN_SCOPE_ID = 256 2 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true, 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "sourceMaps": "inline" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixtures/win1251.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atom/text-buffer/HEAD/spec/fixtures/win1251.txt -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./construction') 4 | require('./mutation') 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | lib 4 | npm-debug.log 5 | .coffee 6 | api.json 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /spec/support/runner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | spec 2 | script 3 | src 4 | *.coffee 5 | .npmignore 6 | .DS_Store 7 | npm-debug.log 8 | .travis.yml 9 | .pairs 10 | .coffee 11 | -------------------------------------------------------------------------------- /spec/support/runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runner", 3 | "main": "main.js", 4 | "scripts": { 5 | "start": "electron ." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/helpers/sample-text.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | module.exports = fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'sample.js'), 'utf8') 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Specs depend on character counts, if we don't specify the line endings the 2 | # fixtures will vary depending on platform 3 | spec/fixtures/**/*.js text eol=lf 4 | 5 | # Git 1.7 does not support **/* patterns 6 | spec/fixtures/sample.js text eol=lf 7 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*-spec.js", 5 | "**/*-spec.coffee" 6 | ], 7 | "helpers": [ 8 | "helpers/**/*.js" 9 | ], 10 | "stopSpecOnExpectationFailure": false, 11 | "random": false 12 | } 13 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # Atom TextBuffer Core 3 | [![CI](https://github.com/atom/text-buffer/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/text-buffer/actions/workflows/ci.yml) 4 | 5 | This is the core of the Atom text buffer, separated into its own module so its tests can be run headless. It handles the storage and manipulation of text and associated regions (markers). 6 | -------------------------------------------------------------------------------- /benchmarks/mutation.js: -------------------------------------------------------------------------------- 1 | const helpers = require('./helpers') 2 | const TextBuffer = require('..') 3 | 4 | let text = helpers.getRandomText(100) 5 | let buffer = new TextBuffer({text}) 6 | let displayLayer = buffer.addDisplayLayer({}) 7 | 8 | let t0 = Date.now() 9 | 10 | for (let i = 0; i < 1000; i++) { 11 | buffer.setTextInRange( 12 | helpers.getRandomRange(buffer), 13 | helpers.getRandomText(0.5) 14 | ) 15 | } 16 | 17 | let t1 = Date.now() 18 | 19 | console.log('Mutation') 20 | console.log('------------') 21 | console.log('TextBuffer: %s ms', t1 - t0) 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '14' 19 | - name: Install windows-build-tools 20 | if: ${{ matrix.os == 'windows-latest' }} 21 | run: npm config set msvs_version 2019 22 | - name: Install dependencies 23 | run: npm i 24 | - name: Run tests 25 | run: npm test 26 | -------------------------------------------------------------------------------- /src/null-language-mode.js: -------------------------------------------------------------------------------- 1 | const {Disposable} = require('event-kit') 2 | const Point = require('./point') 3 | 4 | const EMPTY = [] 5 | 6 | module.exports = 7 | class NullLanguageMode { 8 | bufferDidChange () {} 9 | bufferDidFinishTransaction () {} 10 | buildHighlightIterator () { return new NullHighlightIterator() } 11 | onDidChangeHighlighting () { return new Disposable(() => {}) } 12 | getLanguageId () { return null } 13 | } 14 | 15 | class NullHighlightIterator { 16 | seek (position) { return EMPTY } 17 | moveToSuccessor () { return false } 18 | getPosition () { return Point.INFINITY } 19 | getCloseTags () { return EMPTY } 20 | getOpenTags () { return EMPTY } 21 | } 22 | -------------------------------------------------------------------------------- /spec/support/runner/renderer.js: -------------------------------------------------------------------------------- 1 | const {remote} = require('electron') 2 | 3 | const path = require('path') 4 | const Command = require('jasmine/lib/command.js') 5 | const Jasmine = require('jasmine/lib/jasmine.js') 6 | 7 | const jasmine = new Jasmine({ projectBaseDir: path.resolve(), color: false }) 8 | const examplesDir = path.join(path.dirname(require.resolve('jasmine-core')), 'jasmine-core', 'example', 'node_example') 9 | const command = new Command(path.resolve(), examplesDir, console.log) 10 | 11 | process.stdout.write = function (output) { 12 | console.log(output) 13 | } 14 | 15 | process.exit = function () {} 16 | command.run(jasmine, ['--no-color', '--stop-on-failure=true', ...remote.process.argv.slice(2)]) 17 | -------------------------------------------------------------------------------- /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 | if set.size > valuesToRemove.size 10 | valuesToRemove.forEach (value) -> set.delete(value) 11 | else 12 | set.forEach (value) -> set.delete(value) if valuesToRemove.has(value) 13 | 14 | addSet = (set, valuesToAdd) -> 15 | valuesToAdd.forEach (value) -> set.add(value) 16 | 17 | intersectSet = (set, other) -> 18 | set.forEach (value) -> set.delete(value) unless other.has(value) 19 | 20 | module.exports = {setEqual, subtractSet, addSet, intersectSet} 21 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /script/generate-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('coffee-script').register() 4 | 5 | const fs = require('fs') 6 | const path = require('path') 7 | const tello = require('tello') 8 | const donna = require('donna') 9 | const joanna = require('joanna') 10 | 11 | const rootDir = path.join(__dirname, '..') 12 | 13 | const sourceDir = 'src' 14 | const jsFiles = [] 15 | for (const entry of fs.readdirSync('src')) { 16 | if (entry.endsWith('.js')) { 17 | jsFiles.push(path.join(sourceDir, entry)) 18 | } 19 | } 20 | 21 | const jsMetadata = joanna(jsFiles) 22 | const coffeeMetadata = donna.generateMetadata([rootDir]) 23 | Object.assign(coffeeMetadata[0].files, jsMetadata.files) 24 | const docs = tello.digest(coffeeMetadata) 25 | fs.writeFileSync(path.join(rootDir, 'api.json'), JSON.stringify(docs, null, 2)) 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/is-character-pair.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (character1, character2) -> 2 | charCodeA = character1.charCodeAt(0) 3 | charCodeB = character2.charCodeAt(0) 4 | isSurrogatePair(charCodeA, charCodeB) or 5 | isVariationSequence(charCodeA, charCodeB) or 6 | isCombinedCharacter(charCodeA, charCodeB) 7 | 8 | isCombinedCharacter = (charCodeA, charCodeB) -> 9 | not isCombiningCharacter(charCodeA) and isCombiningCharacter(charCodeB) 10 | 11 | isSurrogatePair = (charCodeA, charCodeB) -> 12 | isHighSurrogate(charCodeA) and isLowSurrogate(charCodeB) 13 | 14 | isVariationSequence = (charCodeA, charCodeB) -> 15 | not isVariationSelector(charCodeA) and isVariationSelector(charCodeB) 16 | 17 | isHighSurrogate = (charCode) -> 18 | 0xD800 <= charCode <= 0xDBFF 19 | 20 | isLowSurrogate = (charCode) -> 21 | 0xDC00 <= charCode <= 0xDFFF 22 | 23 | isVariationSelector = (charCode) -> 24 | 0xFE00 <= charCode <= 0xFE0F 25 | 26 | isCombiningCharacter = (charCode) -> 27 | 0x0300 <= charCode <= 0x036F or 28 | 0x1AB0 <= charCode <= 0x1AFF or 29 | 0x1DC0 <= charCode <= 0x1DFF or 30 | 0x20D0 <= charCode <= 0x20FF or 31 | 0xFE20 <= charCode <= 0xFE2F 32 | -------------------------------------------------------------------------------- /spec/helpers/random.js: -------------------------------------------------------------------------------- 1 | const WORDS = require('./words') 2 | const Point = require('../../src/point') 3 | const Range = require('../../src/range') 4 | 5 | exports.getRandomBufferRange = function getRandomBufferRange (random, buffer) { 6 | const endRow = random(buffer.getLineCount()) 7 | const startRow = random.intBetween(0, endRow) 8 | const startColumn = random(buffer.lineForRow(startRow).length + 1) 9 | const endColumn = random(buffer.lineForRow(endRow).length + 1) 10 | return Range(Point(startRow, startColumn), Point(endRow, endColumn)) 11 | } 12 | 13 | exports.buildRandomLines = function buildRandomLines (random, maxLines) { 14 | const lines = [] 15 | 16 | for (let i = 0; i < random(maxLines); i++) { 17 | lines.push(buildRandomLine(random)) 18 | } 19 | 20 | return lines.join('\n') 21 | } 22 | 23 | function buildRandomLine (random) { 24 | const line = [] 25 | 26 | for (let i = 0; i < random(5); i++) { 27 | const n = random(10) 28 | 29 | if (n < 2) { 30 | line.push('\t') 31 | } else if (n < 4) { 32 | line.push(' ') 33 | } else { 34 | if (line.length > 0 && !/\s/.test(line[line.length - 1])) { 35 | line.push(' ') 36 | } 37 | 38 | line.push(WORDS[random(WORDS.length)]) 39 | } 40 | } 41 | 42 | return line.join('') 43 | } 44 | -------------------------------------------------------------------------------- /spec/text-buffer-spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TextBuffer = require('../src/text-buffer') 3 | 4 | describe('when a buffer is already open', () => { 5 | const filePath = path.join(__dirname, 'fixtures', 'sample.js') 6 | const buffer = new TextBuffer() 7 | 8 | it('replaces foo( with bar( using /\bfoo\\(\b/gim', () => { 9 | buffer.setPath(filePath) 10 | buffer.setText('foo(x)') 11 | buffer.replace(/\bfoo\(\b/gim, 'bar(') 12 | 13 | expect(buffer.getText()).toBe('bar(x)') 14 | }) 15 | 16 | describe('Texts should be replaced properly with strings containing literals when using the regex option', () => { 17 | it('replaces tstat_fvars()->curr_setpoint[HEAT_EN] with tstat_set_curr_setpoint($1, $2);', () => { 18 | buffer.setPath(filePath) 19 | buffer.setText('tstat_fvars()->curr_setpoint[HEAT_EN] = new_tptr->heat_limit;') 20 | buffer.replace(/tstat_fvars\(\)->curr_setpoint\[(.+?)\] = (.+?);/, 'tstat_set_curr_setpoint($1, $2);') 21 | 22 | expect(buffer.getText()).toBe('tstat_set_curr_setpoint(HEAT_EN, new_tptr->heat_limit);') 23 | }) 24 | 25 | it('replaces atom/flight-manualatomio with $1', () => { 26 | buffer.setText('atom/flight-manualatomio') 27 | buffer.replace(/\.(atom)\./, '$1') 28 | 29 | expect(buffer.getText()).toBe('atom/flight-manualatomio') 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/point-helpers.coffee: -------------------------------------------------------------------------------- 1 | Point = require './point' 2 | 3 | exports.compare = (a, b) -> 4 | if a.row is b.row 5 | compareNumbers(a.column, b.column) 6 | else 7 | compareNumbers(a.row, b.row) 8 | 9 | compareNumbers = (a, b) -> 10 | if a < b 11 | -1 12 | else if a > b 13 | 1 14 | else 15 | 0 16 | 17 | exports.isEqual = (a, b) -> 18 | a.row is b.row and a.column is b.column 19 | 20 | exports.traverse = (start, distance) -> 21 | if distance.row is 0 22 | Point(start.row, start.column + distance.column) 23 | else 24 | Point(start.row + distance.row, distance.column) 25 | 26 | exports.traversal = (end, start) -> 27 | if end.row is start.row 28 | Point(0, end.column - start.column) 29 | else 30 | Point(end.row - start.row, end.column) 31 | 32 | NEWLINE_REG_EXP = /\n/g 33 | 34 | exports.characterIndexForPoint = (text, point) -> 35 | row = point.row 36 | column = point.column 37 | NEWLINE_REG_EXP.lastIndex = 0 38 | while row-- > 0 39 | unless NEWLINE_REG_EXP.exec(text) 40 | return text.length 41 | 42 | NEWLINE_REG_EXP.lastIndex + column 43 | 44 | exports.clipNegativePoint = (point) -> 45 | if point.row < 0 46 | Point(0, 0) 47 | else if point.column < 0 48 | Point(point.row, 0) 49 | else 50 | point 51 | 52 | exports.max = (a, b) -> 53 | if exports.compare(a, b) >= 0 54 | a 55 | else 56 | b 57 | 58 | exports.min = (a, b) -> 59 | if exports.compare(a, b) <= 0 60 | a 61 | else 62 | b 63 | -------------------------------------------------------------------------------- /benchmarks/construction.js: -------------------------------------------------------------------------------- 1 | const regression = require('regression') 2 | const helpers = require('./helpers') 3 | const TextBuffer = require('..') 4 | 5 | const TRIAL_COUNT = 3 6 | const SIZES_IN_KB = [ 7 | 1, 8 | 10, 9 | 100, 10 | 1000, 11 | 10000, 12 | ] 13 | 14 | const bufferTimesInMS = [] 15 | const displayLayerTimesInMS = [] 16 | 17 | for (let sizeInKB of SIZES_IN_KB) { 18 | let text = helpers.getRandomText(sizeInKB) 19 | let buffer = new TextBuffer({text}) 20 | 21 | let t0 = Date.now() 22 | for (let i = 0; i < TRIAL_COUNT; i++) { 23 | buffer = new TextBuffer({text}) 24 | buffer.getTextInRange([[0, 0], [50, 0]]) 25 | } 26 | 27 | let t1 = Date.now() 28 | for (let i = 0; i < TRIAL_COUNT; i++) { 29 | let displayLayer = buffer.addDisplayLayer({}) 30 | displayLayer.getScreenLines(0, 50) 31 | } 32 | 33 | let t2 = Date.now() 34 | bufferTimesInMS.push((t1 - t0) / TRIAL_COUNT) 35 | displayLayerTimesInMS.push((t2 - t1) / TRIAL_COUNT) 36 | } 37 | 38 | function getMillisecondsPerMegabyte(timesInMS) { 39 | const series = timesInMS.map((time, i) => [SIZES_IN_KB[i], time * 1024]) 40 | const slownessRegression = regression('linear', series) 41 | return slownessRegression.equation[0] 42 | } 43 | 44 | console.log('Construction') 45 | console.log('------------') 46 | console.log('TextBuffer: %s ms/MB', getMillisecondsPerMegabyte(bufferTimesInMS).toFixed(1)) 47 | console.log('DisplayLayer: %s ms/MB', getMillisecondsPerMegabyte(displayLayerTimesInMS).toFixed(1)) 48 | -------------------------------------------------------------------------------- /benchmarks/helpers.js: -------------------------------------------------------------------------------- 1 | const WORDS = require('../spec/helpers/words') 2 | const Random = require('random-seed') 3 | const random = new Random(Date.now()) 4 | const {Point, Range} = require('..') 5 | 6 | exports.getRandomText = function (sizeInKB) { 7 | const goalLength = Math.round(sizeInKB * 1024) 8 | 9 | let length = 0 10 | let lines = [] 11 | let currentLine = '' 12 | let lastLineStartIndex = 0 13 | let goalLineLength = random(100) 14 | 15 | for (;;) { 16 | if (currentLine.length >= goalLineLength) { 17 | length++ 18 | lines.push(currentLine) 19 | if (length >= goalLength) break 20 | 21 | currentLine = '' 22 | goalLineLength = random(100) 23 | } 24 | 25 | let choice = random(10) 26 | if (choice < 2) { 27 | length++ 28 | currentLine += '\t' 29 | } else if (choice < 4) { 30 | length++ 31 | currentLine += ' ' 32 | } else { 33 | if (currentLine.length > 0 && !/\s$/.test(currentLine)) { 34 | length++ 35 | currentLine += ' ' 36 | } 37 | word = WORDS[random(WORDS.length)] 38 | length += word.length 39 | currentLine += word 40 | } 41 | } 42 | 43 | return lines.join('\n') + '\n' 44 | } 45 | 46 | exports.getRandomRange = function (buffer) { 47 | const start = getRandomPoint(buffer) 48 | const end = getRandomPoint(buffer) 49 | if (end.isLessThan(start)) { 50 | return new Range(end, start) 51 | } else { 52 | return new Range(start, end) 53 | } 54 | } 55 | 56 | function getRandomPoint (buffer) { 57 | const row = random(buffer.getLineCount()) 58 | const column = random(buffer.lineLengthForRow(row)) 59 | return new Point(row, column) 60 | } 61 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const childProcess = require('child_process') 4 | const path = require('path') 5 | 6 | const argv = 7 | require('yargs') 8 | .usage('Run tests') 9 | .boolean('interactive') 10 | .describe('interactive', 'Run tests in an Electron window') 11 | .alias('i', 'interactive') 12 | .boolean('rebuild') 13 | .describe('rebuild', 'Rebuild against the correct Node headers before running tests') 14 | .alias('r', 'rebuild') 15 | .help() 16 | .argv 17 | 18 | // Rebuild module against correct Node headers if requested 19 | if (argv.rebuild) { 20 | let env 21 | if (argv.interactive) { 22 | const [major, minor] = Array.from(require('../package.json').devDependencies.electron.match(/(\d+)\.(\d+)/)).slice(1) 23 | const electronVersion = `${major}.${minor}.0` 24 | env = Object.assign({}, process.env, { 25 | npm_config_runtime: 'electron', 26 | npm_config_target: electronVersion, 27 | npm_config_disturl: 'https://atom.io/download/atom-shell' 28 | }) 29 | console.log(`Rebuilding native modules against Electron ${electronVersion}`) 30 | } else { 31 | env = process.env 32 | } 33 | childProcess.spawnSync('npm', ['rebuild'], {env, stdio: 'inherit'}) 34 | } 35 | 36 | normalizeCommand = (command) => { 37 | return process.platform === 'win32' ? path.normalize(command + '.cmd') : command 38 | } 39 | 40 | // Run tests 41 | if (argv.interactive) { 42 | childProcess.spawnSync(normalizeCommand('./node_modules/.bin/electron'), ['spec/support/runner', 'spec/**/*-spec.*'], {stdio: 'inherit', cwd: path.join(__dirname, '..')}) 43 | } else { 44 | process.exit(childProcess.spawnSync(normalizeCommand('./node_modules/.bin/jasmine'), ['--stop-on-failure=true'], {stdio: 'inherit'}).status) 45 | } 46 | -------------------------------------------------------------------------------- /spec/support/runner/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | // Module to control application life. 3 | const app = electron.app 4 | // Module to create native browser window. 5 | const BrowserWindow = electron.BrowserWindow 6 | 7 | // Keep a global reference of the window object, if you don't, the window will 8 | // be closed automatically when the JavaScript object is garbage collected. 9 | let mainWindow 10 | 11 | function createWindow () { 12 | // Create the browser window. 13 | mainWindow = new BrowserWindow({width: 800, height: 600}) 14 | 15 | // and load the index.html of the app. 16 | mainWindow.loadURL(`file://${__dirname}/index.html`) 17 | 18 | // Open the DevTools. 19 | mainWindow.webContents.openDevTools() 20 | 21 | // Emitted when the window is closed. 22 | mainWindow.on('closed', function () { 23 | // Dereference the window object, usually you would store windows 24 | // in an array if your app supports multi windows, this is the time 25 | // when you should delete the corresponding element. 26 | mainWindow = null 27 | }) 28 | } 29 | 30 | // This method will be called when Electron has finished 31 | // initialization and is ready to create browser windows. 32 | // Some APIs can only be used after this event occurs. 33 | app.on('ready', createWindow) 34 | 35 | // Quit when all windows are closed. 36 | app.on('window-all-closed', function () { 37 | // On OS X it is common for applications and their menu bar 38 | // to stay active until the user quits explicitly with Cmd + Q 39 | if (process.platform !== 'darwin') { 40 | app.quit() 41 | } 42 | }) 43 | 44 | app.on('activate', function () { 45 | // On OS X it's common to re-create a window in the app when the 46 | // dock icon is clicked and there are no other windows open. 47 | if (mainWindow === null) { 48 | createWindow() 49 | } 50 | }) 51 | 52 | // In this file you can include the rest of your app's specific main process 53 | // code. You can also put them in separate files and require them here. 54 | -------------------------------------------------------------------------------- /spec/helpers/set.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const setEqual = require('../../src/set-helpers').setEqual 4 | 5 | Set.prototype.isEqual = function (other) { // eslint-disable-line no-extend-native 6 | if (other instanceof Set) { 7 | return setEqual(this, other) 8 | } else { 9 | return undefined 10 | } 11 | } 12 | 13 | Set.prototype.jasmineToString = function () { // eslint-disable-line no-extend-native 14 | let result = 'Set {' 15 | let first = true 16 | this.forEach((element) => { 17 | if (!first) { 18 | result += ', ' 19 | } 20 | result += element.toString() 21 | return result 22 | }) 23 | first = false 24 | return result + '}' 25 | } 26 | 27 | let toEqualSet = (expectedItems, customMessage) => { 28 | let pass = true 29 | let expectedSet = new Set(expectedItems) 30 | if (customMessage == null) { 31 | customMessage = '' 32 | } 33 | 34 | expectedSet.forEach((item) => { 35 | if (!this.actual.has(item)) { 36 | pass = false 37 | this.message = () => { 38 | return 'Expected set ' + (formatSet(this.actual)) + ' to have item ' + item + '. ' + customMessage 39 | } 40 | return this.message 41 | } 42 | }) 43 | this.actual.forEach((item) => { 44 | if (!expectedSet.has(item)) { 45 | pass = false 46 | this.message = () => { 47 | return 'Expected set ' + (formatSet(this.actual)) + ' not to have item ' + item + '. ' + customMessage 48 | } 49 | return this.message 50 | } 51 | }) 52 | return pass 53 | } 54 | 55 | let formatSet = (set) => { 56 | return '(' + (setToArray(set).join(' ')) + ')' 57 | } 58 | 59 | let setToArray = (set) => { 60 | let items = [] 61 | set.forEach((item) => { 62 | return items.push(item) 63 | }) 64 | return items.sort() 65 | } 66 | 67 | // let currentSpecFailed = () => { 68 | // console.log(jasmine.getEnv()) 69 | // return jasmine 70 | // .getEnv() 71 | // .currentSpec 72 | // .results() 73 | // .getItems() 74 | // .some((item) => { 75 | // return !item.passed() 76 | // }) 77 | // } 78 | 79 | module.exports = {toEqualSet, formatSet} 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-buffer", 3 | "version": "13.18.6", 4 | "description": "A container for large mutable strings with annotated regions", 5 | "main": "./lib/text-buffer", 6 | "scripts": { 7 | "prepublish": "npm run clean && npm run compile && npm run lint && npm run docs", 8 | "docs": "node script/generate-docs", 9 | "clean": "rimraf lib api.json", 10 | "compile": "coffee --no-header --output lib --compile src && cpy src/*.js lib/", 11 | "lint": "coffeelint -r src spec && standard src/*.js spec/*.js", 12 | "test": "node script/test", 13 | "ci": "npm run compile && npm run lint && npm run test && npm run bench", 14 | "bench": "node benchmarks/index" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/atom/text-buffer.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/atom/text-buffer/issues" 22 | }, 23 | "atomTestRunner": "atom-jasmine2-test-runner", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "atom-jasmine2-test-runner": "^1.0.0", 27 | "coffee-cache": "^0.2.0", 28 | "coffee-script": "^1.10.0", 29 | "coffeelint": "1.16.0", 30 | "cpy-cli": "^1.0.1", 31 | "dedent": "^0.6.0", 32 | "donna": "^1.0.16", 33 | "electron": "^6", 34 | "jasmine": "^2.4.1", 35 | "jasmine-core": "^2.4.1", 36 | "joanna": "0.0.11", 37 | "json-diff": "^0.3.1", 38 | "random-seed": "^0.2.0", 39 | "regression": "^1.2.1", 40 | "rimraf": "~2.2.2", 41 | "standard": "^10.0.3", 42 | "tello": "^1.0.7", 43 | "temp": "^0.8.3", 44 | "yargs": "^6.5.0" 45 | }, 46 | "dependencies": { 47 | "delegato": "^1.0.0", 48 | "diff": "^2.2.1", 49 | "emissary": "^1.0.0", 50 | "event-kit": "^2.4.0", 51 | "fs-admin": "^0.19.0", 52 | "fs-plus": "^3.0.0", 53 | "grim": "^2.0.2", 54 | "mkdirp": "^0.5.1", 55 | "pathwatcher": "^8.1.0", 56 | "serializable": "^1.0.3", 57 | "superstring": "^2.4.4", 58 | "underscore-plus": "^1.0.0", 59 | "winattr": "^3.0.0" 60 | }, 61 | "standard": { 62 | "env": { 63 | "atomtest": true, 64 | "browser": true, 65 | "jasmine": true, 66 | "node": true 67 | }, 68 | "globals": [ 69 | "atom", 70 | "snapshotResult" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /spec/range-spec.coffee: -------------------------------------------------------------------------------- 1 | Range = require '../src/range' 2 | 3 | describe "Range", -> 4 | beforeEach -> 5 | jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) 6 | 7 | describe "::intersectsWith(other, [exclusive])", -> 8 | intersectsWith = (range1, range2, exclusive) -> 9 | range1 = Range.fromObject(range1) 10 | range2 = Range.fromObject(range2) 11 | range1.intersectsWith(range2, exclusive) 12 | 13 | describe "when the exclusive argument is false (the default)", -> 14 | it "returns true if the ranges intersect, exclusive of their endpoints", -> 15 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]])).toBe false 16 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]])).toBe true 17 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]])).toBe true 18 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]])).toBe true 19 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]])).toBe true 20 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]])).toBe true 21 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]])).toBe false 22 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false 23 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false 24 | 25 | describe "when the exclusive argument is true", -> 26 | it "returns true if the ranges intersect, exclusive of their endpoints", -> 27 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 0], [1, 1]], true)).toBe false 28 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 2]], true)).toBe false 29 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 1], [1, 3]], true)).toBe true 30 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [4, 5]], true)).toBe false 31 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 3], [4, 5]], true)).toBe true 32 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 5], [2, 2]], true)).toBe true 33 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 5], [4, 4]], true)).toBe false 34 | expect(intersectsWith([[1, 2], [3, 4]], [[1, 2], [1, 2]], true)).toBe false 35 | expect(intersectsWith([[1, 2], [3, 4]], [[3, 4], [3, 4]], true)).toBe false 36 | 37 | describe "::negate()", -> 38 | it "should negate the start and end points", -> 39 | expect(new Range([ 0, 0], [ 0, 0]).negate().toString()).toBe "[(0, 0) - (0, 0)]" 40 | expect(new Range([ 1, 2], [ 3, 4]).negate().toString()).toBe "[(-3, -4) - (-1, -2)]" 41 | expect(new Range([-1, -2], [-3, -4]).negate().toString()).toBe "[(1, 2) - (3, 4)]" 42 | expect(new Range([-1, 2], [ 3, -4]).negate().toString()).toBe "[(-3, 4) - (1, -2)]" 43 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const {Patch} = require('superstring') 2 | const Range = require('./range') 3 | const {traversal} = require('./point-helpers') 4 | 5 | const LF_REGEX = /\n/g 6 | 7 | exports.newlineRegex = /\r\n|\n|\r/g 8 | 9 | exports.debounce = function debounce (fn, wait) { 10 | let timestamp, timeout 11 | 12 | function later () { 13 | const last = Date.now() - timestamp 14 | if (last < wait && last >= 0) { 15 | timeout = setTimeout(later, wait - last) 16 | } else { 17 | timeout = null 18 | fn() 19 | } 20 | } 21 | 22 | return function () { 23 | timestamp = Date.now() 24 | if (!timeout) timeout = setTimeout(later, wait) 25 | } 26 | } 27 | 28 | exports.spliceArray = function (array, start, removedCount, insertedItems = []) { 29 | const oldLength = array.length 30 | const insertedCount = insertedItems.length 31 | removedCount = Math.min(removedCount, oldLength - start) 32 | const lengthDelta = insertedCount - removedCount 33 | const newLength = oldLength + lengthDelta 34 | 35 | if (lengthDelta > 0) { 36 | array.length = newLength 37 | for (let i = newLength - 1, end = start + insertedCount; i >= end; i--) { 38 | array[i] = array[i - lengthDelta] 39 | } 40 | } else if (lengthDelta < 0) { 41 | for (let i = start + insertedCount, end = newLength; i < end; i++) { 42 | array[i] = array[i - lengthDelta] 43 | } 44 | array.length = newLength 45 | } 46 | 47 | for (let i = 0; i < insertedItems.length; i++) { 48 | array[start + i] = insertedItems[i] 49 | } 50 | } 51 | 52 | exports.patchFromChanges = function (changes) { 53 | const patch = new Patch() 54 | for (let i = 0; i < changes.length; i++) { 55 | const {oldStart, oldEnd, oldText, newStart, newEnd, newText} = changes[i] 56 | const oldExtent = traversal(oldEnd, oldStart) 57 | const newExtent = traversal(newEnd, newStart) 58 | patch.splice(newStart, oldExtent, newExtent, oldText, newText) 59 | } 60 | return patch 61 | } 62 | 63 | exports.normalizePatchChanges = function (changes) { 64 | return changes.map((change) => 65 | new TextChange( 66 | Range(change.oldStart, change.oldEnd), 67 | Range(change.newStart, change.newEnd), 68 | change.oldText, change.newText 69 | ) 70 | ) 71 | } 72 | 73 | exports.extentForText = function (text) { 74 | let lastLineStartIndex = 0 75 | let row = 0 76 | LF_REGEX.lastIndex = 0 77 | while (LF_REGEX.exec(text)) { 78 | row++ 79 | lastLineStartIndex = LF_REGEX.lastIndex 80 | } 81 | return {row, column: text.length - lastLineStartIndex} 82 | } 83 | 84 | class TextChange { 85 | constructor (oldRange, newRange, oldText, newText) { 86 | this.oldRange = oldRange 87 | this.newRange = newRange 88 | this.oldText = oldText 89 | this.newText = newText 90 | } 91 | 92 | isEqual (other) { 93 | return ( 94 | this.oldRange.isEqual(other.oldRange) && 95 | this.newRange.isEqual(other.newRange) && 96 | this.oldText === other.oldText && 97 | this.newText === other.newText 98 | ) 99 | } 100 | } 101 | 102 | Object.defineProperty(TextChange.prototype, 'start', { 103 | get () { return this.newRange.start }, 104 | enumerable: false 105 | }) 106 | 107 | Object.defineProperty(TextChange.prototype, 'oldStart', { 108 | get () { return this.oldRange.start }, 109 | enumerable: false 110 | }) 111 | 112 | Object.defineProperty(TextChange.prototype, 'newStart', { 113 | get () { return this.newRange.start }, 114 | enumerable: false 115 | }) 116 | 117 | Object.defineProperty(TextChange.prototype, 'oldEnd', { 118 | get () { return this.oldRange.end }, 119 | enumerable: false 120 | }) 121 | 122 | Object.defineProperty(TextChange.prototype, 'newEnd', { 123 | get () { return this.newRange.end }, 124 | enumerable: false 125 | }) 126 | 127 | Object.defineProperty(TextChange.prototype, 'oldExtent', { 128 | get () { return this.oldRange.getExtent() }, 129 | enumerable: false 130 | }) 131 | 132 | Object.defineProperty(TextChange.prototype, 'newExtent', { 133 | get () { return this.newRange.getExtent() }, 134 | enumerable: false 135 | }) 136 | -------------------------------------------------------------------------------- /spec/helpers/test-language-mode.js: -------------------------------------------------------------------------------- 1 | const {MarkerIndex} = require('superstring') 2 | const {Emitter} = require('event-kit') 3 | const Point = require('../../src/point') 4 | const Range = require('../../src/range') 5 | const {MAX_BUILT_IN_SCOPE_ID} = require('../../src/constants') 6 | 7 | module.exports = 8 | class TestLanguageMode { 9 | constructor (decorations, buffer, random) { 10 | this.buffer = buffer 11 | this.random = random 12 | this.nextMarkerId = MAX_BUILT_IN_SCOPE_ID + 1 13 | this.markerIndex = new MarkerIndex() 14 | this.classNamesByScopeId = new Map() 15 | this.emitter = new Emitter() 16 | this.invalidatedRanges = [] 17 | 18 | if (this.buffer) { 19 | this.buffer.onDidChange(() => { 20 | for (const invalidatedRange of this.invalidatedRanges) { 21 | this.emitHighlightingChangeEvent(invalidatedRange) 22 | } 23 | this.invalidatedRanges = [] 24 | }) 25 | } 26 | 27 | for (let value of decorations) { 28 | const className = value[0] 29 | const [rangeStart, rangeEnd] = Array.from(value[1]) 30 | const markerId = this.getNextMarkerId() 31 | this.markerIndex.insert(markerId, Point.fromObject(rangeStart), Point.fromObject(rangeEnd)) 32 | this.classNamesByScopeId.set(markerId, className) 33 | } 34 | } 35 | 36 | buildHighlightIterator () { 37 | return new TestHighlightIterator(this) 38 | } 39 | 40 | onDidChangeHighlighting (fn) { 41 | return this.emitter.on('did-change-highlighting', fn) 42 | } 43 | 44 | bufferDidChange ({oldRange, newRange}) { 45 | this.invalidatedRanges.push(Range.fromObject(newRange)) 46 | const {inside, overlap} = this.markerIndex.splice(oldRange.start, oldRange.getExtent(), newRange.getExtent()) 47 | overlap.forEach((id) => this.invalidatedRanges.push(Range.fromObject(this.markerIndex.getRange(id)))) 48 | inside.forEach((id) => this.invalidatedRanges.push(Range.fromObject(this.markerIndex.getRange(id)))) 49 | 50 | this.insertRandomDecorations(oldRange, newRange) 51 | } 52 | 53 | bufferDidFinishTransaction () {} 54 | 55 | emitHighlightingChangeEvent (range) { 56 | this.emitter.emit('did-change-highlighting', range) 57 | } 58 | 59 | getNextMarkerId () { 60 | const nextMarkerId = this.nextMarkerId 61 | this.nextMarkerId += 2 62 | return nextMarkerId 63 | } 64 | 65 | classNameForScopeId (scopeId) { 66 | return this.classNamesByScopeId.get(scopeId) 67 | } 68 | 69 | insertRandomDecorations (oldRange, newRange) { 70 | if (this.invalidatedRanges == null) { this.invalidatedRanges = [] } 71 | 72 | const j = this.random(5) 73 | for (let i = 0; i < j; i++) { 74 | const markerId = this.getNextMarkerId() 75 | const className = String.fromCharCode('a'.charCodeAt(0) + this.random(27)) 76 | this.classNamesByScopeId.set(markerId, className) 77 | const range = this.getRandomRangeCloseTo(oldRange.union(newRange)) 78 | this.markerIndex.insert(markerId, range.start, range.end) 79 | this.invalidatedRanges.push(range) 80 | } 81 | } 82 | 83 | getRandomRangeCloseTo (range) { 84 | let minRow 85 | if (this.random(10) < 7) { 86 | minRow = this.constrainRow(range.start.row + this.random.intBetween(-20, 20)) 87 | } else { 88 | minRow = 0 89 | } 90 | 91 | let maxRow 92 | if (this.random(10) < 7) { 93 | maxRow = this.constrainRow(range.end.row + this.random.intBetween(-20, 20)) 94 | } else { 95 | maxRow = this.buffer.getLastRow() 96 | } 97 | 98 | const startRow = this.random.intBetween(minRow, maxRow) 99 | const endRow = this.random.intBetween(startRow, maxRow) 100 | const startColumn = this.random(this.buffer.lineForRow(startRow).length + 1) 101 | const endColumn = this.random(this.buffer.lineForRow(endRow).length + 1) 102 | return Range(Point(startRow, startColumn), Point(endRow, endColumn)) 103 | } 104 | 105 | constrainRow (row) { 106 | return Math.max(0, Math.min(this.buffer.getLastRow(), row)) 107 | } 108 | } 109 | 110 | class TestHighlightIterator { 111 | constructor (layer) { 112 | this.layer = layer 113 | } 114 | 115 | seek (position, endBufferRow) { 116 | const {containingStart, boundaries} = this.layer.markerIndex.findBoundariesAfter(position, Number.MAX_SAFE_INTEGER) 117 | this.boundaries = boundaries 118 | this.boundaryIndex = 0 119 | this.endBufferRow = endBufferRow 120 | return containingStart 121 | } 122 | 123 | moveToSuccessor () { 124 | this.boundaryIndex++ 125 | } 126 | 127 | getPosition () { 128 | const boundary = this.boundaries[this.boundaryIndex] 129 | return boundary ? boundary.position : Point.INFINITY 130 | } 131 | 132 | getCloseScopeIds () { 133 | const result = [] 134 | const boundary = this.boundaries[this.boundaryIndex] 135 | if (boundary) { 136 | expect(boundary.position.row).not.toBeGreaterThan(this.endBufferRow) 137 | boundary.ending.forEach((markerId) => { 138 | if (!boundary.starting.has(markerId)) { 139 | result.push(markerId) 140 | } 141 | }) 142 | } 143 | return result 144 | } 145 | 146 | getOpenScopeIds () { 147 | const result = [] 148 | const boundary = this.boundaries[this.boundaryIndex] 149 | if (boundary) { 150 | expect(boundary.position.row).not.toBeGreaterThan(this.endBufferRow) 151 | boundary.starting.forEach((markerId) => { 152 | if (!boundary.ending.has(markerId)) { 153 | result.push(markerId) 154 | } 155 | }) 156 | } 157 | return result 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/point.coffee: -------------------------------------------------------------------------------- 1 | # Public: Represents a point in a buffer in row/column coordinates. 2 | # 3 | # Every public method that takes a point also accepts a *point-compatible* 4 | # {Array}. This means a 2-element array containing {Number}s representing the 5 | # row and column. So the following are equivalent: 6 | # 7 | # ```coffee 8 | # new Point(1, 2) 9 | # [1, 2] # Point compatible Array 10 | # ``` 11 | module.exports = 12 | class Point 13 | ### 14 | Section: Properties 15 | ### 16 | 17 | # Public: A zero-indexed {Number} representing the row of the {Point}. 18 | row: null 19 | 20 | # Public: A zero-indexed {Number} representing the column of the {Point}. 21 | column: null 22 | 23 | ### 24 | Section: Construction 25 | ### 26 | 27 | # Public: Convert any point-compatible object to a {Point}. 28 | # 29 | # * `object` This can be an object that's already a {Point}, in which case it's 30 | # simply returned, or an array containing two {Number}s representing the 31 | # row and column. 32 | # * `copy` An optional boolean indicating whether to force the copying of objects 33 | # that are already points. 34 | # 35 | # Returns: A {Point} based on the given object. 36 | @fromObject: (object, copy) -> 37 | if object instanceof Point 38 | if copy then object.copy() else object 39 | else 40 | if Array.isArray(object) 41 | [row, column] = object 42 | else 43 | {row, column} = object 44 | 45 | new Point(row, column) 46 | 47 | ### 48 | Section: Comparison 49 | ### 50 | 51 | # Public: Returns the given {Point} that is earlier in the buffer. 52 | # 53 | # * `point1` {Point} 54 | # * `point2` {Point} 55 | @min: (point1, point2) -> 56 | point1 = @fromObject(point1) 57 | point2 = @fromObject(point2) 58 | if point1.isLessThanOrEqual(point2) 59 | point1 60 | else 61 | point2 62 | 63 | @max: (point1, point2) -> 64 | point1 = Point.fromObject(point1) 65 | point2 = Point.fromObject(point2) 66 | if point1.compare(point2) >= 0 67 | point1 68 | else 69 | point2 70 | 71 | @assertValid: (point) -> 72 | unless isNumber(point.row) and isNumber(point.column) 73 | throw new TypeError("Invalid Point: #{point}") 74 | 75 | @ZERO: Object.freeze(new Point(0, 0)) 76 | 77 | @INFINITY: Object.freeze(new Point(Infinity, Infinity)) 78 | 79 | ### 80 | Section: Construction 81 | ### 82 | 83 | # Public: Construct a {Point} object 84 | # 85 | # * `row` {Number} row 86 | # * `column` {Number} column 87 | constructor: (row=0, column=0) -> 88 | unless this instanceof Point 89 | return new Point(row, column) 90 | @row = row 91 | @column = column 92 | 93 | # Public: Returns a new {Point} with the same row and column. 94 | copy: -> 95 | new Point(@row, @column) 96 | 97 | # Public: Returns a new {Point} with the row and column negated. 98 | negate: -> 99 | new Point(-@row, -@column) 100 | 101 | ### 102 | Section: Operations 103 | ### 104 | 105 | # Public: Makes this point immutable and returns itself. 106 | # 107 | # Returns an immutable version of this {Point} 108 | freeze: -> 109 | Object.freeze(this) 110 | 111 | # Public: Build and return a new point by adding the rows and columns of 112 | # the given point. 113 | # 114 | # * `other` A {Point} whose row and column will be added to this point's row 115 | # and column to build the returned point. 116 | # 117 | # Returns a {Point}. 118 | translate: (other) -> 119 | {row, column} = Point.fromObject(other) 120 | new Point(@row + row, @column + column) 121 | 122 | # Public: Build and return a new {Point} by traversing the rows and columns 123 | # specified by the given point. 124 | # 125 | # * `other` A {Point} providing the rows and columns to traverse by. 126 | # 127 | # This method differs from the direct, vector-style addition offered by 128 | # {::translate}. Rather than adding the rows and columns directly, it derives 129 | # the new point from traversing in "typewriter space". At the end of every row 130 | # traversed, a carriage return occurs that returns the columns to 0 before 131 | # continuing the traversal. 132 | # 133 | # ## Examples 134 | # 135 | # Traversing 0 rows, 2 columns: 136 | # `new Point(10, 5).traverse(new Point(0, 2)) # => [10, 7]` 137 | # 138 | # Traversing 2 rows, 2 columns. Note the columns reset from 0 before adding: 139 | # `new Point(10, 5).traverse(new Point(2, 2)) # => [12, 2]` 140 | # 141 | # Returns a {Point}. 142 | traverse: (other) -> 143 | other = Point.fromObject(other) 144 | row = @row + other.row 145 | if other.row is 0 146 | column = @column + other.column 147 | else 148 | column = other.column 149 | 150 | new Point(row, column) 151 | 152 | traversalFrom: (other) -> 153 | other = Point.fromObject(other) 154 | if @row is other.row 155 | if @column is Infinity and other.column is Infinity 156 | new Point(0, 0) 157 | else 158 | new Point(0, @column - other.column) 159 | else 160 | new Point(@row - other.row, @column) 161 | 162 | splitAt: (column) -> 163 | if @row is 0 164 | rightColumn = @column - column 165 | else 166 | rightColumn = @column 167 | 168 | [new Point(0, column), new Point(@row, rightColumn)] 169 | 170 | ### 171 | Section: Comparison 172 | ### 173 | 174 | # Public: 175 | # 176 | # * `other` A {Point} or point-compatible {Array}. 177 | # 178 | # Returns `-1` if this point precedes the argument. 179 | # Returns `0` if this point is equivalent to the argument. 180 | # Returns `1` if this point follows the argument. 181 | compare: (other) -> 182 | other = Point.fromObject(other) 183 | if @row > other.row 184 | 1 185 | else if @row < other.row 186 | -1 187 | else 188 | if @column > other.column 189 | 1 190 | else if @column < other.column 191 | -1 192 | else 193 | 0 194 | 195 | # Public: Returns a {Boolean} indicating whether this point has the same row 196 | # and column as the given {Point} or point-compatible {Array}. 197 | # 198 | # * `other` A {Point} or point-compatible {Array}. 199 | isEqual: (other) -> 200 | return false unless other 201 | other = Point.fromObject(other) 202 | @row is other.row and @column is other.column 203 | 204 | # Public: Returns a {Boolean} indicating whether this point precedes the given 205 | # {Point} or point-compatible {Array}. 206 | # 207 | # * `other` A {Point} or point-compatible {Array}. 208 | isLessThan: (other) -> 209 | @compare(other) < 0 210 | 211 | # Public: Returns a {Boolean} indicating whether this point precedes or is 212 | # equal to the given {Point} or point-compatible {Array}. 213 | # 214 | # * `other` A {Point} or point-compatible {Array}. 215 | isLessThanOrEqual: (other) -> 216 | @compare(other) <= 0 217 | 218 | # Public: Returns a {Boolean} indicating whether this point follows the given 219 | # {Point} or point-compatible {Array}. 220 | # 221 | # * `other` A {Point} or point-compatible {Array}. 222 | isGreaterThan: (other) -> 223 | @compare(other) > 0 224 | 225 | # Public: Returns a {Boolean} indicating whether this point follows or is 226 | # equal to the given {Point} or point-compatible {Array}. 227 | # 228 | # * `other` A {Point} or point-compatible {Array}. 229 | isGreaterThanOrEqual: (other) -> 230 | @compare(other) >= 0 231 | 232 | isZero: -> 233 | @row is 0 and @column is 0 234 | 235 | isPositive: -> 236 | if @row > 0 237 | true 238 | else if @row < 0 239 | false 240 | else 241 | @column > 0 242 | 243 | isNegative: -> 244 | if @row < 0 245 | true 246 | else if @row > 0 247 | false 248 | else 249 | @column < 0 250 | 251 | ### 252 | Section: Conversion 253 | ### 254 | 255 | # Public: Returns an array of this point's row and column. 256 | toArray: -> 257 | [@row, @column] 258 | 259 | # Public: Returns an array of this point's row and column. 260 | serialize: -> 261 | @toArray() 262 | 263 | # Public: Returns a string representation of the point. 264 | toString: -> 265 | "(#{@row}, #{@column})" 266 | 267 | isNumber = (value) -> 268 | (typeof value is 'number') and (not Number.isNaN(value)) 269 | -------------------------------------------------------------------------------- /spec/point-spec.coffee: -------------------------------------------------------------------------------- 1 | Point = require '../src/point' 2 | 3 | describe "Point", -> 4 | beforeEach -> 5 | jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) 6 | 7 | describe "::negate()", -> 8 | it "should negate the row and column", -> 9 | expect(new Point( 0, 0).negate().toString()).toBe "(0, 0)" 10 | expect(new Point( 1, 2).negate().toString()).toBe "(-1, -2)" 11 | expect(new Point(-1, -2).negate().toString()).toBe "(1, 2)" 12 | expect(new Point(-1, 2).negate().toString()).toBe "(1, -2)" 13 | 14 | describe "::fromObject(object, copy)", -> 15 | it "returns a new Point if object is point-compatible array ", -> 16 | expect(Point.fromObject([1, 3])).toEqual Point(1, 3) 17 | expect(Point.fromObject([Infinity, Infinity])).toEqual Point.INFINITY 18 | 19 | it "returns the copy of object if it is an instanceof Point", -> 20 | origin = Point(0, 0) 21 | expect(Point.fromObject(origin, false) is origin).toBe true 22 | expect(Point.fromObject(origin, true) is origin).toBe false 23 | 24 | describe "::copy()", -> 25 | it "returns a copy of the object", -> 26 | expect(Point(3, 4).copy()).toEqual Point(3, 4) 27 | expect(Point.ZERO.copy()).toEqual [0, 0] 28 | 29 | describe "::negate()", -> 30 | it "returns a new point with row and column negated", -> 31 | expect(Point(3, 4).negate()).toEqual Point(-3, -4) 32 | expect(Point.ZERO.negate()).toEqual [0, 0] 33 | 34 | describe "::freeze()", -> 35 | it "makes the Point object immutable", -> 36 | expect(Object.isFrozen(Point(3, 4).freeze())).toBe true 37 | expect(Object.isFrozen(Point.ZERO.freeze())).toBe true 38 | 39 | describe "::compare(other)", -> 40 | it "returns -1 for <, 0 for =, 1 for > comparisions", -> 41 | expect(Point(2, 3).compare(Point(2, 6))).toBe -1 42 | expect(Point(2, 3).compare(Point(3, 4))).toBe -1 43 | expect(Point(1, 1).compare(Point(1, 1))).toBe 0 44 | expect(Point(2, 3).compare(Point(2, 0))).toBe 1 45 | expect(Point(2, 3).compare(Point(1, 3))).toBe 1 46 | 47 | expect(Point(2, 3).compare([2, 6])).toBe -1 48 | expect(Point(2, 3).compare([3, 4])).toBe -1 49 | expect(Point(1, 1).compare([1, 1])).toBe 0 50 | expect(Point(2, 3).compare([2, 0])).toBe 1 51 | expect(Point(2, 3).compare([1, 3])).toBe 1 52 | 53 | describe "::isLessThan(other)", -> 54 | it "returns a boolean indicating whether a point precedes the given Point ", -> 55 | expect(Point(2, 3).isLessThan(Point(2, 5))).toBe true 56 | expect(Point(2, 3).isLessThan(Point(3, 4))).toBe true 57 | expect(Point(2, 3).isLessThan(Point(2, 3))).toBe false 58 | expect(Point(2, 3).isLessThan(Point(2, 1))).toBe false 59 | expect(Point(2, 3).isLessThan(Point(1, 2))).toBe false 60 | 61 | expect(Point(2, 3).isLessThan([2, 5])).toBe true 62 | expect(Point(2, 3).isLessThan([3, 4])).toBe true 63 | expect(Point(2, 3).isLessThan([2, 3])).toBe false 64 | expect(Point(2, 3).isLessThan([2, 1])).toBe false 65 | expect(Point(2, 3).isLessThan([1, 2])).toBe false 66 | 67 | describe "::isLessThanOrEqual(other)", -> 68 | it "returns a boolean indicating whether a point precedes or equal the given Point ", -> 69 | expect(Point(2, 3).isLessThanOrEqual(Point(2, 5))).toBe true 70 | expect(Point(2, 3).isLessThanOrEqual(Point(3, 4))).toBe true 71 | expect(Point(2, 3).isLessThanOrEqual(Point(2, 3))).toBe true 72 | expect(Point(2, 3).isLessThanOrEqual(Point(2, 1))).toBe false 73 | expect(Point(2, 3).isLessThanOrEqual(Point(1, 2))).toBe false 74 | 75 | expect(Point(2, 3).isLessThanOrEqual([2, 5])).toBe true 76 | expect(Point(2, 3).isLessThanOrEqual([3, 4])).toBe true 77 | expect(Point(2, 3).isLessThanOrEqual([2, 3])).toBe true 78 | expect(Point(2, 3).isLessThanOrEqual([2, 1])).toBe false 79 | expect(Point(2, 3).isLessThanOrEqual([1, 2])).toBe false 80 | 81 | describe "::isGreaterThan(other)", -> 82 | it "returns a boolean indicating whether a point follows the given Point ", -> 83 | expect(Point(2, 3).isGreaterThan(Point(2, 5))).toBe false 84 | expect(Point(2, 3).isGreaterThan(Point(3, 4))).toBe false 85 | expect(Point(2, 3).isGreaterThan(Point(2, 3))).toBe false 86 | expect(Point(2, 3).isGreaterThan(Point(2, 1))).toBe true 87 | expect(Point(2, 3).isGreaterThan(Point(1, 2))).toBe true 88 | 89 | expect(Point(2, 3).isGreaterThan([2, 5])).toBe false 90 | expect(Point(2, 3).isGreaterThan([3, 4])).toBe false 91 | expect(Point(2, 3).isGreaterThan([2, 3])).toBe false 92 | expect(Point(2, 3).isGreaterThan([2, 1])).toBe true 93 | expect(Point(2, 3).isGreaterThan([1, 2])).toBe true 94 | 95 | describe "::isGreaterThanOrEqual(other)", -> 96 | it "returns a boolean indicating whether a point follows or equal the given Point ", -> 97 | expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 5))).toBe false 98 | expect(Point(2, 3).isGreaterThanOrEqual(Point(3, 4))).toBe false 99 | expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 3))).toBe true 100 | expect(Point(2, 3).isGreaterThanOrEqual(Point(2, 1))).toBe true 101 | expect(Point(2, 3).isGreaterThanOrEqual(Point(1, 2))).toBe true 102 | 103 | expect(Point(2, 3).isGreaterThanOrEqual([2, 5])).toBe false 104 | expect(Point(2, 3).isGreaterThanOrEqual([3, 4])).toBe false 105 | expect(Point(2, 3).isGreaterThanOrEqual([2, 3])).toBe true 106 | expect(Point(2, 3).isGreaterThanOrEqual([2, 1])).toBe true 107 | expect(Point(2, 3).isGreaterThanOrEqual([1, 2])).toBe true 108 | 109 | describe "::isEqual()", -> 110 | it "returns if whether two points are equal", -> 111 | expect(Point(1, 1).isEqual(Point(1, 1))).toBe true 112 | expect(Point(1, 1).isEqual([1, 1])).toBe true 113 | expect(Point(1, 2).isEqual(Point(3, 3))).toBe false 114 | expect(Point(1, 2).isEqual([3, 3])).toBe false 115 | 116 | describe "::isPositive()", -> 117 | it "returns true if the point represents a forward traversal", -> 118 | expect(Point(-1, -1).isPositive()).toBe false 119 | expect(Point(-1, 0).isPositive()).toBe false 120 | expect(Point(-1, Infinity).isPositive()).toBe false 121 | expect(Point(0, 0).isPositive()).toBe false 122 | 123 | expect(Point(0, 1).isPositive()).toBe true 124 | expect(Point(5, 0).isPositive()).toBe true 125 | expect(Point(5, -1).isPositive()).toBe true 126 | 127 | describe "::isZero()", -> 128 | it "returns true if the point is zero", -> 129 | expect(Point(1, 1).isZero()).toBe false 130 | expect(Point(0, 1).isZero()).toBe false 131 | expect(Point(1, 0).isZero()).toBe false 132 | expect(Point(0, 0).isZero()).toBe true 133 | 134 | describe "::min(a, b)", -> 135 | it "returns the minimum of two points", -> 136 | expect(Point.min(Point(3, 4), Point(1, 1))).toEqual Point(1, 1) 137 | expect(Point.min(Point(1, 2), Point(5, 6))).toEqual Point(1, 2) 138 | expect(Point.min([3, 4], [1, 1])).toEqual [1, 1] 139 | expect(Point.min([1, 2], [5, 6])).toEqual [1, 2] 140 | 141 | describe "::max(a, b)", -> 142 | it "returns the minimum of two points", -> 143 | expect(Point.max(Point(3, 4), Point(1, 1))).toEqual Point(3, 4) 144 | expect(Point.max(Point(1, 2), Point(5, 6))).toEqual Point(5, 6) 145 | expect(Point.max([3, 4], [1, 1])).toEqual [3, 4] 146 | expect(Point.max([1, 2], [5, 6])).toEqual [5, 6] 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 | -------------------------------------------------------------------------------- /src/range.coffee: -------------------------------------------------------------------------------- 1 | Point = require './point' 2 | newlineRegex = null 3 | 4 | # Public: Represents a region in a buffer in row/column coordinates. 5 | # 6 | # Every public method that takes a range also accepts a *range-compatible* 7 | # {Array}. This means a 2-element array containing {Point}s or point-compatible 8 | # arrays. So the following are equivalent: 9 | # 10 | # ## Examples 11 | # 12 | # ```coffee 13 | # new Range(new Point(0, 1), new Point(2, 3)) 14 | # new Range([0, 1], [2, 3]) 15 | # [[0, 1], [2, 3]] # Range compatible array 16 | # ``` 17 | module.exports = 18 | class Range 19 | ### 20 | Section: Properties 21 | ### 22 | 23 | # Public: A {Point} representing the start of the {Range}. 24 | start: null 25 | 26 | # Public: A {Point} representing the end of the {Range}. 27 | end: null 28 | 29 | ### 30 | Section: Construction 31 | ### 32 | 33 | # Public: Convert any range-compatible object to a {Range}. 34 | # 35 | # * `object` This can be an object that's already a {Range}, in which case it's 36 | # simply returned, or an array containing two {Point}s or point-compatible 37 | # arrays. 38 | # * `copy` An optional boolean indicating whether to force the copying of objects 39 | # that are already ranges. 40 | # 41 | # Returns: A {Range} based on the given object. 42 | @fromObject: (object, copy) -> 43 | if Array.isArray(object) 44 | new this(object[0], object[1]) 45 | else if object instanceof this 46 | if copy then object.copy() else object 47 | else 48 | new this(object.start, object.end) 49 | 50 | # Returns a range based on an optional starting point and the given text. If 51 | # no starting point is given it will be assumed to be [0, 0]. 52 | # 53 | # * `startPoint` (optional) {Point} where the range should start. 54 | # * `text` A {String} after which the range should end. The range will have as many 55 | # rows as the text has lines have an end column based on the length of the 56 | # last line. 57 | # 58 | # Returns: A {Range} 59 | @fromText: (args...) -> 60 | newlineRegex ?= require('./helpers').newlineRegex 61 | 62 | if args.length > 1 63 | startPoint = Point.fromObject(args.shift()) 64 | else 65 | startPoint = new Point(0, 0) 66 | text = args.shift() 67 | endPoint = startPoint.copy() 68 | lines = text.split(newlineRegex) 69 | if lines.length > 1 70 | lastIndex = lines.length - 1 71 | endPoint.row += lastIndex 72 | endPoint.column = lines[lastIndex].length 73 | else 74 | endPoint.column += lines[0].length 75 | new this(startPoint, endPoint) 76 | 77 | # Returns a {Range} that starts at the given point and ends at the 78 | # start point plus the given row and column deltas. 79 | # 80 | # * `startPoint` A {Point} or point-compatible {Array} 81 | # * `rowDelta` A {Number} indicating how many rows to add to the start point 82 | # to get the end point. 83 | # * `columnDelta` A {Number} indicating how many rows to columns to the start 84 | # point to get the end point. 85 | @fromPointWithDelta: (startPoint, rowDelta, columnDelta) -> 86 | startPoint = Point.fromObject(startPoint) 87 | endPoint = new Point(startPoint.row + rowDelta, startPoint.column + columnDelta) 88 | new this(startPoint, endPoint) 89 | 90 | @fromPointWithTraversalExtent: (startPoint, extent) -> 91 | startPoint = Point.fromObject(startPoint) 92 | new this(startPoint, startPoint.traverse(extent)) 93 | 94 | 95 | ### 96 | Section: Serialization and Deserialization 97 | ### 98 | 99 | # Public: Call this with the result of {Range::serialize} to construct a new Range. 100 | # 101 | # * `array` {Array} of params to pass to the {::constructor} 102 | @deserialize: (array) -> 103 | if Array.isArray(array) 104 | new this(array[0], array[1]) 105 | else 106 | new this() 107 | 108 | ### 109 | Section: Construction 110 | ### 111 | 112 | # Public: Construct a {Range} object 113 | # 114 | # * `pointA` {Point} or Point compatible {Array} (default: [0,0]) 115 | # * `pointB` {Point} or Point compatible {Array} (default: [0,0]) 116 | constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) -> 117 | unless this instanceof Range 118 | return new Range(pointA, pointB) 119 | 120 | pointA = Point.fromObject(pointA) 121 | pointB = Point.fromObject(pointB) 122 | 123 | if pointA.isLessThanOrEqual(pointB) 124 | @start = pointA 125 | @end = pointB 126 | else 127 | @start = pointB 128 | @end = pointA 129 | 130 | # Public: Returns a new range with the same start and end positions. 131 | copy: -> 132 | new @constructor(@start.copy(), @end.copy()) 133 | 134 | # Public: Returns a new range with the start and end positions negated. 135 | negate: -> 136 | new @constructor(@start.negate(), @end.negate()) 137 | 138 | ### 139 | Section: Serialization and Deserialization 140 | ### 141 | 142 | # Public: Returns a plain javascript object representation of the range. 143 | serialize: -> 144 | [@start.serialize(), @end.serialize()] 145 | 146 | ### 147 | Section: Range Details 148 | ### 149 | 150 | # Public: Is the start position of this range equal to the end position? 151 | # 152 | # Returns a {Boolean}. 153 | isEmpty: -> 154 | @start.isEqual(@end) 155 | 156 | # Public: Returns a {Boolean} indicating whether this range starts and ends on 157 | # the same row. 158 | isSingleLine: -> 159 | @start.row is @end.row 160 | 161 | # Public: Get the number of rows in this range. 162 | # 163 | # Returns a {Number}. 164 | getRowCount: -> 165 | @end.row - @start.row + 1 166 | 167 | # Public: Returns an array of all rows in the range. 168 | getRows: -> 169 | [@start.row..@end.row] 170 | 171 | ### 172 | Section: Operations 173 | ### 174 | 175 | # Public: Freezes the range and its start and end point so it becomes 176 | # immutable and returns itself. 177 | # 178 | # Returns an immutable version of this {Range} 179 | freeze: -> 180 | @start.freeze() 181 | @end.freeze() 182 | Object.freeze(this) 183 | 184 | # Public: Returns a new range that contains this range and the given range. 185 | # 186 | # * `otherRange` A {Range} or range-compatible {Array} 187 | union: (otherRange) -> 188 | start = if @start.isLessThan(otherRange.start) then @start else otherRange.start 189 | end = if @end.isGreaterThan(otherRange.end) then @end else otherRange.end 190 | new @constructor(start, end) 191 | 192 | # Public: Build and return a new range by translating this range's start and 193 | # end points by the given delta(s). 194 | # 195 | # * `startDelta` A {Point} by which to translate the start of this range. 196 | # * `endDelta` (optional) A {Point} to by which to translate the end of this 197 | # range. If omitted, the `startDelta` will be used instead. 198 | # 199 | # Returns a {Range}. 200 | translate: (startDelta, endDelta=startDelta) -> 201 | new @constructor(@start.translate(startDelta), @end.translate(endDelta)) 202 | 203 | # Public: Build and return a new range by traversing this range's start and 204 | # end points by the given delta. 205 | # 206 | # See {Point::traverse} for details of how traversal differs from translation. 207 | # 208 | # * `delta` A {Point} containing the rows and columns to traverse to derive 209 | # the new range. 210 | # 211 | # Returns a {Range}. 212 | traverse: (delta) -> 213 | new @constructor(@start.traverse(delta), @end.traverse(delta)) 214 | 215 | ### 216 | Section: Comparison 217 | ### 218 | 219 | # Public: Compare two Ranges 220 | # 221 | # * `otherRange` A {Range} or range-compatible {Array}. 222 | # 223 | # Returns `-1` if this range starts before the argument or contains it. 224 | # Returns `0` if this range is equivalent to the argument. 225 | # Returns `1` if this range starts after the argument or is contained by it. 226 | compare: (other) -> 227 | other = @constructor.fromObject(other) 228 | if value = @start.compare(other.start) 229 | value 230 | else 231 | other.end.compare(@end) 232 | 233 | # Public: Returns a {Boolean} indicating whether this range has the same start 234 | # and end points as the given {Range} or range-compatible {Array}. 235 | # 236 | # * `otherRange` A {Range} or range-compatible {Array}. 237 | isEqual: (other) -> 238 | return false unless other? 239 | other = @constructor.fromObject(other) 240 | other.start.isEqual(@start) and other.end.isEqual(@end) 241 | 242 | # Public: Returns a {Boolean} indicating whether this range starts and ends on 243 | # the same row as the argument. 244 | # 245 | # * `otherRange` A {Range} or range-compatible {Array}. 246 | coversSameRows: (other) -> 247 | @start.row is other.start.row and @end.row is other.end.row 248 | 249 | # Public: Determines whether this range intersects with the argument. 250 | # 251 | # * `otherRange` A {Range} or range-compatible {Array} 252 | # * `exclusive` (optional) {Boolean} indicating whether to exclude endpoints 253 | # when testing for intersection. Defaults to `false`. 254 | # 255 | # Returns a {Boolean}. 256 | intersectsWith: (otherRange, exclusive) -> 257 | if exclusive 258 | not (@end.isLessThanOrEqual(otherRange.start) or @start.isGreaterThanOrEqual(otherRange.end)) 259 | else 260 | not (@end.isLessThan(otherRange.start) or @start.isGreaterThan(otherRange.end)) 261 | 262 | # Public: Returns a {Boolean} indicating whether this range contains the given 263 | # range. 264 | # 265 | # * `otherRange` A {Range} or range-compatible {Array} 266 | # * `exclusive` (optional) {Boolean} including that the containment should be exclusive of 267 | # endpoints. Defaults to false. 268 | containsRange: (otherRange, exclusive) -> 269 | {start, end} = @constructor.fromObject(otherRange) 270 | @containsPoint(start, exclusive) and @containsPoint(end, exclusive) 271 | 272 | # Public: Returns a {Boolean} indicating whether this range contains the given 273 | # point. 274 | # 275 | # * `point` A {Point} or point-compatible {Array} 276 | # * `exclusive` (optional) {Boolean} including that the containment should be exclusive of 277 | # endpoints. Defaults to false. 278 | containsPoint: (point, exclusive) -> 279 | point = Point.fromObject(point) 280 | if exclusive 281 | point.isGreaterThan(@start) and point.isLessThan(@end) 282 | else 283 | point.isGreaterThanOrEqual(@start) and point.isLessThanOrEqual(@end) 284 | 285 | # Public: Returns a {Boolean} indicating whether this range intersects the 286 | # given row {Number}. 287 | # 288 | # * `row` Row {Number} 289 | intersectsRow: (row) -> 290 | @start.row <= row <= @end.row 291 | 292 | # Public: Returns a {Boolean} indicating whether this range intersects the 293 | # row range indicated by the given startRow and endRow {Number}s. 294 | # 295 | # * `startRow` {Number} start row 296 | # * `endRow` {Number} end row 297 | intersectsRowRange: (startRow, endRow) -> 298 | [startRow, endRow] = [endRow, startRow] if startRow > endRow 299 | @end.row >= startRow and endRow >= @start.row 300 | 301 | getExtent: -> 302 | @end.traversalFrom(@start) 303 | 304 | ### 305 | Section: Conversion 306 | ### 307 | 308 | toDelta: -> 309 | rows = @end.row - @start.row 310 | if rows is 0 311 | columns = @end.column - @start.column 312 | else 313 | columns = @end.column 314 | new Point(rows, columns) 315 | 316 | # Public: Returns a string representation of the range. 317 | toString: -> 318 | "[#{@start} - #{@end}]" 319 | -------------------------------------------------------------------------------- /src/default-history-provider.coffee: -------------------------------------------------------------------------------- 1 | {Patch} = require 'superstring' 2 | MarkerLayer = require './marker-layer' 3 | {traversal} = require './point-helpers' 4 | {patchFromChanges} = require './helpers' 5 | 6 | SerializationVersion = 6 7 | 8 | class Checkpoint 9 | constructor: (@id, @snapshot, @isBarrier) -> 10 | unless @snapshot? 11 | global.atom?.assert(false, "Checkpoint created without snapshot") 12 | @snapshot = {} 13 | 14 | class Transaction 15 | constructor: (@markerSnapshotBefore, @patch, @markerSnapshotAfter, @groupingInterval=0) -> 16 | @timestamp = Date.now() 17 | 18 | shouldGroupWith: (previousTransaction) -> 19 | timeBetweenTransactions = @timestamp - previousTransaction.timestamp 20 | timeBetweenTransactions < Math.min(@groupingInterval, previousTransaction.groupingInterval) 21 | 22 | groupWith: (previousTransaction) -> 23 | new Transaction( 24 | previousTransaction.markerSnapshotBefore, 25 | Patch.compose([previousTransaction.patch, @patch]), 26 | @markerSnapshotAfter, 27 | @groupingInterval 28 | ) 29 | 30 | # Manages undo/redo for {TextBuffer} 31 | module.exports = 32 | class DefaultHistoryProvider 33 | constructor: (@buffer) -> 34 | @maxUndoEntries = @buffer.maxUndoEntries 35 | @nextCheckpointId = 1 36 | @undoStack = [] 37 | @redoStack = [] 38 | 39 | createCheckpoint: (options) -> 40 | checkpoint = new Checkpoint(@nextCheckpointId++, options?.markers, options?.isBarrier) 41 | @undoStack.push(checkpoint) 42 | checkpoint.id 43 | 44 | groupChangesSinceCheckpoint: (checkpointId, options) -> 45 | deleteCheckpoint = options?.deleteCheckpoint ? false 46 | markerSnapshotAfter = options?.markers 47 | checkpointIndex = null 48 | markerSnapshotBefore = null 49 | patchesSinceCheckpoint = [] 50 | 51 | for entry, i in @undoStack by -1 52 | break if checkpointIndex? 53 | 54 | switch entry.constructor 55 | when Checkpoint 56 | if entry.id is checkpointId 57 | checkpointIndex = i 58 | markerSnapshotBefore = entry.snapshot 59 | else if entry.isBarrier 60 | return false 61 | when Transaction 62 | patchesSinceCheckpoint.unshift(entry.patch) 63 | when Patch 64 | patchesSinceCheckpoint.unshift(entry) 65 | else 66 | throw new Error("Unexpected undo stack entry type: #{entry.constructor.name}") 67 | 68 | if checkpointIndex? 69 | composedPatches = Patch.compose(patchesSinceCheckpoint) 70 | if patchesSinceCheckpoint.length > 0 71 | @undoStack.splice(checkpointIndex + 1) 72 | @undoStack.push(new Transaction(markerSnapshotBefore, composedPatches, markerSnapshotAfter)) 73 | if deleteCheckpoint 74 | @undoStack.splice(checkpointIndex, 1) 75 | composedPatches.getChanges() 76 | else 77 | false 78 | 79 | getChangesSinceCheckpoint: (checkpointId) -> 80 | checkpointIndex = null 81 | patchesSinceCheckpoint = [] 82 | 83 | for entry, i in @undoStack by -1 84 | break if checkpointIndex? 85 | 86 | switch entry.constructor 87 | when Checkpoint 88 | if entry.id is checkpointId 89 | checkpointIndex = i 90 | when Transaction 91 | patchesSinceCheckpoint.unshift(entry.patch) 92 | when Patch 93 | patchesSinceCheckpoint.unshift(entry) 94 | else 95 | throw new Error("Unexpected undo stack entry type: #{entry.constructor.name}") 96 | 97 | if checkpointIndex? 98 | Patch.compose(patchesSinceCheckpoint).getChanges() 99 | else 100 | null 101 | 102 | groupLastChanges: -> 103 | markerSnapshotAfter = null 104 | markerSnapshotBefore = null 105 | patchesSinceCheckpoint = [] 106 | 107 | for entry, i in @undoStack by -1 108 | switch entry.constructor 109 | when Checkpoint 110 | return false if entry.isBarrier 111 | when Transaction 112 | if patchesSinceCheckpoint.length is 0 113 | markerSnapshotAfter = entry.markerSnapshotAfter 114 | else if patchesSinceCheckpoint.length is 1 115 | markerSnapshotBefore = entry.markerSnapshotBefore 116 | patchesSinceCheckpoint.unshift(entry.patch) 117 | when Patch 118 | patchesSinceCheckpoint.unshift(entry) 119 | else 120 | throw new Error("Unexpected undo stack entry type: #{entry.constructor.name}") 121 | 122 | if patchesSinceCheckpoint.length is 2 123 | composedPatch = Patch.compose(patchesSinceCheckpoint) 124 | @undoStack.splice(i) 125 | @undoStack.push(new Transaction(markerSnapshotBefore, composedPatch, markerSnapshotAfter)) 126 | return true 127 | return 128 | 129 | enforceUndoStackSizeLimit: -> 130 | if @undoStack.length > @maxUndoEntries 131 | @undoStack.splice(0, @undoStack.length - @maxUndoEntries) 132 | 133 | applyGroupingInterval: (groupingInterval) -> 134 | topEntry = @undoStack[@undoStack.length - 1] 135 | previousEntry = @undoStack[@undoStack.length - 2] 136 | 137 | if topEntry instanceof Transaction 138 | topEntry.groupingInterval = groupingInterval 139 | else 140 | return 141 | 142 | return if groupingInterval is 0 143 | 144 | if previousEntry instanceof Transaction and topEntry.shouldGroupWith(previousEntry) 145 | @undoStack.splice(@undoStack.length - 2, 2, topEntry.groupWith(previousEntry)) 146 | 147 | pushChange: ({newStart, oldExtent, newExtent, oldText, newText}) -> 148 | patch = new Patch 149 | patch.splice(newStart, oldExtent, newExtent, oldText, newText) 150 | @pushPatch(patch) 151 | 152 | pushPatch: (patch) -> 153 | @undoStack.push(patch) 154 | @clearRedoStack() 155 | 156 | undo: -> 157 | snapshotBelow = null 158 | patch = null 159 | spliceIndex = null 160 | 161 | for entry, i in @undoStack by -1 162 | break if spliceIndex? 163 | 164 | switch entry.constructor 165 | when Checkpoint 166 | if entry.isBarrier 167 | return false 168 | when Transaction 169 | snapshotBelow = entry.markerSnapshotBefore 170 | patch = entry.patch.invert() 171 | spliceIndex = i 172 | when Patch 173 | patch = entry.invert() 174 | spliceIndex = i 175 | else 176 | throw new Error("Unexpected entry type when popping undoStack: #{entry.constructor.name}") 177 | 178 | if spliceIndex? 179 | @redoStack.push(@undoStack.splice(spliceIndex).reverse()...) 180 | { 181 | textUpdates: patch.getChanges() 182 | markers: snapshotBelow 183 | } 184 | else 185 | false 186 | 187 | redo: -> 188 | snapshotBelow = null 189 | patch = null 190 | spliceIndex = null 191 | 192 | for entry, i in @redoStack by -1 193 | break if spliceIndex? 194 | 195 | switch entry.constructor 196 | when Checkpoint 197 | if entry.isBarrier 198 | throw new Error("Invalid redo stack state") 199 | when Transaction 200 | snapshotBelow = entry.markerSnapshotAfter 201 | patch = entry.patch 202 | spliceIndex = i 203 | when Patch 204 | patch = entry 205 | spliceIndex = i 206 | else 207 | throw new Error("Unexpected entry type when popping redoStack: #{entry.constructor.name}") 208 | 209 | while @redoStack[spliceIndex - 1] instanceof Checkpoint 210 | spliceIndex-- 211 | 212 | if spliceIndex? 213 | @undoStack.push(@redoStack.splice(spliceIndex).reverse()...) 214 | { 215 | textUpdates: patch.getChanges() 216 | markers: snapshotBelow 217 | } 218 | else 219 | false 220 | 221 | revertToCheckpoint: (checkpointId) -> 222 | snapshotBelow = null 223 | spliceIndex = null 224 | patchesSinceCheckpoint = [] 225 | 226 | for entry, i in @undoStack by -1 227 | break if spliceIndex? 228 | 229 | switch entry.constructor 230 | when Checkpoint 231 | if entry.id is checkpointId 232 | snapshotBelow = entry.snapshot 233 | spliceIndex = i 234 | else if entry.isBarrier 235 | return false 236 | when Transaction 237 | patchesSinceCheckpoint.push(entry.patch.invert()) 238 | else 239 | patchesSinceCheckpoint.push(entry.invert()) 240 | 241 | if spliceIndex? 242 | @undoStack.splice(spliceIndex) 243 | { 244 | textUpdates: Patch.compose(patchesSinceCheckpoint).getChanges() 245 | markers: snapshotBelow 246 | } 247 | else 248 | false 249 | 250 | clear: -> 251 | @clearUndoStack() 252 | @clearRedoStack() 253 | 254 | clearUndoStack: -> 255 | @undoStack.length = 0 256 | 257 | clearRedoStack: -> 258 | @redoStack.length = 0 259 | 260 | toString: -> 261 | output = '' 262 | for entry in @undoStack 263 | switch entry.constructor 264 | when Checkpoint 265 | output += "Checkpoint, " 266 | when Transaction 267 | output += "Transaction, " 268 | when Patch 269 | output += "Patch, " 270 | else 271 | output += "Unknown {#{JSON.stringify(entry)}}, " 272 | '[' + output.slice(0, -2) + ']' 273 | 274 | serialize: (options) -> 275 | version: SerializationVersion 276 | nextCheckpointId: @nextCheckpointId 277 | undoStack: @serializeStack(@undoStack, options) 278 | redoStack: @serializeStack(@redoStack, options) 279 | maxUndoEntries: @maxUndoEntries 280 | 281 | deserialize: (state) -> 282 | return unless state.version is SerializationVersion 283 | @nextCheckpointId = state.nextCheckpointId 284 | @maxUndoEntries = state.maxUndoEntries 285 | @undoStack = @deserializeStack(state.undoStack) 286 | @redoStack = @deserializeStack(state.redoStack) 287 | 288 | getSnapshot: (maxEntries) -> 289 | undoStackPatches = [] 290 | undoStack = [] 291 | for entry in @undoStack by -1 292 | switch entry.constructor 293 | when Checkpoint 294 | undoStack.unshift(snapshotFromCheckpoint(entry)) 295 | when Transaction 296 | undoStack.unshift(snapshotFromTransaction(entry)) 297 | undoStackPatches.unshift(entry.patch) 298 | 299 | break if undoStack.length is maxEntries 300 | 301 | redoStack = [] 302 | for entry in @redoStack by -1 303 | switch entry.constructor 304 | when Checkpoint 305 | redoStack.unshift(snapshotFromCheckpoint(entry)) 306 | when Transaction 307 | redoStack.unshift(snapshotFromTransaction(entry)) 308 | 309 | break if redoStack.length is maxEntries 310 | 311 | { 312 | nextCheckpointId: @nextCheckpointId, 313 | undoStackChanges: Patch.compose(undoStackPatches).getChanges(), 314 | undoStack, 315 | redoStack 316 | } 317 | 318 | restoreFromSnapshot: ({@nextCheckpointId, undoStack, redoStack}) -> 319 | @undoStack = undoStack.map (entry) -> 320 | switch entry.type 321 | when 'transaction' 322 | transactionFromSnapshot(entry) 323 | when 'checkpoint' 324 | checkpointFromSnapshot(entry) 325 | 326 | @redoStack = redoStack.map (entry) -> 327 | switch entry.type 328 | when 'transaction' 329 | transactionFromSnapshot(entry) 330 | when 'checkpoint' 331 | checkpointFromSnapshot(entry) 332 | 333 | ### 334 | Section: Private 335 | ### 336 | 337 | getCheckpointIndex: (checkpointId) -> 338 | for entry, i in @undoStack by -1 339 | if entry instanceof Checkpoint and entry.id is checkpointId 340 | return i 341 | return null 342 | 343 | serializeStack: (stack, options) -> 344 | for entry in stack 345 | switch entry.constructor 346 | when Checkpoint 347 | { 348 | type: 'checkpoint' 349 | id: entry.id 350 | snapshot: @serializeSnapshot(entry.snapshot, options) 351 | isBarrier: entry.isBarrier 352 | } 353 | when Transaction 354 | { 355 | type: 'transaction' 356 | markerSnapshotBefore: @serializeSnapshot(entry.markerSnapshotBefore, options) 357 | markerSnapshotAfter: @serializeSnapshot(entry.markerSnapshotAfter, options) 358 | patch: entry.patch.serialize().toString('base64') 359 | } 360 | when Patch 361 | { 362 | type: 'patch' 363 | data: entry.serialize().toString('base64') 364 | } 365 | else 366 | throw new Error("Unexpected undoStack entry type during serialization: #{entry.constructor.name}") 367 | 368 | deserializeStack: (stack) -> 369 | for entry in stack 370 | switch entry.type 371 | when 'checkpoint' 372 | new Checkpoint( 373 | entry.id 374 | MarkerLayer.deserializeSnapshot(entry.snapshot) 375 | entry.isBarrier 376 | ) 377 | when 'transaction' 378 | new Transaction( 379 | MarkerLayer.deserializeSnapshot(entry.markerSnapshotBefore) 380 | Patch.deserialize(Buffer.from(entry.patch, 'base64')) 381 | MarkerLayer.deserializeSnapshot(entry.markerSnapshotAfter) 382 | ) 383 | when 'patch' 384 | Patch.deserialize(Buffer.from(entry.data, 'base64')) 385 | else 386 | throw new Error("Unexpected undoStack entry type during deserialization: #{entry.type}") 387 | 388 | serializeSnapshot: (snapshot, options) -> 389 | return unless options.markerLayers 390 | 391 | serializedLayerSnapshots = {} 392 | for layerId, layerSnapshot of snapshot 393 | continue unless @buffer.getMarkerLayer(layerId)?.persistent 394 | serializedMarkerSnapshots = {} 395 | for markerId, markerSnapshot of layerSnapshot 396 | serializedMarkerSnapshot = Object.assign({}, markerSnapshot) 397 | delete serializedMarkerSnapshot.marker 398 | serializedMarkerSnapshots[markerId] = serializedMarkerSnapshot 399 | serializedLayerSnapshots[layerId] = serializedMarkerSnapshots 400 | serializedLayerSnapshots 401 | 402 | snapshotFromCheckpoint = (checkpoint) -> 403 | { 404 | type: 'checkpoint', 405 | id: checkpoint.id, 406 | markers: checkpoint.snapshot 407 | } 408 | 409 | checkpointFromSnapshot = ({id, markers}) -> 410 | new Checkpoint(id, markers, false) 411 | 412 | snapshotFromTransaction = (transaction) -> 413 | changes = [] 414 | for change in transaction.patch.getChanges() by 1 415 | changes.push({ 416 | oldStart: change.oldStart, 417 | oldEnd: change.oldEnd, 418 | newStart: change.newStart, 419 | newEnd: change.newEnd, 420 | oldText: change.oldText, 421 | newText: change.newText 422 | }) 423 | 424 | { 425 | type: 'transaction', 426 | changes, 427 | markersBefore: transaction.markerSnapshotBefore 428 | markersAfter: transaction.markerSnapshotAfter 429 | } 430 | 431 | transactionFromSnapshot = ({changes, markersBefore, markersAfter}) -> 432 | # TODO: Return raw patch if there's no markersBefore && markersAfter 433 | new Transaction(markersBefore, patchFromChanges(changes), markersAfter) 434 | -------------------------------------------------------------------------------- /spec/marker-layer-spec.coffee: -------------------------------------------------------------------------------- 1 | {uniq, times} = require 'underscore-plus' 2 | TextBuffer = require '../src/text-buffer' 3 | 4 | describe "MarkerLayer", -> 5 | [buffer, layer1, layer2] = [] 6 | 7 | beforeEach -> 8 | jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) 9 | buffer = new TextBuffer(text: "abcdefghijklmnopqrstuvwxyz") 10 | layer1 = buffer.addMarkerLayer() 11 | layer2 = buffer.addMarkerLayer() 12 | 13 | it "ensures that marker ids are unique across layers", -> 14 | times 5, -> 15 | buffer.markRange([[0, 3], [0, 6]]) 16 | layer1.markRange([[0, 4], [0, 7]]) 17 | layer2.markRange([[0, 5], [0, 8]]) 18 | 19 | ids = buffer.getMarkers() 20 | .concat(layer1.getMarkers()) 21 | .concat(layer2.getMarkers()) 22 | .map (marker) -> marker.id 23 | 24 | expect(uniq(ids).length).toEqual ids.length 25 | 26 | it "updates each layer's markers when the text changes", -> 27 | defaultMarker = buffer.markRange([[0, 3], [0, 6]]) 28 | layer1Marker = layer1.markRange([[0, 4], [0, 7]]) 29 | layer2Marker = layer2.markRange([[0, 5], [0, 8]]) 30 | 31 | buffer.setTextInRange([[0, 1], [0, 2]], "BBB") 32 | expect(defaultMarker.getRange()).toEqual [[0, 5], [0, 8]] 33 | expect(layer1Marker.getRange()).toEqual [[0, 6], [0, 9]] 34 | expect(layer2Marker.getRange()).toEqual [[0, 7], [0, 10]] 35 | 36 | layer2.destroy() 37 | expect(layer2.isAlive()).toBe false 38 | expect(layer2.isDestroyed()).toBe true 39 | 40 | expect(layer1.isAlive()).toBe true 41 | expect(layer1.isDestroyed()).toBe false 42 | 43 | buffer.undo() 44 | expect(defaultMarker.getRange()).toEqual [[0, 3], [0, 6]] 45 | expect(layer1Marker.getRange()).toEqual [[0, 4], [0, 7]] 46 | 47 | expect(layer2Marker.isDestroyed()).toBe true 48 | expect(layer2Marker.getRange()).toEqual [[0, 0], [0, 0]] 49 | 50 | it "emits onDidCreateMarker events synchronously when markers are created", -> 51 | createdMarkers = [] 52 | layer1.onDidCreateMarker (marker) -> createdMarkers.push(marker) 53 | marker = layer1.markRange([[0, 1], [2, 3]]) 54 | expect(createdMarkers).toEqual [marker] 55 | 56 | it "does not emit marker events on the TextBuffer for non-default layers", -> 57 | createEventCount = updateEventCount = 0 58 | buffer.onDidCreateMarker -> createEventCount++ 59 | buffer.onDidUpdateMarkers -> updateEventCount++ 60 | 61 | marker1 = buffer.markRange([[0, 1], [0, 2]]) 62 | marker1.setRange([[0, 1], [0, 3]]) 63 | 64 | expect(createEventCount).toBe 1 65 | expect(updateEventCount).toBe 2 66 | 67 | marker2 = layer1.markRange([[0, 1], [0, 2]]) 68 | marker2.setRange([[0, 1], [0, 3]]) 69 | 70 | expect(createEventCount).toBe 1 71 | expect(updateEventCount).toBe 2 72 | 73 | describe "when destroyInvalidatedMarkers is enabled for the layer", -> 74 | it "destroys markers when they are invalidated via a splice", -> 75 | layer3 = buffer.addMarkerLayer(destroyInvalidatedMarkers: true) 76 | 77 | marker1 = layer3.markRange([[0, 0], [0, 3]], invalidate: 'inside') 78 | marker2 = layer3.markRange([[0, 2], [0, 6]], invalidate: 'inside') 79 | 80 | destroyedMarkers = [] 81 | marker1.onDidDestroy -> destroyedMarkers.push(marker1) 82 | marker2.onDidDestroy -> destroyedMarkers.push(marker2) 83 | 84 | buffer.insert([0, 5], 'x') 85 | 86 | expect(destroyedMarkers).toEqual [marker2] 87 | expect(marker2.isDestroyed()).toBe true 88 | expect(marker1.isDestroyed()).toBe false 89 | 90 | describe "when maintainHistory is enabled for the layer", -> 91 | layer3 = null 92 | 93 | beforeEach -> 94 | layer3 = buffer.addMarkerLayer(maintainHistory: true) 95 | 96 | it "restores the state of all markers in the layer on undo and redo", -> 97 | buffer.setText('') 98 | buffer.transact -> buffer.append('foo') 99 | layer3 = buffer.addMarkerLayer(maintainHistory: true) 100 | 101 | marker1 = layer3.markRange([[0, 0], [0, 0]], a: 'b', invalidate: 'never') 102 | marker2 = layer3.markRange([[0, 0], [0, 0]], c: 'd', invalidate: 'never') 103 | 104 | marker2ChangeCount = 0 105 | marker2.onDidChange -> marker2ChangeCount++ 106 | 107 | buffer.transact -> 108 | buffer.append('\n') 109 | buffer.append('bar') 110 | 111 | marker1.destroy() 112 | marker2.setRange([[0, 2], [0, 3]]) 113 | marker3 = layer3.markRange([[0, 0], [0, 3]], e: 'f', invalidate: 'never') 114 | marker4 = layer3.markRange([[1, 0], [1, 3]], g: 'h', invalidate: 'never') 115 | expect(marker2ChangeCount).toBe(1) 116 | 117 | createdMarker = null 118 | layer3.onDidCreateMarker((m) -> createdMarker = m) 119 | buffer.undo() 120 | 121 | expect(buffer.getText()).toBe 'foo' 122 | expect(marker1.isDestroyed()).toBe false 123 | expect(createdMarker).toBe(marker1) 124 | markers = layer3.findMarkers({}) 125 | expect(markers.length).toBe 2 126 | expect(markers[0]).toBe marker1 127 | expect(markers[0].getProperties()).toEqual {a: 'b'} 128 | expect(markers[0].getRange()).toEqual [[0, 0], [0, 0]] 129 | expect(markers[1].getProperties()).toEqual {c: 'd'} 130 | expect(markers[1].getRange()).toEqual [[0, 0], [0, 0]] 131 | expect(marker2ChangeCount).toBe(2) 132 | 133 | buffer.redo() 134 | 135 | expect(buffer.getText()).toBe 'foo\nbar' 136 | markers = layer3.findMarkers({}) 137 | expect(markers.length).toBe 3 138 | expect(markers[0].getProperties()).toEqual {e: 'f'} 139 | expect(markers[0].getRange()).toEqual [[0, 0], [0, 3]] 140 | expect(markers[1].getProperties()).toEqual {c: 'd'} 141 | expect(markers[1].getRange()).toEqual [[0, 2], [0, 3]] 142 | expect(markers[2].getProperties()).toEqual {g: 'h'} 143 | expect(markers[2].getRange()).toEqual [[1, 0], [1, 3]] 144 | 145 | it "does not undo marker manipulations that aren't associated with text changes", -> 146 | marker = layer3.markRange([[0, 6], [0, 9]]) 147 | 148 | # Can't undo changes in a transaction without other buffer changes 149 | buffer.transact -> marker.setRange([[0, 4], [0, 20]]) 150 | buffer.undo() 151 | expect(marker.getRange()).toEqual [[0, 4], [0, 20]] 152 | 153 | # Can undo changes in a transaction with other buffer changes 154 | buffer.transact -> 155 | marker.setRange([[0, 5], [0, 9]]) 156 | buffer.setTextInRange([[0, 2], [0, 3]], 'XYZ') 157 | marker.setRange([[0, 8], [0, 12]]) 158 | 159 | buffer.undo() 160 | expect(marker.getRange()).toEqual [[0, 4], [0, 20]] 161 | 162 | buffer.redo() 163 | expect(marker.getRange()).toEqual [[0, 8], [0, 12]] 164 | 165 | it "ignores snapshot references to marker layers that no longer exist", -> 166 | layer3.markRange([[0, 6], [0, 9]]) 167 | buffer.append("stuff") 168 | layer3.destroy() 169 | 170 | # Should not throw an exception 171 | buffer.undo() 172 | 173 | describe "when a role is provided for the layer", -> 174 | it "getRole() returns its role and keeps track of ids of 'selections' role", -> 175 | expect(buffer.selectionsMarkerLayerIds.size).toBe 0 176 | 177 | selectionsMarkerLayer1 = buffer.addMarkerLayer(role: "selections") 178 | expect(selectionsMarkerLayer1.getRole()).toBe "selections" 179 | 180 | expect(buffer.addMarkerLayer(role: "role-1").getRole()).toBe "role-1" 181 | expect(buffer.addMarkerLayer().getRole()).toBe undefined 182 | 183 | expect(buffer.selectionsMarkerLayerIds.size).toBe 1 184 | expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe true 185 | 186 | selectionsMarkerLayer2 = buffer.addMarkerLayer(role: "selections") 187 | expect(selectionsMarkerLayer2.getRole()).toBe "selections" 188 | 189 | expect(buffer.selectionsMarkerLayerIds.size).toBe 2 190 | expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe true 191 | 192 | selectionsMarkerLayer1.destroy() 193 | selectionsMarkerLayer2.destroy() 194 | expect(buffer.selectionsMarkerLayerIds.size).toBe 2 195 | expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer1.id)).toBe true 196 | expect(buffer.selectionsMarkerLayerIds.has(selectionsMarkerLayer2.id)).toBe true 197 | 198 | describe "::findMarkers(params)", -> 199 | it "does not find markers from other layers", -> 200 | defaultMarker = buffer.markRange([[0, 3], [0, 6]]) 201 | layer1Marker = layer1.markRange([[0, 3], [0, 6]]) 202 | layer2Marker = layer2.markRange([[0, 3], [0, 6]]) 203 | 204 | expect(buffer.findMarkers(containsPoint: [0, 4])).toEqual [defaultMarker] 205 | expect(layer1.findMarkers(containsPoint: [0, 4])).toEqual [layer1Marker] 206 | expect(layer2.findMarkers(containsPoint: [0, 4])).toEqual [layer2Marker] 207 | 208 | describe "::onDidUpdate", -> 209 | it "notifies observers at the end of the outermost transaction when markers are created, updated, or destroyed", -> 210 | [marker1, marker2] = [] 211 | 212 | displayLayer = buffer.addDisplayLayer() 213 | displayLayerDidChange = false 214 | 215 | changeCount = 0 216 | buffer.onDidChange -> 217 | changeCount++ 218 | 219 | updateCount = 0 220 | layer1.onDidUpdate -> 221 | updateCount++ 222 | if updateCount is 1 223 | expect(changeCount).toBe(0) 224 | buffer.transact -> 225 | marker1.setRange([[1, 2], [3, 4]]) 226 | marker2.setRange([[4, 5], [6, 7]]) 227 | else if updateCount is 2 228 | expect(changeCount).toBe(0) 229 | buffer.transact -> 230 | buffer.insert([0, 1], "xxx") 231 | buffer.insert([0, 1], "yyy") 232 | else if updateCount is 3 233 | expect(changeCount).toBe(1) 234 | marker1.destroy() 235 | marker2.destroy() 236 | else if updateCount is 7 237 | expect(changeCount).toBe(2) 238 | expect(displayLayerDidChange).toBe(true, 'Display layer was updated after marker layer.') 239 | 240 | buffer.transact -> 241 | buffer.transact -> 242 | marker1 = layer1.markRange([[0, 2], [0, 4]]) 243 | marker2 = layer1.markRange([[0, 6], [0, 8]]) 244 | 245 | expect(updateCount).toBe(5) 246 | 247 | # update events happen immediately when there is no parent transaction 248 | layer1.markRange([[0, 2], [0, 4]]) 249 | expect(updateCount).toBe(6) 250 | 251 | # update events happen after updating display layers when there is no parent transaction. 252 | displayLayer.onDidChange -> 253 | displayLayerDidChange = true 254 | buffer.undo() 255 | expect(updateCount).toBe(7) 256 | 257 | describe "::clear()", -> 258 | it "destroys all of the layer's markers", (done) -> 259 | buffer = new TextBuffer(text: 'abc') 260 | displayLayer = buffer.addDisplayLayer() 261 | markerLayer = buffer.addMarkerLayer() 262 | displayMarkerLayer = displayLayer.getMarkerLayer(markerLayer.id) 263 | marker1 = markerLayer.markRange([[0, 1], [0, 2]]) 264 | marker2 = markerLayer.markRange([[0, 1], [0, 2]]) 265 | marker3 = markerLayer.markRange([[0, 1], [0, 2]]) 266 | displayMarker1 = displayMarkerLayer.getMarker(marker1.id) 267 | # intentionally omit a display marker for marker2 just to cover that case 268 | displayMarker3 = displayMarkerLayer.getMarker(marker3.id) 269 | 270 | marker1DestroyCount = 0 271 | marker2DestroyCount = 0 272 | displayMarker1DestroyCount = 0 273 | displayMarker3DestroyCount = 0 274 | markerLayerUpdateCount = 0 275 | displayMarkerLayerUpdateCount = 0 276 | marker1.onDidDestroy -> marker1DestroyCount++ 277 | marker2.onDidDestroy -> marker2DestroyCount++ 278 | displayMarker1.onDidDestroy -> displayMarker1DestroyCount++ 279 | displayMarker3.onDidDestroy -> displayMarker3DestroyCount++ 280 | markerLayer.onDidUpdate -> 281 | markerLayerUpdateCount++ 282 | done() if markerLayerUpdateCount is 1 and displayMarkerLayerUpdateCount is 1 283 | displayMarkerLayer.onDidUpdate -> 284 | displayMarkerLayerUpdateCount++ 285 | done() if markerLayerUpdateCount is 1 and displayMarkerLayerUpdateCount is 1 286 | 287 | markerLayer.clear() 288 | expect(marker1.isDestroyed()).toBe(true) 289 | expect(marker2.isDestroyed()).toBe(true) 290 | expect(marker3.isDestroyed()).toBe(true) 291 | expect(displayMarker1.isDestroyed()).toBe(true) 292 | expect(displayMarker3.isDestroyed()).toBe(true) 293 | expect(marker1DestroyCount).toBe(1) 294 | expect(marker2DestroyCount).toBe(1) 295 | expect(displayMarker1DestroyCount).toBe(1) 296 | expect(displayMarker3DestroyCount).toBe(1) 297 | expect(markerLayer.getMarkers()).toEqual([]) 298 | expect(displayMarkerLayer.getMarkers()).toEqual([]) 299 | expect(displayMarkerLayer.getMarker(displayMarker3.id)).toBeUndefined() 300 | 301 | describe "::copy", -> 302 | it "creates a new marker layer with markers in the same states", -> 303 | originalLayer = buffer.addMarkerLayer(maintainHistory: true) 304 | originalLayer.markRange([[0, 1], [0, 3]], a: 'b') 305 | originalLayer.markPosition([0, 2]) 306 | 307 | copy = originalLayer.copy() 308 | expect(copy).not.toBe originalLayer 309 | 310 | markers = copy.getMarkers() 311 | expect(markers.length).toBe 2 312 | expect(markers[0].getRange()).toEqual [[0, 1], [0, 3]] 313 | expect(markers[0].getProperties()).toEqual {a: 'b'} 314 | expect(markers[1].getRange()).toEqual [[0, 2], [0, 2]] 315 | expect(markers[1].hasTail()).toBe false 316 | 317 | it "copies the marker layer role", -> 318 | originalLayer = buffer.addMarkerLayer(maintainHistory: true, role: "selections") 319 | copy = originalLayer.copy() 320 | expect(copy).not.toBe originalLayer 321 | expect(copy.getRole()).toBe("selections") 322 | expect(buffer.selectionsMarkerLayerIds.has(originalLayer.id)).toBe true 323 | expect(buffer.selectionsMarkerLayerIds.has(copy.id)).toBe true 324 | expect(buffer.selectionsMarkerLayerIds.size).toBe 2 325 | 326 | describe "::destroy", -> 327 | it "destroys the layer's markers", -> 328 | buffer = new TextBuffer() 329 | markerLayer = buffer.addMarkerLayer() 330 | 331 | marker1 = markerLayer.markRange([[0, 0], [0, 0]]) 332 | marker2 = markerLayer.markRange([[0, 0], [0, 0]]) 333 | 334 | destroyListener = jasmine.createSpy('onDidDestroy listener') 335 | marker1.onDidDestroy(destroyListener) 336 | 337 | markerLayer.destroy() 338 | 339 | expect(destroyListener).toHaveBeenCalled() 340 | expect(marker1.isDestroyed()).toBe(true) 341 | 342 | # Markers states are updated regardless of whether they have an 343 | # ::onDidDestroy listener 344 | expect(marker2.isDestroyed()).toBe(true) 345 | 346 | describe "trackDestructionInOnDidCreateMarkerCallbacks", -> 347 | it "stores a stack trace when destroy is called during onDidCreateMarker callbacks", -> 348 | layer1.onDidCreateMarker (m) -> m.destroy() if destroyInCreateCallback 349 | 350 | layer1.trackDestructionInOnDidCreateMarkerCallbacks = true 351 | destroyInCreateCallback = true 352 | marker1 = layer1.markPosition([0, 0]) 353 | expect(marker1.isDestroyed()).toBe(true) 354 | expect(marker1.destroyStackTrace).toBeDefined() 355 | 356 | destroyInCreateCallback = false 357 | marker2 = layer1.markPosition([0, 0]) 358 | expect(marker2.isDestroyed()).toBe(false) 359 | expect(marker2.destroyStackTrace).toBeUndefined() 360 | marker2.destroy() 361 | expect(marker2.isDestroyed()).toBe(true) 362 | expect(marker2.destroyStackTrace).toBeUndefined() 363 | 364 | destroyInCreateCallback = true 365 | layer1.trackDestructionInOnDidCreateMarkerCallbacks = false 366 | marker3 = layer1.markPosition([0, 0]) 367 | expect(marker3.isDestroyed()).toBe(true) 368 | expect(marker3.destroyStackTrace).toBeUndefined() 369 | -------------------------------------------------------------------------------- /src/screen-line-builder.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-labels */ 2 | 3 | const Point = require('./point') 4 | 5 | const HARD_TAB = 1 << 0 6 | const LEADING_WHITESPACE = 1 << 2 7 | const TRAILING_WHITESPACE = 1 << 3 8 | const INVISIBLE_CHARACTER = 1 << 4 9 | const INDENT_GUIDE = 1 << 5 10 | const LINE_ENDING = 1 << 6 11 | const FOLD = 1 << 7 12 | 13 | let nextScreenLineId = 1 14 | 15 | module.exports = 16 | class ScreenLineBuilder { 17 | constructor (displayLayer) { 18 | this.displayLayer = displayLayer 19 | } 20 | 21 | buildScreenLines (startScreenRow, endScreenRow) { 22 | this.requestedStartScreenRow = startScreenRow 23 | this.requestedEndScreenRow = endScreenRow 24 | this.displayLayer.populateSpatialIndexIfNeeded(this.displayLayer.buffer.getLineCount(), endScreenRow) 25 | 26 | this.bufferPosition = { 27 | row: this.displayLayer.findBoundaryPrecedingBufferRow( 28 | this.displayLayer.translateScreenPositionWithSpatialIndex(Point(startScreenRow, 0)).row 29 | ), 30 | column: 0 31 | } 32 | 33 | this.screenRow = this.displayLayer.translateBufferPositionWithSpatialIndex(Point(this.bufferPosition.row, 0)).row 34 | 35 | const endBufferRow = this.displayLayer.translateScreenPositionWithSpatialIndex(Point(endScreenRow, Infinity)).row 36 | 37 | let didSeekDecorationIterator = false 38 | const decorationIterator = this.displayLayer.buffer.languageMode.buildHighlightIterator() 39 | const hunks = this.displayLayer.spatialIndex.getChangesInNewRange(Point(this.screenRow, 0), Point(endScreenRow, 0)) 40 | let hunkIndex = 0 41 | 42 | this.containingScopeIds = [] 43 | this.scopeIdsToReopen = [] 44 | this.screenLines = [] 45 | this.bufferPosition.column = 0 46 | this.beginLine() 47 | 48 | // Loop through all characters spanning the given screen row range, building 49 | // up screen lines based on the contents of the spatial index and the 50 | // buffer. 51 | screenRowLoop: 52 | while (this.screenRow < endScreenRow) { 53 | var cachedScreenLine = this.displayLayer.cachedScreenLines[this.screenRow] 54 | if (cachedScreenLine) { 55 | this.pushScreenLine(cachedScreenLine) 56 | 57 | let nextHunk = hunks[hunkIndex] 58 | while (nextHunk && nextHunk.newStart.row <= this.screenRow) { 59 | if (nextHunk.newStart.row === this.screenRow) { 60 | if (nextHunk.newEnd.row > nextHunk.newStart.row) { 61 | this.screenRow++ 62 | this.bufferPosition.column = nextHunk.oldEnd.column 63 | hunkIndex++ 64 | continue screenRowLoop 65 | } else { 66 | this.bufferPosition.row = nextHunk.oldEnd.row 67 | this.bufferPosition.column = nextHunk.oldEnd.column 68 | } 69 | } 70 | 71 | hunkIndex++ 72 | nextHunk = hunks[hunkIndex] 73 | } 74 | 75 | this.screenRow++ 76 | this.screenColumn = 0 77 | this.bufferPosition.row++ 78 | this.bufferPosition.column = 0 79 | continue 80 | } 81 | 82 | this.currentBuiltInClassNameFlags = 0 83 | this.bufferLineLength = this.displayLayer.buffer.lineLengthForRow(this.bufferPosition.row) 84 | 85 | if (this.bufferPosition.row > this.displayLayer.buffer.getLastRow()) break 86 | this.trailingWhitespaceStartColumn = this.displayLayer.findTrailingWhitespaceStartColumn(this.bufferPosition.row) 87 | this.inLeadingWhitespace = true 88 | this.inTrailingWhitespace = false 89 | 90 | if (!didSeekDecorationIterator || this.compareBufferPosition(decorationIterator.getPosition()) > 0) { 91 | didSeekDecorationIterator = true 92 | this.scopeIdsToReopen = decorationIterator.seek(this.bufferPosition, endBufferRow) || [] 93 | } 94 | 95 | var prevCachedScreenLine = this.displayLayer.cachedScreenLines[this.screenRow - 1] 96 | if (prevCachedScreenLine && prevCachedScreenLine.softWrapIndent >= 0) { 97 | this.inLeadingWhitespace = false 98 | if (prevCachedScreenLine.softWrapIndent > 0) this.emitIndentWhitespace(prevCachedScreenLine.softWrapIndent) 99 | } 100 | 101 | // This loop may visit multiple buffer rows if there are folds and 102 | // multiple screen rows if there are soft wraps. 103 | while (this.bufferPosition.column <= this.bufferLineLength) { 104 | // Handle folds or soft wraps at the current position. 105 | var nextHunk = hunks[hunkIndex] 106 | while (nextHunk && nextHunk.oldStart.row === this.bufferPosition.row && nextHunk.oldStart.column === this.bufferPosition.column) { 107 | if (this.displayLayer.isSoftWrapHunk(nextHunk)) { 108 | this.emitSoftWrap(nextHunk) 109 | if (this.screenRow === endScreenRow) { 110 | break screenRowLoop 111 | } 112 | } else { 113 | this.emitFold(nextHunk, decorationIterator, endBufferRow) 114 | } 115 | 116 | hunkIndex++ 117 | nextHunk = hunks[hunkIndex] 118 | } 119 | 120 | var nextCharacter = this.displayLayer.buffer.getCharacterAtPosition(this.bufferPosition) 121 | if (this.bufferPosition.column >= this.trailingWhitespaceStartColumn) { 122 | this.inTrailingWhitespace = true 123 | this.inLeadingWhitespace = false 124 | } else if (nextCharacter !== ' ' && nextCharacter !== '\t') { 125 | this.inLeadingWhitespace = false 126 | } 127 | 128 | // Compute a token flags describing built-in decorations for the token 129 | // containing the next character 130 | var previousBuiltInTagFlags = this.currentBuiltInClassNameFlags 131 | this.updateCurrentTokenFlags(nextCharacter) 132 | 133 | if (this.emitBuiltInTagBoundary) { 134 | this.emitCloseTag(this.getBuiltInScopeId(previousBuiltInTagFlags)) 135 | } 136 | 137 | this.emitDecorationBoundaries(decorationIterator) 138 | 139 | // Are we at the end of the line? 140 | if (this.bufferPosition.column === this.bufferLineLength) { 141 | this.emitLineEnding() 142 | break 143 | } 144 | 145 | if (this.emitBuiltInTagBoundary) { 146 | this.emitOpenTag(this.getBuiltInScopeId(this.currentBuiltInClassNameFlags)) 147 | } 148 | 149 | // Emit the next character, handling hard tabs whitespace invisibles 150 | // specially. 151 | if (nextCharacter === '\t') { 152 | this.emitHardTab() 153 | } else if ((this.inLeadingWhitespace || this.inTrailingWhitespace) && 154 | nextCharacter === ' ' && this.displayLayer.invisibles.space) { 155 | this.emitText(this.displayLayer.invisibles.space) 156 | } else { 157 | this.emitText(nextCharacter) 158 | } 159 | this.bufferPosition.column++ 160 | } 161 | } 162 | 163 | return this.screenLines 164 | } 165 | 166 | getBuiltInScopeId (flags) { 167 | if (flags === 0) return 0 168 | 169 | let scopeId = this.displayLayer.getBuiltInScopeId(flags) 170 | if (scopeId === -1) { 171 | let className = '' 172 | if (flags & INVISIBLE_CHARACTER) className += 'invisible-character ' 173 | if (flags & HARD_TAB) className += 'hard-tab ' 174 | if (flags & LEADING_WHITESPACE) className += 'leading-whitespace ' 175 | if (flags & TRAILING_WHITESPACE) className += 'trailing-whitespace ' 176 | if (flags & LINE_ENDING) className += 'eol ' 177 | if (flags & INDENT_GUIDE) className += 'indent-guide ' 178 | if (flags & FOLD) className += 'fold-marker ' 179 | className = className.trim() 180 | scopeId = this.displayLayer.registerBuiltInScope(flags, className) 181 | } 182 | return scopeId 183 | } 184 | 185 | beginLine () { 186 | this.currentScreenLineText = '' 187 | this.currentScreenLineTags = [] 188 | this.screenColumn = 0 189 | this.currentTokenLength = 0 190 | } 191 | 192 | updateCurrentTokenFlags (nextCharacter) { 193 | const previousBuiltInTagFlags = this.currentBuiltInClassNameFlags 194 | this.currentBuiltInClassNameFlags = 0 195 | this.emitBuiltInTagBoundary = false 196 | 197 | if (nextCharacter === ' ' || nextCharacter === '\t') { 198 | const showIndentGuides = this.displayLayer.showIndentGuides && (this.inLeadingWhitespace || this.trailingWhitespaceStartColumn === 0) 199 | if (this.inLeadingWhitespace) this.currentBuiltInClassNameFlags |= LEADING_WHITESPACE 200 | if (this.inTrailingWhitespace) this.currentBuiltInClassNameFlags |= TRAILING_WHITESPACE 201 | 202 | if (nextCharacter === ' ') { 203 | if ((this.inLeadingWhitespace || this.inTrailingWhitespace) && this.displayLayer.invisibles.space) { 204 | this.currentBuiltInClassNameFlags |= INVISIBLE_CHARACTER 205 | } 206 | 207 | if (showIndentGuides) { 208 | this.currentBuiltInClassNameFlags |= INDENT_GUIDE 209 | if (this.screenColumn % this.displayLayer.tabLength === 0) this.emitBuiltInTagBoundary = true 210 | } 211 | } else { // nextCharacter === \t 212 | this.currentBuiltInClassNameFlags |= HARD_TAB 213 | if (this.displayLayer.invisibles.tab) this.currentBuiltInClassNameFlags |= INVISIBLE_CHARACTER 214 | if (showIndentGuides && this.screenColumn % this.displayLayer.tabLength === 0) { 215 | this.currentBuiltInClassNameFlags |= INDENT_GUIDE 216 | } 217 | 218 | this.emitBuiltInTagBoundary = true 219 | } 220 | } 221 | 222 | if (!this.emitBuiltInTagBoundary) { 223 | this.emitBuiltInTagBoundary = this.currentBuiltInClassNameFlags !== previousBuiltInTagFlags 224 | } 225 | } 226 | 227 | emitDecorationBoundaries (decorationIterator) { 228 | while (this.compareBufferPosition(decorationIterator.getPosition()) === 0) { 229 | var closeScopeIds = decorationIterator.getCloseScopeIds() 230 | for (let i = 0, n = closeScopeIds.length; i < n; i++) { 231 | this.emitCloseTag(closeScopeIds[i]) 232 | } 233 | 234 | var openScopeIds = decorationIterator.getOpenScopeIds() 235 | for (let i = 0, n = openScopeIds.length; i < n; i++) { 236 | this.emitOpenTag(openScopeIds[i]) 237 | } 238 | 239 | decorationIterator.moveToSuccessor() 240 | } 241 | } 242 | 243 | emitFold (nextHunk, decorationIterator, endBufferRow) { 244 | this.emitCloseTag(this.getBuiltInScopeId(this.currentBuiltInClassNameFlags)) 245 | this.currentBuiltInClassNameFlags = 0 246 | 247 | this.closeContainingScopes() 248 | this.scopeIdsToReopen.length = 0 249 | 250 | this.emitOpenTag(this.getBuiltInScopeId(FOLD)) 251 | this.emitText(this.displayLayer.foldCharacter) 252 | this.emitCloseTag(this.getBuiltInScopeId(FOLD)) 253 | 254 | this.bufferPosition.row = nextHunk.oldEnd.row 255 | this.bufferPosition.column = nextHunk.oldEnd.column 256 | 257 | this.scopeIdsToReopen = decorationIterator.seek(this.bufferPosition, endBufferRow) 258 | 259 | this.bufferLineLength = this.displayLayer.buffer.lineLengthForRow(this.bufferPosition.row) 260 | this.trailingWhitespaceStartColumn = this.displayLayer.findTrailingWhitespaceStartColumn(this.bufferPosition.row) 261 | } 262 | 263 | emitSoftWrap (nextHunk) { 264 | this.emitCloseTag(this.getBuiltInScopeId(this.currentBuiltInClassNameFlags)) 265 | this.currentBuiltInClassNameFlags = 0 266 | this.closeContainingScopes() 267 | this.emitNewline(nextHunk.newEnd.column) 268 | this.emitIndentWhitespace(nextHunk.newEnd.column) 269 | } 270 | 271 | emitLineEnding () { 272 | this.emitCloseTag(this.getBuiltInScopeId(this.currentBuiltInClassNameFlags)) 273 | 274 | let lineEnding = this.displayLayer.buffer.lineEndingForRow(this.bufferPosition.row) 275 | const eolInvisible = this.displayLayer.eolInvisibles[lineEnding] 276 | if (eolInvisible) { 277 | let eolFlags = INVISIBLE_CHARACTER | LINE_ENDING 278 | if (this.bufferLineLength === 0 && this.displayLayer.showIndentGuides) eolFlags |= INDENT_GUIDE 279 | this.emitOpenTag(this.getBuiltInScopeId(eolFlags)) 280 | this.emitText(eolInvisible, false) 281 | this.emitCloseTag(this.getBuiltInScopeId(eolFlags)) 282 | } 283 | 284 | if (this.bufferLineLength === 0 && this.displayLayer.showIndentGuides) { 285 | let whitespaceLength = this.displayLayer.leadingWhitespaceLengthForSurroundingLines(this.bufferPosition.row) 286 | this.emitIndentWhitespace(whitespaceLength) 287 | } 288 | 289 | this.closeContainingScopes() 290 | 291 | // Ensure empty lines have at least one empty token to make it easier on 292 | // the caller 293 | if (this.currentScreenLineTags.length === 0) this.currentScreenLineTags.push(0) 294 | this.emitNewline() 295 | this.bufferPosition.row++ 296 | this.bufferPosition.column = 0 297 | } 298 | 299 | emitNewline (softWrapIndent = -1) { 300 | const screenLine = { 301 | id: nextScreenLineId++, 302 | lineText: this.currentScreenLineText, 303 | tags: this.currentScreenLineTags, 304 | softWrapIndent 305 | } 306 | this.pushScreenLine(screenLine) 307 | this.displayLayer.cachedScreenLines[this.screenRow] = screenLine 308 | this.screenRow++ 309 | this.beginLine() 310 | } 311 | 312 | emitIndentWhitespace (endColumn) { 313 | if (this.displayLayer.showIndentGuides) { 314 | let openedIndentGuide = false 315 | while (this.screenColumn < endColumn) { 316 | if (this.screenColumn % this.displayLayer.tabLength === 0) { 317 | if (openedIndentGuide) { 318 | this.emitCloseTag(this.getBuiltInScopeId(INDENT_GUIDE)) 319 | } 320 | 321 | this.emitOpenTag(this.getBuiltInScopeId(INDENT_GUIDE)) 322 | openedIndentGuide = true 323 | } 324 | this.emitText(' ', false) 325 | } 326 | 327 | if (openedIndentGuide) this.emitCloseTag(this.getBuiltInScopeId(INDENT_GUIDE)) 328 | } else { 329 | this.emitText(' '.repeat(endColumn - this.screenColumn), false) 330 | } 331 | } 332 | 333 | emitHardTab () { 334 | const distanceToNextTabStop = this.displayLayer.tabLength - (this.screenColumn % this.displayLayer.tabLength) 335 | if (this.displayLayer.invisibles.tab) { 336 | this.emitText(this.displayLayer.invisibles.tab) 337 | this.emitText(' '.repeat(distanceToNextTabStop - 1)) 338 | } else { 339 | this.emitText(' '.repeat(distanceToNextTabStop)) 340 | } 341 | } 342 | 343 | emitText (text, reopenTags = true) { 344 | if (reopenTags) this.reopenTags() 345 | this.currentScreenLineText += text 346 | const length = text.length 347 | this.screenColumn += length 348 | this.currentTokenLength += length 349 | } 350 | 351 | emitTokenBoundary () { 352 | if (this.currentTokenLength > 0) { 353 | this.currentScreenLineTags.push(this.currentTokenLength) 354 | this.currentTokenLength = 0 355 | } 356 | } 357 | 358 | emitEmptyTokenIfNeeded () { 359 | const lastTag = this.currentScreenLineTags[this.currentScreenLineTags.length - 1] 360 | if (this.displayLayer.isOpenTag(lastTag)) { 361 | this.currentScreenLineTags.push(0) 362 | } 363 | } 364 | 365 | emitCloseTag (scopeId) { 366 | this.emitTokenBoundary() 367 | 368 | if (scopeId === 0) return 369 | 370 | for (let i = this.scopeIdsToReopen.length - 1; i >= 0; i--) { 371 | if (this.scopeIdsToReopen[i] === scopeId) { 372 | this.scopeIdsToReopen.splice(i, 1) 373 | return 374 | } 375 | } 376 | 377 | this.emitEmptyTokenIfNeeded() 378 | 379 | var containingScopeId 380 | while ((containingScopeId = this.containingScopeIds.pop())) { 381 | this.currentScreenLineTags.push(this.displayLayer.closeTagForScopeId(containingScopeId)) 382 | if (containingScopeId === scopeId) { 383 | return 384 | } else { 385 | this.scopeIdsToReopen.unshift(containingScopeId) 386 | } 387 | } 388 | } 389 | 390 | emitOpenTag (scopeId, reopenTags = true) { 391 | if (reopenTags) this.reopenTags() 392 | this.emitTokenBoundary() 393 | if (scopeId > 0) { 394 | this.containingScopeIds.push(scopeId) 395 | this.currentScreenLineTags.push(this.displayLayer.openTagForScopeId(scopeId)) 396 | } 397 | } 398 | 399 | closeContainingScopes () { 400 | if (this.containingScopeIds.length > 0) this.emitEmptyTokenIfNeeded() 401 | 402 | for (let i = this.containingScopeIds.length - 1; i >= 0; i--) { 403 | const containingScopeId = this.containingScopeIds[i] 404 | this.currentScreenLineTags.push(this.displayLayer.closeTagForScopeId(containingScopeId)) 405 | this.scopeIdsToReopen.unshift(containingScopeId) 406 | } 407 | this.containingScopeIds.length = 0 408 | } 409 | 410 | reopenTags () { 411 | for (let i = 0, n = this.scopeIdsToReopen.length; i < n; i++) { 412 | const scopeIdToReopen = this.scopeIdsToReopen[i] 413 | this.containingScopeIds.push(scopeIdToReopen) 414 | this.currentScreenLineTags.push(this.displayLayer.openTagForScopeId(scopeIdToReopen)) 415 | } 416 | this.scopeIdsToReopen.length = 0 417 | } 418 | 419 | pushScreenLine (screenLine) { 420 | if (this.requestedStartScreenRow <= this.screenRow && this.screenRow < this.requestedEndScreenRow) { 421 | this.screenLines.push(screenLine) 422 | } 423 | } 424 | 425 | compareBufferPosition (position) { 426 | const rowComparison = this.bufferPosition.row - position.row 427 | return rowComparison === 0 ? (this.bufferPosition.column - position.column) : rowComparison 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/marker.coffee: -------------------------------------------------------------------------------- 1 | {extend, isEqual, omit, pick, size} = require 'underscore-plus' 2 | {Emitter} = require 'event-kit' 3 | Delegator = require 'delegato' 4 | Point = require './point' 5 | Range = require './range' 6 | Grim = require 'grim' 7 | 8 | OptionKeys = new Set(['reversed', 'tailed', 'invalidate', 'exclusive']) 9 | 10 | # Private: Represents a buffer annotation that remains logically stationary 11 | # even as the buffer changes. This is used to represent cursors, folds, snippet 12 | # targets, misspelled words, and anything else that needs to track a logical 13 | # location in the buffer over time. 14 | # 15 | # Head and Tail: 16 | # Markers always have a *head* and sometimes have a *tail*. If you think of a 17 | # marker as an editor selection, the tail is the part that's stationary and the 18 | # head is the part that moves when the mouse is moved. A marker without a tail 19 | # always reports an empty range at the head position. A marker with a head position 20 | # greater than the tail is in a "normal" orientation. If the head precedes the 21 | # tail the marker is in a "reversed" orientation. 22 | # 23 | # Validity: 24 | # Markers are considered *valid* when they are first created. Depending on the 25 | # invalidation strategy you choose, certain changes to the buffer can cause a 26 | # marker to become invalid, for example if the text surrounding the marker is 27 | # deleted. See {TextBuffer::markRange} for invalidation strategies. 28 | module.exports = 29 | class Marker 30 | Delegator.includeInto(this) 31 | 32 | @extractParams: (inputParams) -> 33 | outputParams = {} 34 | containsCustomProperties = false 35 | if inputParams? 36 | for key in Object.keys(inputParams) 37 | if OptionKeys.has(key) 38 | outputParams[key] = inputParams[key] 39 | else if key is 'clipDirection' or key is 'skipSoftWrapIndentation' 40 | # TODO: Ignore these two keys for now. Eventually, when the 41 | # deprecation below will be gone, we can remove this conditional as 42 | # well, and just return standard marker properties. 43 | else 44 | containsCustomProperties = true 45 | outputParams.properties ?= {} 46 | outputParams.properties[key] = inputParams[key] 47 | 48 | # TODO: Remove both this deprecation and the conditional above on the 49 | # release after the one where we'll ship `DisplayLayer`. 50 | if containsCustomProperties 51 | Grim.deprecate(""" 52 | Assigning custom properties to a marker when creating/copying it is 53 | deprecated. Please, consider storing the custom properties you need in 54 | some other object in your package, keyed by the marker's id property. 55 | """) 56 | 57 | outputParams 58 | 59 | @delegatesMethods 'containsPoint', 'containsRange', 'intersectsRow', toMethod: 'getRange' 60 | 61 | constructor: (@id, @layer, range, params, exclusivitySet = false) -> 62 | {@tailed, @reversed, @valid, @invalidate, @exclusive, @properties} = params 63 | @emitter = new Emitter 64 | @tailed ?= true 65 | @reversed ?= false 66 | @valid ?= true 67 | @invalidate ?= 'overlap' 68 | @properties ?= {} 69 | @hasChangeObservers = false 70 | Object.freeze(@properties) 71 | @layer.setMarkerIsExclusive(@id, @isExclusive()) unless exclusivitySet 72 | 73 | ### 74 | Section: Event Subscription 75 | ### 76 | 77 | # Public: Invoke the given callback when the marker is destroyed. 78 | # 79 | # * `callback` {Function} to be called when the marker is destroyed. 80 | # 81 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 82 | onDidDestroy: (callback) -> 83 | @layer.markersWithDestroyListeners.add(this) 84 | @emitter.on 'did-destroy', callback 85 | 86 | # Public: Invoke the given callback when the state of the marker changes. 87 | # 88 | # * `callback` {Function} to be called when the marker changes. 89 | # * `event` {Object} with the following keys: 90 | # * `oldHeadPosition` {Point} representing the former head position 91 | # * `newHeadPosition` {Point} representing the new head position 92 | # * `oldTailPosition` {Point} representing the former tail position 93 | # * `newTailPosition` {Point} representing the new tail position 94 | # * `wasValid` {Boolean} indicating whether the marker was valid before the change 95 | # * `isValid` {Boolean} indicating whether the marker is now valid 96 | # * `hadTail` {Boolean} indicating whether the marker had a tail before the change 97 | # * `hasTail` {Boolean} indicating whether the marker now has a tail 98 | # * `oldProperties` {Object} containing the marker's custom properties before the change. 99 | # * `newProperties` {Object} containing the marker's custom properties after the change. 100 | # * `textChanged` {Boolean} indicating whether this change was caused by a textual change 101 | # to the buffer or whether the marker was manipulated directly via its public API. 102 | # 103 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 104 | onDidChange: (callback) -> 105 | unless @hasChangeObservers 106 | @previousEventState = @getSnapshot(@getRange()) 107 | @hasChangeObservers = true 108 | @layer.markersWithChangeListeners.add(this) 109 | @emitter.on 'did-change', callback 110 | 111 | # Public: Returns the current {Range} of the marker. The range is immutable. 112 | getRange: -> @layer.getMarkerRange(@id) 113 | 114 | # Public: Sets the range of the marker. 115 | # 116 | # * `range` A {Range} or range-compatible {Array}. The range will be clipped 117 | # before it is assigned. 118 | # * `params` (optional) An {Object} with the following keys: 119 | # * `reversed` {Boolean} indicating the marker will to be in a reversed 120 | # orientation. 121 | # * `exclusive` {Boolean} indicating that changes occurring at either end of 122 | # the marker will be considered *outside* the marker rather than inside. 123 | # This defaults to `false` unless the marker's invalidation strategy is 124 | # `inside` or the marker has no tail, in which case it defaults to `true`. 125 | setRange: (range, params) -> 126 | params ?= {} 127 | @update(@getRange(), {reversed: params.reversed, tailed: true, range: Range.fromObject(range, true), exclusive: params.exclusive}) 128 | 129 | # Public: Returns a {Point} representing the marker's current head position. 130 | getHeadPosition: -> 131 | if @reversed 132 | @getStartPosition() 133 | else 134 | @getEndPosition() 135 | 136 | # Public: Sets the head position of the marker. 137 | # 138 | # * `position` A {Point} or point-compatible {Array}. The position will be 139 | # clipped before it is assigned. 140 | setHeadPosition: (position) -> 141 | position = Point.fromObject(position) 142 | oldRange = @getRange() 143 | params = {} 144 | 145 | if @hasTail() 146 | if @isReversed() 147 | if position.isLessThan(oldRange.end) 148 | params.range = new Range(position, oldRange.end) 149 | else 150 | params.reversed = false 151 | params.range = new Range(oldRange.end, position) 152 | else 153 | if position.isLessThan(oldRange.start) 154 | params.reversed = true 155 | params.range = new Range(position, oldRange.start) 156 | else 157 | params.range = new Range(oldRange.start, position) 158 | else 159 | params.range = new Range(position, position) 160 | @update(oldRange, params) 161 | 162 | # Public: Returns a {Point} representing the marker's current tail position. 163 | # If the marker has no tail, the head position will be returned instead. 164 | getTailPosition: -> 165 | if @reversed 166 | @getEndPosition() 167 | else 168 | @getStartPosition() 169 | 170 | # Public: Sets the tail position of the marker. If the marker doesn't have a 171 | # tail, it will after calling this method. 172 | # 173 | # * `position` A {Point} or point-compatible {Array}. The position will be 174 | # clipped before it is assigned. 175 | setTailPosition: (position) -> 176 | position = Point.fromObject(position) 177 | oldRange = @getRange() 178 | params = {tailed: true} 179 | 180 | if @reversed 181 | if position.isLessThan(oldRange.start) 182 | params.reversed = false 183 | params.range = new Range(position, oldRange.start) 184 | else 185 | params.range = new Range(oldRange.start, position) 186 | else 187 | if position.isLessThan(oldRange.end) 188 | params.range = new Range(position, oldRange.end) 189 | else 190 | params.reversed = true 191 | params.range = new Range(oldRange.end, position) 192 | 193 | @update(oldRange, params) 194 | 195 | # Public: Returns a {Point} representing the start position of the marker, 196 | # which could be the head or tail position, depending on its orientation. 197 | getStartPosition: -> @layer.getMarkerStartPosition(@id) 198 | 199 | # Public: Returns a {Point} representing the end position of the marker, 200 | # which could be the head or tail position, depending on its orientation. 201 | getEndPosition: -> @layer.getMarkerEndPosition(@id) 202 | 203 | # Public: Removes the marker's tail. After calling the marker's head position 204 | # will be reported as its current tail position until the tail is planted 205 | # again. 206 | clearTail: -> 207 | headPosition = @getHeadPosition() 208 | @update(@getRange(), {tailed: false, reversed: false, range: Range(headPosition, headPosition)}) 209 | 210 | # Public: Plants the marker's tail at the current head position. After calling 211 | # the marker's tail position will be its head position at the time of the 212 | # call, regardless of where the marker's head is moved. 213 | plantTail: -> 214 | unless @hasTail() 215 | headPosition = @getHeadPosition() 216 | @update(@getRange(), {tailed: true, range: new Range(headPosition, headPosition)}) 217 | 218 | # Public: Returns a {Boolean} indicating whether the head precedes the tail. 219 | isReversed: -> 220 | @tailed and @reversed 221 | 222 | # Public: Returns a {Boolean} indicating whether the marker has a tail. 223 | hasTail: -> 224 | @tailed 225 | 226 | # Public: Is the marker valid? 227 | # 228 | # Returns a {Boolean}. 229 | isValid: -> 230 | not @isDestroyed() and @valid 231 | 232 | # Public: Is the marker destroyed? 233 | # 234 | # Returns a {Boolean}. 235 | isDestroyed: -> 236 | not @layer.hasMarker(@id) 237 | 238 | # Public: Returns a {Boolean} indicating whether changes that occur exactly at 239 | # the marker's head or tail cause it to move. 240 | isExclusive: -> 241 | if @exclusive? 242 | @exclusive 243 | else 244 | @getInvalidationStrategy() is 'inside' or not @hasTail() 245 | 246 | # Public: Returns a {Boolean} indicating whether this marker is equivalent to 247 | # another marker, meaning they have the same range and options. 248 | # 249 | # * `other` {Marker} other marker 250 | isEqual: (other) -> 251 | @invalidate is other.invalidate and 252 | @tailed is other.tailed and 253 | @reversed is other.reversed and 254 | @exclusive is other.exclusive and 255 | isEqual(@properties, other.properties) and 256 | @getRange().isEqual(other.getRange()) 257 | 258 | # Public: Get the invalidation strategy for this marker. 259 | # 260 | # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. 261 | # 262 | # Returns a {String}. 263 | getInvalidationStrategy: -> 264 | @invalidate 265 | 266 | # Public: Returns an {Object} containing any custom properties associated with 267 | # the marker. 268 | getProperties: -> 269 | @properties 270 | 271 | # Public: Merges an {Object} containing new properties into the marker's 272 | # existing properties. 273 | # 274 | # * `properties` {Object} 275 | setProperties: (properties) -> 276 | @update(@getRange(), properties: extend({}, @properties, properties)) 277 | 278 | # Public: Creates and returns a new {Marker} with the same properties as this 279 | # marker. 280 | # 281 | # * `params` {Object} 282 | copy: (options={}) -> 283 | snapshot = @getSnapshot() 284 | options = Marker.extractParams(options) 285 | @layer.createMarker(@getRange(), extend( 286 | {} 287 | snapshot, 288 | options, 289 | properties: extend({}, snapshot.properties, options.properties) 290 | )) 291 | 292 | # Public: Destroys the marker, causing it to emit the 'destroyed' event. 293 | destroy: (suppressMarkerLayerUpdateEvents) -> 294 | return if @isDestroyed() 295 | 296 | if @trackDestruction 297 | error = new Error 298 | Error.captureStackTrace(error) 299 | @destroyStackTrace = error.stack 300 | 301 | @layer.destroyMarker(this, suppressMarkerLayerUpdateEvents) 302 | @emitter.emit 'did-destroy' 303 | @emitter.clear() 304 | 305 | # Public: Compares this marker to another based on their ranges. 306 | # 307 | # * `other` {Marker} 308 | compare: (other) -> 309 | @layer.compareMarkers(@id, other.id) 310 | 311 | # Returns whether this marker matches the given parameters. The parameters 312 | # are the same as {MarkerLayer::findMarkers}. 313 | matchesParams: (params) -> 314 | for key in Object.keys(params) 315 | return false unless @matchesParam(key, params[key]) 316 | true 317 | 318 | # Returns whether this marker matches the given parameter name and value. 319 | # The parameters are the same as {MarkerLayer::findMarkers}. 320 | matchesParam: (key, value) -> 321 | switch key 322 | when 'startPosition' 323 | @getStartPosition().isEqual(value) 324 | when 'endPosition' 325 | @getEndPosition().isEqual(value) 326 | when 'containsPoint', 'containsPosition' 327 | @containsPoint(value) 328 | when 'containsRange' 329 | @containsRange(value) 330 | when 'startRow' 331 | @getStartPosition().row is value 332 | when 'endRow' 333 | @getEndPosition().row is value 334 | when 'intersectsRow' 335 | @intersectsRow(value) 336 | when 'invalidate', 'reversed', 'tailed' 337 | isEqual(@[key], value) 338 | when 'valid' 339 | @isValid() is value 340 | else 341 | isEqual(@properties[key], value) 342 | 343 | update: (oldRange, {range, reversed, tailed, valid, exclusive, properties}, textChanged=false, suppressMarkerLayerUpdateEvents=false) -> 344 | return if @isDestroyed() 345 | 346 | oldRange = Range.fromObject(oldRange) 347 | range = Range.fromObject(range) if range? 348 | 349 | wasExclusive = @isExclusive() 350 | updated = propertiesChanged = false 351 | 352 | if range? and not range.isEqual(oldRange) 353 | @layer.setMarkerRange(@id, range) 354 | updated = true 355 | 356 | if reversed? and reversed isnt @reversed 357 | @reversed = reversed 358 | updated = true 359 | 360 | if tailed? and tailed isnt @tailed 361 | @tailed = tailed 362 | updated = true 363 | 364 | if valid? and valid isnt @valid 365 | @valid = valid 366 | updated = true 367 | 368 | if exclusive? and exclusive isnt @exclusive 369 | @exclusive = exclusive 370 | updated = true 371 | 372 | if wasExclusive isnt @isExclusive() 373 | @layer.setMarkerIsExclusive(@id, @isExclusive()) 374 | updated = true 375 | 376 | if properties? and not isEqual(properties, @properties) 377 | @properties = Object.freeze(properties) 378 | propertiesChanged = true 379 | updated = true 380 | 381 | @emitChangeEvent(range ? oldRange, textChanged, propertiesChanged) 382 | @layer.markerUpdated() if updated and not suppressMarkerLayerUpdateEvents 383 | updated 384 | 385 | getSnapshot: (range, includeMarker=true) -> 386 | snapshot = {range, @properties, @reversed, @tailed, @valid, @invalidate, @exclusive} 387 | snapshot.marker = this if includeMarker 388 | Object.freeze(snapshot) 389 | 390 | toString: -> 391 | "[Marker #{@id}, #{@getRange()}]" 392 | 393 | ### 394 | Section: Private 395 | ### 396 | 397 | inspect: -> 398 | @toString() 399 | 400 | emitChangeEvent: (currentRange, textChanged, propertiesChanged) -> 401 | return unless @hasChangeObservers 402 | oldState = @previousEventState 403 | 404 | currentRange ?= @getRange() 405 | 406 | return false unless propertiesChanged or 407 | oldState.valid isnt @valid or 408 | oldState.tailed isnt @tailed or 409 | oldState.reversed isnt @reversed or 410 | oldState.range.compare(currentRange) isnt 0 411 | 412 | newState = @previousEventState = @getSnapshot(currentRange) 413 | 414 | if oldState.reversed 415 | oldHeadPosition = oldState.range.start 416 | oldTailPosition = oldState.range.end 417 | else 418 | oldHeadPosition = oldState.range.end 419 | oldTailPosition = oldState.range.start 420 | 421 | if newState.reversed 422 | newHeadPosition = newState.range.start 423 | newTailPosition = newState.range.end 424 | else 425 | newHeadPosition = newState.range.end 426 | newTailPosition = newState.range.start 427 | 428 | @emitter.emit("did-change", { 429 | wasValid: oldState.valid, isValid: newState.valid 430 | hadTail: oldState.tailed, hasTail: newState.tailed 431 | oldProperties: oldState.properties, newProperties: newState.properties 432 | oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition 433 | textChanged 434 | }) 435 | true 436 | -------------------------------------------------------------------------------- /src/marker-layer.coffee: -------------------------------------------------------------------------------- 1 | {clone} = require "underscore-plus" 2 | {Emitter} = require 'event-kit' 3 | Point = require "./point" 4 | Range = require "./range" 5 | Marker = require "./marker" 6 | {MarkerIndex} = require "superstring" 7 | {intersectSet} = require "./set-helpers" 8 | 9 | SerializationVersion = 2 10 | 11 | # Public: *Experimental:* A container for a related set of markers. 12 | # 13 | # This API is experimental and subject to change on any release. 14 | module.exports = 15 | class MarkerLayer 16 | @deserialize: (delegate, state) -> 17 | store = new MarkerLayer(delegate, 0) 18 | store.deserialize(state) 19 | store 20 | 21 | @deserializeSnapshot: (snapshot) -> 22 | result = {} 23 | for layerId, markerSnapshots of snapshot 24 | result[layerId] = {} 25 | for markerId, markerSnapshot of markerSnapshots 26 | result[layerId][markerId] = clone(markerSnapshot) 27 | result[layerId][markerId].range = Range.fromObject(markerSnapshot.range) 28 | result 29 | 30 | ### 31 | Section: Lifecycle 32 | ### 33 | 34 | constructor: (@delegate, @id, options) -> 35 | @maintainHistory = options?.maintainHistory ? false 36 | @destroyInvalidatedMarkers = options?.destroyInvalidatedMarkers ? false 37 | @role = options?.role 38 | @delegate.registerSelectionsMarkerLayer(this) if @role is "selections" 39 | @persistent = options?.persistent ? false 40 | @emitter = new Emitter 41 | @index = new MarkerIndex 42 | @markersById = {} 43 | @markersWithChangeListeners = new Set 44 | @markersWithDestroyListeners = new Set 45 | @displayMarkerLayers = new Set 46 | @destroyed = false 47 | @emitCreateMarkerEvents = false 48 | 49 | # Public: Create a copy of this layer with markers in the same state and 50 | # locations. 51 | copy: -> 52 | copy = @delegate.addMarkerLayer({@maintainHistory, @role}) 53 | for markerId, marker of @markersById 54 | snapshot = marker.getSnapshot(null) 55 | copy.createMarker(marker.getRange(), marker.getSnapshot()) 56 | copy 57 | 58 | # Public: Destroy this layer. 59 | destroy: -> 60 | return if @destroyed 61 | @clear() 62 | @delegate.markerLayerDestroyed(this) 63 | @displayMarkerLayers.forEach (displayMarkerLayer) -> displayMarkerLayer.destroy() 64 | @displayMarkerLayers.clear() 65 | @destroyed = true 66 | @emitter.emit 'did-destroy' 67 | @emitter.clear() 68 | 69 | # Public: Remove all markers from this layer. 70 | clear: -> 71 | @markersWithDestroyListeners.forEach (marker) -> marker.destroy() 72 | @markersWithDestroyListeners.clear() 73 | @markersById = {} 74 | @index = new MarkerIndex 75 | @displayMarkerLayers.forEach (layer) -> layer.didClearBufferMarkerLayer() 76 | @delegate.markersUpdated(this) 77 | 78 | # Public: Determine whether this layer has been destroyed. 79 | isDestroyed: -> 80 | @destroyed 81 | 82 | isAlive: -> 83 | not @destroyed 84 | 85 | ### 86 | Section: Querying 87 | ### 88 | 89 | # Public: Get an existing marker by its id. 90 | # 91 | # Returns a {Marker}. 92 | getMarker: (id) -> 93 | @markersById[id] 94 | 95 | # Public: Get all existing markers on the marker layer. 96 | # 97 | # Returns an {Array} of {Marker}s. 98 | getMarkers: -> 99 | marker for id, marker of @markersById 100 | 101 | # Public: Get the number of markers in the marker layer. 102 | # 103 | # Returns a {Number}. 104 | getMarkerCount: -> 105 | Object.keys(@markersById).length 106 | 107 | # Public: Find markers in the layer conforming to the given parameters. 108 | # 109 | # See the documentation for {TextBuffer::findMarkers}. 110 | findMarkers: (params) -> 111 | markerIds = null 112 | 113 | for key in Object.keys(params) 114 | value = params[key] 115 | switch key 116 | when 'startPosition' 117 | markerIds = filterSet(markerIds, @index.findStartingAt(Point.fromObject(value))) 118 | when 'endPosition' 119 | markerIds = filterSet(markerIds, @index.findEndingAt(Point.fromObject(value))) 120 | when 'startsInRange' 121 | {start, end} = Range.fromObject(value) 122 | markerIds = filterSet(markerIds, @index.findStartingIn(start, end)) 123 | when 'endsInRange' 124 | {start, end} = Range.fromObject(value) 125 | markerIds = filterSet(markerIds, @index.findEndingIn(start, end)) 126 | when 'containsPoint', 'containsPosition' 127 | position = Point.fromObject(value) 128 | markerIds = filterSet(markerIds, @index.findContaining(position, position)) 129 | when 'containsRange' 130 | {start, end} = Range.fromObject(value) 131 | markerIds = filterSet(markerIds, @index.findContaining(start, end)) 132 | when 'intersectsRange' 133 | {start, end} = Range.fromObject(value) 134 | markerIds = filterSet(markerIds, @index.findIntersecting(start, end)) 135 | when 'startRow' 136 | markerIds = filterSet(markerIds, @index.findStartingIn(Point(value, 0), Point(value, Infinity))) 137 | when 'endRow' 138 | markerIds = filterSet(markerIds, @index.findEndingIn(Point(value, 0), Point(value, Infinity))) 139 | when 'intersectsRow' 140 | markerIds = filterSet(markerIds, @index.findIntersecting(Point(value, 0), Point(value, Infinity))) 141 | when 'intersectsRowRange' 142 | markerIds = filterSet(markerIds, @index.findIntersecting(Point(value[0], 0), Point(value[1], Infinity))) 143 | when 'containedInRange' 144 | {start, end} = Range.fromObject(value) 145 | markerIds = filterSet(markerIds, @index.findContainedIn(start, end)) 146 | else 147 | continue 148 | delete params[key] 149 | 150 | markerIds ?= new Set(Object.keys(@markersById)) 151 | 152 | result = [] 153 | markerIds.forEach (markerId) => 154 | marker = @markersById[markerId] 155 | return unless marker.matchesParams(params) 156 | result.push(marker) 157 | result.sort (a, b) -> a.compare(b) 158 | 159 | # Public: Get the role of the marker layer e.g. `atom.selection`. 160 | # 161 | # Returns a {String}. 162 | getRole: -> 163 | @role 164 | 165 | ### 166 | Section: Marker creation 167 | ### 168 | 169 | # Public: Create a marker with the given range. 170 | # 171 | # * `range` A {Range} or range-compatible {Array} 172 | # * `options` A hash of key-value pairs to associate with the marker. There 173 | # are also reserved property names that have marker-specific meaning. 174 | # * `reversed` (optional) {Boolean} Creates the marker in a reversed 175 | # orientation. (default: false) 176 | # * `invalidate` (optional) {String} Determines the rules by which changes 177 | # to the buffer *invalidate* the marker. (default: 'overlap') It can be 178 | # any of the following strategies, in order of fragility: 179 | # * __never__: The marker is never marked as invalid. This is a good choice for 180 | # markers representing selections in an editor. 181 | # * __surround__: The marker is invalidated by changes that completely surround it. 182 | # * __overlap__: The marker is invalidated by changes that surround the 183 | # start or end of the marker. This is the default. 184 | # * __inside__: The marker is invalidated by changes that extend into the 185 | # inside of the marker. Changes that end at the marker's start or 186 | # start at the marker's end do not invalidate the marker. 187 | # * __touch__: The marker is invalidated by a change that touches the marked 188 | # region in any way, including changes that end at the marker's 189 | # start or start at the marker's end. This is the most fragile strategy. 190 | # * `exclusive` {Boolean} indicating whether insertions at the start or end 191 | # of the marked range should be interpreted as happening *outside* the 192 | # marker. Defaults to `false`, except when using the `inside` 193 | # invalidation strategy or when when the marker has no tail, in which 194 | # case it defaults to true. Explicitly assigning this option overrides 195 | # behavior in all circumstances. 196 | # 197 | # Returns a {Marker}. 198 | markRange: (range, options={}) -> 199 | @createMarker(@delegate.clipRange(range), Marker.extractParams(options)) 200 | 201 | # Public: Create a marker at with its head at the given position with no tail. 202 | # 203 | # * `position` {Point} or point-compatible {Array} 204 | # * `options` (optional) An {Object} with the following keys: 205 | # * `invalidate` (optional) {String} Determines the rules by which changes 206 | # to the buffer *invalidate* the marker. (default: 'overlap') It can be 207 | # any of the following strategies, in order of fragility: 208 | # * __never__: The marker is never marked as invalid. This is a good choice for 209 | # markers representing selections in an editor. 210 | # * __surround__: The marker is invalidated by changes that completely surround it. 211 | # * __overlap__: The marker is invalidated by changes that surround the 212 | # start or end of the marker. This is the default. 213 | # * __inside__: The marker is invalidated by changes that extend into the 214 | # inside of the marker. Changes that end at the marker's start or 215 | # start at the marker's end do not invalidate the marker. 216 | # * __touch__: The marker is invalidated by a change that touches the marked 217 | # region in any way, including changes that end at the marker's 218 | # start or start at the marker's end. This is the most fragile strategy. 219 | # * `exclusive` {Boolean} indicating whether insertions at the start or end 220 | # of the marked range should be interpreted as happening *outside* the 221 | # marker. Defaults to `false`, except when using the `inside` 222 | # invalidation strategy or when when the marker has no tail, in which 223 | # case it defaults to true. Explicitly assigning this option overrides 224 | # behavior in all circumstances. 225 | # 226 | # Returns a {Marker}. 227 | markPosition: (position, options={}) -> 228 | position = @delegate.clipPosition(position) 229 | options = Marker.extractParams(options) 230 | options.tailed = false 231 | @createMarker(@delegate.clipRange(new Range(position, position)), options) 232 | 233 | ### 234 | Section: Event subscription 235 | ### 236 | 237 | # Public: Subscribe to be notified asynchronously whenever markers are 238 | # created, updated, or destroyed on this layer. *Prefer this method for 239 | # optimal performance when interacting with layers that could contain large 240 | # numbers of markers.* 241 | # 242 | # * `callback` A {Function} that will be called with no arguments when changes 243 | # occur on this layer. 244 | # 245 | # Subscribers are notified once, asynchronously when any number of changes 246 | # occur in a given tick of the event loop. You should re-query the layer 247 | # to determine the state of markers in which you're interested in. It may 248 | # be counter-intuitive, but this is much more efficient than subscribing to 249 | # events on individual markers, which are expensive to deliver. 250 | # 251 | # Returns a {Disposable}. 252 | onDidUpdate: (callback) -> 253 | @emitter.on 'did-update', callback 254 | 255 | # Public: Subscribe to be notified synchronously whenever markers are created 256 | # on this layer. *Avoid this method for optimal performance when interacting 257 | # with layers that could contain large numbers of markers.* 258 | # 259 | # * `callback` A {Function} that will be called with a {Marker} whenever a 260 | # new marker is created. 261 | # 262 | # You should prefer {::onDidUpdate} when synchronous notifications aren't 263 | # absolutely necessary. 264 | # 265 | # Returns a {Disposable}. 266 | onDidCreateMarker: (callback) -> 267 | @emitCreateMarkerEvents = true 268 | @emitter.on 'did-create-marker', callback 269 | 270 | # Public: Subscribe to be notified synchronously when this layer is destroyed. 271 | # 272 | # Returns a {Disposable}. 273 | onDidDestroy: (callback) -> 274 | @emitter.on 'did-destroy', callback 275 | 276 | ### 277 | Section: Private - TextBuffer interface 278 | ### 279 | 280 | splice: (start, oldExtent, newExtent) -> 281 | invalidated = @index.splice(start, oldExtent, newExtent) 282 | invalidated.touch.forEach (id) => 283 | marker = @markersById[id] 284 | if invalidated[marker.getInvalidationStrategy()]?.has(id) 285 | if @destroyInvalidatedMarkers 286 | marker.destroy() 287 | else 288 | marker.valid = false 289 | 290 | restoreFromSnapshot: (snapshots, alwaysCreate) -> 291 | return unless snapshots? 292 | 293 | snapshotIds = Object.keys(snapshots) 294 | existingMarkerIds = Object.keys(@markersById) 295 | 296 | for id in snapshotIds 297 | snapshot = snapshots[id] 298 | if alwaysCreate 299 | @createMarker(snapshot.range, snapshot, true) 300 | continue 301 | 302 | if marker = @markersById[id] 303 | marker.update(marker.getRange(), snapshot, true, true) 304 | else 305 | {marker} = snapshot 306 | if marker 307 | @markersById[marker.id] = marker 308 | {range} = snapshot 309 | @index.insert(marker.id, range.start, range.end) 310 | marker.update(marker.getRange(), snapshot, true, true) 311 | @emitter.emit 'did-create-marker', marker if @emitCreateMarkerEvents 312 | else 313 | newMarker = @createMarker(snapshot.range, snapshot, true) 314 | 315 | for id in existingMarkerIds 316 | if (marker = @markersById[id]) and (not snapshots[id]?) 317 | marker.destroy(true) 318 | 319 | createSnapshot: -> 320 | result = {} 321 | ranges = @index.dump() 322 | for id in Object.keys(@markersById) 323 | marker = @markersById[id] 324 | result[id] = marker.getSnapshot(Range.fromObject(ranges[id])) 325 | result 326 | 327 | emitChangeEvents: (snapshot) -> 328 | @markersWithChangeListeners.forEach (marker) -> 329 | unless marker.isDestroyed() # event handlers could destroy markers 330 | marker.emitChangeEvent(snapshot?[marker.id]?.range, true, false) 331 | 332 | serialize: -> 333 | ranges = @index.dump() 334 | markersById = {} 335 | for id in Object.keys(@markersById) 336 | marker = @markersById[id] 337 | snapshot = marker.getSnapshot(Range.fromObject(ranges[id]), false) 338 | markersById[id] = snapshot 339 | 340 | {@id, @maintainHistory, @role, @persistent, markersById, version: SerializationVersion} 341 | 342 | deserialize: (state) -> 343 | return unless state.version is SerializationVersion 344 | @id = state.id 345 | @maintainHistory = state.maintainHistory 346 | @role = state.role 347 | @delegate.registerSelectionsMarkerLayer(this) if @role is "selections" 348 | @persistent = state.persistent 349 | for id, markerState of state.markersById 350 | range = Range.fromObject(markerState.range) 351 | delete markerState.range 352 | @addMarker(id, range, markerState) 353 | return 354 | 355 | ### 356 | Section: Private - Marker interface 357 | ### 358 | 359 | markerUpdated: -> 360 | @delegate.markersUpdated(this) 361 | 362 | destroyMarker: (marker, suppressMarkerLayerUpdateEvents=false) -> 363 | if @markersById.hasOwnProperty(marker.id) 364 | delete @markersById[marker.id] 365 | @index.remove(marker.id) 366 | @markersWithChangeListeners.delete(marker) 367 | @markersWithDestroyListeners.delete(marker) 368 | @displayMarkerLayers.forEach (displayMarkerLayer) -> displayMarkerLayer.destroyMarker(marker.id) 369 | @delegate.markersUpdated(this) unless suppressMarkerLayerUpdateEvents 370 | 371 | hasMarker: (id) -> 372 | not @destroyed and @index.has(id) 373 | 374 | getMarkerRange: (id) -> 375 | Range.fromObject(@index.getRange(id)) 376 | 377 | getMarkerStartPosition: (id) -> 378 | Point.fromObject(@index.getStart(id)) 379 | 380 | getMarkerEndPosition: (id) -> 381 | Point.fromObject(@index.getEnd(id)) 382 | 383 | compareMarkers: (id1, id2) -> 384 | @index.compare(id1, id2) 385 | 386 | setMarkerRange: (id, range) -> 387 | {start, end} = Range.fromObject(range) 388 | start = @delegate.clipPosition(start) 389 | end = @delegate.clipPosition(end) 390 | @index.remove(id) 391 | @index.insert(id, start, end) 392 | 393 | setMarkerIsExclusive: (id, exclusive) -> 394 | @index.setExclusive(id, exclusive) 395 | 396 | createMarker: (range, params, suppressMarkerLayerUpdateEvents=false) -> 397 | id = @delegate.getNextMarkerId() 398 | marker = @addMarker(id, range, params) 399 | @delegate.markerCreated(this, marker) 400 | @delegate.markersUpdated(this) unless suppressMarkerLayerUpdateEvents 401 | marker.trackDestruction = @trackDestructionInOnDidCreateMarkerCallbacks ? false 402 | @emitter.emit 'did-create-marker', marker if @emitCreateMarkerEvents 403 | marker.trackDestruction = false 404 | marker 405 | 406 | ### 407 | Section: Internal 408 | ### 409 | 410 | addMarker: (id, range, params) -> 411 | range = Range.fromObject(range) 412 | Point.assertValid(range.start) 413 | Point.assertValid(range.end) 414 | @index.insert(id, range.start, range.end) 415 | @markersById[id] = new Marker(id, this, range, params) 416 | 417 | emitUpdateEvent: -> 418 | @emitter.emit('did-update') 419 | 420 | filterSet = (set1, set2) -> 421 | if set1 422 | intersectSet(set1, set2) 423 | set1 424 | else 425 | set2 426 | -------------------------------------------------------------------------------- /src/display-marker.coffee: -------------------------------------------------------------------------------- 1 | {Emitter} = require 'event-kit' 2 | 3 | # Essential: Represents a buffer annotation that remains logically stationary 4 | # even as the buffer changes. This is used to represent cursors, folds, snippet 5 | # targets, misspelled words, and anything else that needs to track a logical 6 | # location in the buffer over time. 7 | # 8 | # ### DisplayMarker Creation 9 | # 10 | # Use {DisplayMarkerLayer::markBufferRange} or {DisplayMarkerLayer::markScreenRange} 11 | # rather than creating Markers directly. 12 | # 13 | # ### Head and Tail 14 | # 15 | # Markers always have a *head* and sometimes have a *tail*. If you think of a 16 | # marker as an editor selection, the tail is the part that's stationary and the 17 | # head is the part that moves when the mouse is moved. A marker without a tail 18 | # always reports an empty range at the head position. A marker with a head position 19 | # greater than the tail is in a "normal" orientation. If the head precedes the 20 | # tail the marker is in a "reversed" orientation. 21 | # 22 | # ### Validity 23 | # 24 | # Markers are considered *valid* when they are first created. Depending on the 25 | # invalidation strategy you choose, certain changes to the buffer can cause a 26 | # marker to become invalid, for example if the text surrounding the marker is 27 | # deleted. The strategies, in order of descending fragility: 28 | # 29 | # * __never__: The marker is never marked as invalid. This is a good choice for 30 | # markers representing selections in an editor. 31 | # * __surround__: The marker is invalidated by changes that completely surround it. 32 | # * __overlap__: The marker is invalidated by changes that surround the 33 | # start or end of the marker. This is the default. 34 | # * __inside__: The marker is invalidated by changes that extend into the 35 | # inside of the marker. Changes that end at the marker's start or 36 | # start at the marker's end do not invalidate the marker. 37 | # * __touch__: The marker is invalidated by a change that touches the marked 38 | # region in any way, including changes that end at the marker's 39 | # start or start at the marker's end. This is the most fragile strategy. 40 | # 41 | # See {TextBuffer::markRange} for usage. 42 | module.exports = 43 | class DisplayMarker 44 | ### 45 | Section: Construction and Destruction 46 | ### 47 | 48 | constructor: (@layer, @bufferMarker) -> 49 | {@id} = @bufferMarker 50 | @hasChangeObservers = false 51 | @emitter = new Emitter 52 | @bufferMarkerSubscription = null 53 | 54 | # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once 55 | # destroyed, a marker cannot be restored by undo/redo operations. 56 | destroy: -> 57 | unless @isDestroyed() 58 | @bufferMarker.destroy() 59 | 60 | didDestroyBufferMarker: -> 61 | @emitter.emit('did-destroy') 62 | @layer.didDestroyMarker(this) 63 | @emitter.dispose() 64 | @emitter.clear() 65 | @bufferMarkerSubscription?.dispose() 66 | 67 | # Essential: Creates and returns a new {DisplayMarker} with the same properties as 68 | # this marker. 69 | # 70 | # {Selection} markers (markers with a custom property `type: "selection"`) 71 | # should be copied with a different `type` value, for example with 72 | # `marker.copy({type: null})`. Otherwise, the new marker's selection will 73 | # be merged with this marker's selection, and a `null` value will be 74 | # returned. 75 | # 76 | # * `properties` (optional) {Object} properties to associate with the new 77 | # marker. The new marker's properties are computed by extending this marker's 78 | # properties with `properties`. 79 | # 80 | # Returns a {DisplayMarker}. 81 | copy: (params) -> 82 | @layer.getMarker(@bufferMarker.copy(params).id) 83 | 84 | ### 85 | Section: Event Subscription 86 | ### 87 | 88 | # Essential: Invoke the given callback when the state of the marker changes. 89 | # 90 | # * `callback` {Function} to be called when the marker changes. 91 | # * `event` {Object} with the following keys: 92 | # * `oldHeadBufferPosition` {Point} representing the former head buffer position 93 | # * `newHeadBufferPosition` {Point} representing the new head buffer position 94 | # * `oldTailBufferPosition` {Point} representing the former tail buffer position 95 | # * `newTailBufferPosition` {Point} representing the new tail buffer position 96 | # * `oldHeadScreenPosition` {Point} representing the former head screen position 97 | # * `newHeadScreenPosition` {Point} representing the new head screen position 98 | # * `oldTailScreenPosition` {Point} representing the former tail screen position 99 | # * `newTailScreenPosition` {Point} representing the new tail screen position 100 | # * `wasValid` {Boolean} indicating whether the marker was valid before the change 101 | # * `isValid` {Boolean} indicating whether the marker is now valid 102 | # * `hadTail` {Boolean} indicating whether the marker had a tail before the change 103 | # * `hasTail` {Boolean} indicating whether the marker now has a tail 104 | # * `oldProperties` {Object} containing the marker's custom properties before the change. 105 | # * `newProperties` {Object} containing the marker's custom properties after the change. 106 | # * `textChanged` {Boolean} indicating whether this change was caused by a textual change 107 | # to the buffer or whether the marker was manipulated directly via its public API. 108 | # 109 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 110 | onDidChange: (callback) -> 111 | unless @hasChangeObservers 112 | @oldHeadBufferPosition = @getHeadBufferPosition() 113 | @oldHeadScreenPosition = @getHeadScreenPosition() 114 | @oldTailBufferPosition = @getTailBufferPosition() 115 | @oldTailScreenPosition = @getTailScreenPosition() 116 | @wasValid = @isValid() 117 | @bufferMarkerSubscription = @bufferMarker.onDidChange (event) => @notifyObservers(event.textChanged) 118 | @hasChangeObservers = true 119 | @emitter.on 'did-change', callback 120 | 121 | # Essential: Invoke the given callback when the marker is destroyed. 122 | # 123 | # * `callback` {Function} to be called when the marker is destroyed. 124 | # 125 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 126 | onDidDestroy: (callback) -> 127 | @layer.markersWithDestroyListeners.add(this) 128 | @emitter.on('did-destroy', callback) 129 | 130 | ### 131 | Section: TextEditorMarker Details 132 | ### 133 | 134 | # Essential: Returns a {Boolean} indicating whether the marker is valid. 135 | # Markers can be invalidated when a region surrounding them in the buffer is 136 | # changed. 137 | isValid: -> 138 | @bufferMarker.isValid() 139 | 140 | # Essential: Returns a {Boolean} indicating whether the marker has been 141 | # destroyed. A marker can be invalid without being destroyed, in which case 142 | # undoing the invalidating operation would restore the marker. Once a marker 143 | # is destroyed by calling {DisplayMarker::destroy}, no undo/redo operation 144 | # can ever bring it back. 145 | isDestroyed: -> 146 | @layer.isDestroyed() or @bufferMarker.isDestroyed() 147 | 148 | # Essential: Returns a {Boolean} indicating whether the head precedes the tail. 149 | isReversed: -> 150 | @bufferMarker.isReversed() 151 | 152 | # Essential: Returns a {Boolean} indicating whether changes that occur exactly 153 | # at the marker's head or tail cause it to move. 154 | isExclusive: -> 155 | @bufferMarker.isExclusive() 156 | 157 | # Essential: Get the invalidation strategy for this marker. 158 | # 159 | # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. 160 | # 161 | # Returns a {String}. 162 | getInvalidationStrategy: -> 163 | @bufferMarker.getInvalidationStrategy() 164 | 165 | # Essential: Returns an {Object} containing any custom properties associated with 166 | # the marker. 167 | getProperties: -> 168 | @bufferMarker.getProperties() 169 | 170 | # Essential: Merges an {Object} containing new properties into the marker's 171 | # existing properties. 172 | # 173 | # * `properties` {Object} 174 | setProperties: (properties) -> 175 | @bufferMarker.setProperties(properties) 176 | 177 | # Essential: Returns whether this marker matches the given parameters. The 178 | # parameters are the same as {DisplayMarkerLayer::findMarkers}. 179 | matchesProperties: (attributes) -> 180 | attributes = @layer.translateToBufferMarkerParams(attributes) 181 | @bufferMarker.matchesParams(attributes) 182 | 183 | ### 184 | Section: Comparing to other markers 185 | ### 186 | 187 | # Essential: Compares this marker to another based on their ranges. 188 | # 189 | # * `other` {DisplayMarker} 190 | # 191 | # Returns a {Number} 192 | compare: (otherMarker) -> 193 | @bufferMarker.compare(otherMarker.bufferMarker) 194 | 195 | # Essential: Returns a {Boolean} indicating whether this marker is equivalent to 196 | # another marker, meaning they have the same range and options. 197 | # 198 | # * `other` {DisplayMarker} other marker 199 | isEqual: (other) -> 200 | return false unless other instanceof @constructor 201 | @bufferMarker.isEqual(other.bufferMarker) 202 | 203 | ### 204 | Section: Managing the marker's range 205 | ### 206 | 207 | # Essential: Gets the buffer range of this marker. 208 | # 209 | # Returns a {Range}. 210 | getBufferRange: -> 211 | @bufferMarker.getRange() 212 | 213 | # Essential: Gets the screen range of this marker. 214 | # 215 | # Returns a {Range}. 216 | getScreenRange: -> 217 | @layer.translateBufferRange(@getBufferRange()) 218 | 219 | # Essential: Modifies the buffer range of this marker. 220 | # 221 | # * `bufferRange` The new {Range} to use 222 | # * `properties` (optional) {Object} properties to associate with the marker. 223 | # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. 224 | setBufferRange: (bufferRange, properties) -> 225 | @bufferMarker.setRange(bufferRange, properties) 226 | 227 | # Essential: Modifies the screen range of this marker. 228 | # 229 | # * `screenRange` The new {Range} to use 230 | # * `options` (optional) An {Object} with the following keys: 231 | # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. 232 | # * `clipDirection` {String} If `'backward'`, returns the first valid 233 | # position preceding an invalid position. If `'forward'`, returns the 234 | # first valid position following an invalid position. If `'closest'`, 235 | # returns the first valid position closest to an invalid position. 236 | # Defaults to `'closest'`. Applies to the start and end of the given range. 237 | setScreenRange: (screenRange, options) -> 238 | @setBufferRange(@layer.translateScreenRange(screenRange, options), options) 239 | 240 | # Extended: Retrieves the buffer position of the marker's head. 241 | # 242 | # Returns a {Point}. 243 | getHeadBufferPosition: -> 244 | @bufferMarker.getHeadPosition() 245 | 246 | # Extended: Sets the buffer position of the marker's head. 247 | # 248 | # * `bufferPosition` The new {Point} to use 249 | setHeadBufferPosition: (bufferPosition) -> 250 | @bufferMarker.setHeadPosition(bufferPosition) 251 | 252 | # Extended: Retrieves the screen position of the marker's head. 253 | # 254 | # * `options` (optional) An {Object} with the following keys: 255 | # * `clipDirection` {String} If `'backward'`, returns the first valid 256 | # position preceding an invalid position. If `'forward'`, returns the 257 | # first valid position following an invalid position. If `'closest'`, 258 | # returns the first valid position closest to an invalid position. 259 | # Defaults to `'closest'`. Applies to the start and end of the given range. 260 | # 261 | # Returns a {Point}. 262 | getHeadScreenPosition: (options) -> 263 | @layer.translateBufferPosition(@bufferMarker.getHeadPosition(), options) 264 | 265 | # Extended: Sets the screen position of the marker's head. 266 | # 267 | # * `screenPosition` The new {Point} to use 268 | # * `options` (optional) An {Object} with the following keys: 269 | # * `clipDirection` {String} If `'backward'`, returns the first valid 270 | # position preceding an invalid position. If `'forward'`, returns the 271 | # first valid position following an invalid position. If `'closest'`, 272 | # returns the first valid position closest to an invalid position. 273 | # Defaults to `'closest'`. Applies to the start and end of the given range. 274 | setHeadScreenPosition: (screenPosition, options) -> 275 | @setHeadBufferPosition(@layer.translateScreenPosition(screenPosition, options)) 276 | 277 | # Extended: Retrieves the buffer position of the marker's tail. 278 | # 279 | # Returns a {Point}. 280 | getTailBufferPosition: -> 281 | @bufferMarker.getTailPosition() 282 | 283 | # Extended: Sets the buffer position of the marker's tail. 284 | # 285 | # * `bufferPosition` The new {Point} to use 286 | setTailBufferPosition: (bufferPosition) -> 287 | @bufferMarker.setTailPosition(bufferPosition) 288 | 289 | # Extended: Retrieves the screen position of the marker's tail. 290 | # 291 | # * `options` (optional) An {Object} with the following keys: 292 | # * `clipDirection` {String} If `'backward'`, returns the first valid 293 | # position preceding an invalid position. If `'forward'`, returns the 294 | # first valid position following an invalid position. If `'closest'`, 295 | # returns the first valid position closest to an invalid position. 296 | # Defaults to `'closest'`. Applies to the start and end of the given range. 297 | # 298 | # Returns a {Point}. 299 | getTailScreenPosition: (options) -> 300 | @layer.translateBufferPosition(@bufferMarker.getTailPosition(), options) 301 | 302 | # Extended: Sets the screen position of the marker's tail. 303 | # 304 | # * `screenPosition` The new {Point} to use 305 | # * `options` (optional) An {Object} with the following keys: 306 | # * `clipDirection` {String} If `'backward'`, returns the first valid 307 | # position preceding an invalid position. If `'forward'`, returns the 308 | # first valid position following an invalid position. If `'closest'`, 309 | # returns the first valid position closest to an invalid position. 310 | # Defaults to `'closest'`. Applies to the start and end of the given range. 311 | setTailScreenPosition: (screenPosition, options) -> 312 | @bufferMarker.setTailPosition(@layer.translateScreenPosition(screenPosition, options)) 313 | 314 | # Extended: Retrieves the buffer position of the marker's start. This will always be 315 | # less than or equal to the result of {DisplayMarker::getEndBufferPosition}. 316 | # 317 | # Returns a {Point}. 318 | getStartBufferPosition: -> 319 | @bufferMarker.getStartPosition() 320 | 321 | # Essential: Retrieves the screen position of the marker's start. This will always be 322 | # less than or equal to the result of {DisplayMarker::getEndScreenPosition}. 323 | # 324 | # * `options` (optional) An {Object} with the following keys: 325 | # * `clipDirection` {String} If `'backward'`, returns the first valid 326 | # position preceding an invalid position. If `'forward'`, returns the 327 | # first valid position following an invalid position. If `'closest'`, 328 | # returns the first valid position closest to an invalid position. 329 | # Defaults to `'closest'`. Applies to the start and end of the given range. 330 | # 331 | # Returns a {Point}. 332 | getStartScreenPosition: (options) -> 333 | @layer.translateBufferPosition(@getStartBufferPosition(), options) 334 | 335 | # Extended: Retrieves the buffer position of the marker's end. This will always be 336 | # greater than or equal to the result of {DisplayMarker::getStartBufferPosition}. 337 | # 338 | # Returns a {Point}. 339 | getEndBufferPosition: -> 340 | @bufferMarker.getEndPosition() 341 | 342 | # Essential: Retrieves the screen position of the marker's end. This will always be 343 | # greater than or equal to the result of {DisplayMarker::getStartScreenPosition}. 344 | # 345 | # * `options` (optional) An {Object} with the following keys: 346 | # * `clipDirection` {String} If `'backward'`, returns the first valid 347 | # position preceding an invalid position. If `'forward'`, returns the 348 | # first valid position following an invalid position. If `'closest'`, 349 | # returns the first valid position closest to an invalid position. 350 | # Defaults to `'closest'`. Applies to the start and end of the given range. 351 | # 352 | # Returns a {Point}. 353 | getEndScreenPosition: (options) -> 354 | @layer.translateBufferPosition(@getEndBufferPosition(), options) 355 | 356 | # Extended: Returns a {Boolean} indicating whether the marker has a tail. 357 | hasTail: -> 358 | @bufferMarker.hasTail() 359 | 360 | # Extended: Plants the marker's tail at the current head position. After calling 361 | # the marker's tail position will be its head position at the time of the 362 | # call, regardless of where the marker's head is moved. 363 | plantTail: -> 364 | @bufferMarker.plantTail() 365 | 366 | # Extended: Removes the marker's tail. After calling the marker's head position 367 | # will be reported as its current tail position until the tail is planted 368 | # again. 369 | clearTail: -> 370 | @bufferMarker.clearTail() 371 | 372 | toString: -> 373 | "[Marker #{@id}, bufferRange: #{@getBufferRange()}, screenRange: #{@getScreenRange()}}]" 374 | 375 | ### 376 | Section: Private 377 | ### 378 | 379 | inspect: -> 380 | @toString() 381 | 382 | notifyObservers: (textChanged) -> 383 | return unless @hasChangeObservers 384 | textChanged ?= false 385 | 386 | newHeadBufferPosition = @getHeadBufferPosition() 387 | newHeadScreenPosition = @getHeadScreenPosition() 388 | newTailBufferPosition = @getTailBufferPosition() 389 | newTailScreenPosition = @getTailScreenPosition() 390 | isValid = @isValid() 391 | 392 | return if isValid is @wasValid and 393 | newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and 394 | newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and 395 | newTailBufferPosition.isEqual(@oldTailBufferPosition) and 396 | newTailScreenPosition.isEqual(@oldTailScreenPosition) 397 | 398 | changeEvent = { 399 | @oldHeadScreenPosition, newHeadScreenPosition, 400 | @oldTailScreenPosition, newTailScreenPosition, 401 | @oldHeadBufferPosition, newHeadBufferPosition, 402 | @oldTailBufferPosition, newTailBufferPosition, 403 | textChanged, 404 | @wasValid, 405 | isValid 406 | } 407 | 408 | @oldHeadBufferPosition = newHeadBufferPosition 409 | @oldHeadScreenPosition = newHeadScreenPosition 410 | @oldTailBufferPosition = newTailBufferPosition 411 | @oldTailScreenPosition = newTailScreenPosition 412 | @wasValid = isValid 413 | 414 | @emitter.emit 'did-change', changeEvent 415 | -------------------------------------------------------------------------------- /spec/display-marker-layer-spec.coffee: -------------------------------------------------------------------------------- 1 | TextBuffer = require '../src/text-buffer' 2 | Point = require '../src/point' 3 | Range = require '../src/range' 4 | SampleText = require './helpers/sample-text' 5 | 6 | describe "DisplayMarkerLayer", -> 7 | beforeEach -> 8 | jasmine.addCustomEqualityTester(require("underscore-plus").isEqual) 9 | 10 | it "allows DisplayMarkers to be created and manipulated in screen coordinates", -> 11 | buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') 12 | displayLayer = buffer.addDisplayLayer(tabLength: 4) 13 | markerLayer = displayLayer.addMarkerLayer() 14 | 15 | marker = markerLayer.markScreenRange([[3, 4], [4, 2]]) 16 | expect(marker.getScreenRange()).toEqual [[3, 4], [4, 2]] 17 | expect(marker.getBufferRange()).toEqual [[3, 2], [4, 2]] 18 | 19 | markerChangeEvents = [] 20 | marker.onDidChange (change) -> markerChangeEvents.push(change) 21 | 22 | marker.setScreenRange([[3, 8], [4, 3]]) 23 | 24 | expect(marker.getBufferRange()).toEqual([[3, 4], [4, 3]]) 25 | expect(marker.getScreenRange()).toEqual([[3, 8], [4, 3]]) 26 | expect(markerChangeEvents[0]).toEqual { 27 | oldHeadBufferPosition: [4, 2] 28 | newHeadBufferPosition: [4, 3] 29 | oldTailBufferPosition: [3, 2] 30 | newTailBufferPosition: [3, 4] 31 | oldHeadScreenPosition: [4, 2] 32 | newHeadScreenPosition: [4, 3] 33 | oldTailScreenPosition: [3, 4] 34 | newTailScreenPosition: [3, 8] 35 | wasValid: true 36 | isValid: true 37 | textChanged: false 38 | } 39 | 40 | markerChangeEvents = [] 41 | buffer.insert([4, 0], '\t') 42 | 43 | expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) 44 | expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]) 45 | expect(markerChangeEvents[0]).toEqual { 46 | oldHeadBufferPosition: [4, 3] 47 | newHeadBufferPosition: [4, 4] 48 | oldTailBufferPosition: [3, 4] 49 | newTailBufferPosition: [3, 4] 50 | oldHeadScreenPosition: [4, 3] 51 | newHeadScreenPosition: [4, 7] 52 | oldTailScreenPosition: [3, 8] 53 | newTailScreenPosition: [3, 8] 54 | wasValid: true 55 | isValid: true 56 | textChanged: true 57 | } 58 | 59 | expect(markerLayer.getMarker(marker.id)).toBe marker 60 | 61 | markerChangeEvents = [] 62 | foldId = displayLayer.foldBufferRange([[0, 2], [2, 2]]) 63 | 64 | expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) 65 | expect(marker.getScreenRange()).toEqual([[1, 8], [2, 7]]) 66 | expect(markerChangeEvents[0]).toEqual { 67 | oldHeadBufferPosition: [4, 4] 68 | newHeadBufferPosition: [4, 4] 69 | oldTailBufferPosition: [3, 4] 70 | newTailBufferPosition: [3, 4] 71 | oldHeadScreenPosition: [4, 7] 72 | newHeadScreenPosition: [2, 7] 73 | oldTailScreenPosition: [3, 8] 74 | newTailScreenPosition: [1, 8] 75 | wasValid: true 76 | isValid: true 77 | textChanged: false 78 | } 79 | 80 | markerChangeEvents = [] 81 | displayLayer.destroyFold(foldId) 82 | 83 | expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) 84 | expect(marker.getScreenRange()).toEqual([[3, 8], [4, 7]]) 85 | expect(markerChangeEvents[0]).toEqual { 86 | oldHeadBufferPosition: [4, 4] 87 | newHeadBufferPosition: [4, 4] 88 | oldTailBufferPosition: [3, 4] 89 | newTailBufferPosition: [3, 4] 90 | oldHeadScreenPosition: [2, 7] 91 | newHeadScreenPosition: [4, 7] 92 | oldTailScreenPosition: [1, 8] 93 | newTailScreenPosition: [3, 8] 94 | wasValid: true 95 | isValid: true 96 | textChanged: false 97 | } 98 | 99 | markerChangeEvents = [] 100 | displayLayer.reset({tabLength: 3}) 101 | 102 | expect(marker.getBufferRange()).toEqual([[3, 4], [4, 4]]) 103 | expect(marker.getScreenRange()).toEqual([[3, 6], [4, 6]]) 104 | expect(markerChangeEvents[0]).toEqual { 105 | oldHeadBufferPosition: [4, 4] 106 | newHeadBufferPosition: [4, 4] 107 | oldTailBufferPosition: [3, 4] 108 | newTailBufferPosition: [3, 4] 109 | oldHeadScreenPosition: [4, 7] 110 | newHeadScreenPosition: [4, 6] 111 | oldTailScreenPosition: [3, 8] 112 | newTailScreenPosition: [3, 6] 113 | wasValid: true 114 | isValid: true 115 | textChanged: false 116 | } 117 | 118 | it "does not create duplicate DisplayMarkers when it has onDidCreateMarker observers (regression)", -> 119 | buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') 120 | displayLayer = buffer.addDisplayLayer(tabLength: 4) 121 | markerLayer = displayLayer.addMarkerLayer() 122 | 123 | emittedMarker = null 124 | markerLayer.onDidCreateMarker (marker) -> 125 | emittedMarker = marker 126 | 127 | createdMarker = markerLayer.markBufferRange([[0, 1], [2, 3]]) 128 | expect(createdMarker).toBe(emittedMarker) 129 | 130 | it "emits events when markers are created and destroyed", -> 131 | buffer = new TextBuffer(text: 'hello world') 132 | displayLayer = buffer.addDisplayLayer(tabLength: 4) 133 | markerLayer = displayLayer.addMarkerLayer() 134 | createdMarkers = [] 135 | markerLayer.onDidCreateMarker (m) -> createdMarkers.push(m) 136 | marker = markerLayer.markScreenRange([[0, 4], [1, 4]]) 137 | 138 | expect(createdMarkers).toEqual [marker] 139 | 140 | destroyEventCount = 0 141 | marker.onDidDestroy -> destroyEventCount++ 142 | 143 | marker.destroy() 144 | expect(destroyEventCount).toBe 1 145 | 146 | it "emits update events when markers are created, updated directly, updated indirectly, or destroyed", (done) -> 147 | buffer = new TextBuffer(text: 'hello world') 148 | displayLayer = buffer.addDisplayLayer(tabLength: 4) 149 | markerLayer = displayLayer.addMarkerLayer() 150 | marker = null 151 | 152 | updateEventCount = 0 153 | markerLayer.onDidUpdate -> 154 | updateEventCount++ 155 | if updateEventCount is 1 156 | marker.setScreenRange([[0, 5], [1, 0]]) 157 | else if updateEventCount is 2 158 | buffer.insert([0, 0], '\t') 159 | else if updateEventCount is 3 160 | marker.destroy() 161 | else if updateEventCount is 4 162 | done() 163 | 164 | buffer.transact -> 165 | marker = markerLayer.markScreenRange([[0, 4], [1, 4]]) 166 | 167 | it "allows markers to be copied", -> 168 | buffer = new TextBuffer(text: '\ta\tbc\tdef\tg\n\th') 169 | displayLayer = buffer.addDisplayLayer(tabLength: 4) 170 | markerLayer = displayLayer.addMarkerLayer() 171 | 172 | markerA = markerLayer.markScreenRange([[0, 4], [1, 4]], a: 1, b: 2) 173 | markerB = markerA.copy(b: 3, c: 4) 174 | 175 | expect(markerB.id).not.toBe(markerA.id) 176 | expect(markerB.getProperties()).toEqual({a: 1, b: 3, c: 4}) 177 | expect(markerB.getScreenRange()).toEqual(markerA.getScreenRange()) 178 | 179 | describe "::destroy()", -> 180 | it "only destroys the underlying buffer MarkerLayer if the DisplayMarkerLayer was created by calling addMarkerLayer on its parent DisplayLayer", -> 181 | buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') 182 | displayLayer1 = buffer.addDisplayLayer(tabLength: 2) 183 | displayLayer2 = buffer.addDisplayLayer(tabLength: 4) 184 | bufferMarkerLayer = buffer.addMarkerLayer() 185 | bufferMarker1 = bufferMarkerLayer.markRange [[2, 1], [2, 2]] 186 | displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) 187 | displayMarker1 = displayMarkerLayer1.markBufferRange [[1, 0], [1, 2]] 188 | displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) 189 | displayMarker2 = displayMarkerLayer2.markBufferRange [[2, 0], [2, 1]] 190 | displayMarkerLayer3 = displayLayer2.addMarkerLayer() 191 | displayMarker3 = displayMarkerLayer3.markBufferRange [[0, 0], [0, 0]] 192 | 193 | displayMarkerLayer1DestroyEventCount = 0 194 | displayMarkerLayer1.onDidDestroy -> displayMarkerLayer1DestroyEventCount++ 195 | displayMarkerLayer2DestroyEventCount = 0 196 | displayMarkerLayer2.onDidDestroy -> displayMarkerLayer2DestroyEventCount++ 197 | displayMarkerLayer3DestroyEventCount = 0 198 | displayMarkerLayer3.onDidDestroy -> displayMarkerLayer3DestroyEventCount++ 199 | 200 | displayMarkerLayer1.destroy() 201 | expect(bufferMarkerLayer.isDestroyed()).toBe(false) 202 | expect(displayMarkerLayer1.isDestroyed()).toBe(true) 203 | expect(displayMarkerLayer1DestroyEventCount).toBe(1) 204 | expect(bufferMarker1.isDestroyed()).toBe(false) 205 | expect(displayMarker1.isDestroyed()).toBe(true) 206 | expect(displayMarker2.isDestroyed()).toBe(false) 207 | expect(displayMarker3.isDestroyed()).toBe(false) 208 | 209 | displayMarkerLayer2.destroy() 210 | expect(bufferMarkerLayer.isDestroyed()).toBe(false) 211 | expect(displayMarkerLayer2.isDestroyed()).toBe(true) 212 | expect(displayMarkerLayer2DestroyEventCount).toBe(1) 213 | expect(bufferMarker1.isDestroyed()).toBe(false) 214 | expect(displayMarker1.isDestroyed()).toBe(true) 215 | expect(displayMarker2.isDestroyed()).toBe(true) 216 | expect(displayMarker3.isDestroyed()).toBe(false) 217 | 218 | bufferMarkerLayer.destroy() 219 | expect(bufferMarkerLayer.isDestroyed()).toBe(true) 220 | expect(displayMarkerLayer1DestroyEventCount).toBe(1) 221 | expect(displayMarkerLayer2DestroyEventCount).toBe(1) 222 | expect(bufferMarker1.isDestroyed()).toBe(true) 223 | expect(displayMarker1.isDestroyed()).toBe(true) 224 | expect(displayMarker2.isDestroyed()).toBe(true) 225 | expect(displayMarker3.isDestroyed()).toBe(false) 226 | 227 | displayMarkerLayer3.destroy() 228 | expect(displayMarkerLayer3.bufferMarkerLayer.isDestroyed()).toBe(true) 229 | expect(displayMarkerLayer3.isDestroyed()).toBe(true) 230 | expect(displayMarkerLayer3DestroyEventCount).toBe(1) 231 | expect(displayMarker3.isDestroyed()).toBe(true) 232 | 233 | it "destroys the layer's markers", -> 234 | buffer = new TextBuffer() 235 | displayLayer = buffer.addDisplayLayer() 236 | displayMarkerLayer = displayLayer.addMarkerLayer() 237 | 238 | marker1 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]) 239 | marker2 = displayMarkerLayer.markBufferRange([[0, 0], [0, 0]]) 240 | 241 | destroyListener = jasmine.createSpy('onDidDestroy listener') 242 | marker1.onDidDestroy(destroyListener) 243 | 244 | displayMarkerLayer.destroy() 245 | 246 | expect(destroyListener).toHaveBeenCalled() 247 | expect(marker1.isDestroyed()).toBe(true) 248 | 249 | # Markers states are updated regardless of whether they have an 250 | # ::onDidDestroy listener 251 | expect(marker2.isDestroyed()).toBe(true) 252 | 253 | it "destroys display markers when their underlying buffer markers are destroyed", -> 254 | buffer = new TextBuffer(text: '\tabc') 255 | displayLayer1 = buffer.addDisplayLayer(tabLength: 2) 256 | displayLayer2 = buffer.addDisplayLayer(tabLength: 4) 257 | bufferMarkerLayer = buffer.addMarkerLayer() 258 | displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) 259 | displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) 260 | 261 | bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]) 262 | 263 | displayMarker1 = displayMarkerLayer1.getMarker(bufferMarker.id) 264 | displayMarker2 = displayMarkerLayer2.getMarker(bufferMarker.id) 265 | expect(displayMarker1.getScreenRange()).toEqual([[0, 2], [0, 3]]) 266 | expect(displayMarker2.getScreenRange()).toEqual([[0, 4], [0, 5]]) 267 | 268 | displayMarker1DestroyCount = 0 269 | displayMarker2DestroyCount = 0 270 | displayMarker1.onDidDestroy -> displayMarker1DestroyCount++ 271 | displayMarker2.onDidDestroy -> displayMarker2DestroyCount++ 272 | 273 | bufferMarker.destroy() 274 | expect(displayMarker1DestroyCount).toBe(1) 275 | expect(displayMarker2DestroyCount).toBe(1) 276 | 277 | it "does not throw exceptions when buffer markers are destroyed that don't have corresponding display markers", -> 278 | buffer = new TextBuffer(text: '\tabc') 279 | displayLayer1 = buffer.addDisplayLayer(tabLength: 2) 280 | displayLayer2 = buffer.addDisplayLayer(tabLength: 4) 281 | bufferMarkerLayer = buffer.addMarkerLayer() 282 | displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) 283 | displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) 284 | 285 | bufferMarker = bufferMarkerLayer.markRange([[0, 1], [0, 2]]) 286 | bufferMarker.destroy() 287 | 288 | it "destroys itself when the underlying buffer marker layer is destroyed", -> 289 | buffer = new TextBuffer(text: 'abc\ndef\nghi\nj\tk\tl\nmno') 290 | displayLayer1 = buffer.addDisplayLayer(tabLength: 2) 291 | displayLayer2 = buffer.addDisplayLayer(tabLength: 4) 292 | 293 | bufferMarkerLayer = buffer.addMarkerLayer() 294 | displayMarkerLayer1 = displayLayer1.getMarkerLayer(bufferMarkerLayer.id) 295 | displayMarkerLayer2 = displayLayer2.getMarkerLayer(bufferMarkerLayer.id) 296 | displayMarkerLayer1DestroyEventCount = 0 297 | displayMarkerLayer1.onDidDestroy -> displayMarkerLayer1DestroyEventCount++ 298 | displayMarkerLayer2DestroyEventCount = 0 299 | displayMarkerLayer2.onDidDestroy -> displayMarkerLayer2DestroyEventCount++ 300 | 301 | bufferMarkerLayer.destroy() 302 | expect(displayMarkerLayer1.isDestroyed()).toBe(true) 303 | expect(displayMarkerLayer1DestroyEventCount).toBe(1) 304 | expect(displayMarkerLayer2.isDestroyed()).toBe(true) 305 | expect(displayMarkerLayer2DestroyEventCount).toBe(1) 306 | 307 | describe "findMarkers(params)", -> 308 | [markerLayer, displayLayer] = [] 309 | 310 | beforeEach -> 311 | buffer = new TextBuffer(text: SampleText) 312 | displayLayer = buffer.addDisplayLayer(tabLength: 4) 313 | markerLayer = displayLayer.addMarkerLayer() 314 | 315 | it "allows the startBufferRow and endBufferRow to be specified", -> 316 | marker1 = markerLayer.markBufferRange([[0, 0], [3, 0]], class: 'a') 317 | marker2 = markerLayer.markBufferRange([[0, 0], [5, 0]], class: 'a') 318 | marker3 = markerLayer.markBufferRange([[9, 0], [10, 0]], class: 'b') 319 | 320 | expect(markerLayer.findMarkers(class: 'a', startBufferRow: 0)).toEqual [marker2, marker1] 321 | expect(markerLayer.findMarkers(class: 'a', startBufferRow: 0, endBufferRow: 3)).toEqual [marker1] 322 | expect(markerLayer.findMarkers(endBufferRow: 10)).toEqual [marker3] 323 | 324 | it "allows the startScreenRow and endScreenRow to be specified", -> 325 | marker1 = markerLayer.markBufferRange([[6, 0], [7, 0]], class: 'a') 326 | marker2 = markerLayer.markBufferRange([[9, 0], [10, 0]], class: 'a') 327 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 328 | expect(markerLayer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2] 329 | 330 | displayLayer.destroyFoldsIntersectingBufferRange([[4, 0], [7, 0]]) 331 | displayLayer.foldBufferRange([[0, 20], [12, 2]]) 332 | marker3 = markerLayer.markBufferRange([[12, 0], [12, 0]], class: 'a') 333 | expect(markerLayer.findMarkers(class: 'a', startScreenRow: 0)).toEqual [marker1, marker2, marker3] 334 | expect(markerLayer.findMarkers(class: 'a', endScreenRow: 0)).toEqual [marker1, marker2, marker3] 335 | 336 | it "allows the startsInBufferRange/endsInBufferRange and startsInScreenRange/endsInScreenRange to be specified", -> 337 | marker1 = markerLayer.markBufferRange([[5, 2], [5, 4]], class: 'a') 338 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 2]], class: 'a') 339 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 340 | expect(markerLayer.findMarkers(class: 'a', startsInBufferRange: [[5, 1], [5, 3]])).toEqual [marker1] 341 | expect(markerLayer.findMarkers(class: 'a', endsInBufferRange: [[8, 1], [8, 3]])).toEqual [marker2] 342 | expect(markerLayer.findMarkers(class: 'a', startsInScreenRange: [[4, 0], [4, 1]])).toEqual [marker1] 343 | expect(markerLayer.findMarkers(class: 'a', endsInScreenRange: [[5, 1], [5, 3]])).toEqual [marker2] 344 | 345 | it "allows intersectsBufferRowRange to be specified", -> 346 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 347 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 348 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 349 | expect(markerLayer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1] 350 | 351 | it "allows intersectsScreenRowRange to be specified", -> 352 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 353 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 354 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 355 | expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2] 356 | 357 | displayLayer.destroyAllFolds() 358 | displayLayer.foldBufferRange([[0, 20], [12, 2]]) 359 | expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [0, 0])).toEqual [marker1, marker2] 360 | 361 | displayLayer.destroyAllFolds() 362 | displayLayer.reset({softWrapColumn: 10}) 363 | marker1.setHeadScreenPosition([6, 5]) 364 | marker2.setHeadScreenPosition([9, 2]) 365 | expect(markerLayer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 7])).toEqual [marker1] 366 | 367 | it "allows containedInScreenRange to be specified", -> 368 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 369 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 370 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 371 | expect(markerLayer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2] 372 | 373 | it "allows intersectsBufferRange to be specified", -> 374 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 375 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 376 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 377 | expect(markerLayer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1] 378 | 379 | it "allows intersectsScreenRange to be specified", -> 380 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 381 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 382 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 383 | expect(markerLayer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2] 384 | 385 | it "allows containsBufferPosition to be specified", -> 386 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 387 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 388 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 389 | expect(markerLayer.findMarkers(class: 'a', containsBufferPosition: [8, 0])).toEqual [marker2] 390 | 391 | it "allows containsScreenPosition to be specified", -> 392 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 0]], class: 'a') 393 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 0]], class: 'a') 394 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 395 | expect(markerLayer.findMarkers(class: 'a', containsScreenPosition: [5, 0])).toEqual [marker2] 396 | 397 | it "allows containsBufferRange to be specified", -> 398 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], class: 'a') 399 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], class: 'a') 400 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 401 | expect(markerLayer.findMarkers(class: 'a', containsBufferRange: [[8, 2], [8, 4]])).toEqual [marker2] 402 | 403 | it "allows containsScreenRange to be specified", -> 404 | marker1 = markerLayer.markBufferRange([[5, 0], [5, 10]], class: 'a') 405 | marker2 = markerLayer.markBufferRange([[8, 0], [8, 10]], class: 'a') 406 | displayLayer.foldBufferRange([[4, 0], [7, 0]]) 407 | expect(markerLayer.findMarkers(class: 'a', containsScreenRange: [[5, 2], [5, 4]])).toEqual [marker2] 408 | 409 | it "works when used from within a Marker.onDidDestroy callback (regression)", -> 410 | displayMarker = markerLayer.markBufferRange([[0, 3], [0, 6]]) 411 | displayMarker.onDidDestroy -> 412 | expect(markerLayer.findMarkers({containsBufferPosition: [0, 4]})).not.toContain(displayMarker) 413 | displayMarker.destroy() 414 | -------------------------------------------------------------------------------- /src/display-marker-layer.coffee: -------------------------------------------------------------------------------- 1 | {Emitter, CompositeDisposable} = require 'event-kit' 2 | DisplayMarker = require './display-marker' 3 | Range = require './range' 4 | Point = require './point' 5 | 6 | # Public: *Experimental:* A container for a related set of markers at the 7 | # {DisplayLayer} level. Wraps an underlying {MarkerLayer} on the {TextBuffer}. 8 | # 9 | # This API is experimental and subject to change on any release. 10 | module.exports = 11 | class DisplayMarkerLayer 12 | constructor: (@displayLayer, @bufferMarkerLayer, @ownsBufferMarkerLayer) -> 13 | {@id} = @bufferMarkerLayer 14 | @bufferMarkerLayer.displayMarkerLayers.add(this) 15 | @markersById = {} 16 | @destroyed = false 17 | @emitter = new Emitter 18 | @subscriptions = new CompositeDisposable 19 | @markersWithDestroyListeners = new Set 20 | @subscriptions.add(@bufferMarkerLayer.onDidUpdate(@emitDidUpdate.bind(this))) 21 | 22 | ### 23 | Section: Lifecycle 24 | ### 25 | 26 | # Essential: Destroy this layer. 27 | destroy: -> 28 | return if @destroyed 29 | @destroyed = true 30 | @clear() if @ownsBufferMarkerLayer 31 | @subscriptions.dispose() 32 | @bufferMarkerLayer.displayMarkerLayers.delete(this) 33 | @bufferMarkerLayer.destroy() if @ownsBufferMarkerLayer 34 | @displayLayer.didDestroyMarkerLayer(@id) 35 | @emitter.emit('did-destroy') 36 | @emitter.clear() 37 | 38 | # Public: Destroy all markers in this layer. 39 | clear: -> 40 | @bufferMarkerLayer.clear() 41 | 42 | didClearBufferMarkerLayer: -> 43 | @markersWithDestroyListeners.forEach (marker) -> marker.didDestroyBufferMarker() 44 | @markersById = {} 45 | 46 | # Essential: Determine whether this layer has been destroyed. 47 | # 48 | # Returns a {Boolean}. 49 | isDestroyed: -> 50 | @destroyed 51 | 52 | ### 53 | Section: Event Subscription 54 | ### 55 | 56 | # Public: Subscribe to be notified synchronously when this layer is destroyed. 57 | # 58 | # Returns a {Disposable}. 59 | onDidDestroy: (callback) -> 60 | @emitter.on('did-destroy', callback) 61 | 62 | # Public: Subscribe to be notified asynchronously whenever markers are 63 | # created, updated, or destroyed on this layer. *Prefer this method for 64 | # optimal performance when interacting with layers that could contain large 65 | # numbers of markers.* 66 | # 67 | # * `callback` A {Function} that will be called with no arguments when changes 68 | # occur on this layer. 69 | # 70 | # Subscribers are notified once, asynchronously when any number of changes 71 | # occur in a given tick of the event loop. You should re-query the layer 72 | # to determine the state of markers in which you're interested in. It may 73 | # be counter-intuitive, but this is much more efficient than subscribing to 74 | # events on individual markers, which are expensive to deliver. 75 | # 76 | # Returns a {Disposable}. 77 | onDidUpdate: (callback) -> 78 | @emitter.on('did-update', callback) 79 | 80 | # Public: Subscribe to be notified synchronously whenever markers are created 81 | # on this layer. *Avoid this method for optimal performance when interacting 82 | # with layers that could contain large numbers of markers.* 83 | # 84 | # * `callback` A {Function} that will be called with a {TextEditorMarker} 85 | # whenever a new marker is created. 86 | # 87 | # You should prefer {::onDidUpdate} when synchronous notifications aren't 88 | # absolutely necessary. 89 | # 90 | # Returns a {Disposable}. 91 | onDidCreateMarker: (callback) -> 92 | @bufferMarkerLayer.onDidCreateMarker (bufferMarker) => 93 | callback(@getMarker(bufferMarker.id)) 94 | 95 | ### 96 | Section: Marker creation 97 | ### 98 | 99 | # Public: Create a marker with the given screen range. 100 | # 101 | # * `range` A {Range} or range-compatible {Array} 102 | # * `options` A hash of key-value pairs to associate with the marker. There 103 | # are also reserved property names that have marker-specific meaning. 104 | # * `reversed` (optional) {Boolean} Creates the marker in a reversed 105 | # orientation. (default: false) 106 | # * `invalidate` (optional) {String} Determines the rules by which changes 107 | # to the buffer *invalidate* the marker. (default: 'overlap') It can be 108 | # any of the following strategies, in order of fragility: 109 | # * __never__: The marker is never marked as invalid. This is a good choice for 110 | # markers representing selections in an editor. 111 | # * __surround__: The marker is invalidated by changes that completely surround it. 112 | # * __overlap__: The marker is invalidated by changes that surround the 113 | # start or end of the marker. This is the default. 114 | # * __inside__: The marker is invalidated by changes that extend into the 115 | # inside of the marker. Changes that end at the marker's start or 116 | # start at the marker's end do not invalidate the marker. 117 | # * __touch__: The marker is invalidated by a change that touches the marked 118 | # region in any way, including changes that end at the marker's 119 | # start or start at the marker's end. This is the most fragile strategy. 120 | # * `exclusive` {Boolean} indicating whether insertions at the start or end 121 | # of the marked range should be interpreted as happening *outside* the 122 | # marker. Defaults to `false`, except when using the `inside` 123 | # invalidation strategy or when when the marker has no tail, in which 124 | # case it defaults to true. Explicitly assigning this option overrides 125 | # behavior in all circumstances. 126 | # * `clipDirection` {String} If `'backward'`, returns the first valid 127 | # position preceding an invalid position. If `'forward'`, returns the 128 | # first valid position following an invalid position. If `'closest'`, 129 | # returns the first valid position closest to an invalid position. 130 | # Defaults to `'closest'`. Applies to the start and end of the given range. 131 | # 132 | # Returns a {DisplayMarker}. 133 | markScreenRange: (screenRange, options) -> 134 | screenRange = Range.fromObject(screenRange) 135 | bufferRange = @displayLayer.translateScreenRange(screenRange, options) 136 | @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) 137 | 138 | # Public: Create a marker on this layer with its head at the given screen 139 | # position and no tail. 140 | # 141 | # * `screenPosition` A {Point} or point-compatible {Array} 142 | # * `options` (optional) An {Object} with the following keys: 143 | # * `invalidate` (optional) {String} Determines the rules by which changes 144 | # to the buffer *invalidate* the marker. (default: 'overlap') It can be 145 | # any of the following strategies, in order of fragility: 146 | # * __never__: The marker is never marked as invalid. This is a good choice for 147 | # markers representing selections in an editor. 148 | # * __surround__: The marker is invalidated by changes that completely surround it. 149 | # * __overlap__: The marker is invalidated by changes that surround the 150 | # start or end of the marker. This is the default. 151 | # * __inside__: The marker is invalidated by changes that extend into the 152 | # inside of the marker. Changes that end at the marker's start or 153 | # start at the marker's end do not invalidate the marker. 154 | # * __touch__: The marker is invalidated by a change that touches the marked 155 | # region in any way, including changes that end at the marker's 156 | # start or start at the marker's end. This is the most fragile strategy. 157 | # * `exclusive` {Boolean} indicating whether insertions at the start or end 158 | # of the marked range should be interpreted as happening *outside* the 159 | # marker. Defaults to `false`, except when using the `inside` 160 | # invalidation strategy or when when the marker has no tail, in which 161 | # case it defaults to true. Explicitly assigning this option overrides 162 | # behavior in all circumstances. 163 | # * `clipDirection` {String} If `'backward'`, returns the first valid 164 | # position preceding an invalid position. If `'forward'`, returns the 165 | # first valid position following an invalid position. If `'closest'`, 166 | # returns the first valid position closest to an invalid position. 167 | # Defaults to `'closest'`. 168 | # 169 | # Returns a {DisplayMarker}. 170 | markScreenPosition: (screenPosition, options) -> 171 | screenPosition = Point.fromObject(screenPosition) 172 | bufferPosition = @displayLayer.translateScreenPosition(screenPosition, options) 173 | @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id) 174 | 175 | # Public: Create a marker with the given buffer range. 176 | # 177 | # * `range` A {Range} or range-compatible {Array} 178 | # * `options` A hash of key-value pairs to associate with the marker. There 179 | # are also reserved property names that have marker-specific meaning. 180 | # * `reversed` (optional) {Boolean} Creates the marker in a reversed 181 | # orientation. (default: false) 182 | # * `invalidate` (optional) {String} Determines the rules by which changes 183 | # to the buffer *invalidate* the marker. (default: 'overlap') It can be 184 | # any of the following strategies, in order of fragility: 185 | # * __never__: The marker is never marked as invalid. This is a good choice for 186 | # markers representing selections in an editor. 187 | # * __surround__: The marker is invalidated by changes that completely surround it. 188 | # * __overlap__: The marker is invalidated by changes that surround the 189 | # start or end of the marker. This is the default. 190 | # * __inside__: The marker is invalidated by changes that extend into the 191 | # inside of the marker. Changes that end at the marker's start or 192 | # start at the marker's end do not invalidate the marker. 193 | # * __touch__: The marker is invalidated by a change that touches the marked 194 | # region in any way, including changes that end at the marker's 195 | # start or start at the marker's end. This is the most fragile strategy. 196 | # * `exclusive` {Boolean} indicating whether insertions at the start or end 197 | # of the marked range should be interpreted as happening *outside* the 198 | # marker. Defaults to `false`, except when using the `inside` 199 | # invalidation strategy or when when the marker has no tail, in which 200 | # case it defaults to true. Explicitly assigning this option overrides 201 | # behavior in all circumstances. 202 | # 203 | # Returns a {DisplayMarker}. 204 | markBufferRange: (bufferRange, options) -> 205 | bufferRange = Range.fromObject(bufferRange) 206 | @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) 207 | 208 | # Public: Create a marker on this layer with its head at the given buffer 209 | # position and no tail. 210 | # 211 | # * `bufferPosition` A {Point} or point-compatible {Array} 212 | # * `options` (optional) An {Object} with the following keys: 213 | # * `invalidate` (optional) {String} Determines the rules by which changes 214 | # to the buffer *invalidate* the marker. (default: 'overlap') It can be 215 | # any of the following strategies, in order of fragility: 216 | # * __never__: The marker is never marked as invalid. This is a good choice for 217 | # markers representing selections in an editor. 218 | # * __surround__: The marker is invalidated by changes that completely surround it. 219 | # * __overlap__: The marker is invalidated by changes that surround the 220 | # start or end of the marker. This is the default. 221 | # * __inside__: The marker is invalidated by changes that extend into the 222 | # inside of the marker. Changes that end at the marker's start or 223 | # start at the marker's end do not invalidate the marker. 224 | # * __touch__: The marker is invalidated by a change that touches the marked 225 | # region in any way, including changes that end at the marker's 226 | # start or start at the marker's end. This is the most fragile strategy. 227 | # * `exclusive` {Boolean} indicating whether insertions at the start or end 228 | # of the marked range should be interpreted as happening *outside* the 229 | # marker. Defaults to `false`, except when using the `inside` 230 | # invalidation strategy or when when the marker has no tail, in which 231 | # case it defaults to true. Explicitly assigning this option overrides 232 | # behavior in all circumstances. 233 | # 234 | # Returns a {DisplayMarker}. 235 | markBufferPosition: (bufferPosition, options) -> 236 | @getMarker(@bufferMarkerLayer.markPosition(Point.fromObject(bufferPosition), options).id) 237 | 238 | ### 239 | Section: Querying 240 | ### 241 | 242 | # Essential: Get an existing marker by its id. 243 | # 244 | # Returns a {DisplayMarker}. 245 | getMarker: (id) -> 246 | if displayMarker = @markersById[id] 247 | displayMarker 248 | else if bufferMarker = @bufferMarkerLayer.getMarker(id) 249 | @markersById[id] = new DisplayMarker(this, bufferMarker) 250 | 251 | # Essential: Get all markers in the layer. 252 | # 253 | # Returns an {Array} of {DisplayMarker}s. 254 | getMarkers: -> 255 | @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id) 256 | 257 | # Public: Get the number of markers in the marker layer. 258 | # 259 | # Returns a {Number}. 260 | getMarkerCount: -> 261 | @bufferMarkerLayer.getMarkerCount() 262 | 263 | # Public: Find markers in the layer conforming to the given parameters. 264 | # 265 | # This method finds markers based on the given properties. Markers can be 266 | # associated with custom properties that will be compared with basic equality. 267 | # In addition, there are several special properties that will be compared 268 | # with the range of the markers rather than their properties. 269 | # 270 | # * `properties` An {Object} containing properties that each returned marker 271 | # must satisfy. Markers can be associated with custom properties, which are 272 | # compared with basic equality. In addition, several reserved properties 273 | # can be used to filter markers based on their current range: 274 | # * `startBufferPosition` Only include markers starting at this {Point} in buffer coordinates. 275 | # * `endBufferPosition` Only include markers ending at this {Point} in buffer coordinates. 276 | # * `startScreenPosition` Only include markers starting at this {Point} in screen coordinates. 277 | # * `endScreenPosition` Only include markers ending at this {Point} in screen coordinates. 278 | # * `startsInBufferRange` Only include markers starting inside this {Range} in buffer coordinates. 279 | # * `endsInBufferRange` Only include markers ending inside this {Range} in buffer coordinates. 280 | # * `startsInScreenRange` Only include markers starting inside this {Range} in screen coordinates. 281 | # * `endsInScreenRange` Only include markers ending inside this {Range} in screen coordinates. 282 | # * `startBufferRow` Only include markers starting at this row in buffer coordinates. 283 | # * `endBufferRow` Only include markers ending at this row in buffer coordinates. 284 | # * `startScreenRow` Only include markers starting at this row in screen coordinates. 285 | # * `endScreenRow` Only include markers ending at this row in screen coordinates. 286 | # * `intersectsBufferRowRange` Only include markers intersecting this {Array} 287 | # of `[startRow, endRow]` in buffer coordinates. 288 | # * `intersectsScreenRowRange` Only include markers intersecting this {Array} 289 | # of `[startRow, endRow]` in screen coordinates. 290 | # * `containsBufferRange` Only include markers containing this {Range} in buffer coordinates. 291 | # * `containsBufferPosition` Only include markers containing this {Point} in buffer coordinates. 292 | # * `containedInBufferRange` Only include markers contained in this {Range} in buffer coordinates. 293 | # * `containedInScreenRange` Only include markers contained in this {Range} in screen coordinates. 294 | # * `intersectsBufferRange` Only include markers intersecting this {Range} in buffer coordinates. 295 | # * `intersectsScreenRange` Only include markers intersecting this {Range} in screen coordinates. 296 | # 297 | # Returns an {Array} of {DisplayMarker}s 298 | findMarkers: (params) -> 299 | params = @translateToBufferMarkerLayerFindParams(params) 300 | @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) 301 | 302 | ### 303 | Section: Private 304 | ### 305 | 306 | translateBufferPosition: (bufferPosition, options) -> 307 | @displayLayer.translateBufferPosition(bufferPosition, options) 308 | 309 | translateBufferRange: (bufferRange, options) -> 310 | @displayLayer.translateBufferRange(bufferRange, options) 311 | 312 | translateScreenPosition: (screenPosition, options) -> 313 | @displayLayer.translateScreenPosition(screenPosition, options) 314 | 315 | translateScreenRange: (screenRange, options) -> 316 | @displayLayer.translateScreenRange(screenRange, options) 317 | 318 | emitDidUpdate: -> 319 | @emitter.emit('did-update') 320 | 321 | notifyObserversIfMarkerScreenPositionsChanged: -> 322 | for marker in @getMarkers() 323 | marker.notifyObservers(false) 324 | return 325 | 326 | destroyMarker: (id) -> 327 | if marker = @markersById[id] 328 | marker.didDestroyBufferMarker() 329 | 330 | didDestroyMarker: (marker) -> 331 | @markersWithDestroyListeners.delete(marker) 332 | delete @markersById[marker.id] 333 | 334 | translateToBufferMarkerLayerFindParams: (params) -> 335 | bufferMarkerLayerFindParams = {} 336 | for key, value of params 337 | switch key 338 | when 'startBufferPosition' 339 | key = 'startPosition' 340 | when 'endBufferPosition' 341 | key = 'endPosition' 342 | when 'startScreenPosition' 343 | key = 'startPosition' 344 | value = @displayLayer.translateScreenPosition(value) 345 | when 'endScreenPosition' 346 | key = 'endPosition' 347 | value = @displayLayer.translateScreenPosition(value) 348 | when 'startsInBufferRange' 349 | key = 'startsInRange' 350 | when 'endsInBufferRange' 351 | key = 'endsInRange' 352 | when 'startsInScreenRange' 353 | key = 'startsInRange' 354 | value = @displayLayer.translateScreenRange(value) 355 | when 'endsInScreenRange' 356 | key = 'endsInRange' 357 | value = @displayLayer.translateScreenRange(value) 358 | when 'startBufferRow' 359 | key = 'startRow' 360 | when 'endBufferRow' 361 | key = 'endRow' 362 | when 'startScreenRow' 363 | key = 'startsInRange' 364 | startBufferPosition = @displayLayer.translateScreenPosition(Point(value, 0)) 365 | endBufferPosition = @displayLayer.translateScreenPosition(Point(value, Infinity)) 366 | value = Range(startBufferPosition, endBufferPosition) 367 | when 'endScreenRow' 368 | key = 'endsInRange' 369 | startBufferPosition = @displayLayer.translateScreenPosition(Point(value, 0)) 370 | endBufferPosition = @displayLayer.translateScreenPosition(Point(value, Infinity)) 371 | value = Range(startBufferPosition, endBufferPosition) 372 | when 'intersectsBufferRowRange' 373 | key = 'intersectsRowRange' 374 | when 'intersectsScreenRowRange' 375 | key = 'intersectsRange' 376 | [startScreenRow, endScreenRow] = value 377 | startBufferPosition = @displayLayer.translateScreenPosition(Point(startScreenRow, 0)) 378 | endBufferPosition = @displayLayer.translateScreenPosition(Point(endScreenRow, Infinity)) 379 | value = Range(startBufferPosition, endBufferPosition) 380 | when 'containsBufferRange' 381 | key = 'containsRange' 382 | when 'containsScreenRange' 383 | key = 'containsRange' 384 | value = @displayLayer.translateScreenRange(value) 385 | when 'containsBufferPosition' 386 | key = 'containsPosition' 387 | when 'containsScreenPosition' 388 | key = 'containsPosition' 389 | value = @displayLayer.translateScreenPosition(value) 390 | when 'containedInBufferRange' 391 | key = 'containedInRange' 392 | when 'containedInScreenRange' 393 | key = 'containedInRange' 394 | value = @displayLayer.translateScreenRange(value) 395 | when 'intersectsBufferRange' 396 | key = 'intersectsRange' 397 | when 'intersectsScreenRange' 398 | key = 'intersectsRange' 399 | value = @displayLayer.translateScreenRange(value) 400 | bufferMarkerLayerFindParams[key] = value 401 | 402 | bufferMarkerLayerFindParams 403 | --------------------------------------------------------------------------------