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 === ' { + 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 === ' { + 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 0) { tagCharacterOffset++ } + tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 2) // include 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(``) + } + } + + 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+[^>]*?)?((/)?>))|(]*>)`, '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 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(`\ +
+
+
test
+
+
\ +` + ) + + 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 @@ + + +
+
+
+ hello +
+
+
+ +
+
+ world +
+
+ + 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"