diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4500e8f9b..6c85c2176 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@
## [Unreleased]
+- Migrated away from `node-oniguruma` in favor of `vscode-oniguruma` (WASM
+version). This fixes issues with Electron 21
+
## 1.103.0
- Added a new feature to Search for Pulsar's settings
diff --git a/package.json b/package.json
index 239f7b586..c273982c7 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
"base16-tomorrow-dark-theme": "file:packages/base16-tomorrow-dark-theme",
"base16-tomorrow-light-theme": "file:packages/base16-tomorrow-light-theme",
"bookmarks": "file:packages/bookmarks",
- "bracket-matcher": "https://github.com/pulsar-edit/bracket-matcher.git#c877977",
+ "bracket-matcher": "file:packages/bracket-matcher",
"chai": "4.3.4",
"clear-cut": "^2.0.2",
"coffeescript": "1.12.7",
@@ -62,7 +62,7 @@
"exception-reporting": "file:packages/exception-reporting",
"find-and-replace": "https://github.com/atom-community/find-and-replace/archive/refs/tags/v0.220.1.tar.gz",
"find-parent-dir": "^0.3.0",
- "first-mate": "7.4.3",
+ "second-mate": "https://github.com/pulsar-edit/second-mate.git#14aa7bd",
"focus-trap": "6.3.0",
"fs-admin": "0.19.0",
"fs-plus": "^3.1.1",
diff --git a/packages/bracket-matcher/CONTRIBUTING.md b/packages/bracket-matcher/CONTRIBUTING.md
new file mode 100644
index 000000000..9c8ac3e5b
--- /dev/null
+++ b/packages/bracket-matcher/CONTRIBUTING.md
@@ -0,0 +1 @@
+[See how you can contribute](https://github.com/pulsar-edit/.github/blob/main/CONTRIBUTING.md)
diff --git a/packages/bracket-matcher/LICENSE.md b/packages/bracket-matcher/LICENSE.md
new file mode 100644
index 000000000..4d231b456
--- /dev/null
+++ b/packages/bracket-matcher/LICENSE.md
@@ -0,0 +1,20 @@
+Copyright (c) 2014 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/bracket-matcher/README.md b/packages/bracket-matcher/README.md
new file mode 100644
index 000000000..1b8bac027
--- /dev/null
+++ b/packages/bracket-matcher/README.md
@@ -0,0 +1,52 @@
+# Bracket Matcher package
+
+Highlights and jumps between `[]`, `()`, and `{}`. Also highlights matching XML
+and HTML tags.
+
+Autocompletes `[]`, `()`, `{}`, `""`, `''`, `“”`, `‘’`, `«»`, `‹›`, and
+backticks by default.
+
+Use ctrl-m to jump to the bracket matching the one adjacent to the cursor.
+It jumps to the nearest enclosing bracket when there's no adjacent bracket,
+
+Use ctrl-cmd-m to select all the text inside the current brackets.
+
+Use alt-cmd-. to close the current XML/HTML tag.
+
+---
+### Configuration
+
+Matching brackets and quotes are sensibly inserted for you. If you dislike this
+functionality, you can disable it from the Bracket Matcher section of the
+Settings View.
+
+#### Custom Pairs
+
+You can customize matching pairs in Bracket Matcher at any time. You can do so either globally via the Settings View or at the scope level via your `config.cson`. Changes take effect immediately.
+
+* **Autocomplete Characters** - Comma-separated pairs that the editor will treat as brackets / quotes. Entries in this field override the package defaults.
+ * For example: `<>, (), []`
+
+* **Pairs With Extra Newline** - Comma-separated pairs that enhance the editor's auto indent feature. When used, a newline is automatically added between the pair when enter is pressed between them. Note: This feature is meant to be used in combination with brackets defined for indentation by the active language package (`increaseIndentPattern` / `decreaseIndentPattern`).
+Example:
+```
+fn main() {
+ | <---- Cursor positioned at one indent level higher
+}
+```
+
+#### Scoped settings
+In addition to the global settings, you are also able to add scope-specific modifications to Pulsar in your `config.cson`. This is especially useful for editor rule changes specific to each language. Scope-specific settings override package defaults _and_ global settings.
+Example:
+```cson
+".rust.source":
+ "bracket-matcher":
+ autocompleteCharacters: [
+ "()"
+ "[]"
+ "{}"
+ "<>"
+ "\"\""
+ "``"
+ ]
+```
diff --git a/packages/bracket-matcher/keymaps/bracket-matcher.cson b/packages/bracket-matcher/keymaps/bracket-matcher.cson
new file mode 100644
index 000000000..bf708da70
--- /dev/null
+++ b/packages/bracket-matcher/keymaps/bracket-matcher.cson
@@ -0,0 +1,18 @@
+'atom-text-editor':
+ 'ctrl-m': 'bracket-matcher:go-to-matching-bracket'
+ 'ctrl-]': 'bracket-matcher:remove-brackets-from-selection'
+
+'.platform-darwin atom-text-editor':
+ 'ctrl-cmd-m': 'bracket-matcher:select-inside-brackets'
+ 'alt-cmd-.': 'bracket-matcher:close-tag'
+ 'ctrl-backspace': 'bracket-matcher:remove-matching-brackets'
+
+'.platform-linux atom-text-editor':
+ 'ctrl-alt-,': 'bracket-matcher:select-inside-brackets'
+ 'ctrl-alt-.': 'bracket-matcher:close-tag'
+ 'ctrl-alt-backspace': 'bracket-matcher:remove-matching-brackets'
+
+'.platform-win32 atom-text-editor':
+ 'ctrl-alt-,': 'bracket-matcher:select-inside-brackets'
+ 'ctrl-alt-.': 'bracket-matcher:close-tag'
+ 'ctrl-alt-backspace': 'bracket-matcher:remove-matching-brackets'
diff --git a/packages/bracket-matcher/lib/bracket-matcher-view.js b/packages/bracket-matcher/lib/bracket-matcher-view.js
new file mode 100644
index 000000000..4290d1968
--- /dev/null
+++ b/packages/bracket-matcher/lib/bracket-matcher-view.js
@@ -0,0 +1,585 @@
+const {CompositeDisposable} = require('atom')
+const _ = require('underscore-plus')
+const {Range, Point} = require('atom')
+const TagFinder = require('./tag-finder')
+
+const MAX_ROWS_TO_SCAN = 10000
+const ONE_CHAR_FORWARD_TRAVERSAL = Object.freeze(Point(0, 1))
+const ONE_CHAR_BACKWARD_TRAVERSAL = Object.freeze(Point(0, -1))
+const TWO_CHARS_BACKWARD_TRAVERSAL = Object.freeze(Point(0, -2))
+const MAX_ROWS_TO_SCAN_FORWARD_TRAVERSAL = Object.freeze(Point(MAX_ROWS_TO_SCAN, 0))
+const MAX_ROWS_TO_SCAN_BACKWARD_TRAVERSAL = Object.freeze(Point(-MAX_ROWS_TO_SCAN, 0))
+
+module.exports =
+class BracketMatcherView {
+ constructor (editor, editorElement, matchManager) {
+ this.destroy = this.destroy.bind(this)
+ this.updateMatch = this.updateMatch.bind(this)
+ this.editor = editor
+ this.matchManager = matchManager
+ this.gutter = this.editor.gutterWithName('line-number')
+ this.subscriptions = new CompositeDisposable()
+ this.tagFinder = new TagFinder(this.editor)
+ this.pairHighlighted = false
+ this.tagHighlighted = false
+
+ // ranges for possible selection
+ this.bracket1Range = null
+ this.bracket2Range = null
+
+ this.subscriptions.add(
+ this.editor.onDidTokenize(this.updateMatch),
+ this.editor.getBuffer().onDidChangeText(this.updateMatch),
+ this.editor.onDidChangeGrammar(this.updateMatch),
+ this.editor.onDidChangeSelectionRange(this.updateMatch),
+ this.editor.onDidAddCursor(this.updateMatch),
+ this.editor.onDidRemoveCursor(this.updateMatch),
+
+ atom.commands.add(editorElement, 'bracket-matcher:go-to-matching-bracket', () =>
+ this.goToMatchingBracket()
+ ),
+
+ atom.commands.add(editorElement, 'bracket-matcher:go-to-enclosing-bracket', () =>
+ this.gotoPrecedingStartBracket()
+ ),
+
+ atom.commands.add(editorElement, 'bracket-matcher:select-inside-brackets', () =>
+ this.selectInsideBrackets()
+ ),
+
+ atom.commands.add(editorElement, 'bracket-matcher:close-tag', () =>
+ this.closeTag()
+ ),
+
+ atom.commands.add(editorElement, 'bracket-matcher:remove-matching-brackets', () =>
+ this.removeMatchingBrackets()
+ ),
+
+ atom.commands.add(editorElement, 'bracket-matcher:select-matching-brackets', () =>
+ this.selectMatchingBrackets()
+ ),
+
+ this.editor.onDidDestroy(this.destroy)
+ )
+
+ this.updateMatch()
+ }
+
+ destroy () {
+ this.subscriptions.dispose()
+ }
+
+ updateMatch () {
+ if (this.pairHighlighted) {
+ this.editor.destroyMarker(this.startMarker.id)
+ this.editor.destroyMarker(this.endMarker.id)
+ }
+
+ this.pairHighlighted = false
+ this.tagHighlighted = false
+
+ if (!this.editor.getLastSelection().isEmpty()) return
+
+ const {position, matchPosition} = this.findCurrentPair()
+
+ let startRange = null
+ let endRange = null
+ let highlightTag = false
+ let highlightPair = false
+ if (position && matchPosition) {
+ this.bracket1Range = (startRange = Range(position, position.traverse(ONE_CHAR_FORWARD_TRAVERSAL)))
+ this.bracket2Range = (endRange = Range(matchPosition, matchPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL)))
+ highlightPair = true
+ } else {
+ this.bracket1Range = null
+ this.bracket2Range = null
+ if (this.hasSyntaxTree()) {
+ ({startRange, endRange} = this.findMatchingTagNameRangesWithSyntaxTree())
+ } else {
+ ({startRange, endRange} = this.tagFinder.findMatchingTags())
+ if (this.isCursorOnCommentOrString()) return
+ }
+ if (startRange) {
+ highlightTag = true
+ highlightPair = true
+ }
+ }
+
+ if (!highlightTag && !highlightPair) return
+
+ this.startMarker = this.createMarker(startRange)
+ this.endMarker = this.createMarker(endRange)
+ this.pairHighlighted = highlightPair
+ this.tagHighlighted = highlightTag
+ }
+
+ selectMatchingBrackets () {
+ if (!this.bracket1Range && !this.bracket2Range) return
+ this.editor.setSelectedBufferRanges([this.bracket1Range, this.bracket2Range])
+ this.matchManager.changeBracketsMode = true
+ }
+
+ removeMatchingBrackets () {
+ if (this.editor.hasMultipleCursors()) {
+ this.editor.backspace()
+ return
+ }
+
+ this.editor.transact(() => {
+ if (this.editor.getLastSelection().isEmpty()) {
+ this.editor.selectLeft()
+ }
+
+ const text = this.editor.getSelectedText()
+ this.editor.moveRight()
+
+ // check if the character to the left is part of a pair
+ if (
+ this.matchManager.pairedCharacters.hasOwnProperty(text) ||
+ this.matchManager.pairedCharactersInverse.hasOwnProperty(text)
+ ) {
+ let {position, matchPosition, bracket} = this.findCurrentPair()
+
+ if (position && matchPosition) {
+ this.editor.setCursorBufferPosition(matchPosition)
+ this.editor.delete()
+ // if on the same line and the cursor is in front of an end pair
+ // offset by one to make up for the missing character
+ if (position.row === matchPosition.row && this.matchManager.pairedCharactersInverse.hasOwnProperty(bracket)) {
+ position = position.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
+ }
+ this.editor.setCursorBufferPosition(position)
+ this.editor.delete()
+ } else {
+ this.editor.backspace()
+ }
+ } else {
+ this.editor.backspace()
+ }
+ })
+ }
+
+ findMatchingEndBracket (startBracketPosition, startBracket, endBracket) {
+ if (startBracket === endBracket) return
+
+ if (this.hasSyntaxTree()) {
+ return this.findMatchingEndBracketWithSyntaxTree(startBracketPosition, startBracket, endBracket)
+ } else {
+ const scopeDescriptor = this.editor.scopeDescriptorForBufferPosition(startBracketPosition)
+ if (this.isScopeCommentedOrString(scopeDescriptor.getScopesArray())) return
+ return this.findMatchingEndBracketWithRegexSearch(startBracketPosition, startBracket, endBracket)
+ }
+ }
+
+ findMatchingStartBracket (endBracketPosition, startBracket, endBracket) {
+ if (startBracket === endBracket) return
+
+ if (this.hasSyntaxTree()) {
+ return this.findMatchingStartBracketWithSyntaxTree(endBracketPosition, startBracket, endBracket)
+ } else {
+ const scopeDescriptor = this.editor.scopeDescriptorForBufferPosition(endBracketPosition)
+ if (this.isScopeCommentedOrString(scopeDescriptor.getScopesArray())) return
+ return this.findMatchingStartBracketWithRegexSearch(endBracketPosition, startBracket, endBracket)
+ }
+ }
+
+ findMatchingEndBracketWithSyntaxTree (bracketPosition, startBracket, endBracket) {
+ let result
+ const bracketEndPosition = bracketPosition.traverse([0, startBracket.length])
+ this.editor.buffer.getLanguageMode().getSyntaxNodeContainingRange(
+ new Range(bracketPosition, bracketEndPosition),
+ node => {
+ if (bracketEndPosition.isGreaterThan(node.startPosition) && bracketEndPosition.isLessThan(node.endPosition)) {
+ const matchNode = node.children.find(child =>
+ bracketEndPosition.isLessThanOrEqual(child.startPosition) &&
+ child.type === endBracket
+ )
+ if (matchNode) result = Point.fromObject(matchNode.startPosition)
+ return true
+ }
+ }
+ )
+ return result
+ }
+
+ findMatchingStartBracketWithSyntaxTree (bracketPosition, startBracket, endBracket) {
+ let result
+ const bracketEndPosition = bracketPosition.traverse([0, startBracket.length])
+ this.editor.buffer.getLanguageMode().getSyntaxNodeContainingRange(
+ new Range(bracketPosition, bracketEndPosition),
+ node => {
+ if (bracketPosition.isGreaterThan(node.startPosition)) {
+ const matchNode = node.children.find(child =>
+ bracketPosition.isGreaterThanOrEqual(child.endPosition) &&
+ child.type === startBracket
+ )
+ if (matchNode) result = Point.fromObject(matchNode.startPosition)
+ return true
+ }
+ }
+ )
+ return result
+ }
+
+ findMatchingTagNameRangesWithSyntaxTree () {
+ const position = this.editor.getCursorBufferPosition()
+ const {startTag, endTag} = this.findContainingTagsWithSyntaxTree(position)
+ if (startTag && (startTag.range.containsPoint(position) || endTag.range.containsPoint(position))) {
+ if (startTag === endTag) {
+ const {range} = startTag.child(1)
+ return {startRange: range, endRange: range}
+ } else if (endTag.firstChild.type === '') {
+ return {
+ startRange: startTag.child(1).range,
+ endRange: endTag.child(1).range
+ }
+ } else {
+ return {
+ startRange: startTag.child(1).range,
+ endRange: endTag.child(2).range
+ }
+ }
+ } else {
+ return {}
+ }
+ }
+
+ findMatchingTagsWithSyntaxTree () {
+ const position = this.editor.getCursorBufferPosition()
+ const {startTag, endTag} = this.findContainingTagsWithSyntaxTree(position)
+ if (startTag) {
+ return {startRange: startTag.range, endRange: endTag.range}
+ } else {
+ return {}
+ }
+ }
+
+ findContainingTagsWithSyntaxTree (position) {
+ let startTag, endTag
+ if (position.column === this.editor.buffer.lineLengthForRow(position.row)) position.column--;
+ this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition(position, node => {
+ if (node.type.includes('element') && node.childCount > 0) {
+ const {firstChild, lastChild} = node
+ if (
+ firstChild.childCount > 2 &&
+ firstChild.firstChild.type === '<'
+ ) {
+ if (lastChild === firstChild && firstChild.lastChild.type === '/>') {
+ startTag = firstChild
+ endTag = firstChild
+ } else if (
+ lastChild.childCount > 2 &&
+ (lastChild.firstChild.type === '' ||
+ lastChild.firstChild.type === '<' && lastChild.child(1).type === '/')
+ ) {
+ startTag = firstChild
+ endTag = lastChild
+ }
+ }
+ return true
+ }
+ })
+ return {startTag, endTag}
+ }
+
+ findMatchingEndBracketWithRegexSearch (startBracketPosition, startBracket, endBracket) {
+ const scanRange = new Range(
+ startBracketPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL),
+ startBracketPosition.traverse(MAX_ROWS_TO_SCAN_FORWARD_TRAVERSAL)
+ )
+ let endBracketPosition = null
+ let unpairedCount = 0
+ this.editor.scanInBufferRange(this.matchManager.pairRegexes[startBracket], scanRange, result => {
+ if (this.isRangeCommentedOrString(result.range)) return
+ switch (result.match[0]) {
+ case startBracket:
+ unpairedCount++
+ break
+ case endBracket:
+ unpairedCount--
+ if (unpairedCount < 0) {
+ endBracketPosition = result.range.start
+ result.stop()
+ }
+ break
+ }
+ })
+
+ return endBracketPosition
+ }
+
+ findMatchingStartBracketWithRegexSearch (endBracketPosition, startBracket, endBracket) {
+ const scanRange = new Range(
+ endBracketPosition.traverse(MAX_ROWS_TO_SCAN_BACKWARD_TRAVERSAL),
+ endBracketPosition
+ )
+ let startBracketPosition = null
+ let unpairedCount = 0
+ this.editor.backwardsScanInBufferRange(this.matchManager.pairRegexes[startBracket], scanRange, result => {
+ if (this.isRangeCommentedOrString(result.range)) return
+ switch (result.match[0]) {
+ case startBracket:
+ unpairedCount--
+ if (unpairedCount < 0) {
+ startBracketPosition = result.range.start
+ result.stop()
+ break
+ }
+ break
+ case endBracket:
+ unpairedCount++
+ }
+ })
+
+ return startBracketPosition
+ }
+
+ findPrecedingStartBracket (cursorPosition) {
+ if (this.hasSyntaxTree()) {
+ return this.findPrecedingStartBracketWithSyntaxTree(cursorPosition)
+ } else {
+ return this.findPrecedingStartBracketWithRegexSearch(cursorPosition)
+ }
+ }
+
+ findPrecedingStartBracketWithSyntaxTree (cursorPosition) {
+ let result
+ this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition(cursorPosition, node => {
+ for (const child of node.children) {
+ if (cursorPosition.isLessThanOrEqual(child.startPosition)) break
+ if (
+ child.type in this.matchManager.pairedCharacters ||
+ child.type in this.matchManager.pairedCharactersInverse
+ ) {
+ result = Point.fromObject(child.startPosition)
+ return true
+ }
+ }
+ })
+ return result
+ }
+
+ findPrecedingStartBracketWithRegexSearch (cursorPosition) {
+ const scanRange = new Range(Point.ZERO, cursorPosition)
+ const startBracket = _.escapeRegExp(_.keys(this.matchManager.pairedCharacters).join(''))
+ const endBracket = _.escapeRegExp(_.keys(this.matchManager.pairedCharactersInverse).join(''))
+ const combinedRegExp = new RegExp(`[${startBracket}${endBracket}]`, 'g')
+ const startBracketRegExp = new RegExp(`[${startBracket}]`, 'g')
+ const endBracketRegExp = new RegExp(`[${endBracket}]`, 'g')
+ let startPosition = null
+ let unpairedCount = 0
+ this.editor.backwardsScanInBufferRange(combinedRegExp, scanRange, result => {
+ if (this.isRangeCommentedOrString(result.range)) return
+ if (result.match[0].match(endBracketRegExp)) {
+ unpairedCount++
+ } else if (result.match[0].match(startBracketRegExp)) {
+ unpairedCount--
+ if (unpairedCount < 0) {
+ startPosition = result.range.start
+ result.stop()
+ }
+ }
+ })
+
+ return startPosition
+ }
+
+ createMarker (bufferRange) {
+ const marker = this.editor.markBufferRange(bufferRange)
+ this.editor.decorateMarker(marker, {type: 'highlight', class: 'bracket-matcher', deprecatedRegionClass: 'bracket-matcher'})
+ if (atom.config.get('bracket-matcher.highlightMatchingLineNumber', {scope: this.editor.getRootScopeDescriptor()}) && this.gutter) {
+ this.gutter.decorateMarker(marker, {type: 'highlight', class: 'bracket-matcher', deprecatedRegionClass: 'bracket-matcher'})
+ }
+ return marker
+ }
+
+ findCurrentPair () {
+ const currentPosition = this.editor.getCursorBufferPosition()
+ const previousPosition = currentPosition.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
+ const nextPosition = currentPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL)
+ const currentCharacter = this.editor.getTextInBufferRange(new Range(currentPosition, nextPosition))
+ const previousCharacter = this.editor.getTextInBufferRange(new Range(previousPosition, currentPosition))
+
+ let position, matchPosition, currentBracket, matchingBracket
+ if ((matchingBracket = this.matchManager.pairedCharacters[currentCharacter])) {
+ position = currentPosition
+ currentBracket = currentCharacter
+ matchPosition = this.findMatchingEndBracket(position, currentBracket, matchingBracket)
+ } else if ((matchingBracket = this.matchManager.pairedCharacters[previousCharacter])) {
+ position = previousPosition
+ currentBracket = previousCharacter
+ matchPosition = this.findMatchingEndBracket(position, currentBracket, matchingBracket)
+ } else if ((matchingBracket = this.matchManager.pairedCharactersInverse[previousCharacter])) {
+ position = previousPosition
+ currentBracket = previousCharacter
+ matchPosition = this.findMatchingStartBracket(position, matchingBracket, currentBracket)
+ } else if ((matchingBracket = this.matchManager.pairedCharactersInverse[currentCharacter])) {
+ position = currentPosition
+ currentBracket = currentCharacter
+ matchPosition = this.findMatchingStartBracket(position, matchingBracket, currentBracket)
+ }
+
+ return {position, matchPosition, bracket: currentBracket}
+ }
+
+ goToMatchingBracket () {
+ if (!this.pairHighlighted) return this.gotoPrecedingStartBracket()
+ const position = this.editor.getCursorBufferPosition()
+
+ if (this.tagHighlighted) {
+ let tagCharacterOffset
+ let startRange = this.startMarker.getBufferRange()
+ const tagLength = startRange.end.column - startRange.start.column
+ let endRange = this.endMarker.getBufferRange()
+ if (startRange.compare(endRange) > 0) {
+ [startRange, endRange] = [endRange, startRange]
+ }
+
+ // include the <
+ startRange = new Range(startRange.start.traverse(ONE_CHAR_BACKWARD_TRAVERSAL), endRange.end.traverse(ONE_CHAR_BACKWARD_TRAVERSAL))
+ // include the
+ endRange = new Range(endRange.start.traverse(TWO_CHARS_BACKWARD_TRAVERSAL), endRange.end.traverse(TWO_CHARS_BACKWARD_TRAVERSAL))
+
+ if (position.isLessThan(endRange.start)) {
+ tagCharacterOffset = position.column - startRange.start.column
+ if (tagCharacterOffset > 0) { tagCharacterOffset++ }
+ tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 2) // include
+ this.editor.setCursorBufferPosition(endRange.start.traverse([0, tagCharacterOffset]))
+ } else {
+ tagCharacterOffset = position.column - endRange.start.column
+ if (tagCharacterOffset > 1) { tagCharacterOffset-- }
+ tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 1) // include <
+ this.editor.setCursorBufferPosition(startRange.start.traverse([0, tagCharacterOffset]))
+ }
+ } else {
+ const previousPosition = position.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
+ const startPosition = this.startMarker.getStartBufferPosition()
+ const endPosition = this.endMarker.getStartBufferPosition()
+
+ if (position.isEqual(startPosition)) {
+ this.editor.setCursorBufferPosition(endPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL))
+ } else if (previousPosition.isEqual(startPosition)) {
+ this.editor.setCursorBufferPosition(endPosition)
+ } else if (position.isEqual(endPosition)) {
+ this.editor.setCursorBufferPosition(startPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL))
+ } else if (previousPosition.isEqual(endPosition)) {
+ this.editor.setCursorBufferPosition(startPosition)
+ }
+ }
+ }
+
+ gotoPrecedingStartBracket () {
+ if (this.pairHighlighted) return
+
+ const matchPosition = this.findPrecedingStartBracket(this.editor.getCursorBufferPosition())
+ if (matchPosition) {
+ this.editor.setCursorBufferPosition(matchPosition)
+ } else {
+ let startRange, endRange
+ if (this.hasSyntaxTree()) {
+ ({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
+ } else {
+ ({startRange, endRange} = this.tagFinder.findStartEndTags())
+ }
+
+ if (startRange) {
+ if (startRange.compare(endRange) > 0) {
+ [startRange, endRange] = [endRange, startRange]
+ }
+ this.editor.setCursorBufferPosition(startRange.start)
+ }
+ }
+ }
+
+ multiCursorSelect() {
+ this.editor.getCursorBufferPositions().forEach(position => {
+ let startPosition = this.findPrecedingStartBracket(position)
+ if(startPosition) {
+ const startBracket = this.editor.getTextInRange(Range.fromPointWithDelta(startPosition, 0, 1))
+ const endPosition = this.findMatchingEndBracket(startPosition, startBracket, this.matchManager.pairedCharacters[startBracket])
+ startPosition = startPosition.traverse([0, 1])
+ if (startPosition && endPosition) {
+ const rangeToSelect = new Range(startPosition, endPosition)
+ this.editor.addSelectionForBufferRange(rangeToSelect)
+ }
+ } else {
+ let startRange, endRange;
+ if (this.hasSyntaxTree()) {
+ ({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
+ } else {
+ ({startRange, endRange} = this.tagFinder.findStartEndTags(true))
+ if (startRange && startRange.compare(endRange) > 0) {
+ [startRange, endRange] = [endRange, startRange]
+ }
+ }
+ if (startRange) {
+ const startPosition = startRange.end
+ const endPosition = endRange.start
+ const rangeToSelect = new Range(startPosition, endPosition)
+ this.editor.setSelectedBufferRange(rangeToSelect)
+ }
+ }
+ })
+ }
+
+ selectInsideBrackets () {
+ let endPosition, endRange, startPosition, startRange
+ if (this.pairHighlighted) {
+ startRange = this.startMarker.getBufferRange()
+ endRange = this.endMarker.getBufferRange()
+
+ if (this.tagHighlighted) {
+ if (this.hasSyntaxTree()) {
+ ({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
+ } else {
+ ({startRange, endRange} = this.tagFinder.findStartEndTags(true))
+ if (startRange && startRange.compare(endRange) > 0) {
+ [startRange, endRange] = [endRange, startRange]
+ }
+ }
+ }
+ startPosition = startRange.end
+ endPosition = endRange.start
+
+ const rangeToSelect = new Range(startPosition, endPosition)
+ this.editor.setSelectedBufferRange(rangeToSelect)
+ } else {
+ this.multiCursorSelect();
+ }
+ }
+
+ // Insert at the current cursor position a closing tag if there exists an
+ // open tag that is not closed afterwards.
+ closeTag () {
+ const cursorPosition = this.editor.getCursorBufferPosition()
+ const preFragment = this.editor.getTextInBufferRange([Point.ZERO, cursorPosition])
+ const postFragment = this.editor.getTextInBufferRange([cursorPosition, Point.INFINITY])
+
+ const tag = this.tagFinder.closingTagForFragments(preFragment, postFragment)
+ if (tag) {
+ this.editor.insertText(`${tag}>`)
+ }
+ }
+
+ isCursorOnCommentOrString () {
+ return this.isScopeCommentedOrString(this.editor.getLastCursor().getScopeDescriptor().getScopesArray())
+ }
+
+ isRangeCommentedOrString (range) {
+ return this.isScopeCommentedOrString(this.editor.scopeDescriptorForBufferPosition(range.start).getScopesArray())
+ }
+
+ isScopeCommentedOrString (scopesArray) {
+ for (let scope of scopesArray.reverse()) {
+ scope = scope.split('.')
+ if (scope.includes('embedded') && scope.includes('source')) return false
+ if (scope.includes('comment') || scope.includes('string')) return true
+ }
+
+ return false
+ }
+
+ hasSyntaxTree () {
+ return this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition
+ }
+}
diff --git a/packages/bracket-matcher/lib/bracket-matcher.js b/packages/bracket-matcher/lib/bracket-matcher.js
new file mode 100644
index 000000000..c6e84ee6f
--- /dev/null
+++ b/packages/bracket-matcher/lib/bracket-matcher.js
@@ -0,0 +1,299 @@
+const _ = require('underscore-plus')
+const {CompositeDisposable} = require('atom')
+const SelectorCache = require('./selector-cache')
+
+module.exports =
+class BracketMatcher {
+ constructor (editor, editorElement, matchManager) {
+ this.insertText = this.insertText.bind(this)
+ this.insertNewline = this.insertNewline.bind(this)
+ this.backspace = this.backspace.bind(this)
+ this.editor = editor
+ this.matchManager = matchManager
+ this.subscriptions = new CompositeDisposable()
+ this.bracketMarkers = []
+
+ this.origEditorInsertText = this.editor.insertText.bind(this.editor)
+ _.adviseBefore(this.editor, 'insertText', this.insertText)
+ _.adviseBefore(this.editor, 'insertNewline', this.insertNewline)
+ _.adviseBefore(this.editor, 'backspace', this.backspace)
+
+ this.subscriptions.add(
+ atom.commands.add(editorElement, 'bracket-matcher:remove-brackets-from-selection', event => {
+ if (!this.removeBrackets()) event.abortKeyBinding()
+ }),
+
+ this.editor.onDidDestroy(() => this.unsubscribe())
+ )
+ }
+
+ insertText (text, options) {
+ if (!text) return true
+ if ((options && options.select) || (options && options.undo === 'skip')) return true
+
+ let autoCompleteOpeningBracket, bracketMarker, pair
+ if (this.matchManager.changeBracketsMode) {
+ this.matchManager.changeBracketsMode = false
+ if (this.isClosingBracket(text)) {
+ text = this.matchManager.pairedCharactersInverse[text]
+ }
+ if (this.isOpeningBracket(text)) {
+ this.editor.mutateSelectedText(selection => {
+ const selectionText = selection.getText()
+ if (this.isOpeningBracket(selectionText)) {
+ selection.insertText(text)
+ }
+ if (this.isClosingBracket(selectionText)) {
+ selection.insertText(this.matchManager.pairedCharacters[text])
+ }
+ })
+ return false
+ }
+ }
+
+ if (this.wrapSelectionInBrackets(text)) return false
+ if (this.editor.hasMultipleCursors()) return true
+
+ const cursorBufferPosition = this.editor.getCursorBufferPosition()
+ const previousCharacters = this.editor.getTextInBufferRange([[cursorBufferPosition.row, 0], cursorBufferPosition])
+ const nextCharacter = this.editor.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.traverse([0, 1])])
+ const previousCharacter = previousCharacters.slice(-1)
+
+ const hasWordAfterCursor = /\w/.test(nextCharacter)
+ const hasWordBeforeCursor = /\w/.test(previousCharacter)
+ const hasQuoteBeforeCursor = this.isQuote(previousCharacter) && (previousCharacter === text[0])
+ const hasEscapeCharacterBeforeCursor = endsWithEscapeCharacter(previousCharacters)
+ const hasEscapeSequenceBeforeCursor = endsWithEscapeSequence(previousCharacters)
+
+ if (text === '#' && this.isCursorOnInterpolatedString()) {
+ autoCompleteOpeningBracket = this.getScopedSetting('bracket-matcher.autocompleteBrackets') && !hasEscapeCharacterBeforeCursor
+ text += '{'
+ pair = '}'
+ } else {
+ autoCompleteOpeningBracket = (
+ this.isOpeningBracket(text) &&
+ !hasWordAfterCursor &&
+ this.getScopedSetting('bracket-matcher.autocompleteBrackets') &&
+ !(this.isQuote(text) && (hasWordBeforeCursor || hasQuoteBeforeCursor || hasEscapeSequenceBeforeCursor)) &&
+ !hasEscapeCharacterBeforeCursor
+ )
+ pair = this.matchManager.pairedCharacters[text]
+ }
+
+ let skipOverExistingClosingBracket = false
+ if (this.isClosingBracket(text) && (nextCharacter === text) && !hasEscapeCharacterBeforeCursor) {
+ bracketMarker = this.bracketMarkers.find(marker => marker.isValid() && marker.getBufferRange().end.isEqual(cursorBufferPosition))
+ if (bracketMarker || this.getScopedSetting('bracket-matcher.alwaysSkipClosingPairs')) {
+ skipOverExistingClosingBracket = true
+ }
+ }
+
+ if (skipOverExistingClosingBracket) {
+ if (bracketMarker) bracketMarker.destroy()
+ _.remove(this.bracketMarkers, bracketMarker)
+ this.editor.moveRight()
+ return false
+ } else if (autoCompleteOpeningBracket) {
+ this.editor.transact(() => {
+ this.origEditorInsertText(text + pair)
+ this.editor.moveLeft()
+ })
+ const range = [cursorBufferPosition, cursorBufferPosition.traverse([0, text.length])]
+ this.bracketMarkers.push(this.editor.markBufferRange(range))
+ return false
+ }
+ }
+
+ insertNewline () {
+ if (this.editor.hasMultipleCursors()) return
+ if (!this.editor.getLastSelection().isEmpty()) return
+
+ const cursorBufferPosition = this.editor.getCursorBufferPosition()
+ const previousCharacters = this.editor.getTextInBufferRange([[cursorBufferPosition.row, 0], cursorBufferPosition])
+ const nextCharacter = this.editor.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.traverse([0, 1])])
+ const previousCharacter = previousCharacters.slice(-1)
+ const hasEscapeCharacterBeforeCursor = endsWithEscapeCharacter(previousCharacters)
+
+ if (
+ this.matchManager.pairsWithExtraNewline[previousCharacter] === nextCharacter &&
+ !hasEscapeCharacterBeforeCursor
+ ) {
+ this.editor.transact(() => {
+ this.origEditorInsertText('\n\n')
+ this.editor.moveUp()
+ if (this.getScopedSetting('editor.autoIndent')) {
+ const cursorRow = this.editor.getCursorBufferPosition().row
+ this.editor.autoIndentBufferRows(cursorRow, cursorRow + 1)
+ }
+ })
+ return false
+ }
+ }
+
+ backspace () {
+ if (this.editor.hasMultipleCursors()) return
+ if (!this.editor.getLastSelection().isEmpty()) return
+
+ const cursorBufferPosition = this.editor.getCursorBufferPosition()
+ const previousCharacters = this.editor.getTextInBufferRange([[cursorBufferPosition.row, 0], cursorBufferPosition])
+ const nextCharacter = this.editor.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.traverse([0, 1])])
+ const previousCharacter = previousCharacters.slice(-1)
+ const hasEscapeCharacterBeforeCursor = endsWithEscapeCharacter(previousCharacters)
+
+ if (
+ this.matchManager.pairedCharacters[previousCharacter] === nextCharacter &&
+ !hasEscapeCharacterBeforeCursor &&
+ this.getScopedSetting('bracket-matcher.autocompleteBrackets')
+ ) {
+ this.editor.transact(() => {
+ this.editor.moveLeft()
+ this.editor.delete()
+ this.editor.delete()
+ })
+ return false
+ }
+ }
+
+ removeBrackets () {
+ let bracketsRemoved = false
+ this.editor.mutateSelectedText(selection => {
+ let selectionEnd
+ if (!this.selectionIsWrappedByMatchingBrackets(selection)) return
+
+ const range = selection.getBufferRange()
+ const options = {reversed: selection.isReversed()}
+ const selectionStart = range.start
+ if (range.start.row === range.end.row) {
+ selectionEnd = range.end.traverse([0, -2])
+ } else {
+ selectionEnd = range.end.traverse([0, -1])
+ }
+
+ const text = selection.getText()
+ selection.insertText(text.substring(1, text.length - 1))
+ selection.setBufferRange([selectionStart, selectionEnd], options)
+ bracketsRemoved = true
+ })
+ return bracketsRemoved
+ }
+
+ wrapSelectionInBrackets (bracket) {
+ let pair
+ if (bracket === '#') {
+ if (!this.isCursorOnInterpolatedString()) return false
+ bracket = '#{'
+ pair = '}'
+ } else {
+ if (!this.isOpeningBracket(bracket)) return false
+ pair = this.matchManager.pairedCharacters[bracket]
+ }
+
+ if (!this.editor.selections.some(s => !s.isEmpty())) return false
+ if (!this.getScopedSetting('bracket-matcher.wrapSelectionsInBrackets')) return false
+
+ let selectionWrapped = false
+ this.editor.mutateSelectedText(selection => {
+ let selectionEnd
+ if (selection.isEmpty()) return
+
+ // Don't wrap in #{} if the selection spans more than one line
+ if ((bracket === '#{') && !selection.getBufferRange().isSingleLine()) return
+
+ selectionWrapped = true
+ const range = selection.getBufferRange()
+ const options = {reversed: selection.isReversed()}
+ selection.insertText(`${bracket}${selection.getText()}${pair}`)
+ const selectionStart = range.start.traverse([0, bracket.length])
+ if (range.start.row === range.end.row) {
+ selectionEnd = range.end.traverse([0, bracket.length])
+ } else {
+ selectionEnd = range.end
+ }
+ selection.setBufferRange([selectionStart, selectionEnd], options)
+ })
+
+ return selectionWrapped
+ }
+
+ isQuote (string) {
+ return /['"`]/.test(string)
+ }
+
+ isCursorOnInterpolatedString () {
+ const cursor = this.editor.getLastCursor()
+ const languageMode = this.editor.getBuffer().getLanguageMode()
+ if (languageMode.getSyntaxNodeAtPosition) {
+ const node = languageMode.getSyntaxNodeAtPosition(
+ cursor.getBufferPosition(),
+ (node, grammar) => grammar.scopeName === 'source.ruby' && /string|symbol/.test(node.type)
+ )
+ if (node) {
+ const {firstChild} = node
+ if (firstChild) {
+ return ['"', ':"', '%('].includes(firstChild.text)
+ }
+ }
+ return false
+ } else {
+ if (this.interpolatedStringSelector == null) {
+ const segments = [
+ '*.*.*.interpolated.ruby',
+ 'string.interpolated.ruby',
+ 'string.regexp.interpolated.ruby',
+ 'string.quoted.double.coffee',
+ 'string.unquoted.heredoc.ruby',
+ 'string.quoted.double.livescript',
+ 'string.quoted.double.heredoc.livescript',
+ 'string.quoted.double.elixir',
+ 'string.quoted.double.heredoc.elixir',
+ 'comment.documentation.heredoc.elixir'
+ ]
+ this.interpolatedStringSelector = SelectorCache.get(segments.join(' | '))
+ }
+ return this.interpolatedStringSelector.matches(this.editor.getLastCursor().getScopeDescriptor().getScopesArray())
+ }
+ }
+
+ isOpeningBracket (string) {
+ return this.matchManager.pairedCharacters.hasOwnProperty(string)
+ }
+
+ isClosingBracket (string) {
+ return this.matchManager.pairedCharactersInverse.hasOwnProperty(string)
+ }
+
+ selectionIsWrappedByMatchingBrackets (selection) {
+ if (selection.isEmpty()) return false
+ const selectedText = selection.getText()
+ const firstCharacter = selectedText[0]
+ const lastCharacter = selectedText[selectedText.length - 1]
+ return this.matchManager.pairedCharacters[firstCharacter] === lastCharacter
+ }
+
+ unsubscribe () {
+ this.subscriptions.dispose()
+ }
+
+ getScopedSetting (key) {
+ return atom.config.get(key, {scope: this.editor.getRootScopeDescriptor()})
+ }
+}
+
+const BACKSLASHES_REGEX = /(\\+)$/
+const ESCAPE_SEQUENCE_REGEX = /(\\+)[^\\]$/
+
+// odd number of backslashes
+function endsWithEscapeCharacter (string) {
+ const backslashesMatch = string.match(BACKSLASHES_REGEX)
+ return backslashesMatch && backslashesMatch[1].length % 2 === 1
+}
+
+// even number of backslashes or odd number of backslashes followed by another character
+function endsWithEscapeSequence (string) {
+ const backslashesMatch = string.match(BACKSLASHES_REGEX)
+ const escapeSequenceMatch = string.match(ESCAPE_SEQUENCE_REGEX)
+ return (
+ (escapeSequenceMatch && escapeSequenceMatch[1].length % 2 === 1) ||
+ (backslashesMatch && backslashesMatch[1].length % 2 === 0)
+ )
+}
diff --git a/packages/bracket-matcher/lib/main.js b/packages/bracket-matcher/lib/main.js
new file mode 100644
index 000000000..91f9ea90c
--- /dev/null
+++ b/packages/bracket-matcher/lib/main.js
@@ -0,0 +1,20 @@
+const MatchManager = require('./match-manager')
+const BracketMatcherView = require('./bracket-matcher-view')
+const BracketMatcher = require('./bracket-matcher')
+
+module.exports = {
+ activate () {
+ const watchedEditors = new WeakSet()
+
+ atom.workspace.observeTextEditors(editor => {
+ if (watchedEditors.has(editor)) return
+
+ const editorElement = atom.views.getView(editor)
+ const matchManager = new MatchManager(editor, editorElement)
+ new BracketMatcherView(editor, editorElement, matchManager)
+ new BracketMatcher(editor, editorElement, matchManager)
+ watchedEditors.add(editor)
+ editor.onDidDestroy(() => watchedEditors.delete(editor))
+ })
+ }
+}
diff --git a/packages/bracket-matcher/lib/match-manager.js b/packages/bracket-matcher/lib/match-manager.js
new file mode 100644
index 000000000..a052c1dc9
--- /dev/null
+++ b/packages/bracket-matcher/lib/match-manager.js
@@ -0,0 +1,60 @@
+const _ = require('underscore-plus')
+const {CompositeDisposable} = require('atom')
+
+module.exports =
+class MatchManager {
+ appendPair (pairList, [itemLeft, itemRight]) {
+ const newPair = {}
+ newPair[itemLeft] = itemRight
+ pairList = _.extend(pairList, newPair)
+ }
+
+ processAutoPairs (autocompletePairs, pairedList, dataFun) {
+ if (autocompletePairs.length) {
+ for (let autocompletePair of autocompletePairs) {
+ const pairArray = autocompletePair.split('')
+ this.appendPair(pairedList, dataFun(pairArray))
+ }
+ }
+ }
+
+ updateConfig () {
+ this.pairedCharacters = {}
+ this.pairedCharactersInverse = {}
+ this.pairRegexes = {}
+ this.pairsWithExtraNewline = {}
+ this.processAutoPairs(this.getScopedSetting('bracket-matcher.autocompleteCharacters'), this.pairedCharacters, x => [x[0], x[1]])
+ this.processAutoPairs(this.getScopedSetting('bracket-matcher.autocompleteCharacters'), this.pairedCharactersInverse, x => [x[1], x[0]])
+ this.processAutoPairs(this.getScopedSetting('bracket-matcher.pairsWithExtraNewline'), this.pairsWithExtraNewline, x => [x[0], x[1]])
+ for (let startPair in this.pairedCharacters) {
+ const endPair = this.pairedCharacters[startPair]
+ this.pairRegexes[startPair] = new RegExp(`[${_.escapeRegExp(startPair + endPair)}]`, 'g')
+ }
+ }
+
+ getScopedSetting (key) {
+ return atom.config.get(key, {scope: this.editor.getRootScopeDescriptor()})
+ }
+
+ constructor (editor, editorElement) {
+ this.destroy = this.destroy.bind(this)
+ this.editor = editor
+ this.subscriptions = new CompositeDisposable()
+
+ this.updateConfig()
+
+ // Subscribe to config changes
+ const scope = this.editor.getRootScopeDescriptor()
+ this.subscriptions.add(
+ atom.config.observe('bracket-matcher.autocompleteCharacters', {scope}, () => this.updateConfig()),
+ atom.config.observe('bracket-matcher.pairsWithExtraNewline', {scope}, () => this.updateConfig()),
+ this.editor.onDidDestroy(this.destroy)
+ )
+
+ this.changeBracketsMode = false
+ }
+
+ destroy () {
+ this.subscriptions.dispose()
+ }
+}
diff --git a/packages/bracket-matcher/lib/selector-cache.js b/packages/bracket-matcher/lib/selector-cache.js
new file mode 100644
index 000000000..a8cd32882
--- /dev/null
+++ b/packages/bracket-matcher/lib/selector-cache.js
@@ -0,0 +1,11 @@
+const {ScopeSelector} = require('second-mate')
+const cache = {}
+
+exports.get = function (selector) {
+ let scopeSelector = cache[selector]
+ if (!scopeSelector) {
+ scopeSelector = new ScopeSelector(selector)
+ cache[selector] = scopeSelector
+ }
+ return scopeSelector
+}
diff --git a/packages/bracket-matcher/lib/self-closing-tags.json b/packages/bracket-matcher/lib/self-closing-tags.json
new file mode 100644
index 000000000..17f829e49
--- /dev/null
+++ b/packages/bracket-matcher/lib/self-closing-tags.json
@@ -0,0 +1,18 @@
+[
+ "area",
+ "base",
+ "br",
+ "col",
+ "command",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "keygen",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr"
+]
diff --git a/packages/bracket-matcher/lib/tag-finder.js b/packages/bracket-matcher/lib/tag-finder.js
new file mode 100644
index 000000000..eda29119c
--- /dev/null
+++ b/packages/bracket-matcher/lib/tag-finder.js
@@ -0,0 +1,258 @@
+const {Range} = require('atom')
+const _ = require('underscore-plus')
+const SelfClosingTags = require('./self-closing-tags')
+
+const TAG_SELECTOR_REGEX = /(\b|\.)(meta\.tag|punctuation\.definition\.tag)/
+const COMMENT_SELECTOR_REGEX = /(\b|\.)comment/
+
+// Creates a regex to match opening tag with match[1] and closing tags with match[2]
+//
+// * tagNameRegexStr - a regex string describing how to match the tagname.
+// Should not contain capturing match groups.
+//
+// Returns a {RegExp}.
+const generateTagStartOrEndRegex = function (tagNameRegexStr) {
+ const notSelfClosingTagEnd = "(?:[^>\\/\"']|\"[^\"]*\"|'[^']*')*>"
+ return new RegExp(`<(${tagNameRegexStr})${notSelfClosingTagEnd}|<\\/(${tagNameRegexStr})>`)
+}
+
+const tagStartOrEndRegex = generateTagStartOrEndRegex('\\w[-\\w]*(?:(?:\\:|\\.)\\w[-\\w]*)*')
+
+// Helper to find the matching start/end tag for the start/end tag under the
+// cursor in XML, HTML, etc. editors.
+module.exports =
+class TagFinder {
+ constructor (editor) {
+ // 1. Tag prefix
+ // 2. Closing tag (optional)
+ // 3. Tag name
+ // 4. Attributes (ids, classes, etc. - optional)
+ // 5. Tag suffix
+ // 6. Self-closing tag (optional)
+ this.editor = editor
+ this.tagPattern = /(<(\/)?)(.+?)(\s+.*?)?((\/)?>|$)/
+ this.wordRegex = /.*?(>|$)/
+ }
+
+ patternForTagName (tagName) {
+ tagName = _.escapeRegExp(tagName)
+ // 1. Start tag
+ // 2. Tag name
+ // 3. Attributes (optional)
+ // 4. Tag suffix
+ // 5. Self-closing tag (optional)
+ // 6. End tag
+ return new RegExp(`(<(${tagName})(\\s+[^>]*?)?((/)?>))|(${tagName}[^>]*>)`, 'gi')
+ }
+
+ isRangeCommented (range) {
+ return this.scopesForPositionMatchRegex(range.start, COMMENT_SELECTOR_REGEX)
+ }
+
+ isCursorOnTag () {
+ return this.scopesForPositionMatchRegex(this.editor.getCursorBufferPosition(), TAG_SELECTOR_REGEX)
+ }
+
+ scopesForPositionMatchRegex (position, regex) {
+ const {tokenizedBuffer, buffer} = this.editor
+ const {grammar} = tokenizedBuffer
+ let column = 0
+ const line = tokenizedBuffer.tokenizedLineForRow(position.row)
+ if (line == null) { return false }
+ const lineLength = buffer.lineLengthForRow(position.row)
+ const scopeIds = line.openScopes.slice()
+ for (let i = 0; i < line.tags.length; i++) {
+ const tag = line.tags[i]
+ if (tag >= 0) {
+ const nextColumn = column + tag
+ if ((nextColumn > position.column) || (nextColumn === lineLength)) { break }
+ column = nextColumn
+ } else if ((tag & 1) === 1) {
+ scopeIds.push(tag)
+ } else {
+ scopeIds.pop()
+ }
+ }
+
+ return scopeIds.some(scopeId => regex.test(grammar.scopeForId(scopeId)))
+ }
+
+ findStartTag (tagName, endPosition, fullRange = false) {
+ const scanRange = new Range([0, 0], endPosition)
+ const pattern = this.patternForTagName(tagName)
+ let startRange = null
+ let unpairedCount = 0
+ this.editor.backwardsScanInBufferRange(pattern, scanRange, ({match, range, stop}) => {
+ if (this.isRangeCommented(range)) return
+
+ const [entireMatch, isStartTag, tagName, attributes, suffix, isSelfClosingTag, isEndTag] = match
+
+ if (isSelfClosingTag) return
+
+ if (isStartTag) {
+ unpairedCount--
+ if (unpairedCount < 0) {
+ stop()
+ startRange = range
+ if (!fullRange) {
+ // Move the start past the initial <
+ startRange.start = startRange.start.translate([0, 1])
+
+ // End right after the tag name
+ startRange.end = startRange.start.translate([0, tagName.length])
+ }
+ }
+ } else {
+ unpairedCount++
+ }
+ })
+
+ return startRange
+ }
+
+ findEndTag (tagName, startPosition, fullRange = false) {
+ const scanRange = new Range(startPosition, this.editor.buffer.getEndPosition())
+ const pattern = this.patternForTagName(tagName)
+ let endRange = null
+ let unpairedCount = 0
+ this.editor.scanInBufferRange(pattern, scanRange, ({match, range, stop}) => {
+ if (this.isRangeCommented(range)) return
+
+ const [entireMatch, isStartTag, tagName, attributes, suffix, isSelfClosingTag, isEndTag] = match
+
+ if (isSelfClosingTag) return
+
+ if (isStartTag) {
+ unpairedCount++
+ } else {
+ unpairedCount--
+ if (unpairedCount < 0) {
+ stop()
+ endRange = range
+ if (!fullRange) {
+ // Subtract and > from range
+ endRange = range.translate([0, 2], [0, -1])
+ }
+ }
+ }
+ })
+
+ return endRange
+ }
+
+ findStartEndTags (fullRange = false) {
+ let ranges = {}
+ const endPosition = this.editor.getLastCursor().getCurrentWordBufferRange({wordRegex: this.wordRegex}).end
+ this.editor.backwardsScanInBufferRange(this.tagPattern, [[0, 0], endPosition], ({match, range, stop}) => {
+ stop()
+
+ const [entireMatch, prefix, isClosingTag, tagName, attributes, suffix, isSelfClosingTag] = Array.from(match)
+
+ let startRange = range
+ if (!fullRange) {
+ if (range.start.row === range.end.row) {
+ // Move the start past the initial <
+ startRange.start = startRange.start.translate([0, prefix.length])
+ // End right after the tag name
+ startRange.end = startRange.start.translate([0, tagName.length])
+ } else {
+ startRange = Range.fromObject([range.start.translate([0, prefix.length]), [range.start.row, Infinity]])
+ }
+ }
+
+ let endRange
+ if (isSelfClosingTag) {
+ endRange = startRange
+ } else if (isClosingTag) {
+ endRange = this.findStartTag(tagName, startRange.start, fullRange)
+ } else {
+ endRange = this.findEndTag(tagName, startRange.end, fullRange)
+ }
+
+ if (startRange && endRange) ranges = {startRange, endRange}
+ })
+
+ return ranges
+ }
+
+ findMatchingTags () {
+ return (this.isCursorOnTag() && this.findStartEndTags()) || {}
+ }
+
+ // Parses a fragment of html returning the stack (i.e., an array) of open tags
+ //
+ // fragment - the fragment of html to be analysed
+ // stack - an array to be populated (can be non-empty)
+ // matchExpr - a RegExp describing how to match opening/closing tags
+ // the opening/closing descriptions must be captured subexpressions
+ // so that the code can refer to match[1] to check if an opening
+ // tag has been found, and to match[2] to check if a closing tag
+ // has been found
+ // cond - a condition to be checked at each iteration. If the function
+ // returns false the processing is immediately interrupted. When
+ // called the current stack is provided to the function.
+ //
+ // Returns an array of strings. Each string is a tag that is still to be closed
+ // (the most recent non closed tag is at the end of the array).
+ parseFragment (fragment, stack, matchExpr, cond) {
+ let match = fragment.match(matchExpr)
+ while (match && cond(stack)) {
+ if (SelfClosingTags.indexOf(match[1]) === -1) {
+ const topElem = stack[stack.length - 1]
+
+ if (match[2] && (topElem === match[2])) {
+ stack.pop()
+ } else if (match[1]) {
+ stack.push(match[1])
+ }
+ }
+
+ fragment = fragment.substr(match.index + match[0].length)
+ match = fragment.match(matchExpr)
+ }
+
+ return stack
+ }
+
+ // Parses the given fragment of html code returning the last unclosed tag.
+ //
+ // fragment - a string containing a fragment of html code.
+ //
+ // Returns an array of strings. Each string is a tag that is still to be closed
+ // (the most recent non closed tag is at the end of the array).
+ tagsNotClosedInFragment (fragment) {
+ return this.parseFragment(fragment, [], tagStartOrEndRegex, () => true)
+ }
+
+ // Parses the given fragment of html code and returns true if the given tag
+ // has a matching closing tag in it. If tag is reopened and reclosed in the
+ // given fragment then the end point of that pair does not count as a matching
+ // closing tag.
+ tagDoesNotCloseInFragment (tags, fragment) {
+ if (tags.length === 0) { return false }
+
+ let stack = tags
+ const stackLength = stack.length
+ const tag = tags[tags.length - 1]
+ const escapedTag = _.escapeRegExp(tag)
+ stack = this.parseFragment(fragment, stack, generateTagStartOrEndRegex(escapedTag), s =>
+ s.length >= stackLength || s[s.length - 1] === tag
+ )
+
+ return (stack.length > 0) && (stack[stack.length - 1] === tag)
+ }
+
+ // Parses preFragment and postFragment returning the last open tag in
+ // preFragment that is not closed in postFragment.
+ //
+ // Returns a tag name or null if it can't find it.
+ closingTagForFragments (preFragment, postFragment) {
+ const tags = this.tagsNotClosedInFragment(preFragment)
+ const tag = tags[tags.length - 1]
+ if (this.tagDoesNotCloseInFragment(tags, postFragment)) {
+ return tag
+ } else {
+ return null
+ }
+ }
+}
diff --git a/packages/bracket-matcher/menus/bracket-matcher.cson b/packages/bracket-matcher/menus/bracket-matcher.cson
new file mode 100644
index 000000000..a74d146f2
--- /dev/null
+++ b/packages/bracket-matcher/menus/bracket-matcher.cson
@@ -0,0 +1,22 @@
+'menu': [
+ {
+ 'label': 'Packages'
+ 'submenu': [
+ 'label': 'Bracket Matcher'
+ 'submenu': [
+ { 'label': 'Go To Matching Bracket', 'command': 'bracket-matcher:go-to-matching-bracket' }
+ { 'label': 'Select Inside Brackets', 'command': 'bracket-matcher:select-inside-brackets' }
+ { 'label': 'Remove Brackets From Selection', 'command': 'bracket-matcher:remove-brackets-from-selection' }
+ { 'label': 'Close Current Tag', 'command': 'bracket-matcher:close-tag' }
+ { 'label': 'Remove Matching Brackets', 'command': 'bracket-matcher:remove-matching-brackets' }
+ { 'label': 'Select Matching Brackets', 'command': 'bracket-matcher:select-matching-brackets' }
+ ]
+ ]
+ },
+ {
+ 'label': 'Selection'
+ 'submenu': [
+ { 'label': 'Select Inside Brackets', 'command': 'bracket-matcher:select-inside-brackets' }
+ ]
+ }
+]
diff --git a/packages/bracket-matcher/package.json b/packages/bracket-matcher/package.json
new file mode 100644
index 000000000..38addbd62
--- /dev/null
+++ b/packages/bracket-matcher/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "bracket-matcher",
+ "version": "0.92.0",
+ "main": "./lib/main",
+ "description": "Highlight the matching bracket for the `(){}[]` character under the cursor. Move the cursor to the matching bracket with `ctrl-m`.",
+ "repository": "https://github.com/pulsar-edit/bracket-matcher",
+ "license": "MIT",
+ "engines": {
+ "atom": "*"
+ },
+ "dependencies": {
+ "underscore-plus": "1.x"
+ },
+ "configSchema": {
+ "autocompleteCharacters": {
+ "description": "Autocompleted characters treated as matching pairs, such as `()`, and `{}`.",
+ "type": "array",
+ "default": [
+ "()",
+ "[]",
+ "{}",
+ "\"\"",
+ "''",
+ "``",
+ "“”",
+ "‘’",
+ "«»",
+ "‹›"
+ ],
+ "items": {
+ "type": "string"
+ }
+ },
+ "pairsWithExtraNewline": {
+ "description": "Automatically add a newline between the pair when enter is pressed.",
+ "type": "array",
+ "default": [
+ "()",
+ "[]",
+ "{}"
+ ],
+ "items": {
+ "type": "string"
+ }
+ },
+ "autocompleteBrackets": {
+ "type": "boolean",
+ "default": true,
+ "description": "Autocomplete bracket and quote characters, such as `(` and `)`, and `\"`."
+ },
+ "wrapSelectionsInBrackets": {
+ "type": "boolean",
+ "default": true,
+ "description": "Wrap selected text in brackets or quotes when the editor contains selections and the opening bracket or quote is typed."
+ },
+ "highlightMatchingLineNumber": {
+ "type": "boolean",
+ "default": false,
+ "description": "Highlight the line number of the matching bracket."
+ },
+ "alwaysSkipClosingPairs": {
+ "type": "boolean",
+ "default": false,
+ "description": "Always skip closing pairs in front of the cursor."
+ }
+ }
+}
diff --git a/packages/bracket-matcher/spec/bracket-matcher-spec.js b/packages/bracket-matcher/spec/bracket-matcher-spec.js
new file mode 100644
index 000000000..25acce374
--- /dev/null
+++ b/packages/bracket-matcher/spec/bracket-matcher-spec.js
@@ -0,0 +1,1913 @@
+const {Point, TextBuffer} = require('atom')
+
+const HAS_NEW_TEXT_BUFFER_VERSION = (new TextBuffer()).getLanguageMode().bufferDidFinishTransaction
+const path = require('path')
+
+describe('bracket matching', () => {
+ let editorElement, editor, buffer
+
+ beforeEach(() => {
+ atom.config.set('bracket-matcher.autocompleteBrackets', true)
+
+ waitsForPromise(() => atom.packages.activatePackage('bracket-matcher'))
+
+ waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
+
+ waitsForPromise(() => atom.packages.activatePackage('language-xml'))
+
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.js')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+ buffer = editor.getBuffer()
+ })
+ })
+
+ describe('matching bracket highlighting', () => {
+ beforeEach(() => {
+ atom.config.set('bracket-matcher.highlightMatchingLineNumber', true)
+ })
+
+ function expectNoHighlights () {
+ const decorations = editor.getHighlightDecorations().filter(decoration => decoration.properties.class === 'bracket-matcher')
+ expect(decorations.length).toBe(0)
+ }
+
+ function expectHighlights (startBufferPosition, endBufferPosition) {
+ const decorations = editor.getHighlightDecorations().filter(decoration => decoration.properties.class === 'bracket-matcher')
+ const gutterDecorations = editor.getLineNumberDecorations().filter(gutterDecoration => gutterDecoration.properties.class === 'bracket-matcher')
+
+ startBufferPosition = Point.fromObject(startBufferPosition)
+ endBufferPosition = Point.fromObject(endBufferPosition)
+ if (startBufferPosition.isGreaterThan(endBufferPosition)) {
+ [startBufferPosition, endBufferPosition] = [endBufferPosition, startBufferPosition]
+ }
+ decorations.sort((a, b) => a.getMarker().compare(b.getMarker()))
+ gutterDecorations.sort((a, b) => a.getMarker().compare(b.getMarker()))
+
+ expect(decorations.length).toBe(2)
+ expect(gutterDecorations.length).toBe(2)
+
+ expect(decorations[0].marker.getStartBufferPosition()).toEqual(startBufferPosition)
+ expect(decorations[1].marker.getStartBufferPosition()).toEqual(endBufferPosition)
+
+ expect(gutterDecorations[0].marker.getStartBufferPosition()).toEqual(startBufferPosition)
+ expect(gutterDecorations[1].marker.getStartBufferPosition()).toEqual(endBufferPosition)
+ }
+
+ describe('when the cursor is before a starting pair', () => {
+ it('highlights the starting pair and ending pair', () => {
+ editor.moveToEndOfLine()
+ editor.moveLeft()
+ expectHighlights([0, 28], [12, 0])
+ })
+ })
+
+ describe('when the cursor is after a starting pair', () => {
+ it('highlights the starting pair and ending pair', () => {
+ editor.moveToEndOfLine()
+ expectHighlights([0, 28], [12, 0])
+ })
+ })
+
+ describe('when the cursor is before an ending pair', () => {
+ it('highlights the starting pair and ending pair', () => {
+ editor.moveToBottom()
+ editor.moveLeft()
+ editor.moveLeft()
+ expectHighlights([12, 0], [0, 28])
+ })
+ })
+
+ describe('when closing multiple pairs', () => {
+ it('always highlights the inner pair', () => {
+ editor.setCursorBufferPosition([8, 53])
+ expectHighlights([8, 53], [8, 47])
+ editor.moveRight()
+ expectHighlights([8, 53], [8, 47])
+ editor.moveRight()
+ expectHighlights([8, 54], [8, 42])
+ })
+ })
+
+ describe('when opening multiple pairs', () => {
+ it('always highlights the inner pair', () => {
+ editor.setText('((1 + 1) * 2)')
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 12])
+ editor.moveRight()
+ expectHighlights([0, 1], [0, 7])
+ editor.moveRight()
+ expectHighlights([0, 1], [0, 7])
+ })
+ })
+
+ describe('when the cursor is after an ending pair', () => {
+ it('highlights the starting pair and ending pair', () => {
+ editor.moveToBottom()
+ editor.moveLeft()
+ expectHighlights([12, 0], [0, 28])
+ })
+ })
+
+ describe('when there are unpaired brackets', () => {
+ it('highlights the correct start/end pairs', () => {
+ editor.setText('(()')
+ editor.setCursorBufferPosition([0, 0])
+ expectNoHighlights()
+
+ editor.setCursorBufferPosition([0, 1])
+ expectHighlights([0, 1], [0, 2])
+
+ editor.setCursorBufferPosition([0, 2])
+ expectHighlights([0, 1], [0, 2])
+
+ editor.setText(('())'))
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 1])
+
+ editor.setCursorBufferPosition([0, 1])
+ expectHighlights([0, 0], [0, 1])
+
+ editor.setCursorBufferPosition([0, 2])
+ expectHighlights([0, 1], [0, 0])
+
+ editor.setCursorBufferPosition([0, 3])
+ expectNoHighlights()
+ })
+ })
+
+ describe('when there are commented brackets', () => {
+ it('highlights the correct start/end pairs', () => {
+ editor.setText('(//)')
+ editor.setCursorBufferPosition([0, 0])
+ expectNoHighlights()
+
+ editor.setCursorBufferPosition([0, 2])
+ expectNoHighlights()
+
+ editor.setCursorBufferPosition([0, 3])
+ expectNoHighlights()
+
+ editor.setText('{/*}*/')
+ editor.setCursorBufferPosition([0, 0])
+ expectNoHighlights()
+
+ editor.setCursorBufferPosition([0, 2])
+ expectNoHighlights()
+
+ editor.setCursorBufferPosition([0, 3])
+ expectNoHighlights()
+
+ editor.setText('[/*]*/]')
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 6])
+
+ editor.setCursorBufferPosition([0, 6])
+ expectHighlights([0, 6], [0, 0])
+
+ editor.setCursorBufferPosition([0, 2])
+ expectNoHighlights()
+ })
+ })
+
+ describe('when there are quoted brackets', () => {
+ it('highlights the correct start/end pairs', () => {
+ editor.setText("(')')")
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 4])
+
+ editor.setCursorBufferPosition([0, 5])
+ expectHighlights([0, 4], [0, 0])
+
+ editor.setCursorBufferPosition([0, 2])
+ expectNoHighlights()
+
+ editor.setText('["]"]')
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 4])
+
+ editor.setCursorBufferPosition([0, 5])
+ expectHighlights([0, 4], [0, 0])
+
+ editor.setCursorBufferPosition([0, 2])
+ expectNoHighlights()
+ })
+ })
+
+ describe('when there are brackets inside code embedded in a string', () => {
+ it('highlights the correct start/end pairs', () => {
+ editor.setText('(`${(1+1)}`)')
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 11])
+
+ editor.setCursorBufferPosition([0, 12])
+ expectHighlights([0, 11], [0, 0])
+
+ editor.setCursorBufferPosition([0, 4])
+ expectHighlights([0, 4], [0, 8])
+ })
+ })
+
+ describe('when there are brackets inside a string inside code embedded in a string', () => {
+ it('highlights the correct start/end pairs', () => {
+ editor.setText("(`${('(1+1)')}`)")
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 15])
+
+ editor.setCursorBufferPosition([0, 16])
+ expectHighlights([0, 15], [0, 0])
+
+ editor.setCursorBufferPosition([0, 6])
+ expectNoHighlights()
+ })
+ })
+
+ describe('when there are brackets in regular expressions', () => {
+ it('highlights the correct start/end pairs', () => {
+ editor.setText('(/[)]/)')
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 0], [0, 6])
+
+ editor.setCursorBufferPosition([0, 7])
+ expectHighlights([0, 6], [0, 0])
+
+ editor.setCursorBufferPosition([0, 3])
+ expectHighlights([0, 2], [0, 4])
+ })
+ })
+
+ describe('when the start character and end character of the pair are equivalent', () => {
+ it('does not attempt to highlight pairs', () => {
+ editor.setText("'hello'")
+ editor.setCursorBufferPosition([0, 0])
+ expectNoHighlights()
+ })
+ })
+
+ describe('when the cursor is moved off a pair', () => {
+ it('removes the starting pair and ending pair highlights', () => {
+ editor.moveToEndOfLine()
+ expectHighlights([0, 28], [12, 0])
+
+ editor.moveToBeginningOfLine()
+ expectNoHighlights()
+ })
+ })
+
+ describe('when the pair moves', () => {
+ it('repositions the highlights', () => {
+ editor.moveToEndOfLine()
+ editor.moveLeft()
+ expectHighlights([0, 28], [12, 0])
+
+ editor.deleteToBeginningOfLine()
+ expectHighlights([0, 0], [12, 0])
+ })
+ })
+
+ describe('pair balancing', () =>
+ describe('when a second starting pair preceeds the first ending pair', () => {
+ it('advances to the second ending pair', () => {
+ editor.setCursorBufferPosition([8, 42])
+ expectHighlights([8, 42], [8, 54])
+ })
+ })
+ )
+
+ describe('when a cursor is added or destroyed', () => {
+ it('updates the highlights to use the new cursor', () => {
+ editor.setCursorBufferPosition([9, 0])
+ expectNoHighlights()
+
+ editor.addCursorAtBufferPosition([0, 29])
+ expectHighlights([0, 28], [12, 0])
+
+ editor.addCursorAtBufferPosition([0, 4])
+ expectNoHighlights()
+
+ editor.getLastCursor().destroy()
+ expectHighlights([0, 28], [12, 0])
+ })
+ })
+
+ describe('when highlightMatchingLineNumber config is disabled', () => {
+ it('does not highlight the gutter', () => {
+ atom.config.set('bracket-matcher.highlightMatchingLineNumber', false)
+ editor.moveToEndOfLine()
+ editor.moveLeft()
+ const gutterDecorations = editor.getLineNumberDecorations().filter(gutterDecoration => gutterDecoration.properties.class === 'bracket-matcher')
+ expect(gutterDecorations.length).toBe(0)
+ })
+ })
+
+ describe('when the cursor moves off (clears) a selection next to a starting or ending pair', () => {
+ it('highlights the starting pair and ending pair', () => {
+ editor.moveToEndOfLine()
+ editor.selectLeft()
+ editor.getLastCursor().clearSelection()
+ expectHighlights([0, 28], [12, 0])
+ })
+ })
+
+ forEachLanguageWithTags(scopeName => {
+ describe(`${scopeName} tag matching`, () => {
+ beforeEach(() => {
+ waitsForPromise(() => atom.packages.activatePackage('language-html'))
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+ buffer = editor.buffer
+ atom.grammars.assignLanguageMode(buffer, scopeName)
+ buffer.getLanguageMode().syncOperationLimit = Infinity
+ })
+ })
+
+ describe('when on an opening tag', () => {
+ it('highlights the opening and closing tag', () => {
+ buffer.setText(`\
+
+ text
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([0, 0])
+ expectHighlights([0, 1], [3, 2])
+
+ editor.setCursorBufferPosition([0, 1])
+ expectHighlights([0, 1], [3, 2])
+ })
+ })
+
+ describe('when on a closing tag', () => {
+ it('highlights the opening and closing tag', () => {
+ buffer.setText(`\
+
+
+ text
+\
+`
+ )
+
+ editor.setCursorBufferPosition([3, 0])
+ expectHighlights([3, 2], [0, 1])
+
+ editor.setCursorBufferPosition([3, 2])
+ expectHighlights([3, 2], [0, 1])
+
+ buffer.setText(`\
+
+ text
+ text
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, Infinity])
+ expectHighlights([1, 14], [1, 3])
+
+ editor.setCursorBufferPosition([2, Infinity])
+ expectHighlights([2, 14], [2, 3])
+ })
+
+ it('highlights the correct opening tag, skipping self-closing tags', () => {
+ buffer.setText(`\
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([2, Infinity])
+ expectHighlights([2, 2], [0, 1])
+ })
+ })
+
+ describe('when on a self-closing tag', () => {
+ it('highlights only the self-closing tag', () => {
+ buffer.setText(`\
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, Infinity])
+ expectHighlights([1, 3], [1, 3])
+ })
+
+ it('highlights a self-closing tag without a space', () => {
+ buffer.setText(`\
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, Infinity])
+ expectHighlights([1, 3], [1, 3])
+ })
+
+ it('highlights a self-closing tag with many spaces', () => {
+ buffer.setText(`\
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, Infinity])
+ expectHighlights([1, 3], [1, 3])
+ })
+
+ it('does not catastrophically backtrack when many attributes are present (regression)', () => {
+ // https://github.com/atom/bracket-matcher/issues/303
+
+ buffer.setText(`\
+
\
+`
+ )
+
+ editor.setCursorBufferPosition([0, 6])
+ expectHighlights([0, 1], [4, 2])
+
+ editor.setCursorBufferPosition([1, 6])
+ expectHighlights([1, 3], [3, 4])
+
+ editor.setCursorBufferPosition([2, 6])
+ expectHighlights([2, 5], [2, 5])
+
+ editor.setCursorBufferPosition([3, 6])
+ expectHighlights([3, 4], [1, 3])
+
+ editor.setCursorBufferPosition([4, 6])
+ expectHighlights([4, 2], [0, 1])
+ })
+ })
+
+ describe('when the tag spans multiple lines', () => {
+ it('highlights the opening and closing tag', () => {
+ buffer.setText(`\
+\
+`
+ )
+
+ editor.setCursorBufferPosition([0, 1])
+ expectHighlights([0, 1], [6, 2])
+ editor.setCursorBufferPosition([6, 2])
+ expectHighlights([6, 2], [0, 1])
+ })
+ })
+
+ describe('when the tag has attributes', () => {
+ it('highlights the opening and closing tags', () => {
+ buffer.setText(`\
+
+ text
+\
+`
+ )
+
+ editor.setCursorBufferPosition([2, 2])
+ expectHighlights([2, 2], [0, 1])
+
+ editor.setCursorBufferPosition([0, 7])
+ expectHighlights([0, 1], [2, 2])
+ })
+ })
+
+ describe("when the tag has an attribute with a value of '/'", () => {
+ it('highlights the opening and closing tags', () => {
+ buffer.setText(`\
+
+ text
+\
+`
+ )
+
+ editor.setCursorBufferPosition([2, 2])
+ expectHighlights([2, 2], [0, 1])
+
+ editor.setCursorBufferPosition([0, 7])
+ expectHighlights([0, 1], [2, 2])
+ })
+ })
+
+ describe('when the opening and closing tags are on the same line', () => {
+ it('highlight the opening and closing tags', () => {
+ buffer.setText('text')
+
+ editor.setCursorBufferPosition([0, 2])
+ expectHighlights([0, 1], [0, 12])
+
+ editor.setCursorBufferPosition([0, 12])
+ expectHighlights([0, 12], [0, 1])
+ })
+ })
+
+ describe('when the closing tag is missing', () => {
+ it('does not highlight anything', () => {
+ buffer.setText('\ntext\n')
+ editor.setCursorBufferPosition([0, 10])
+ expectNoHighlights()
+ })
+ })
+
+ describe('when between the opening and closing tag', () => {
+ it('does not highlight anything', () => {
+ buffer.setText('\nhi\n
\n')
+ editor.setCursorBufferPosition([1, 0])
+ expectNoHighlights()
+ })
+ })
+ })
+ })
+ })
+
+ describe('when bracket-matcher:go-to-matching-bracket is triggered', () => {
+ describe('when the cursor is before the starting pair', () => {
+ it('moves the cursor to after the ending pair', () => {
+ editor.moveToEndOfLine()
+ editor.moveLeft()
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([12, 1])
+ })
+ })
+
+ describe('when the cursor is after the starting pair', () => {
+ it('moves the cursor to before the ending pair', () => {
+ editor.moveToEndOfLine()
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([12, 0])
+ })
+ })
+
+ describe('when the cursor is before the ending pair', () => {
+ it('moves the cursor to after the starting pair', () => {
+ editor.setCursorBufferPosition([12, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 29])
+ })
+ })
+
+ describe('when the cursor is after the ending pair', () => {
+ it('moves the cursor to before the starting pair', () => {
+ editor.setCursorBufferPosition([12, 1])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 28])
+ })
+ })
+
+ describe('when the cursor is not adjacent to a pair', () => {
+ describe('when within a `{}` pair', () => {
+ it('moves the cursor to before the enclosing brace', () => {
+ editor.setCursorBufferPosition([11, 2])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 28])
+ })
+ })
+
+ describe('when within a `()` pair', () => {
+ it('moves the cursor to before the enclosing brace', () => {
+ editor.setCursorBufferPosition([2, 14])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([2, 7])
+ })
+ })
+
+ forEachLanguageWithTags(scopeName => {
+ describe(`in ${scopeName} files`, () => {
+ beforeEach(() => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+ buffer = editor.buffer
+ atom.grammars.assignLanguageMode(buffer, scopeName)
+ buffer.getLanguageMode().syncOperationLimit = Infinity
+ })
+ })
+
+ describe('when within a pair', () => {
+ it('moves the cursor to the starting tag', () => {
+ editor.setCursorBufferPosition([5, 10])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([4, 9])
+ })
+ })
+
+ describe('when on a starting ', () => {
+ it('moves the cursor to the end ', () => {
+ editor.setCursorBufferPosition([1, 2])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 2])
+
+ editor.setCursorBufferPosition([1, 3])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 4])
+
+ editor.setCursorBufferPosition([1, 4])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 5])
+
+ editor.setCursorBufferPosition([1, 5])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 6])
+
+ editor.setCursorBufferPosition([1, 6])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 7])
+
+ editor.setCursorBufferPosition([1, 7])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 8])
+
+ editor.setCursorBufferPosition([1, 8])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 8])
+
+ editor.setCursorBufferPosition([1, 9])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 8])
+
+ editor.setCursorBufferPosition([1, 10])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 8])
+
+ editor.setCursorBufferPosition([1, 16])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([15, 8])
+ })
+ })
+
+ describe('when on an ending ', () => {
+ it('moves the cursor to the start ', () => {
+ editor.setCursorBufferPosition([15, 2])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 2])
+
+ editor.setCursorBufferPosition([15, 3])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 3])
+
+ editor.setCursorBufferPosition([15, 4])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 3])
+
+ editor.setCursorBufferPosition([15, 5])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 4])
+
+ editor.setCursorBufferPosition([15, 6])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 5])
+
+ editor.setCursorBufferPosition([15, 7])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 6])
+
+ editor.setCursorBufferPosition([15, 8])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 7])
+
+ editor.setCursorBufferPosition([15, 9])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-matching-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([1, 7])
+ })
+ })
+ })
+ })
+ })
+ })
+
+ describe('when bracket-matcher:go-to-enclosing-bracket is triggered', () => {
+ describe('when within a `{}` pair', () => {
+ it('moves the cursor to before the enclosing brace', () => {
+ editor.setCursorBufferPosition([11, 2])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-enclosing-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 28])
+ })
+ })
+
+ describe('when within a `()` pair', () => {
+ it('moves the cursor to before the enclosing brace', () => {
+ editor.setCursorBufferPosition([2, 14])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-enclosing-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([2, 7])
+ })
+ })
+
+ describe('when not within a pair', () => {
+ it('does not do anything', () => {
+ editor.setCursorBufferPosition([0, 3])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:go-to-enclosing-bracket')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 3])
+ })
+ })
+ })
+
+ describe('when bracket-match:select-inside-brackets is triggered', () => {
+ describe('when the cursor on the left side of a bracket', () => {
+ it('selects the text inside the brackets', () => {
+ editor.setCursorBufferPosition([0, 28])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 29], [12, 0]])
+ })
+ })
+
+ describe('when the cursor on the right side of a bracket', () => {
+ it('selects the text inside the brackets', () => {
+ editor.setCursorBufferPosition([1, 30])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[1, 30], [9, 2]])
+ })
+ })
+
+ describe('when the cursor is inside the brackets', () => {
+ it('selects the text for the closest outer brackets', () => {
+ editor.setCursorBufferPosition([6, 6])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[4, 29], [7, 4]])
+ })
+ })
+
+ describe('when there are no brackets or tags', () => {
+ it('does not catastrophically backtrack (regression)', () => {
+ buffer.setText(`${'a'.repeat(500)}\n`.repeat(500))
+ editor.setCursorBufferPosition([0, 500])
+
+ const start = Date.now()
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 500], [0, 500]])
+ expect(Date.now() - start).toBeLessThan(5000)
+ })
+ })
+
+ it('does not error when a bracket is already highlighted (regression)', () => {
+ atom.grammars.assignLanguageMode(editor, null)
+ editor.setText("(ok)")
+ editor.selectAll()
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ })
+
+ describe('when there are multiple cursors', () => {
+ beforeEach(() => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'multiplecursor.md')))
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+ })
+ })
+ it('selects text inside the multiple cursors', () => {
+ editor.addCursorAtBufferPosition([0, 6])
+ editor.addCursorAtBufferPosition([1, 6])
+ editor.addCursorAtBufferPosition([2, 6])
+ editor.addCursorAtBufferPosition([3, 6])
+ editor.addCursorAtBufferPosition([4, 6])
+
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+
+ const selectedRanges = editor.getSelectedBufferRanges();
+ expect(selectedRanges.length).toBe(6)
+ expect(selectedRanges).toEqual([
+ [[0, 0], [0, 0]],
+ [[0, 1], [0, 7]],
+ [[1, 1], [1, 15]],
+ [[2, 1], [2, 19]],
+ [[3, 1], [3, 13]],
+ [[4, 1], [4, 15]],
+ ])
+ })
+ })
+
+ forEachLanguageWithTags(scopeName => {
+ describe(`${scopeName} tag matching`, () => {
+ beforeEach(() => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+ buffer = editor.buffer
+ })
+ })
+
+ describe('when the cursor is on a starting tag', () => {
+ it('selects the text inside the starting/closing tag', () => {
+ editor.setCursorBufferPosition([4, 9])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[4, 13], [6, 8]])
+ })
+ })
+
+ describe('when the cursor is on an ending tag', () => {
+ it('selects the text inside the starting/closing tag', () => {
+ editor.setCursorBufferPosition([14, 9])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[10, 9], [14, 4]])
+ })
+ })
+
+ describe('when the cursor is inside a tag', () => {
+ it('selects the text inside the starting/closing tag', () => {
+ editor.setCursorBufferPosition([12, 8])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[11, 11], [13, 6]])
+ })
+ })
+
+ it('does not select attributes inside tags', () => {
+ editor.setCursorBufferPosition([1, 10])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-inside-brackets')
+ expect(editor.getSelectedBufferRange()).toEqual([[1, 17], [15, 2]])
+ })
+ })
+ })
+ })
+
+ describe('when bracket-matcher:remove-matching-brackets is triggered', () => {
+ describe('when the cursor is not in front of any pair', () => {
+ it('performs a regular backspace action', () => {
+ editor.setCursorBufferPosition([0, 1])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(0)).toEqual('ar quicksort = function () {')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 0])
+ })
+ })
+
+ describe('when the cursor is at the beginning of a line', () => {
+ it('performs a regular backspace action', () => {
+ editor.setCursorBufferPosition([12, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(11)).toEqual(' return sort(Array.apply(this, arguments));};')
+ expect(editor.getCursorBufferPosition()).toEqual([11, 44])
+ })
+ })
+
+ describe('when the cursor is on the left side of a starting pair', () => {
+ it('performs a regular backspace action', () => {
+ editor.setCursorBufferPosition([0, 28])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(0)).toEqual('var quicksort = function (){')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 27])
+ })
+ })
+
+ describe('when the cursor is on the left side of an ending pair', () => {
+ it('performs a regular backspace action', () => {
+ editor.setCursorBufferPosition([7, 4])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(7)).toEqual(' }')
+ expect(editor.getCursorBufferPosition()).toEqual([7, 2])
+ })
+ })
+
+ describe('when the cursor is on the right side of a starting pair, the ending pair on another line', () => {
+ it('removes both pairs', () => {
+ editor.setCursorBufferPosition([0, 29])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(0)).toEqual('var quicksort = function () ')
+ expect(editor.lineTextForBufferRow(12)).toEqual(';')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 28])
+ })
+ })
+
+ describe('when the cursor is on the right side of an ending pair, the starting pair on another line', () => {
+ it('removes both pairs', () => {
+ editor.setCursorBufferPosition([7, 5])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(4)).toEqual(' while(items.length > 0) ')
+ expect(editor.lineTextForBufferRow(7)).toEqual(' ')
+ expect(editor.getCursorBufferPosition()).toEqual([7, 4])
+ })
+ })
+
+ describe('when the cursor is on the right side of a starting pair, the ending pair on the same line', () => {
+ it('removes both pairs', () => {
+ editor.setCursorBufferPosition([11, 14])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(11)).toEqual(' return sortArray.apply(this, arguments);')
+ expect(editor.getCursorBufferPosition()).toEqual([11, 13])
+ })
+ })
+
+ describe('when the cursor is on the right side of an ending pair, the starting pair on the same line', () => {
+ it('removes both pairs', () => {
+ editor.setCursorBufferPosition([11, 43])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(11)).toEqual(' return sortArray.apply(this, arguments);')
+ expect(editor.getCursorBufferPosition()).toEqual([11, 41])
+ })
+ })
+
+ describe('when a starting pair is selected', () => {
+ it('removes both pairs', () => {
+ editor.setSelectedBufferRange([[11, 13], [11, 14]])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(11)).toEqual(' return sortArray.apply(this, arguments);')
+ expect(editor.getCursorBufferPosition()).toEqual([11, 13])
+ })
+ })
+
+ describe('when an ending pair is selected', () => {
+ it('removes both pairs', () => {
+ editor.setSelectedBufferRange([[11, 42], [11, 43]])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-matching-brackets')
+ expect(editor.lineTextForBufferRow(11)).toEqual(' return sortArray.apply(this, arguments);')
+ expect(editor.getCursorBufferPosition()).toEqual([11, 41])
+ })
+ })
+ })
+
+ describe('matching bracket deletion', () => {
+ beforeEach(() => {
+ editor.buffer.setText('')
+ })
+
+ describe('when selection is not a matching pair of brackets', () => {
+ it('does not change the text', () => {
+ editor.insertText('"woah(')
+ editor.selectAll()
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-brackets-from-selection')
+ expect(editor.buffer.getText()).toBe('"woah(')
+ })
+ })
+
+ describe('when selecting a matching pair of brackets', () => {
+ describe('on the same line', () => {
+ beforeEach(() => {
+ editor.buffer.setText('it "does something", :meta => true')
+ editor.setSelectedBufferRange([[0, 3], [0, 19]])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-brackets-from-selection')
+ })
+
+ it('removes the brackets', () => {
+ expect(editor.buffer.getText()).toBe('it does something, :meta => true')
+ })
+
+ it('selects the newly unbracketed text', () => {
+ expect(editor.getSelectedText()).toBe('does something')
+ })
+ })
+
+ describe('on separate lines', () => {
+ beforeEach(() => {
+ editor.buffer.setText('it ("does something" do\nend)')
+ editor.setSelectedBufferRange([[0, 3], [1, 4]])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:remove-brackets-from-selection')
+ })
+
+ it('removes the brackets', () => {
+ expect(editor.buffer.getText()).toBe('it "does something" do\nend')
+ })
+
+ it('selects the newly unbracketed text', () => {
+ expect(editor.getSelectedText()).toBe('"does something" do\nend')
+ })
+ })
+ })
+ })
+
+ describe('matching bracket insertion', () => {
+ beforeEach(() => {
+ editor.buffer.setText('')
+ atom.config.set('editor.autoIndent', true)
+ })
+
+ describe('when more than one character is inserted', () => {
+ it('does not insert a matching bracket', () => {
+ editor.insertText('woah(')
+ expect(editor.buffer.getText()).toBe('woah(')
+ })
+ })
+
+ describe('when there is a word character after the cursor', () => {
+ it('does not insert a matching bracket', () => {
+ editor.buffer.setText('ab')
+ editor.setCursorBufferPosition([0, 1])
+ editor.insertText('(')
+
+ expect(editor.buffer.getText()).toBe('a(b')
+ })
+ })
+
+ describe('when autocompleteBrackets configuration is disabled globally', () => {
+ it('does not insert a matching bracket', () => {
+ atom.config.set('bracket-matcher.autocompleteBrackets', false)
+ editor.buffer.setText('}')
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{}')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe('when autocompleteBrackets configuration is disabled in scope', () => {
+ it('does not insert a matching bracket', () => {
+ atom.config.set('bracket-matcher.autocompleteBrackets', true)
+ atom.config.set('bracket-matcher.autocompleteBrackets', false, {scopeSelector: '.source.js'})
+ editor.buffer.setText('}')
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{}')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe('when autocompleteCharacters configuration is set globally', () => {
+ it('inserts a matching angle bracket', () => {
+ atom.config.set('bracket-matcher.autocompleteCharacters', ['<>'])
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('<')
+ expect(buffer.lineForRow(0)).toBe('<>')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe('when autocompleteCharacters configuration is set in scope', () => {
+ it('inserts a matching angle bracket', () => {
+ atom.config.set('bracket-matcher.autocompleteCharacters', ['<>'], {scopeSelector: '.source.js'})
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('<')
+ expect(buffer.lineForRow(0)).toBe('<>')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+
+ it('emits a buffer change event after the cursor is in place', () => {
+ atom.config.set('bracket-matcher.autocompleteCharacters', ['<>'], {scopeSelector: '.source.js'})
+
+ let lastPosition = null
+ const sub = editor.getBuffer().onDidChange(() => {
+ expect(lastPosition).toBeNull()
+ lastPosition = editor.getLastCursor().getBufferPosition()
+ })
+
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('<')
+ expect(lastPosition).toEqual([0, 1])
+ })
+ })
+
+ describe('when there are multiple cursors', () => {
+ it('inserts ) at each cursor', () => {
+ editor.buffer.setText('()\nab\n[]\n12')
+ editor.setCursorBufferPosition([3, 1])
+ editor.addCursorAtBufferPosition([2, 1])
+ editor.addCursorAtBufferPosition([1, 1])
+ editor.addCursorAtBufferPosition([0, 1])
+ editor.insertText(')')
+
+ expect(editor.buffer.getText()).toBe('())\na)b\n[)]\n1)2')
+ })
+ })
+
+ describe('when there is a non-word character after the cursor', () => {
+ it('inserts a closing bracket after an opening bracket is inserted', () => {
+ editor.buffer.setText('}')
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{}}')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe('when the cursor is at the end of the line', () => {
+ it('inserts a closing bracket after an opening bracket is inserted', () => {
+ editor.buffer.setText('')
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{}')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+
+ editor.buffer.setText('')
+ editor.insertText('(')
+ expect(buffer.lineForRow(0)).toBe('()')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+
+ editor.buffer.setText('')
+ editor.insertText('[')
+ expect(buffer.lineForRow(0)).toBe('[]')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+
+ editor.buffer.setText('')
+ editor.insertText('"')
+ expect(buffer.lineForRow(0)).toBe('""')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+
+ editor.buffer.setText('')
+ editor.insertText("'")
+ expect(buffer.lineForRow(0)).toBe("''")
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe('when the cursor follows an escape character', () => {
+ it("doesn't insert a quote to match the escaped quote and overwrites the end quote", () => {
+ editor.buffer.setText('')
+ editor.insertText('"')
+ editor.insertText('\\')
+ editor.insertText('"')
+ editor.insertText('"')
+ expect(buffer.lineForRow(0)).toBe('"\\""')
+ })
+ })
+
+ describe('when the cursor follows an escape sequence', () => {
+ it('inserts a matching quote and overwrites it', () => {
+ editor.buffer.setText('')
+ editor.insertText('"')
+ editor.insertText('\\')
+ editor.insertText('\\')
+ editor.insertText('"')
+ expect(buffer.lineForRow(0)).toBe('"\\\\"')
+ })
+ })
+
+ describe('when the cursor follows a combination of escape characters', () => {
+ it('correctly decides whether to match the quote or not', () => {
+ editor.buffer.setText('')
+ editor.insertText('"')
+ editor.insertText('\\')
+ editor.insertText('\\')
+ editor.insertText('\\')
+ editor.insertText('"')
+ expect(buffer.lineForRow(0)).toBe('"\\\\\\""')
+
+ editor.buffer.setText('')
+ editor.insertText('"')
+ editor.insertText('\\')
+ editor.insertText('\\')
+ editor.insertText('\\')
+ editor.insertText('\\')
+ editor.insertText('"')
+ expect(buffer.lineForRow(0)).toBe('"\\\\\\\\"')
+ })
+ })
+
+ describe('when the cursor is on a closing bracket and a closing bracket is inserted', () => {
+ describe('when the closing bracket was there previously', () => {
+ it('inserts a closing bracket', () => {
+ editor.insertText('()x')
+ editor.setCursorBufferPosition([0, 1])
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('())x')
+ expect(editor.getCursorBufferPosition().column).toBe(2)
+ })
+ })
+
+ describe('when the closing bracket was automatically inserted from inserting an opening bracket', () => {
+ it('only moves cursor over the closing bracket one time', () => {
+ editor.insertText('(')
+ expect(buffer.lineForRow(0)).toBe('()')
+ editor.setCursorBufferPosition([0, 1])
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('()')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 2])
+
+ editor.setCursorBufferPosition([0, 1])
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('())')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 2])
+ })
+
+ it('moves cursor over the closing bracket after other text is inserted', () => {
+ editor.insertText('(')
+ editor.insertText('ok cool')
+ expect(buffer.lineForRow(0)).toBe('(ok cool)')
+ editor.setCursorBufferPosition([0, 8])
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('(ok cool)')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 9])
+ })
+
+ it('works with nested brackets', () => {
+ editor.insertText('(')
+ editor.insertText('1')
+ editor.insertText('(')
+ editor.insertText('2')
+ expect(buffer.lineForRow(0)).toBe('(1(2))')
+ editor.setCursorBufferPosition([0, 4])
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('(1(2))')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 5])
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('(1(2))')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 6])
+ })
+
+ it('works with mixed brackets', () => {
+ editor.insertText('(')
+ editor.insertText('}')
+ expect(buffer.lineForRow(0)).toBe('(})')
+ editor.insertText(')')
+ expect(buffer.lineForRow(0)).toBe('(})')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 3])
+ })
+
+ it('closes brackets with the same begin/end character correctly', () => {
+ editor.insertText('"')
+ editor.insertText('ok')
+ expect(buffer.lineForRow(0)).toBe('"ok"')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 3])
+ editor.insertText('"')
+ expect(buffer.lineForRow(0)).toBe('"ok"')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 4])
+ })
+ })
+ })
+
+ describe('when there is text selected on a single line', () => {
+ it('wraps the selection with brackets', () => {
+ editor.setText('text')
+ editor.moveToBottom()
+ editor.selectToTop()
+ editor.selectAll()
+ editor.insertText('(')
+ expect(buffer.getText()).toBe('(text)')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 5]])
+ expect(editor.getLastSelection().isReversed()).toBeTruthy()
+ })
+
+ describe('when the bracket-matcher.wrapSelectionsInBrackets is falsy globally', () => {
+ it('does not wrap the selection in brackets', () => {
+ atom.config.set('bracket-matcher.wrapSelectionsInBrackets', false)
+ editor.setText('text')
+ editor.moveToBottom()
+ editor.selectToTop()
+ editor.selectAll()
+ editor.insertText('(')
+ expect(buffer.getText()).toBe('(')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 1]])
+ })
+ })
+
+ describe('when the bracket-matcher.wrapSelectionsInBrackets is falsy in scope', () => {
+ it('does not wrap the selection in brackets', () => {
+ atom.config.set('bracket-matcher.wrapSelectionsInBrackets', true)
+ atom.config.set('bracket-matcher.wrapSelectionsInBrackets', false, {scopeSelector: '.source.js'})
+ editor.setText('text')
+ editor.moveToBottom()
+ editor.selectToTop()
+ editor.selectAll()
+ editor.insertText('(')
+ expect(buffer.getText()).toBe('(')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 1]])
+ })
+ })
+ })
+
+ describe('when there is text selected on multiple lines', () => {
+ it('wraps the selection with brackets', () => {
+ editor.insertText('text\nabcd')
+ editor.moveToBottom()
+ editor.selectToTop()
+ editor.selectAll()
+ editor.insertText('(')
+ expect('(text\nabcd)').toBe(buffer.getText())
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [1, 4]])
+ expect(editor.getLastSelection().isReversed()).toBeTruthy()
+ })
+
+ describe('when there are multiple selections', () => {
+ it('wraps each selection with brackets', () => {
+ editor.setText('a b\nb c\nc b')
+ editor.setSelectedBufferRanges([
+ [[0, 2], [0, 3]],
+ [[1, 0], [1, 1]],
+ [[2, 2], [2, 3]]
+ ])
+
+ editor.insertText('"')
+ expect(editor.getSelectedBufferRanges()).toEqual([
+ [[0, 3], [0, 4]],
+ [[1, 1], [1, 2]],
+ [[2, 3], [2, 4]]
+ ])
+
+ expect(buffer.lineForRow(0)).toBe('a "b"')
+ expect(buffer.lineForRow(1)).toBe('"b" c')
+ expect(buffer.lineForRow(2)).toBe('c "b"')
+ })
+ })
+ })
+
+ describe('when inserting a quote', () => {
+ describe('when a word character is before the cursor', () => {
+ it('does not automatically insert the closing quote', () => {
+ editor.buffer.setText('abc')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('abc"')
+
+ editor.buffer.setText('abc')
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("abc'")
+ })
+ })
+
+ describe('when an escape character is before the cursor', () => {
+ it('does not automatically insert the closing quote', () => {
+ editor.buffer.setText('\\')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('\\"')
+
+ editor.buffer.setText('\\')
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("\\'")
+
+ editor.buffer.setText('"\\"')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('"\\""')
+
+ editor.buffer.setText("\"\\'")
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe("\"\\'\"")
+
+ editor.buffer.setText("'\\\"")
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("'\\\"'")
+
+ editor.buffer.setText("'\\'")
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("'\\''")
+ })
+ })
+
+ describe('when an escape sequence is before the cursor', () => {
+ it('does not create a new quote pair', () => {
+ editor.buffer.setText('"\\\\"')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('"\\\\""')
+
+ editor.buffer.setText("'\\\\'")
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("'\\\\''")
+ })
+ })
+
+ describe('when a combination of escape characters is before the cursor', () => {
+ it('correctly determines whether it is an escape character or sequence', () => {
+ editor.buffer.setText('\\\\\\')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('\\\\\\"')
+
+ editor.buffer.setText('\\\\\\')
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("\\\\\\'")
+
+ editor.buffer.setText('"\\\\\\"')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('"\\\\\\""')
+
+ editor.buffer.setText("\"\\\\\\'")
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe("\"\\\\\\'\"")
+
+ editor.buffer.setText("'\\\\\\\"")
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("'\\\\\\\"'")
+
+ editor.buffer.setText("'\\\\\\'")
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("'\\\\\\''")
+ })
+ })
+
+ describe('when a quote is before the cursor', () => {
+ it('does not automatically insert the closing quote', () => {
+ editor.buffer.setText("''")
+ editor.moveToEndOfLine()
+ editor.insertText("'")
+ expect(editor.getText()).toBe("'''")
+
+ editor.buffer.setText('""')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('"""')
+
+ editor.buffer.setText('``')
+ editor.moveToEndOfLine()
+ editor.insertText('`')
+ expect(editor.getText()).toBe('```')
+
+ editor.buffer.setText("''")
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe("''\"\"")
+ })
+ })
+
+ describe('when a non word character is before the cursor', () => {
+ it('automatically inserts the closing quote', () => {
+ editor.buffer.setText('ab@')
+ editor.moveToEndOfLine()
+ editor.insertText('"')
+ expect(editor.getText()).toBe('ab@""')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 4])
+ })
+ })
+
+ describe('when the cursor is on an empty line', () => {
+ it('automatically inserts the closing quote', () => {
+ editor.buffer.setText('')
+ editor.insertText('"')
+ expect(editor.getText()).toBe('""')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe('when the select option to Editor::insertText is true', () => {
+ it('does not automatically insert the closing quote', () => {
+ editor.buffer.setText('')
+ editor.insertText('"', {select: true})
+ expect(editor.getText()).toBe('"')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+
+ describe("when the undo option to Editor::insertText is 'skip'", () => {
+ it('does not automatically insert the closing quote', () => {
+ editor.buffer.setText('')
+ editor.insertText('"', {undo: 'skip'})
+ expect(editor.getText()).toBe('"')
+ expect(editor.getCursorBufferPosition()).toEqual([0, 1])
+ })
+ })
+ })
+
+ describe('when return is pressed inside a matching pair', () => {
+ it('puts the cursor on the indented empty line', () => {
+ editor.insertText('void main() ')
+ editor.insertText('{')
+ expect(editor.getText()).toBe('void main() {}')
+ editor.insertNewline()
+ expect(editor.getCursorBufferPosition()).toEqual([1, 2])
+ expect(buffer.lineForRow(1)).toBe(' ')
+ expect(buffer.lineForRow(2)).toBe('}')
+
+ editor.setText(' void main() ')
+ editor.insertText('{')
+ expect(editor.getText()).toBe(' void main() {}')
+ editor.insertNewline()
+ expect(editor.getCursorBufferPosition()).toEqual([1, 4])
+ expect(buffer.lineForRow(1)).toBe(' ')
+ expect(buffer.lineForRow(2)).toBe(' }')
+ })
+
+ describe('when undo is triggered', () => {
+ it('removes both newlines', () => {
+ editor.insertText('void main() ')
+ editor.insertText('{')
+ editor.insertNewline()
+ editor.undo()
+ expect(editor.getText()).toBe('void main() {}')
+ })
+ })
+
+ describe('when editor.autoIndent is disabled', () => {
+ beforeEach(() => {
+ atom.config.set('editor.autoIndent', false)
+ })
+
+ it('does not auto-indent the empty line and closing bracket', () => {
+ editor.insertText(' void main() ')
+ editor.insertText('{')
+ expect(editor.getText()).toBe(' void main() {}')
+ editor.insertNewline()
+ expect(editor.getCursorBufferPosition()).toEqual([1, 0])
+ expect(buffer.lineForRow(1)).toBe('')
+ expect(buffer.lineForRow(2)).toBe('}')
+ })
+ })
+ })
+
+ describe('when in language specific scope', () => {
+ describe('string interpolation', () => {
+ beforeEach(() => {
+ waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
+
+ runs(() => buffer.setPath('foo.rb'))
+ })
+
+ it('should insert curly braces inside doubly quoted string', () => {
+ editor.insertText('foo = ')
+ editor.insertText('"')
+ editor.insertText('#')
+ expect(editor.getText()).toBe('foo = "#{}"')
+ editor.undo()
+ expect(editor.getText()).toBe('foo = ""')
+ })
+
+ it('should not insert curly braces inside singly quoted string', () => {
+ editor.insertText('foo = ')
+ editor.insertText("'")
+ editor.insertText('#')
+ expect(editor.getText()).toBe("foo = '#'")
+ })
+
+ it('should insert curly braces inside % string', () => {
+ editor.insertText('foo = %')
+ editor.insertText('(')
+ editor.insertText('#')
+ expect(editor.getText()).toBe('foo = %(#{})')
+ })
+
+ it('should not insert curly braces inside non-interpolated % string', () => {
+ editor.insertText('foo = %q')
+ editor.insertText('(')
+ editor.insertText('#')
+ expect(editor.getText()).toBe('foo = %q(#)')
+ })
+
+ it('should insert curly braces inside interpolated symbol', () => {
+ editor.insertText('foo = :')
+ editor.insertText('"')
+ editor.insertText('#')
+ expect(editor.getText()).toBe('foo = :"#{}"')
+ })
+
+ it('wraps the selection in the interpolation brackets when the selection is a single line', () => {
+ editor.setText('foo = "a bar"')
+ editor.setSelectedBufferRange([[0, 9], [0, 12]])
+
+ editor.insertText('#')
+ // coffeelint: disable=no_interpolation_in_single_quotes
+ expect(editor.getText()).toBe('foo = "a #{bar}"')
+ // coffeelint: enable=no_interpolation_in_single_quotes
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 11], [0, 14]])
+
+ editor.undo()
+ expect(editor.getText()).toBe('foo = "a bar"')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 9], [0, 12]])
+ })
+
+ it('does not wrap the selection in the interpolation brackets when the selection is mutli-line', () => {
+ editor.setText('foo = "a bar"\nfoo = "a bar"')
+ editor.setSelectedBufferRange([[0, 9], [1, 12]])
+
+ editor.insertText('#')
+ expect(editor.getText()).toBe('foo = "a #{}"')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 11], [0, 11]])
+
+ editor.undo()
+ expect(editor.getText()).toBe('foo = "a bar"\nfoo = "a bar"')
+ expect(editor.getSelectedBufferRange()).toEqual([[0, 9], [1, 12]])
+ })
+ })
+ })
+ })
+
+ describe('matching bracket deletion', () => {
+ it('deletes the end bracket when it directly precedes a begin bracket that is being backspaced', () => {
+ buffer.setText('')
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{}')
+ editor.backspace()
+ expect(buffer.lineForRow(0)).toBe('')
+ })
+
+ it('does not delete end bracket even if it directly precedes a begin bracket if autocomplete is turned off globally', () => {
+ atom.config.set('bracket-matcher.autocompleteBrackets', false)
+ buffer.setText('')
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{')
+ editor.insertText('}')
+ expect(buffer.lineForRow(0)).toBe('{}')
+ editor.setCursorBufferPosition([0, 1])
+ editor.backspace()
+ expect(buffer.lineForRow(0)).toBe('}')
+ })
+
+ it('does not delete end bracket even if it directly precedes a begin bracket if autocomplete is turned off in scope', () => {
+ atom.config.set('bracket-matcher.autocompleteBrackets', true)
+ atom.config.set('bracket-matcher.autocompleteBrackets', false, {scopeSelector: '.source.js'})
+ buffer.setText('')
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText('{')
+ expect(buffer.lineForRow(0)).toBe('{')
+ editor.insertText('}')
+ expect(buffer.lineForRow(0)).toBe('{}')
+ editor.setCursorBufferPosition([0, 1])
+ editor.backspace()
+ expect(buffer.lineForRow(0)).toBe('}')
+ })
+ })
+
+ describe('bracket-matcher:close-tag', () => {
+ beforeEach(() => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.html')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+ buffer = editor.buffer
+ })
+ })
+
+ it('closes the first unclosed tag', () => {
+ editor.setCursorBufferPosition([5, 14])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition()).toEqual([5, 18])
+ expect(editor.getTextInRange([[5, 14], [5, 18]])).toEqual('')
+ })
+
+ it('closes the following unclosed tags if called repeatedly', () => {
+ editor.setCursorBufferPosition([5, 14])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition()).toEqual([5, 22])
+ expect(editor.getTextInRange([[5, 18], [5, 22]])).toEqual('')
+ })
+
+ it('does not close any tag if no unclosed tag can be found at the insertion point', () => {
+ editor.setCursorBufferPosition([5, 14])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ // closing all currently open tags
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ editor.setCursorBufferPosition([13, 11])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ editor.setCursorBufferPosition([15, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ // positioning on an already closed tag
+ editor.setCursorBufferPosition([11, 9])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ expect(editor.getCursorBufferPosition()).toEqual([11, 9])
+ })
+
+ it('does not get confused in case of nested identical tags -- tag not closing', () => {
+ editor.setCursorBufferPosition([13, 11])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition()).toEqual([13, 16])
+ })
+
+ it('does not get confused in case of nested identical tags -- tag closing', () => {
+ editor.setCursorBufferPosition([13, 11])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition()).toEqual([13, 16])
+ expect(editor.getTextInRange([[13, 10], [13, 16]])).toEqual('')
+
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition()).toEqual([13, 16])
+ })
+
+ it('does not get confused in case of nested self closing tags', () => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
+ editor.setText(`\
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([2, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition().row).toEqual(2)
+ expect(editor.getCursorBufferPosition().column).toEqual(6)
+ expect(editor.getTextInRange([[2, 0], [2, 6]])).toEqual('')
+ })
+ })
+
+ it('does not get confused in case of self closing tags after the cursor', () => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
+ editor.setText(`\
+
+
+
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition().row).toEqual(1)
+ expect(editor.getCursorBufferPosition().column).toEqual(0)
+ expect(editor.getTextInRange([[1, 0], [1, Infinity]])).toEqual('')
+ })
+ })
+
+ it('does not get confused in case of nested self closing tags with `>` in their attributes', () => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
+ editor.setText(`\
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([2, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition().row).toEqual(2)
+ expect(editor.getCursorBufferPosition().column).toEqual(6)
+ expect(editor.getTextInRange([[2, 0], [2, 6]])).toEqual('')
+
+ editor.setText(`\
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition().row).toEqual(1)
+ expect(editor.getCursorBufferPosition().column).toEqual(6)
+ expect(editor.getTextInRange([[1, 0], [1, 6]])).toEqual('')
+ })
+ })
+
+ it('does not get confused in case of self closing tags with `>` in their attributes after the cursor', () => {
+ waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
+ editor.setText(`\
+
+
+
+
+
+\
+`
+ )
+
+ editor.setCursorBufferPosition([1, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:close-tag')
+
+ expect(editor.getCursorBufferPosition().row).toEqual(1)
+ expect(editor.getCursorBufferPosition().column).toEqual(0)
+ expect(editor.getTextInRange([[1, 0], [1, Infinity]])).toEqual('')
+ })
+ })
+ })
+
+ describe('when bracket-matcher:select-matching-brackets is triggered', () => {
+ describe('when the cursor on the left side of an opening bracket', () => {
+ beforeEach(() => {
+ editor.setCursorBufferPosition([0, 28])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-matching-brackets')
+ })
+
+ it('selects the brackets', () => {
+ expect(editor.getSelectedBufferRanges()).toEqual([[[0, 28], [0, 29]], [[12, 0], [12, 1]]])
+ })
+
+ it('select and replace', () => {
+ editor.insertText('[')
+ expect(editor.getTextInRange([[0, 28], [0, 29]])).toEqual('[')
+ expect(editor.getTextInRange([[12, 0], [12, 1]])).toEqual(']')
+ })
+ })
+
+ describe('when the cursor on the right side of an opening bracket', () => {
+ beforeEach(() => {
+ editor.setCursorBufferPosition([1, 30])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-matching-brackets')
+ })
+
+ it('selects the brackets', () => {
+ expect(editor.getSelectedBufferRanges()).toEqual([[[1, 29], [1, 30]], [[9, 2], [9, 3]]])
+ })
+
+ it('select and replace', () => {
+ editor.insertText('[')
+ expect(editor.getTextInRange([[1, 29], [1, 30]])).toEqual('[')
+ expect(editor.getTextInRange([[9, 2], [9, 3]])).toEqual(']')
+ })
+ })
+
+ describe('when the cursor on the left side of an closing bracket', () => {
+ beforeEach(() => {
+ editor.setCursorBufferPosition([12, 0])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-matching-brackets')
+ })
+
+ it('selects the brackets', () => {
+ expect(editor.getSelectedBufferRanges()).toEqual([ [[12, 0], [12, 1]], [[0, 28], [0, 29]] ])
+ })
+
+ it('select and replace', () => {
+ editor.insertText('[')
+ expect(editor.getTextInRange([[12, 0], [12, 1]])).toEqual(']')
+ expect(editor.getTextInRange([[0, 28], [0, 29]])).toEqual('[')
+ })
+ })
+
+ describe("when the cursor isn't near to a bracket", () => {
+ beforeEach(() => {
+ editor.setCursorBufferPosition([1, 5])
+ atom.commands.dispatch(editorElement, 'bracket-matcher:select-matching-brackets')
+ })
+
+ it("doesn't selects the brackets", () => {
+ expect(editor.getSelectedBufferRanges()).toEqual([[[1, 5], [1, 5]]])
+ })
+
+ it("doesn't select and replace the brackets", () => {
+ editor.insertText('[')
+ expect(editor.getTextInRange([[1, 5], [1, 6]])).toEqual('[')
+ })
+ })
+ })
+
+ describe('skipping closed brackets', () => {
+ beforeEach(() => {
+ editor.buffer.setText('')
+ })
+
+ it('skips over brackets', () => {
+ editor.insertText('(')
+ expect(editor.buffer.getText()).toBe('()')
+ editor.insertText(')')
+ expect(editor.buffer.getText()).toBe('()')
+ })
+
+ it('does not skip over brackets that have already been skipped', () => {
+ editor.insertText('()')
+ editor.moveLeft()
+ editor.insertText(')')
+ expect(editor.buffer.getText()).toBe('())')
+ })
+
+ it('does skip over brackets that have already been skipped when alwaysSkipClosingPairs is set', () => {
+ atom.config.set('bracket-matcher.alwaysSkipClosingPairs', true)
+ editor.insertText('()')
+ editor.moveLeft()
+ editor.insertText(')')
+ expect(editor.buffer.getText()).toBe('()')
+ })
+ })
+
+ function forEachLanguageWithTags (callback) {
+ // TODO: remove this conditional after 1.33 stable is released.
+ if (HAS_NEW_TEXT_BUFFER_VERSION) {
+ ['text.html.basic', 'text.xml'].forEach(callback)
+ } else {
+ callback('text.xml')
+ }
+ }
+})
diff --git a/packages/bracket-matcher/spec/close-tag-spec.js b/packages/bracket-matcher/spec/close-tag-spec.js
new file mode 100644
index 000000000..13b86b970
--- /dev/null
+++ b/packages/bracket-matcher/spec/close-tag-spec.js
@@ -0,0 +1,142 @@
+const TagFinder = require('../lib/tag-finder')
+const tagFinder = new TagFinder()
+
+describe('closeTag', () => {
+ describe('TagFinder::parseFragment', () => {
+ let fragment = ''
+
+ beforeEach(() => fragment = '')
+
+ it('returns the last not closed elem in fragment, matching a given pattern', () => {
+ const stack = tagFinder.parseFragment(fragment, [], /<(\w+)|<\/(\w*)/, () => true)
+ expect(stack[stack.length - 1]).toBe('head')
+ })
+
+ it('stops when cond become true', () => {
+ const stack = tagFinder.parseFragment(fragment, [], /<(\w+)|<\/(\w*)/, () => false)
+ expect(stack.length).toBe(0)
+ })
+
+ it('uses the given match expression to match tags', () => {
+ const stack = tagFinder.parseFragment(fragment, [], /<(body)|(notag)/, () => true)
+ expect(stack[stack.length - 1]).toBe('body')
+ })
+ })
+
+ describe('TagFinder::tagsNotClosedInFragment', () => {
+ it('returns the outermost tag not closed in an HTML fragment', () => {
+ const fragment = ''
+ const tags = tagFinder.tagsNotClosedInFragment(fragment)
+ expect(tags).toEqual(['html', 'body', 'h1'])
+ })
+
+ it('is not confused by tag attributes', () => {
+ const fragment = ''
+ const tags = tagFinder.tagsNotClosedInFragment(fragment)
+ expect(tags).toEqual(['html', 'body', 'h1'])
+ })
+
+ it('is not confused by namespace prefixes', () => {
+ const fragment = ''
+ const tags = tagFinder.tagsNotClosedInFragment(fragment)
+ expect(tags).toEqual(['xhtml:html', 'xhtml:body', 'xhtml:h1'])
+ })
+ })
+
+ describe('TagFinder::tagDoesNotCloseInFragment', () => {
+ it('returns true if the given tag is not closed in the given fragment', () => {
+ const fragment = ''
+ expect(tagFinder.tagDoesNotCloseInFragment('body', fragment)).toBe(true)
+ })
+
+ it('returns false if the given tag is closed in the given fragment', () => {
+ const fragment = ''
+ expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(false)
+ })
+
+ it('returns true even if the given tag is re-opened and re-closed', () => {
+ const fragment = ' '
+ expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(true)
+ })
+
+ it('returns false even if the given tag is re-opened and re-closed before closing', () => {
+ const fragment = ' '
+ expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(false)
+ })
+ })
+
+ describe('TagFinder::closingTagForFragments', () => {
+ it('returns the last opened in preFragment tag that is not closed in postFragment', () => {
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('p')
+ })
+
+ it('correctly handles empty postFragment', () => {
+ const preFragment = '
'
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('p')
+ })
+
+ it('correctly handles malformed tags', () => {
+ const preFragment = '
{
+ const preFragment = ''
+ const postFragment = '
'
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe(null)
+ })
+
+ it('correctly closes tags containing hyphens', () => {
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('my-element')
+ })
+
+ it('correctly closes tags containing attributes', () => {
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('div')
+ })
+
+ it('correctly closes tags containing an XML namespace', () => {
+ const preFragment = '
'
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('custom:tag')
+ })
+
+ it('correctly closes tags containing multiple XML namespaces', () => {
+ // This is not exactly valid syntax but it can't hurt to support it
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('custom:custom2:tag')
+ })
+
+ it('correctly closes tags in the present of JSX tags containing member accesses', () => {
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo')
+ })
+
+ it('correctly closes JSX tags containing member accesses', () => {
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo.Bar')
+ })
+
+ it('correctly closes JSX tags containing deep member accesses', () => {
+ const preFragment = ''
+ const postFragment = ''
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo.Bar.Baz')
+ })
+
+ it('correctly closes tags when there are other tags with the same prefix', () => {
+ const preFragment = ''
+ const postFragment = ' | '
+ expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('th')
+ })
+ })
+})
diff --git a/packages/bracket-matcher/spec/fixtures/multiplecursor.md b/packages/bracket-matcher/spec/fixtures/multiplecursor.md
new file mode 100644
index 000000000..23f4341c3
--- /dev/null
+++ b/packages/bracket-matcher/spec/fixtures/multiplecursor.md
@@ -0,0 +1,6 @@
+[link-1](http://example.com/)
+[another-link-2](http://example.com/)
+[yet-another-link-3](http://example.com/)
+[final-link-4](http://example.com/)
+(different-type)
+not anywhere
diff --git a/packages/bracket-matcher/spec/fixtures/sample.html b/packages/bracket-matcher/spec/fixtures/sample.html
new file mode 100644
index 000000000..0aa11ef31
--- /dev/null
+++ b/packages/bracket-matcher/spec/fixtures/sample.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/packages/bracket-matcher/spec/fixtures/sample.js b/packages/bracket-matcher/spec/fixtures/sample.js
new file mode 100644
index 000000000..fb33b0b43
--- /dev/null
+++ b/packages/bracket-matcher/spec/fixtures/sample.js
@@ -0,0 +1,13 @@
+var quicksort = function () {
+ var sort = function(items) {
+ if (items.length <= 1) return items;
+ var pivot = items.shift(), current, left = [], right = [];
+ while(items.length > 0) {
+ current = items.shift();
+ current < pivot ? left.push(current) : right.push(current);
+ }
+ return sort(left).concat(pivot).concat(sort(right));
+ };
+
+ return sort(Array.apply(this, arguments));
+};
\ No newline at end of file
diff --git a/packages/bracket-matcher/spec/fixtures/sample.xml b/packages/bracket-matcher/spec/fixtures/sample.xml
new file mode 100644
index 000000000..e7380888d
--- /dev/null
+++ b/packages/bracket-matcher/spec/fixtures/sample.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/packages/bracket-matcher/styles/bracket-matcher.atom-text-editor.less b/packages/bracket-matcher/styles/bracket-matcher.atom-text-editor.less
new file mode 100644
index 000000000..e128622f3
--- /dev/null
+++ b/packages/bracket-matcher/styles/bracket-matcher.atom-text-editor.less
@@ -0,0 +1,11 @@
+@import "syntax-variables";
+
+.bracket-matcher .region {
+ border-bottom: 1px dotted lime;
+ position: absolute;
+}
+
+.line-number.bracket-matcher.bracket-matcher {
+ color: @syntax-gutter-text-color-selected;
+ background-color: @syntax-gutter-background-color-selected;
+}
diff --git a/packages/language-clojure/spec/clojure-spec.coffee b/packages/language-clojure/spec/clojure-spec.coffee
index 802858c8d..65a5b176f 100644
--- a/packages/language-clojure/spec/clojure-spec.coffee
+++ b/packages/language-clojure/spec/clojure-spec.coffee
@@ -295,7 +295,7 @@ describe "Clojure grammar", ->
#!/usr/bin/env boot
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/boot
@@ -306,7 +306,7 @@ describe "Clojure grammar", ->
#!\t/usr/bin/env --boot=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -325,7 +325,7 @@ describe "Clojure grammar", ->
"-*- font:x;foo : bar ; mode : ClojureScript ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*clojure-*- */
@@ -343,7 +343,7 @@ describe "Clojure grammar", ->
// -*-font:mode;mode:clojure--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -370,7 +370,7 @@ describe "Clojure grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=clojure ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=clojure:
@@ -388,4 +388,4 @@ describe "Clojure grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=clojure ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/packages/language-coffee-script/spec/coffee-script-literate-spec.coffee b/packages/language-coffee-script/spec/coffee-script-literate-spec.coffee
index 1ed4d721b..511a02740 100644
--- a/packages/language-coffee-script/spec/coffee-script-literate-spec.coffee
+++ b/packages/language-coffee-script/spec/coffee-script-literate-spec.coffee
@@ -29,7 +29,7 @@ describe "CoffeeScript (Literate) grammar", ->
#!/usr/local/bin/env coffee --literate -w
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
#!/usr/local/bin/coffee --no-head -literate -w
@@ -37,7 +37,7 @@ describe "CoffeeScript (Literate) grammar", ->
#!/usr/local/bin/env coffee --illiterate -w=l
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -56,7 +56,7 @@ describe "CoffeeScript (Literate) grammar", ->
"-*- font:x;foo : bar ; mode : LiTcOFFEe ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*litcoffee-*- */
@@ -74,7 +74,7 @@ describe "CoffeeScript (Literate) grammar", ->
// -*-font:mode;mode:litcoffee--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -101,7 +101,7 @@ describe "CoffeeScript (Literate) grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=litcoffee ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=litcoffee:
@@ -119,4 +119,4 @@ describe "CoffeeScript (Literate) grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=litcoffee ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/packages/language-coffee-script/spec/coffee-script-spec.coffee b/packages/language-coffee-script/spec/coffee-script-spec.coffee
index 138959779..14940c89a 100644
--- a/packages/language-coffee-script/spec/coffee-script-spec.coffee
+++ b/packages/language-coffee-script/spec/coffee-script-spec.coffee
@@ -1393,7 +1393,7 @@ describe "CoffeeScript grammar", ->
#!/usr/bin/env coffee
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/coffee
@@ -1404,7 +1404,7 @@ describe "CoffeeScript grammar", ->
#!\t/usr/bin/env --coffee=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -1423,7 +1423,7 @@ describe "CoffeeScript grammar", ->
"-*- font:x;foo : bar ; mode : Coffee ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*coffee-*- */
@@ -1441,7 +1441,7 @@ describe "CoffeeScript grammar", ->
// -*-font:mode;mode:coffee--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -1468,7 +1468,7 @@ describe "CoffeeScript grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=cOFFEe ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=coffee:
@@ -1486,4 +1486,4 @@ describe "CoffeeScript grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=coffee ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/packages/language-css/spec/css-spec.coffee b/packages/language-css/spec/css-spec.coffee
index e68cd02dd..3fb1b5d6a 100644
--- a/packages/language-css/spec/css-spec.coffee
+++ b/packages/language-css/spec/css-spec.coffee
@@ -3508,7 +3508,7 @@ describe 'CSS grammar', ->
"-*- font:x;foo : bar ; mode : cSS ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*css-*- */
@@ -3526,7 +3526,7 @@ describe 'CSS grammar', ->
// -*-font:mode;mode:css--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -3553,7 +3553,7 @@ describe 'CSS grammar', ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=css ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=css:
@@ -3571,7 +3571,7 @@ describe 'CSS grammar', ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=CSS ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
describe "Missing supported properties regressions", ->
it "recognises place-items property as supported", ->
diff --git a/packages/language-go/spec/language-go-spec.coffee b/packages/language-go/spec/language-go-spec.coffee
index e2e6255e8..27f2c427b 100644
--- a/packages/language-go/spec/language-go-spec.coffee
+++ b/packages/language-go/spec/language-go-spec.coffee
@@ -18,45 +18,45 @@ describe 'Go settings', ->
it 'matches lines correctly using the increaseIndentPattern', ->
increaseIndentRegex = languageMode.increaseIndentRegexForScopeDescriptor(['source.go'])
- expect(increaseIndentRegex.testSync(' case true:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' default:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('func something() {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' if true {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' else {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' switch {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' switch true {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' select {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' select true {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' for v := range val {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' type something struct {')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' fmt.Printf("some%s",')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' aSlice := []string{}{')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' case true:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' default:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('func something() {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' if true {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' else {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' switch {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' switch true {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' select {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' select true {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' for v := range val {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' type something struct {')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' fmt.Printf("some%s",')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' aSlice := []string{}{')).toBeTruthy()
it 'matches lines correctly using the decreaseIndentPattern', ->
decreaseIndentRegex = languageMode.decreaseIndentRegexForScopeDescriptor(['source.go'])
- expect(decreaseIndentRegex.testSync(' case true:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' default:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' }')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' },')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' )')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' ),')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' case true:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' default:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' }')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' },')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' )')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' ),')).toBeTruthy()
it 'matches lines correctly using the decreaseNextIndentPattern', ->
decreaseNextIndentRegex = languageMode.decreaseNextIndentRegexForScopeDescriptor(['source.go'])
- expect(decreaseNextIndentRegex.testSync(' fmt.Println("something"))')).toBeTruthy()
- expect(decreaseNextIndentRegex.testSync(' fmt.Println("something")),')).toBeTruthy()
- expect(decreaseNextIndentRegex.testSync(' fmt.Println("something"), "x"),')).toBeTruthy()
- expect(decreaseNextIndentRegex.testSync(' fmt.Println(fmt.Sprint("something"))),')).toBeTruthy()
- expect(decreaseNextIndentRegex.testSync(' fmt.Println(fmt.Sprint("something"), "x")),')).toBeTruthy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something"))')).toBeTruthy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something")),')).toBeTruthy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something"), "x"),')).toBeTruthy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println(fmt.Sprint("something"))),')).toBeTruthy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println(fmt.Sprint("something"), "x")),')).toBeTruthy()
- expect(decreaseNextIndentRegex.testSync(' fmt.Println("something")')).toBeFalsy()
- expect(decreaseNextIndentRegex.testSync(' fmt.Println("something"),')).toBeFalsy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something")')).toBeFalsy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something"),')).toBeFalsy()
# a line with many (), testing for catastrophic backtracking.
# see https://github.com/atom/language-go/issues/78
longLine = 'first.second().third().fourth().fifth().sixth().seventh().eighth().ninth().tenth()'
- expect(decreaseNextIndentRegex.testSync(longLine)).toBeFalsy()
+ expect(decreaseNextIndentRegex.findNextMatchSync(longLine)).toBeFalsy()
diff --git a/packages/language-html/spec/html-spec.coffee b/packages/language-html/spec/html-spec.coffee
index 6880a530b..969e32355 100644
--- a/packages/language-html/spec/html-spec.coffee
+++ b/packages/language-html/spec/html-spec.coffee
@@ -601,8 +601,8 @@ describe 'TextMate HTML grammar', ->
describe 'firstLineMatch', ->
it 'recognises HTML5 doctypes', ->
- expect(grammar.firstLineRegex.scanner.findNextMatchSync('')).not.toBeNull()
- expect(grammar.firstLineRegex.scanner.findNextMatchSync('')).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync('')).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync('')).not.toBeNull()
it 'recognises Emacs modelines', ->
valid = '''
@@ -621,7 +621,7 @@ describe 'TextMate HTML grammar', ->
"-*- font:x;foo : bar ; mode : HtML ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
'''
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
/* --*html-*- */
@@ -640,7 +640,7 @@ describe 'TextMate HTML grammar', ->
// -*-font:mode;mode:html--*-
'''
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'recognises Vim modelines', ->
valid = '''
@@ -667,7 +667,7 @@ describe 'TextMate HTML grammar', ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=html ts=4
'''
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
ex: se filetype=html:
@@ -685,7 +685,7 @@ describe 'TextMate HTML grammar', ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=HTML ts=4
'''
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
describe 'tags', ->
it 'tokenizes style tags as such', ->
diff --git a/packages/language-javascript/spec/javascript-spec.coffee b/packages/language-javascript/spec/javascript-spec.coffee
index 1d6a9a0b8..0e5202cbd 100644
--- a/packages/language-javascript/spec/javascript-spec.coffee
+++ b/packages/language-javascript/spec/javascript-spec.coffee
@@ -2400,7 +2400,7 @@ describe "JavaScript grammar", ->
#!/usr/bin/env node
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/node
@@ -2411,7 +2411,7 @@ describe "JavaScript grammar", ->
#!\t/usr/bin/env --node=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -2430,7 +2430,7 @@ describe "JavaScript grammar", ->
"-*- font:x;foo : bar ; mode : jS ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*js-*- */
@@ -2448,7 +2448,7 @@ describe "JavaScript grammar", ->
// -*-font:mode;mode:js--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -2475,7 +2475,7 @@ describe "JavaScript grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=javascript ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=javascript:
@@ -2493,4 +2493,4 @@ describe "JavaScript grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=javascript ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/packages/language-perl/grammars/perl.cson b/packages/language-perl/grammars/perl.cson
index ddc82df93..65c047f6a 100644
--- a/packages/language-perl/grammars/perl.cson
+++ b/packages/language-perl/grammars/perl.cson
@@ -721,9 +721,6 @@
'match': '^\\s*(package)\\s+([^\\s;]+)'
'name': 'meta.class.perl'
}
- {
- 'include: "#sub'
- }
{
'captures':
'1':
diff --git a/packages/language-perl/spec/grammar-perl6-spec.coffee b/packages/language-perl/spec/grammar-perl6-spec.coffee
index 0993f3788..94a458758 100644
--- a/packages/language-perl/spec/grammar-perl6-spec.coffee
+++ b/packages/language-perl/spec/grammar-perl6-spec.coffee
@@ -183,7 +183,7 @@ describe "Perl 6 grammar", ->
#!/usr/bin/env perl6
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
#! pearl6
@@ -200,11 +200,11 @@ describe "Perl 6 grammar", ->
#!\t/usr/bin/env --perl6=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises the Perl6 pragma", ->
line = "use v6;"
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
it "recognises Emacs modelines", ->
modelines = """
@@ -222,7 +222,7 @@ describe "Perl 6 grammar", ->
"-*- font:x;foo : bar ; mode : pErL6 ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in modelines.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*perl6-*- */
@@ -241,7 +241,7 @@ describe "Perl 6 grammar", ->
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -268,7 +268,7 @@ describe "Perl 6 grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=perl6 ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=perl6:
@@ -286,7 +286,7 @@ describe "Perl 6 grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=perl6 ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
# Local variables:
# mode: CoffeeScript
diff --git a/packages/language-perl/spec/grammar-spec.coffee b/packages/language-perl/spec/grammar-spec.coffee
index dcb4b8329..8d56d8767 100644
--- a/packages/language-perl/spec/grammar-spec.coffee
+++ b/packages/language-perl/spec/grammar-spec.coffee
@@ -1439,7 +1439,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#!/usr/bin/env perl
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
#! pearl
@@ -1456,7 +1456,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#!\t/usr/bin/env --perl=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -1474,7 +1474,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
"-*- font:x;foo : bar ; mode : pErL ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*perl-*- */
@@ -1491,7 +1491,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// -*-font:mode;mode:perl--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -1518,7 +1518,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# vim:noexpandtab titlestring=hi\|there\\\\ ft=perl ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=perl:
@@ -1536,7 +1536,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=perl ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
# Local variables:
# mode: CoffeeScript
diff --git a/packages/language-php/spec/html-spec.coffee b/packages/language-php/spec/html-spec.coffee
index 814de60ac..b86f91747 100644
--- a/packages/language-php/spec/html-spec.coffee
+++ b/packages/language-php/spec/html-spec.coffee
@@ -161,11 +161,11 @@ describe 'PHP in HTML', ->
')).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync('')).toBeNull()
it 'recognises interpreter directives', ->
valid = '''
@@ -184,7 +184,7 @@ describe 'PHP in HTML', ->
#!/usr/bin/env php
'''
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
\x20#!/usr/sbin/php
@@ -196,7 +196,7 @@ describe 'PHP in HTML', ->
#!\t/usr/bin/env --php=bar
'''
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'recognises Emacs modelines', ->
valid = '''
@@ -215,7 +215,7 @@ describe 'PHP in HTML', ->
"-*- font:x;foo : bar ; mode : php ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
'''
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
/* --*php-*- */
@@ -233,7 +233,7 @@ describe 'PHP in HTML', ->
// -*-font:mode;mode:php--*-
'''
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'recognises Vim modelines', ->
valid = '''
@@ -260,7 +260,7 @@ describe 'PHP in HTML', ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=phtml ts=4
'''
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
ex: se filetype=php:
@@ -278,7 +278,7 @@ describe 'PHP in HTML', ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=php ts=4
'''
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'should tokenize ', ->
lines = grammar.tokenizeLines '''
diff --git a/packages/language-python/spec/language-python-spec.coffee b/packages/language-python/spec/language-python-spec.coffee
index e21fb828a..d618352ce 100644
--- a/packages/language-python/spec/language-python-spec.coffee
+++ b/packages/language-python/spec/language-python-spec.coffee
@@ -18,66 +18,66 @@ describe 'Python settings', ->
it 'matches lines correctly using the increaseIndentPattern', ->
increaseIndentRegex = languageMode.increaseIndentRegexForScopeDescriptor(['source.python'])
- expect(increaseIndentRegex.testSync('for i in range(n):')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' for i in range(n):')).toBeTruthy()
- expect(increaseIndentRegex.testSync('async for i in range(n):')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' async for i in range(n):')).toBeTruthy()
- expect(increaseIndentRegex.testSync('class TheClass(Object):')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' class TheClass(Object):')).toBeTruthy()
- expect(increaseIndentRegex.testSync('def f(x):')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' def f(x):')).toBeTruthy()
- expect(increaseIndentRegex.testSync('async def f(x):')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' async def f(x):')).toBeTruthy()
- expect(increaseIndentRegex.testSync('if this_var == that_var:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' if this_var == that_var:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('elif this_var == that_var:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' elif this_var == that_var:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('else:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' else:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('except Exception:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' except Exception:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('except Exception as e:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' except Exception as e:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('finally:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' finally:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('with open("filename") as f:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' with open("filename") as f:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('async with open("filename") as f:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' async with open("filename") as f:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('while True:')).toBeTruthy()
- expect(increaseIndentRegex.testSync(' while True:')).toBeTruthy()
- expect(increaseIndentRegex.testSync('\t\t while True:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('for i in range(n):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' for i in range(n):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('async for i in range(n):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' async for i in range(n):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('class TheClass(Object):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' class TheClass(Object):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('def f(x):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' def f(x):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('async def f(x):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' async def f(x):')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('if this_var == that_var:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' if this_var == that_var:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('elif this_var == that_var:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' elif this_var == that_var:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('else:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' else:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('except Exception:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' except Exception:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('except Exception as e:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' except Exception as e:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('finally:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' finally:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('with open("filename") as f:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' with open("filename") as f:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('async with open("filename") as f:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' async with open("filename") as f:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('while True:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync(' while True:')).toBeTruthy()
+ expect(increaseIndentRegex.findNextMatchSync('\t\t while True:')).toBeTruthy()
it 'does not match lines incorrectly using the increaseIndentPattern', ->
increaseIndentRegex = languageMode.increaseIndentRegexForScopeDescriptor(['source.python'])
- expect(increaseIndentRegex.testSync('for i in range(n)')).toBeFalsy()
- expect(increaseIndentRegex.testSync('class TheClass(Object)')).toBeFalsy()
- expect(increaseIndentRegex.testSync('def f(x)')).toBeFalsy()
- expect(increaseIndentRegex.testSync('if this_var == that_var')).toBeFalsy()
- expect(increaseIndentRegex.testSync('"for i in range(n):"')).toBeFalsy()
+ expect(increaseIndentRegex.findNextMatchSync('for i in range(n)')).toBeFalsy()
+ expect(increaseIndentRegex.findNextMatchSync('class TheClass(Object)')).toBeFalsy()
+ expect(increaseIndentRegex.findNextMatchSync('def f(x)')).toBeFalsy()
+ expect(increaseIndentRegex.findNextMatchSync('if this_var == that_var')).toBeFalsy()
+ expect(increaseIndentRegex.findNextMatchSync('"for i in range(n):"')).toBeFalsy()
it 'matches lines correctly using the decreaseIndentPattern', ->
decreaseIndentRegex = languageMode.decreaseIndentRegexForScopeDescriptor(['source.python'])
- expect(decreaseIndentRegex.testSync('elif this_var == that_var:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' elif this_var == that_var:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync('else:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' else:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync('except Exception:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' except Exception:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync('except Exception as e:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' except Exception as e:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync('finally:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync(' finally:')).toBeTruthy()
- expect(decreaseIndentRegex.testSync('\t\t finally:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync('elif this_var == that_var:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' elif this_var == that_var:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync('else:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' else:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync('except Exception:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' except Exception:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync('except Exception as e:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' except Exception as e:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync('finally:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync(' finally:')).toBeTruthy()
+ expect(decreaseIndentRegex.findNextMatchSync('\t\t finally:')).toBeTruthy()
it 'does not match lines incorrectly using the decreaseIndentPattern', ->
decreaseIndentRegex = languageMode.decreaseIndentRegexForScopeDescriptor(['source.python'])
# NOTE! This first one is different from most other rote tests here.
- expect(decreaseIndentRegex.testSync('else: expression()')).toBeFalsy()
- expect(decreaseIndentRegex.testSync('elif this_var == that_var')).toBeFalsy()
- expect(decreaseIndentRegex.testSync(' elif this_var == that_var')).toBeFalsy()
- expect(decreaseIndentRegex.testSync('else')).toBeFalsy()
- expect(decreaseIndentRegex.testSync(' "finally:"')).toBeFalsy()
+ expect(decreaseIndentRegex.findNextMatchSync('else: expression()')).toBeFalsy()
+ expect(decreaseIndentRegex.findNextMatchSync('elif this_var == that_var')).toBeFalsy()
+ expect(decreaseIndentRegex.findNextMatchSync(' elif this_var == that_var')).toBeFalsy()
+ expect(decreaseIndentRegex.findNextMatchSync('else')).toBeFalsy()
+ expect(decreaseIndentRegex.findNextMatchSync(' "finally:"')).toBeFalsy()
diff --git a/packages/language-python/spec/python-spec.coffee b/packages/language-python/spec/python-spec.coffee
index 423f8c17f..ffaac533b 100644
--- a/packages/language-python/spec/python-spec.coffee
+++ b/packages/language-python/spec/python-spec.coffee
@@ -14,8 +14,8 @@ describe "Python grammar", ->
grammar = atom.grammars.grammarForScopeName("source.python")
it "recognises shebang on firstline", ->
- expect(grammar.firstLineRegex.scanner.findNextMatchSync("#!/usr/bin/env python")).not.toBeNull()
- expect(grammar.firstLineRegex.scanner.findNextMatchSync("#! /usr/bin/env python")).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync("#!/usr/bin/env python")).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync("#! /usr/bin/env python")).not.toBeNull()
it "parses the grammar", ->
expect(grammar).toBeDefined()
diff --git a/packages/language-ruby/spec/ruby-spec.coffee b/packages/language-ruby/spec/ruby-spec.coffee
index 37b6b53c3..d14cdbec9 100644
--- a/packages/language-ruby/spec/ruby-spec.coffee
+++ b/packages/language-ruby/spec/ruby-spec.coffee
@@ -964,7 +964,7 @@ describe "TextMate Ruby grammar", ->
#!/usr/bin/env ruby
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/ruby
@@ -975,7 +975,7 @@ describe "TextMate Ruby grammar", ->
#!\t/usr/bin/env --ruby=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -994,7 +994,7 @@ describe "TextMate Ruby grammar", ->
"-*- font:x;foo : bar ; mode : RUBY ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*ruby-*- */
@@ -1012,7 +1012,7 @@ describe "TextMate Ruby grammar", ->
// -*-font:mode;mode:ruby--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -1039,7 +1039,7 @@ describe "TextMate Ruby grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=ruby ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=ruby:
@@ -1057,4 +1057,4 @@ describe "TextMate Ruby grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=ruby ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/packages/language-shellscript/spec/shell-unix-bash-spec.coffee b/packages/language-shellscript/spec/shell-unix-bash-spec.coffee
index 8d8af968c..882b405cc 100644
--- a/packages/language-shellscript/spec/shell-unix-bash-spec.coffee
+++ b/packages/language-shellscript/spec/shell-unix-bash-spec.coffee
@@ -361,7 +361,7 @@ describe "Shell script grammar", ->
#!/usr/bin/env bash
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/bash
@@ -373,7 +373,7 @@ describe "Shell script grammar", ->
#!\t/usr/bin/env --bash=bar
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@@ -392,7 +392,7 @@ describe "Shell script grammar", ->
"-*- font:x;foo : bar ; mode : sH ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*sh-*- */
@@ -410,7 +410,7 @@ describe "Shell script grammar", ->
// -*-font:mode;mode:sh--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -437,7 +437,7 @@ describe "Shell script grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=sh ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=sh:
@@ -455,4 +455,4 @@ describe "Shell script grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=sh ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/packages/language-xml/spec/xml-spec.coffee b/packages/language-xml/spec/xml-spec.coffee
index 6320738e5..bbc993136 100644
--- a/packages/language-xml/spec/xml-spec.coffee
+++ b/packages/language-xml/spec/xml-spec.coffee
@@ -122,7 +122,7 @@ attrName="attrValue">
"-*- font:x;foo : bar ; mode : xMl ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*XML-*- */
@@ -140,7 +140,7 @@ attrName="attrValue">
// -*-font:mode;mode:xml--*-
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@@ -167,7 +167,7 @@ attrName="attrValue">
# vim:noexpandtab titlestring=hi\|there\\\\ ft=xml ts=4
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=xml:
@@ -185,7 +185,7 @@ attrName="attrValue">
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=xml ts=4
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises a valid XML declaration", ->
valid = """
@@ -196,7 +196,7 @@ attrName="attrValue">
"""
for line in valid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
@@ -209,4 +209,4 @@ attrName="attrValue">
"""
for line in invalid.split /\n/
- expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
+ expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js
index 05fd98b18..bae333d2f 100644
--- a/spec/grammar-registry-spec.js
+++ b/spec/grammar-registry-spec.js
@@ -5,13 +5,14 @@ const temp = require('temp').track();
const TextBuffer = require('text-buffer');
const GrammarRegistry = require('../src/grammar-registry');
const TreeSitterGrammar = require('../src/tree-sitter-grammar');
-const FirstMate = require('first-mate');
-const { OnigRegExp } = require('oniguruma');
+const SecondMate = require('second-mate');
+const { OnigScanner } = SecondMate;
describe('GrammarRegistry', () => {
let grammarRegistry;
- beforeEach(() => {
+ beforeEach(async () => {
+ await SecondMate.ready
grammarRegistry = new GrammarRegistry({ config: atom.config });
expect(subscriptionCount(grammarRegistry)).toBe(1);
});
@@ -102,7 +103,7 @@ describe('GrammarRegistry', () => {
);
const grammar = grammarRegistry.grammarForId('source.js');
- expect(grammar instanceof FirstMate.Grammar).toBe(true);
+ expect(grammar instanceof SecondMate.Grammar).toBe(true);
expect(grammar.scopeName).toBe('source.js');
grammarRegistry.removeGrammar(grammar);
@@ -127,7 +128,7 @@ describe('GrammarRegistry', () => {
grammarRegistry.removeGrammar(grammar);
expect(
- grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar
+ grammarRegistry.grammarForId('source.js') instanceof SecondMate.Grammar
).toBe(true);
});
});
@@ -560,7 +561,7 @@ describe('GrammarRegistry', () => {
const grammar = grammarRegistry.selectGrammar('test.js');
expect(grammar.scopeName).toBe('source.js');
- expect(grammar instanceof FirstMate.Grammar).toBe(true);
+ expect(grammar instanceof SecondMate.Grammar).toBe(true);
});
it('favors a tree-sitter grammar over a text-mate grammar when `core.useTreeSitterParsers` is true', () => {
@@ -766,7 +767,7 @@ describe('GrammarRegistry', () => {
grammarRegistry.addGrammar(grammar1);
const grammar2 = {
name: 'foo++',
- contentRegex: new OnigRegExp('.*bar'),
+ contentRegex: new OnigScanner(['.*bar']),
fileTypes: ['foo']
};
grammarRegistry.addGrammar(grammar2);
diff --git a/src/grammar-registry.js b/src/grammar-registry.js
index aa530b6a9..22a141d2d 100644
--- a/src/grammar-registry.js
+++ b/src/grammar-registry.js
@@ -1,7 +1,7 @@
const _ = require('underscore-plus');
const Grim = require('grim');
const CSON = require('season');
-const FirstMate = require('first-mate');
+const SecondMate = require('second-mate');
const { Disposable, CompositeDisposable } = require('event-kit');
const TextMateLanguageMode = require('./text-mate-language-mode');
const TreeSitterLanguageMode = require('./tree-sitter-language-mode');
@@ -20,7 +20,7 @@ module.exports = class GrammarRegistry {
constructor({ config } = {}) {
this.config = config;
this.subscriptions = new CompositeDisposable();
- this.textmateRegistry = new FirstMate.GrammarRegistry({
+ this.textmateRegistry = new SecondMate.GrammarRegistry({
maxTokensPerLine: 100,
maxLineLength: 1000
});
@@ -264,7 +264,7 @@ module.exports = class GrammarRegistry {
if (grammar.contentRegex) {
const contentMatch = isTreeSitter
? grammar.contentRegex.test(contents)
- : grammar.contentRegex.testSync(contents);
+ : grammar.contentRegex.findNextMatchSync(contents);
if (contentMatch) {
score += 0.05;
} else {
@@ -339,8 +339,8 @@ module.exports = class GrammarRegistry {
.split('\n')
.slice(0, numberOfNewlinesInRegex + 1)
.join('\n');
- if (grammar.firstLineRegex.testSync) {
- return grammar.firstLineRegex.testSync(prefix);
+ if (grammar.firstLineRegex.findNextMatchSync) {
+ return grammar.firstLineRegex.findNextMatchSync(prefix);
} else {
return grammar.firstLineRegex.test(prefix);
}
diff --git a/src/text-editor.js b/src/text-editor.js
index ecbbd7f7d..5d8b8711c 100644
--- a/src/text-editor.js
+++ b/src/text-editor.js
@@ -13,7 +13,7 @@ const NullGrammar = require('./null-grammar');
const TextMateLanguageMode = require('./text-mate-language-mode');
const ScopeDescriptor = require('./scope-descriptor');
-const TextMateScopeSelector = require('first-mate').ScopeSelector;
+const TextMateScopeSelector = require('second-mate').ScopeSelector;
const GutterContainer = require('./gutter-container');
let TextEditorComponent = null;
let TextEditorElement = null;
diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js
index 13d5ed13f..aa1b52c9a 100644
--- a/src/text-mate-language-mode.js
+++ b/src/text-mate-language-mode.js
@@ -5,7 +5,7 @@ const TokenizedLine = require('./tokenized-line');
const TokenIterator = require('./token-iterator');
const ScopeDescriptor = require('./scope-descriptor');
const NullGrammar = require('./null-grammar');
-const { OnigRegExp } = require('oniguruma');
+const { OnigScanner } = require('second-mate');
const {
toFirstMateScopeId,
fromFirstMateScopeId
@@ -144,7 +144,7 @@ class TextMateLanguageMode {
);
if (!decreaseIndentRegex) return;
- if (!decreaseIndentRegex.testSync(line)) return;
+ if (!decreaseIndentRegex.findNextMatchSync(line)) return;
const precedingRow = this.buffer.previousNonBlankRow(bufferRow);
if (precedingRow == null) return;
@@ -156,14 +156,14 @@ class TextMateLanguageMode {
scopeDescriptor
);
if (increaseIndentRegex) {
- if (!increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1;
+ if (!increaseIndentRegex.findNextMatchSync(precedingLine)) desiredIndentLevel -= 1;
}
const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(
scopeDescriptor
);
if (decreaseNextIndentRegex) {
- if (decreaseNextIndentRegex.testSync(precedingLine))
+ if (decreaseNextIndentRegex.findNextMatchSync(precedingLine))
desiredIndentLevel -= 1;
}
@@ -203,17 +203,17 @@ class TextMateLanguageMode {
if (!increaseIndentRegex) return desiredIndentLevel;
if (!this.isRowCommented(precedingRow)) {
- if (increaseIndentRegex && increaseIndentRegex.testSync(precedingLine))
+ if (increaseIndentRegex && increaseIndentRegex.findNextMatchSync(precedingLine))
desiredIndentLevel += 1;
if (
decreaseNextIndentRegex &&
- decreaseNextIndentRegex.testSync(precedingLine)
+ decreaseNextIndentRegex.findNextMatchSync(precedingLine)
)
desiredIndentLevel -= 1;
}
if (!this.buffer.isRowBlank(precedingRow)) {
- if (decreaseIndentRegex && decreaseIndentRegex.testSync(line))
+ if (decreaseIndentRegex && decreaseIndentRegex.findNextMatchSync(line))
desiredIndentLevel -= 1;
}
@@ -812,7 +812,7 @@ class TextMateLanguageMode {
if (indentation < startIndentLevel) {
break;
} else if (indentation === startIndentLevel) {
- if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow;
+ if (foldEndRegex && foldEndRegex.findNextMatchSync(line)) foldEndRow = nextRow;
break;
}
foldEndRow = nextRow;
@@ -848,7 +848,7 @@ class TextMateLanguageMode {
regexForPattern(pattern) {
if (pattern) {
if (!this.regexesByPattern[pattern]) {
- this.regexesByPattern[pattern] = new OnigRegExp(pattern);
+ this.regexesByPattern[pattern] = new OnigScanner([pattern]);
}
return this.regexesByPattern[pattern];
}
diff --git a/static/index.js b/static/index.js
index 0d2157b40..9c0ad88a3 100644
--- a/static/index.js
+++ b/static/index.js
@@ -19,10 +19,11 @@
}
StartupTime.addMarker('window:start', startWindowTime);
- window.onload = function() {
+ window.onload = async function() {
try {
StartupTime.addMarker('window:onload:start');
const startTime = Date.now();
+ await require('second-mate').ready
process.on('unhandledRejection', function(error, promise) {
console.error(
diff --git a/yarn.lock b/yarn.lock
index 03b68f672..750a11edf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2659,11 +2659,9 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
-"bracket-matcher@https://github.com/pulsar-edit/bracket-matcher.git#c877977":
+"bracket-matcher@file:packages/bracket-matcher":
version "0.92.0"
- resolved "https://github.com/pulsar-edit/bracket-matcher.git#c877977ac7e9b7fe43c2100a1880c7ffc119280b"
dependencies:
- first-mate "^7.4.1"
underscore-plus "1.x"
browser-stdout@1.3.1:
@@ -3991,7 +3989,7 @@ electron@12.2.3:
"@types/node" "^14.6.2"
extract-zip "^1.0.3"
-emissary@^1, emissary@^1.0.0, emissary@^1.1.0, emissary@^1.2.0, emissary@^1.3.2:
+emissary@^1.0.0, emissary@^1.1.0, emissary@^1.2.0, emissary@^1.3.2, emissary@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/emissary/-/emissary-1.3.3.tgz#a618d92d682b232d31111dc3625a5df661799606"
integrity sha512-pD6FWNBSlEOzSJDCTcSGVLgNnGw5fnCvvGMdQ/TN43efeXZ/QTq8+hZoK3OOEXPRNjMmSJmeOnEJh+bWT5O8rQ==
@@ -4390,7 +4388,7 @@ etch@^0.12.2, etch@^0.12.6:
resolved "https://registry.yarnpkg.com/etch/-/etch-0.12.8.tgz#c24bc9bd3a6148f62204ce8643d2e899b9ecb9de"
integrity sha512-dFLRe4wLroVtwzyy1vGlE3BSDZHiL0kZME5XgNGzZIULcYTvVno8vbiIleAesoKJmwWaxDTzG+4eppg2zk14JQ==
-event-kit@2.5.3, event-kit@^2.0.0, event-kit@^2.1.0, event-kit@^2.2.0, event-kit@^2.4.0, event-kit@^2.5.3:
+event-kit@2.5.3, event-kit@^2.0.0, event-kit@^2.1.0, event-kit@^2.4.0, event-kit@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/event-kit/-/event-kit-2.5.3.tgz#d47e4bc116ec0aacd00263791fa1a55eb5e79ba1"
integrity sha512-b7Qi1JNzY4BfAYfnIRanLk0DOD1gdkWHT4GISIn8Q2tAf3LpU8SP2CMwWaq40imYoKWbtN4ZhbSRxvsnikooZQ==
@@ -4605,19 +4603,6 @@ find-up@^5.0.0:
locate-path "^6.0.0"
path-exists "^4.0.0"
-first-mate@7.4.3, first-mate@^7.4.1:
- version "7.4.3"
- resolved "https://registry.yarnpkg.com/first-mate/-/first-mate-7.4.3.tgz#058b9b6d2f43e38a5f0952669338cff2c46ae2dd"
- integrity sha512-PtZUpaPmcV5KV4Rw5TfwczEnExN+X1o3Q/G82E4iRJ0tW91fm3Yi7pa5t4cBH8r3D6EyoBKvfpG2jKE+TZ0/nw==
- dependencies:
- emissary "^1"
- event-kit "^2.2.0"
- fs-plus "^3.0.0"
- grim "^2.0.1"
- oniguruma "^7.2.3"
- season "^6.0.2"
- underscore-plus "^1"
-
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@@ -5194,7 +5179,7 @@ graphql@14.5.8:
dependencies:
iterall "^1.2.2"
-grim@2.0.3, grim@^2.0.1, grim@^2.0.2:
+grim@2.0.3, grim@^2.0.1, grim@^2.0.2, grim@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/grim/-/grim-2.0.3.tgz#66e575efc4577981d959da0764926b4aaded4b0d"
integrity sha512-FM20Ump11qYLK9k9DbL8yzVpy+YBieya1JG15OeH8s+KbHq8kL4SdwRtURwIUHniSxb24EoBUpwKfFjGNVi4/Q==
@@ -7422,13 +7407,6 @@ onetime@^5.1.0:
dependencies:
mimic-fn "^2.1.0"
-oniguruma@^7.2.3:
- version "7.2.3"
- resolved "https://registry.yarnpkg.com/oniguruma/-/oniguruma-7.2.3.tgz#e0b0b415302de8cdd6564e57a1a822ac0ab57012"
- integrity sha512-PZZcE0yfg8Q1IvaJImh21RUTHl8ep0zwwyoE912KqlWVrsGByjjj29sdACcD1BFyX2bLkfuOJeP+POzAGVWtbA==
- dependencies:
- nan "^2.14.0"
-
"open-on-github@file:packages/open-on-github":
version "1.3.2"
@@ -8503,6 +8481,18 @@ season@^6.0.2:
fs-plus "^3.0.0"
yargs "^3.23.0"
+"second-mate@https://github.com/pulsar-edit/second-mate.git#14aa7bd":
+ version "8.0.0"
+ resolved "https://github.com/pulsar-edit/second-mate.git#14aa7bd94b90c47aa99f000394301b9573b8898b"
+ dependencies:
+ emissary "^1.3.3"
+ event-kit "^2.5.3"
+ fs-plus "^3.0.0"
+ grim "^2.0.3"
+ season "^6.0.2"
+ underscore-plus "^1"
+ vscode-oniguruma "^1.7.0"
+
selector-kit@^0.1:
version "0.1.0"
resolved "https://registry.yarnpkg.com/selector-kit/-/selector-kit-0.1.0.tgz#304338fceccea35ec28ffaddb792ab7715633e6f"
@@ -9810,6 +9800,11 @@ verror@^1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
+vscode-oniguruma@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b"
+ integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==
+
vscode-ripgrep@1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.9.0.tgz#d6cdea4d290f3c2919472cdcfe2440d5fb1f99db"