mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-11 19:17:09 +03:00
Bundling bracket-matcher
This commit is contained in:
parent
e9b935b10e
commit
0dc1d92821
@ -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",
|
||||
|
1
packages/bracket-matcher/CONTRIBUTING.md
Normal file
1
packages/bracket-matcher/CONTRIBUTING.md
Normal file
@ -0,0 +1 @@
|
||||
[See how you can contribute](https://github.com/pulsar-edit/.github/blob/main/CONTRIBUTING.md)
|
20
packages/bracket-matcher/LICENSE.md
Normal file
20
packages/bracket-matcher/LICENSE.md
Normal file
@ -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.
|
52
packages/bracket-matcher/README.md
Normal file
52
packages/bracket-matcher/README.md
Normal file
@ -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 <kbd>ctrl-m</kbd> 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 <kbd>ctrl-cmd-m</kbd> to select all the text inside the current brackets.
|
||||
|
||||
Use <kbd>alt-cmd-.</kbd> 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: [
|
||||
"()"
|
||||
"[]"
|
||||
"{}"
|
||||
"<>"
|
||||
"\"\""
|
||||
"``"
|
||||
]
|
||||
```
|
18
packages/bracket-matcher/keymaps/bracket-matcher.cson
Normal file
18
packages/bracket-matcher/keymaps/bracket-matcher.cson
Normal file
@ -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'
|
585
packages/bracket-matcher/lib/bracket-matcher-view.js
Normal file
585
packages/bracket-matcher/lib/bracket-matcher-view.js
Normal file
@ -0,0 +1,585 @@
|
||||
const {CompositeDisposable} = require('atom')
|
||||
const _ = require('underscore-plus')
|
||||
const {Range, Point} = require('atom')
|
||||
const TagFinder = require('./tag-finder')
|
||||
|
||||
const MAX_ROWS_TO_SCAN = 10000
|
||||
const ONE_CHAR_FORWARD_TRAVERSAL = Object.freeze(Point(0, 1))
|
||||
const ONE_CHAR_BACKWARD_TRAVERSAL = Object.freeze(Point(0, -1))
|
||||
const TWO_CHARS_BACKWARD_TRAVERSAL = Object.freeze(Point(0, -2))
|
||||
const MAX_ROWS_TO_SCAN_FORWARD_TRAVERSAL = Object.freeze(Point(MAX_ROWS_TO_SCAN, 0))
|
||||
const MAX_ROWS_TO_SCAN_BACKWARD_TRAVERSAL = Object.freeze(Point(-MAX_ROWS_TO_SCAN, 0))
|
||||
|
||||
module.exports =
|
||||
class BracketMatcherView {
|
||||
constructor (editor, editorElement, matchManager) {
|
||||
this.destroy = this.destroy.bind(this)
|
||||
this.updateMatch = this.updateMatch.bind(this)
|
||||
this.editor = editor
|
||||
this.matchManager = matchManager
|
||||
this.gutter = this.editor.gutterWithName('line-number')
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.tagFinder = new TagFinder(this.editor)
|
||||
this.pairHighlighted = false
|
||||
this.tagHighlighted = false
|
||||
|
||||
// ranges for possible selection
|
||||
this.bracket1Range = null
|
||||
this.bracket2Range = null
|
||||
|
||||
this.subscriptions.add(
|
||||
this.editor.onDidTokenize(this.updateMatch),
|
||||
this.editor.getBuffer().onDidChangeText(this.updateMatch),
|
||||
this.editor.onDidChangeGrammar(this.updateMatch),
|
||||
this.editor.onDidChangeSelectionRange(this.updateMatch),
|
||||
this.editor.onDidAddCursor(this.updateMatch),
|
||||
this.editor.onDidRemoveCursor(this.updateMatch),
|
||||
|
||||
atom.commands.add(editorElement, 'bracket-matcher:go-to-matching-bracket', () =>
|
||||
this.goToMatchingBracket()
|
||||
),
|
||||
|
||||
atom.commands.add(editorElement, 'bracket-matcher:go-to-enclosing-bracket', () =>
|
||||
this.gotoPrecedingStartBracket()
|
||||
),
|
||||
|
||||
atom.commands.add(editorElement, 'bracket-matcher:select-inside-brackets', () =>
|
||||
this.selectInsideBrackets()
|
||||
),
|
||||
|
||||
atom.commands.add(editorElement, 'bracket-matcher:close-tag', () =>
|
||||
this.closeTag()
|
||||
),
|
||||
|
||||
atom.commands.add(editorElement, 'bracket-matcher:remove-matching-brackets', () =>
|
||||
this.removeMatchingBrackets()
|
||||
),
|
||||
|
||||
atom.commands.add(editorElement, 'bracket-matcher:select-matching-brackets', () =>
|
||||
this.selectMatchingBrackets()
|
||||
),
|
||||
|
||||
this.editor.onDidDestroy(this.destroy)
|
||||
)
|
||||
|
||||
this.updateMatch()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
}
|
||||
|
||||
updateMatch () {
|
||||
if (this.pairHighlighted) {
|
||||
this.editor.destroyMarker(this.startMarker.id)
|
||||
this.editor.destroyMarker(this.endMarker.id)
|
||||
}
|
||||
|
||||
this.pairHighlighted = false
|
||||
this.tagHighlighted = false
|
||||
|
||||
if (!this.editor.getLastSelection().isEmpty()) return
|
||||
|
||||
const {position, matchPosition} = this.findCurrentPair()
|
||||
|
||||
let startRange = null
|
||||
let endRange = null
|
||||
let highlightTag = false
|
||||
let highlightPair = false
|
||||
if (position && matchPosition) {
|
||||
this.bracket1Range = (startRange = Range(position, position.traverse(ONE_CHAR_FORWARD_TRAVERSAL)))
|
||||
this.bracket2Range = (endRange = Range(matchPosition, matchPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL)))
|
||||
highlightPair = true
|
||||
} else {
|
||||
this.bracket1Range = null
|
||||
this.bracket2Range = null
|
||||
if (this.hasSyntaxTree()) {
|
||||
({startRange, endRange} = this.findMatchingTagNameRangesWithSyntaxTree())
|
||||
} else {
|
||||
({startRange, endRange} = this.tagFinder.findMatchingTags())
|
||||
if (this.isCursorOnCommentOrString()) return
|
||||
}
|
||||
if (startRange) {
|
||||
highlightTag = true
|
||||
highlightPair = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!highlightTag && !highlightPair) return
|
||||
|
||||
this.startMarker = this.createMarker(startRange)
|
||||
this.endMarker = this.createMarker(endRange)
|
||||
this.pairHighlighted = highlightPair
|
||||
this.tagHighlighted = highlightTag
|
||||
}
|
||||
|
||||
selectMatchingBrackets () {
|
||||
if (!this.bracket1Range && !this.bracket2Range) return
|
||||
this.editor.setSelectedBufferRanges([this.bracket1Range, this.bracket2Range])
|
||||
this.matchManager.changeBracketsMode = true
|
||||
}
|
||||
|
||||
removeMatchingBrackets () {
|
||||
if (this.editor.hasMultipleCursors()) {
|
||||
this.editor.backspace()
|
||||
return
|
||||
}
|
||||
|
||||
this.editor.transact(() => {
|
||||
if (this.editor.getLastSelection().isEmpty()) {
|
||||
this.editor.selectLeft()
|
||||
}
|
||||
|
||||
const text = this.editor.getSelectedText()
|
||||
this.editor.moveRight()
|
||||
|
||||
// check if the character to the left is part of a pair
|
||||
if (
|
||||
this.matchManager.pairedCharacters.hasOwnProperty(text) ||
|
||||
this.matchManager.pairedCharactersInverse.hasOwnProperty(text)
|
||||
) {
|
||||
let {position, matchPosition, bracket} = this.findCurrentPair()
|
||||
|
||||
if (position && matchPosition) {
|
||||
this.editor.setCursorBufferPosition(matchPosition)
|
||||
this.editor.delete()
|
||||
// if on the same line and the cursor is in front of an end pair
|
||||
// offset by one to make up for the missing character
|
||||
if (position.row === matchPosition.row && this.matchManager.pairedCharactersInverse.hasOwnProperty(bracket)) {
|
||||
position = position.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
|
||||
}
|
||||
this.editor.setCursorBufferPosition(position)
|
||||
this.editor.delete()
|
||||
} else {
|
||||
this.editor.backspace()
|
||||
}
|
||||
} else {
|
||||
this.editor.backspace()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
findMatchingEndBracket (startBracketPosition, startBracket, endBracket) {
|
||||
if (startBracket === endBracket) return
|
||||
|
||||
if (this.hasSyntaxTree()) {
|
||||
return this.findMatchingEndBracketWithSyntaxTree(startBracketPosition, startBracket, endBracket)
|
||||
} else {
|
||||
const scopeDescriptor = this.editor.scopeDescriptorForBufferPosition(startBracketPosition)
|
||||
if (this.isScopeCommentedOrString(scopeDescriptor.getScopesArray())) return
|
||||
return this.findMatchingEndBracketWithRegexSearch(startBracketPosition, startBracket, endBracket)
|
||||
}
|
||||
}
|
||||
|
||||
findMatchingStartBracket (endBracketPosition, startBracket, endBracket) {
|
||||
if (startBracket === endBracket) return
|
||||
|
||||
if (this.hasSyntaxTree()) {
|
||||
return this.findMatchingStartBracketWithSyntaxTree(endBracketPosition, startBracket, endBracket)
|
||||
} else {
|
||||
const scopeDescriptor = this.editor.scopeDescriptorForBufferPosition(endBracketPosition)
|
||||
if (this.isScopeCommentedOrString(scopeDescriptor.getScopesArray())) return
|
||||
return this.findMatchingStartBracketWithRegexSearch(endBracketPosition, startBracket, endBracket)
|
||||
}
|
||||
}
|
||||
|
||||
findMatchingEndBracketWithSyntaxTree (bracketPosition, startBracket, endBracket) {
|
||||
let result
|
||||
const bracketEndPosition = bracketPosition.traverse([0, startBracket.length])
|
||||
this.editor.buffer.getLanguageMode().getSyntaxNodeContainingRange(
|
||||
new Range(bracketPosition, bracketEndPosition),
|
||||
node => {
|
||||
if (bracketEndPosition.isGreaterThan(node.startPosition) && bracketEndPosition.isLessThan(node.endPosition)) {
|
||||
const matchNode = node.children.find(child =>
|
||||
bracketEndPosition.isLessThanOrEqual(child.startPosition) &&
|
||||
child.type === endBracket
|
||||
)
|
||||
if (matchNode) result = Point.fromObject(matchNode.startPosition)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
findMatchingStartBracketWithSyntaxTree (bracketPosition, startBracket, endBracket) {
|
||||
let result
|
||||
const bracketEndPosition = bracketPosition.traverse([0, startBracket.length])
|
||||
this.editor.buffer.getLanguageMode().getSyntaxNodeContainingRange(
|
||||
new Range(bracketPosition, bracketEndPosition),
|
||||
node => {
|
||||
if (bracketPosition.isGreaterThan(node.startPosition)) {
|
||||
const matchNode = node.children.find(child =>
|
||||
bracketPosition.isGreaterThanOrEqual(child.endPosition) &&
|
||||
child.type === startBracket
|
||||
)
|
||||
if (matchNode) result = Point.fromObject(matchNode.startPosition)
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
findMatchingTagNameRangesWithSyntaxTree () {
|
||||
const position = this.editor.getCursorBufferPosition()
|
||||
const {startTag, endTag} = this.findContainingTagsWithSyntaxTree(position)
|
||||
if (startTag && (startTag.range.containsPoint(position) || endTag.range.containsPoint(position))) {
|
||||
if (startTag === endTag) {
|
||||
const {range} = startTag.child(1)
|
||||
return {startRange: range, endRange: range}
|
||||
} else if (endTag.firstChild.type === '</') {
|
||||
return {
|
||||
startRange: startTag.child(1).range,
|
||||
endRange: endTag.child(1).range
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
startRange: startTag.child(1).range,
|
||||
endRange: endTag.child(2).range
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
findMatchingTagsWithSyntaxTree () {
|
||||
const position = this.editor.getCursorBufferPosition()
|
||||
const {startTag, endTag} = this.findContainingTagsWithSyntaxTree(position)
|
||||
if (startTag) {
|
||||
return {startRange: startTag.range, endRange: endTag.range}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
findContainingTagsWithSyntaxTree (position) {
|
||||
let startTag, endTag
|
||||
if (position.column === this.editor.buffer.lineLengthForRow(position.row)) position.column--;
|
||||
this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition(position, node => {
|
||||
if (node.type.includes('element') && node.childCount > 0) {
|
||||
const {firstChild, lastChild} = node
|
||||
if (
|
||||
firstChild.childCount > 2 &&
|
||||
firstChild.firstChild.type === '<'
|
||||
) {
|
||||
if (lastChild === firstChild && firstChild.lastChild.type === '/>') {
|
||||
startTag = firstChild
|
||||
endTag = firstChild
|
||||
} else if (
|
||||
lastChild.childCount > 2 &&
|
||||
(lastChild.firstChild.type === '</' ||
|
||||
lastChild.firstChild.type === '<' && lastChild.child(1).type === '/')
|
||||
) {
|
||||
startTag = firstChild
|
||||
endTag = lastChild
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
return {startTag, endTag}
|
||||
}
|
||||
|
||||
findMatchingEndBracketWithRegexSearch (startBracketPosition, startBracket, endBracket) {
|
||||
const scanRange = new Range(
|
||||
startBracketPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL),
|
||||
startBracketPosition.traverse(MAX_ROWS_TO_SCAN_FORWARD_TRAVERSAL)
|
||||
)
|
||||
let endBracketPosition = null
|
||||
let unpairedCount = 0
|
||||
this.editor.scanInBufferRange(this.matchManager.pairRegexes[startBracket], scanRange, result => {
|
||||
if (this.isRangeCommentedOrString(result.range)) return
|
||||
switch (result.match[0]) {
|
||||
case startBracket:
|
||||
unpairedCount++
|
||||
break
|
||||
case endBracket:
|
||||
unpairedCount--
|
||||
if (unpairedCount < 0) {
|
||||
endBracketPosition = result.range.start
|
||||
result.stop()
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return endBracketPosition
|
||||
}
|
||||
|
||||
findMatchingStartBracketWithRegexSearch (endBracketPosition, startBracket, endBracket) {
|
||||
const scanRange = new Range(
|
||||
endBracketPosition.traverse(MAX_ROWS_TO_SCAN_BACKWARD_TRAVERSAL),
|
||||
endBracketPosition
|
||||
)
|
||||
let startBracketPosition = null
|
||||
let unpairedCount = 0
|
||||
this.editor.backwardsScanInBufferRange(this.matchManager.pairRegexes[startBracket], scanRange, result => {
|
||||
if (this.isRangeCommentedOrString(result.range)) return
|
||||
switch (result.match[0]) {
|
||||
case startBracket:
|
||||
unpairedCount--
|
||||
if (unpairedCount < 0) {
|
||||
startBracketPosition = result.range.start
|
||||
result.stop()
|
||||
break
|
||||
}
|
||||
break
|
||||
case endBracket:
|
||||
unpairedCount++
|
||||
}
|
||||
})
|
||||
|
||||
return startBracketPosition
|
||||
}
|
||||
|
||||
findPrecedingStartBracket (cursorPosition) {
|
||||
if (this.hasSyntaxTree()) {
|
||||
return this.findPrecedingStartBracketWithSyntaxTree(cursorPosition)
|
||||
} else {
|
||||
return this.findPrecedingStartBracketWithRegexSearch(cursorPosition)
|
||||
}
|
||||
}
|
||||
|
||||
findPrecedingStartBracketWithSyntaxTree (cursorPosition) {
|
||||
let result
|
||||
this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition(cursorPosition, node => {
|
||||
for (const child of node.children) {
|
||||
if (cursorPosition.isLessThanOrEqual(child.startPosition)) break
|
||||
if (
|
||||
child.type in this.matchManager.pairedCharacters ||
|
||||
child.type in this.matchManager.pairedCharactersInverse
|
||||
) {
|
||||
result = Point.fromObject(child.startPosition)
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
findPrecedingStartBracketWithRegexSearch (cursorPosition) {
|
||||
const scanRange = new Range(Point.ZERO, cursorPosition)
|
||||
const startBracket = _.escapeRegExp(_.keys(this.matchManager.pairedCharacters).join(''))
|
||||
const endBracket = _.escapeRegExp(_.keys(this.matchManager.pairedCharactersInverse).join(''))
|
||||
const combinedRegExp = new RegExp(`[${startBracket}${endBracket}]`, 'g')
|
||||
const startBracketRegExp = new RegExp(`[${startBracket}]`, 'g')
|
||||
const endBracketRegExp = new RegExp(`[${endBracket}]`, 'g')
|
||||
let startPosition = null
|
||||
let unpairedCount = 0
|
||||
this.editor.backwardsScanInBufferRange(combinedRegExp, scanRange, result => {
|
||||
if (this.isRangeCommentedOrString(result.range)) return
|
||||
if (result.match[0].match(endBracketRegExp)) {
|
||||
unpairedCount++
|
||||
} else if (result.match[0].match(startBracketRegExp)) {
|
||||
unpairedCount--
|
||||
if (unpairedCount < 0) {
|
||||
startPosition = result.range.start
|
||||
result.stop()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return startPosition
|
||||
}
|
||||
|
||||
createMarker (bufferRange) {
|
||||
const marker = this.editor.markBufferRange(bufferRange)
|
||||
this.editor.decorateMarker(marker, {type: 'highlight', class: 'bracket-matcher', deprecatedRegionClass: 'bracket-matcher'})
|
||||
if (atom.config.get('bracket-matcher.highlightMatchingLineNumber', {scope: this.editor.getRootScopeDescriptor()}) && this.gutter) {
|
||||
this.gutter.decorateMarker(marker, {type: 'highlight', class: 'bracket-matcher', deprecatedRegionClass: 'bracket-matcher'})
|
||||
}
|
||||
return marker
|
||||
}
|
||||
|
||||
findCurrentPair () {
|
||||
const currentPosition = this.editor.getCursorBufferPosition()
|
||||
const previousPosition = currentPosition.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
|
||||
const nextPosition = currentPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL)
|
||||
const currentCharacter = this.editor.getTextInBufferRange(new Range(currentPosition, nextPosition))
|
||||
const previousCharacter = this.editor.getTextInBufferRange(new Range(previousPosition, currentPosition))
|
||||
|
||||
let position, matchPosition, currentBracket, matchingBracket
|
||||
if ((matchingBracket = this.matchManager.pairedCharacters[currentCharacter])) {
|
||||
position = currentPosition
|
||||
currentBracket = currentCharacter
|
||||
matchPosition = this.findMatchingEndBracket(position, currentBracket, matchingBracket)
|
||||
} else if ((matchingBracket = this.matchManager.pairedCharacters[previousCharacter])) {
|
||||
position = previousPosition
|
||||
currentBracket = previousCharacter
|
||||
matchPosition = this.findMatchingEndBracket(position, currentBracket, matchingBracket)
|
||||
} else if ((matchingBracket = this.matchManager.pairedCharactersInverse[previousCharacter])) {
|
||||
position = previousPosition
|
||||
currentBracket = previousCharacter
|
||||
matchPosition = this.findMatchingStartBracket(position, matchingBracket, currentBracket)
|
||||
} else if ((matchingBracket = this.matchManager.pairedCharactersInverse[currentCharacter])) {
|
||||
position = currentPosition
|
||||
currentBracket = currentCharacter
|
||||
matchPosition = this.findMatchingStartBracket(position, matchingBracket, currentBracket)
|
||||
}
|
||||
|
||||
return {position, matchPosition, bracket: currentBracket}
|
||||
}
|
||||
|
||||
goToMatchingBracket () {
|
||||
if (!this.pairHighlighted) return this.gotoPrecedingStartBracket()
|
||||
const position = this.editor.getCursorBufferPosition()
|
||||
|
||||
if (this.tagHighlighted) {
|
||||
let tagCharacterOffset
|
||||
let startRange = this.startMarker.getBufferRange()
|
||||
const tagLength = startRange.end.column - startRange.start.column
|
||||
let endRange = this.endMarker.getBufferRange()
|
||||
if (startRange.compare(endRange) > 0) {
|
||||
[startRange, endRange] = [endRange, startRange]
|
||||
}
|
||||
|
||||
// include the <
|
||||
startRange = new Range(startRange.start.traverse(ONE_CHAR_BACKWARD_TRAVERSAL), endRange.end.traverse(ONE_CHAR_BACKWARD_TRAVERSAL))
|
||||
// include the </
|
||||
endRange = new Range(endRange.start.traverse(TWO_CHARS_BACKWARD_TRAVERSAL), endRange.end.traverse(TWO_CHARS_BACKWARD_TRAVERSAL))
|
||||
|
||||
if (position.isLessThan(endRange.start)) {
|
||||
tagCharacterOffset = position.column - startRange.start.column
|
||||
if (tagCharacterOffset > 0) { tagCharacterOffset++ }
|
||||
tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 2) // include </
|
||||
this.editor.setCursorBufferPosition(endRange.start.traverse([0, tagCharacterOffset]))
|
||||
} else {
|
||||
tagCharacterOffset = position.column - endRange.start.column
|
||||
if (tagCharacterOffset > 1) { tagCharacterOffset-- }
|
||||
tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 1) // include <
|
||||
this.editor.setCursorBufferPosition(startRange.start.traverse([0, tagCharacterOffset]))
|
||||
}
|
||||
} else {
|
||||
const previousPosition = position.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
|
||||
const startPosition = this.startMarker.getStartBufferPosition()
|
||||
const endPosition = this.endMarker.getStartBufferPosition()
|
||||
|
||||
if (position.isEqual(startPosition)) {
|
||||
this.editor.setCursorBufferPosition(endPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL))
|
||||
} else if (previousPosition.isEqual(startPosition)) {
|
||||
this.editor.setCursorBufferPosition(endPosition)
|
||||
} else if (position.isEqual(endPosition)) {
|
||||
this.editor.setCursorBufferPosition(startPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL))
|
||||
} else if (previousPosition.isEqual(endPosition)) {
|
||||
this.editor.setCursorBufferPosition(startPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gotoPrecedingStartBracket () {
|
||||
if (this.pairHighlighted) return
|
||||
|
||||
const matchPosition = this.findPrecedingStartBracket(this.editor.getCursorBufferPosition())
|
||||
if (matchPosition) {
|
||||
this.editor.setCursorBufferPosition(matchPosition)
|
||||
} else {
|
||||
let startRange, endRange
|
||||
if (this.hasSyntaxTree()) {
|
||||
({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
|
||||
} else {
|
||||
({startRange, endRange} = this.tagFinder.findStartEndTags())
|
||||
}
|
||||
|
||||
if (startRange) {
|
||||
if (startRange.compare(endRange) > 0) {
|
||||
[startRange, endRange] = [endRange, startRange]
|
||||
}
|
||||
this.editor.setCursorBufferPosition(startRange.start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multiCursorSelect() {
|
||||
this.editor.getCursorBufferPositions().forEach(position => {
|
||||
let startPosition = this.findPrecedingStartBracket(position)
|
||||
if(startPosition) {
|
||||
const startBracket = this.editor.getTextInRange(Range.fromPointWithDelta(startPosition, 0, 1))
|
||||
const endPosition = this.findMatchingEndBracket(startPosition, startBracket, this.matchManager.pairedCharacters[startBracket])
|
||||
startPosition = startPosition.traverse([0, 1])
|
||||
if (startPosition && endPosition) {
|
||||
const rangeToSelect = new Range(startPosition, endPosition)
|
||||
this.editor.addSelectionForBufferRange(rangeToSelect)
|
||||
}
|
||||
} else {
|
||||
let startRange, endRange;
|
||||
if (this.hasSyntaxTree()) {
|
||||
({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
|
||||
} else {
|
||||
({startRange, endRange} = this.tagFinder.findStartEndTags(true))
|
||||
if (startRange && startRange.compare(endRange) > 0) {
|
||||
[startRange, endRange] = [endRange, startRange]
|
||||
}
|
||||
}
|
||||
if (startRange) {
|
||||
const startPosition = startRange.end
|
||||
const endPosition = endRange.start
|
||||
const rangeToSelect = new Range(startPosition, endPosition)
|
||||
this.editor.setSelectedBufferRange(rangeToSelect)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
selectInsideBrackets () {
|
||||
let endPosition, endRange, startPosition, startRange
|
||||
if (this.pairHighlighted) {
|
||||
startRange = this.startMarker.getBufferRange()
|
||||
endRange = this.endMarker.getBufferRange()
|
||||
|
||||
if (this.tagHighlighted) {
|
||||
if (this.hasSyntaxTree()) {
|
||||
({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
|
||||
} else {
|
||||
({startRange, endRange} = this.tagFinder.findStartEndTags(true))
|
||||
if (startRange && startRange.compare(endRange) > 0) {
|
||||
[startRange, endRange] = [endRange, startRange]
|
||||
}
|
||||
}
|
||||
}
|
||||
startPosition = startRange.end
|
||||
endPosition = endRange.start
|
||||
|
||||
const rangeToSelect = new Range(startPosition, endPosition)
|
||||
this.editor.setSelectedBufferRange(rangeToSelect)
|
||||
} else {
|
||||
this.multiCursorSelect();
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the current cursor position a closing tag if there exists an
|
||||
// open tag that is not closed afterwards.
|
||||
closeTag () {
|
||||
const cursorPosition = this.editor.getCursorBufferPosition()
|
||||
const preFragment = this.editor.getTextInBufferRange([Point.ZERO, cursorPosition])
|
||||
const postFragment = this.editor.getTextInBufferRange([cursorPosition, Point.INFINITY])
|
||||
|
||||
const tag = this.tagFinder.closingTagForFragments(preFragment, postFragment)
|
||||
if (tag) {
|
||||
this.editor.insertText(`</${tag}>`)
|
||||
}
|
||||
}
|
||||
|
||||
isCursorOnCommentOrString () {
|
||||
return this.isScopeCommentedOrString(this.editor.getLastCursor().getScopeDescriptor().getScopesArray())
|
||||
}
|
||||
|
||||
isRangeCommentedOrString (range) {
|
||||
return this.isScopeCommentedOrString(this.editor.scopeDescriptorForBufferPosition(range.start).getScopesArray())
|
||||
}
|
||||
|
||||
isScopeCommentedOrString (scopesArray) {
|
||||
for (let scope of scopesArray.reverse()) {
|
||||
scope = scope.split('.')
|
||||
if (scope.includes('embedded') && scope.includes('source')) return false
|
||||
if (scope.includes('comment') || scope.includes('string')) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
hasSyntaxTree () {
|
||||
return this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition
|
||||
}
|
||||
}
|
299
packages/bracket-matcher/lib/bracket-matcher.js
Normal file
299
packages/bracket-matcher/lib/bracket-matcher.js
Normal file
@ -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)
|
||||
)
|
||||
}
|
20
packages/bracket-matcher/lib/main.js
Normal file
20
packages/bracket-matcher/lib/main.js
Normal file
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
60
packages/bracket-matcher/lib/match-manager.js
Normal file
60
packages/bracket-matcher/lib/match-manager.js
Normal file
@ -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()
|
||||
}
|
||||
}
|
11
packages/bracket-matcher/lib/selector-cache.js
Normal file
11
packages/bracket-matcher/lib/selector-cache.js
Normal file
@ -0,0 +1,11 @@
|
||||
const {ScopeSelector} = require('first-mate')
|
||||
const cache = {}
|
||||
|
||||
exports.get = function (selector) {
|
||||
let scopeSelector = cache[selector]
|
||||
if (!scopeSelector) {
|
||||
scopeSelector = new ScopeSelector(selector)
|
||||
cache[selector] = scopeSelector
|
||||
}
|
||||
return scopeSelector
|
||||
}
|
18
packages/bracket-matcher/lib/self-closing-tags.json
Normal file
18
packages/bracket-matcher/lib/self-closing-tags.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"command",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"keygen",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr"
|
||||
]
|
258
packages/bracket-matcher/lib/tag-finder.js
Normal file
258
packages/bracket-matcher/lib/tag-finder.js
Normal file
@ -0,0 +1,258 @@
|
||||
const {Range} = require('atom')
|
||||
const _ = require('underscore-plus')
|
||||
const SelfClosingTags = require('./self-closing-tags')
|
||||
|
||||
const TAG_SELECTOR_REGEX = /(\b|\.)(meta\.tag|punctuation\.definition\.tag)/
|
||||
const COMMENT_SELECTOR_REGEX = /(\b|\.)comment/
|
||||
|
||||
// Creates a regex to match opening tag with match[1] and closing tags with match[2]
|
||||
//
|
||||
// * tagNameRegexStr - a regex string describing how to match the tagname.
|
||||
// Should not contain capturing match groups.
|
||||
//
|
||||
// Returns a {RegExp}.
|
||||
const generateTagStartOrEndRegex = function (tagNameRegexStr) {
|
||||
const notSelfClosingTagEnd = "(?:[^>\\/\"']|\"[^\"]*\"|'[^']*')*>"
|
||||
return new RegExp(`<(${tagNameRegexStr})${notSelfClosingTagEnd}|<\\/(${tagNameRegexStr})>`)
|
||||
}
|
||||
|
||||
const tagStartOrEndRegex = generateTagStartOrEndRegex('\\w[-\\w]*(?:(?:\\:|\\.)\\w[-\\w]*)*')
|
||||
|
||||
// Helper to find the matching start/end tag for the start/end tag under the
|
||||
// cursor in XML, HTML, etc. editors.
|
||||
module.exports =
|
||||
class TagFinder {
|
||||
constructor (editor) {
|
||||
// 1. Tag prefix
|
||||
// 2. Closing tag (optional)
|
||||
// 3. Tag name
|
||||
// 4. Attributes (ids, classes, etc. - optional)
|
||||
// 5. Tag suffix
|
||||
// 6. Self-closing tag (optional)
|
||||
this.editor = editor
|
||||
this.tagPattern = /(<(\/)?)(.+?)(\s+.*?)?((\/)?>|$)/
|
||||
this.wordRegex = /.*?(>|$)/
|
||||
}
|
||||
|
||||
patternForTagName (tagName) {
|
||||
tagName = _.escapeRegExp(tagName)
|
||||
// 1. Start tag
|
||||
// 2. Tag name
|
||||
// 3. Attributes (optional)
|
||||
// 4. Tag suffix
|
||||
// 5. Self-closing tag (optional)
|
||||
// 6. End tag
|
||||
return new RegExp(`(<(${tagName})(\\s+[^>]*?)?((/)?>))|(</${tagName}[^>]*>)`, 'gi')
|
||||
}
|
||||
|
||||
isRangeCommented (range) {
|
||||
return this.scopesForPositionMatchRegex(range.start, COMMENT_SELECTOR_REGEX)
|
||||
}
|
||||
|
||||
isCursorOnTag () {
|
||||
return this.scopesForPositionMatchRegex(this.editor.getCursorBufferPosition(), TAG_SELECTOR_REGEX)
|
||||
}
|
||||
|
||||
scopesForPositionMatchRegex (position, regex) {
|
||||
const {tokenizedBuffer, buffer} = this.editor
|
||||
const {grammar} = tokenizedBuffer
|
||||
let column = 0
|
||||
const line = tokenizedBuffer.tokenizedLineForRow(position.row)
|
||||
if (line == null) { return false }
|
||||
const lineLength = buffer.lineLengthForRow(position.row)
|
||||
const scopeIds = line.openScopes.slice()
|
||||
for (let i = 0; i < line.tags.length; i++) {
|
||||
const tag = line.tags[i]
|
||||
if (tag >= 0) {
|
||||
const nextColumn = column + tag
|
||||
if ((nextColumn > position.column) || (nextColumn === lineLength)) { break }
|
||||
column = nextColumn
|
||||
} else if ((tag & 1) === 1) {
|
||||
scopeIds.push(tag)
|
||||
} else {
|
||||
scopeIds.pop()
|
||||
}
|
||||
}
|
||||
|
||||
return scopeIds.some(scopeId => regex.test(grammar.scopeForId(scopeId)))
|
||||
}
|
||||
|
||||
findStartTag (tagName, endPosition, fullRange = false) {
|
||||
const scanRange = new Range([0, 0], endPosition)
|
||||
const pattern = this.patternForTagName(tagName)
|
||||
let startRange = null
|
||||
let unpairedCount = 0
|
||||
this.editor.backwardsScanInBufferRange(pattern, scanRange, ({match, range, stop}) => {
|
||||
if (this.isRangeCommented(range)) return
|
||||
|
||||
const [entireMatch, isStartTag, tagName, attributes, suffix, isSelfClosingTag, isEndTag] = match
|
||||
|
||||
if (isSelfClosingTag) return
|
||||
|
||||
if (isStartTag) {
|
||||
unpairedCount--
|
||||
if (unpairedCount < 0) {
|
||||
stop()
|
||||
startRange = range
|
||||
if (!fullRange) {
|
||||
// Move the start past the initial <
|
||||
startRange.start = startRange.start.translate([0, 1])
|
||||
|
||||
// End right after the tag name
|
||||
startRange.end = startRange.start.translate([0, tagName.length])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unpairedCount++
|
||||
}
|
||||
})
|
||||
|
||||
return startRange
|
||||
}
|
||||
|
||||
findEndTag (tagName, startPosition, fullRange = false) {
|
||||
const scanRange = new Range(startPosition, this.editor.buffer.getEndPosition())
|
||||
const pattern = this.patternForTagName(tagName)
|
||||
let endRange = null
|
||||
let unpairedCount = 0
|
||||
this.editor.scanInBufferRange(pattern, scanRange, ({match, range, stop}) => {
|
||||
if (this.isRangeCommented(range)) return
|
||||
|
||||
const [entireMatch, isStartTag, tagName, attributes, suffix, isSelfClosingTag, isEndTag] = match
|
||||
|
||||
if (isSelfClosingTag) return
|
||||
|
||||
if (isStartTag) {
|
||||
unpairedCount++
|
||||
} else {
|
||||
unpairedCount--
|
||||
if (unpairedCount < 0) {
|
||||
stop()
|
||||
endRange = range
|
||||
if (!fullRange) {
|
||||
// Subtract </ and > from range
|
||||
endRange = range.translate([0, 2], [0, -1])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return endRange
|
||||
}
|
||||
|
||||
findStartEndTags (fullRange = false) {
|
||||
let ranges = {}
|
||||
const endPosition = this.editor.getLastCursor().getCurrentWordBufferRange({wordRegex: this.wordRegex}).end
|
||||
this.editor.backwardsScanInBufferRange(this.tagPattern, [[0, 0], endPosition], ({match, range, stop}) => {
|
||||
stop()
|
||||
|
||||
const [entireMatch, prefix, isClosingTag, tagName, attributes, suffix, isSelfClosingTag] = Array.from(match)
|
||||
|
||||
let startRange = range
|
||||
if (!fullRange) {
|
||||
if (range.start.row === range.end.row) {
|
||||
// Move the start past the initial <
|
||||
startRange.start = startRange.start.translate([0, prefix.length])
|
||||
// End right after the tag name
|
||||
startRange.end = startRange.start.translate([0, tagName.length])
|
||||
} else {
|
||||
startRange = Range.fromObject([range.start.translate([0, prefix.length]), [range.start.row, Infinity]])
|
||||
}
|
||||
}
|
||||
|
||||
let endRange
|
||||
if (isSelfClosingTag) {
|
||||
endRange = startRange
|
||||
} else if (isClosingTag) {
|
||||
endRange = this.findStartTag(tagName, startRange.start, fullRange)
|
||||
} else {
|
||||
endRange = this.findEndTag(tagName, startRange.end, fullRange)
|
||||
}
|
||||
|
||||
if (startRange && endRange) ranges = {startRange, endRange}
|
||||
})
|
||||
|
||||
return ranges
|
||||
}
|
||||
|
||||
findMatchingTags () {
|
||||
return (this.isCursorOnTag() && this.findStartEndTags()) || {}
|
||||
}
|
||||
|
||||
// Parses a fragment of html returning the stack (i.e., an array) of open tags
|
||||
//
|
||||
// fragment - the fragment of html to be analysed
|
||||
// stack - an array to be populated (can be non-empty)
|
||||
// matchExpr - a RegExp describing how to match opening/closing tags
|
||||
// the opening/closing descriptions must be captured subexpressions
|
||||
// so that the code can refer to match[1] to check if an opening
|
||||
// tag has been found, and to match[2] to check if a closing tag
|
||||
// has been found
|
||||
// cond - a condition to be checked at each iteration. If the function
|
||||
// returns false the processing is immediately interrupted. When
|
||||
// called the current stack is provided to the function.
|
||||
//
|
||||
// Returns an array of strings. Each string is a tag that is still to be closed
|
||||
// (the most recent non closed tag is at the end of the array).
|
||||
parseFragment (fragment, stack, matchExpr, cond) {
|
||||
let match = fragment.match(matchExpr)
|
||||
while (match && cond(stack)) {
|
||||
if (SelfClosingTags.indexOf(match[1]) === -1) {
|
||||
const topElem = stack[stack.length - 1]
|
||||
|
||||
if (match[2] && (topElem === match[2])) {
|
||||
stack.pop()
|
||||
} else if (match[1]) {
|
||||
stack.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
fragment = fragment.substr(match.index + match[0].length)
|
||||
match = fragment.match(matchExpr)
|
||||
}
|
||||
|
||||
return stack
|
||||
}
|
||||
|
||||
// Parses the given fragment of html code returning the last unclosed tag.
|
||||
//
|
||||
// fragment - a string containing a fragment of html code.
|
||||
//
|
||||
// Returns an array of strings. Each string is a tag that is still to be closed
|
||||
// (the most recent non closed tag is at the end of the array).
|
||||
tagsNotClosedInFragment (fragment) {
|
||||
return this.parseFragment(fragment, [], tagStartOrEndRegex, () => true)
|
||||
}
|
||||
|
||||
// Parses the given fragment of html code and returns true if the given tag
|
||||
// has a matching closing tag in it. If tag is reopened and reclosed in the
|
||||
// given fragment then the end point of that pair does not count as a matching
|
||||
// closing tag.
|
||||
tagDoesNotCloseInFragment (tags, fragment) {
|
||||
if (tags.length === 0) { return false }
|
||||
|
||||
let stack = tags
|
||||
const stackLength = stack.length
|
||||
const tag = tags[tags.length - 1]
|
||||
const escapedTag = _.escapeRegExp(tag)
|
||||
stack = this.parseFragment(fragment, stack, generateTagStartOrEndRegex(escapedTag), s =>
|
||||
s.length >= stackLength || s[s.length - 1] === tag
|
||||
)
|
||||
|
||||
return (stack.length > 0) && (stack[stack.length - 1] === tag)
|
||||
}
|
||||
|
||||
// Parses preFragment and postFragment returning the last open tag in
|
||||
// preFragment that is not closed in postFragment.
|
||||
//
|
||||
// Returns a tag name or null if it can't find it.
|
||||
closingTagForFragments (preFragment, postFragment) {
|
||||
const tags = this.tagsNotClosedInFragment(preFragment)
|
||||
const tag = tags[tags.length - 1]
|
||||
if (this.tagDoesNotCloseInFragment(tags, postFragment)) {
|
||||
return tag
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
22
packages/bracket-matcher/menus/bracket-matcher.cson
Normal file
22
packages/bracket-matcher/menus/bracket-matcher.cson
Normal file
@ -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' }
|
||||
]
|
||||
}
|
||||
]
|
68
packages/bracket-matcher/package.json
Normal file
68
packages/bracket-matcher/package.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"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": {
|
||||
"first-mate": "^7.4.1",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
1913
packages/bracket-matcher/spec/bracket-matcher-spec.js
Normal file
1913
packages/bracket-matcher/spec/bracket-matcher-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
142
packages/bracket-matcher/spec/close-tag-spec.js
Normal file
142
packages/bracket-matcher/spec/close-tag-spec.js
Normal file
@ -0,0 +1,142 @@
|
||||
const TagFinder = require('../lib/tag-finder')
|
||||
const tagFinder = new TagFinder()
|
||||
|
||||
describe('closeTag', () => {
|
||||
describe('TagFinder::parseFragment', () => {
|
||||
let fragment = ''
|
||||
|
||||
beforeEach(() => fragment = '<html><head><body></body>')
|
||||
|
||||
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 = '<html><head></head><body><h1><p></p>'
|
||||
const tags = tagFinder.tagsNotClosedInFragment(fragment)
|
||||
expect(tags).toEqual(['html', 'body', 'h1'])
|
||||
})
|
||||
|
||||
it('is not confused by tag attributes', () => {
|
||||
const fragment = '<html><head></head><body class="c"><h1 class="p"><p></p>'
|
||||
const tags = tagFinder.tagsNotClosedInFragment(fragment)
|
||||
expect(tags).toEqual(['html', 'body', 'h1'])
|
||||
})
|
||||
|
||||
it('is not confused by namespace prefixes', () => {
|
||||
const fragment = '<xhtml:html><xhtml:body><xhtml:h1>'
|
||||
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 = '</other1></other2></html>'
|
||||
expect(tagFinder.tagDoesNotCloseInFragment('body', fragment)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if the given tag is closed in the given fragment', () => {
|
||||
const fragment = '</other1></body></html>'
|
||||
expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true even if the given tag is re-opened and re-closed', () => {
|
||||
const fragment = '<other> </other><body></body><html>'
|
||||
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 = '<other> </other><body></body></body><html>'
|
||||
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 = '<html><head></head><body><h1></h1><p>'
|
||||
const postFragment = '</body></html>'
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('p')
|
||||
})
|
||||
|
||||
it('correctly handles empty postFragment', () => {
|
||||
const preFragment = '<html><head></head><body><h1></h1><p>'
|
||||
const postFragment = ''
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('p')
|
||||
})
|
||||
|
||||
it('correctly handles malformed tags', () => {
|
||||
const preFragment = '<html><head></head></htm'
|
||||
const postFragment = ''
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('html')
|
||||
})
|
||||
|
||||
it('returns null if there is no open tag to be closed', () => {
|
||||
const preFragment = '<html><head></head><body><h1></h1><p>'
|
||||
const postFragment = '</p></body></html>'
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe(null)
|
||||
})
|
||||
|
||||
it('correctly closes tags containing hyphens', () => {
|
||||
const preFragment = '<html><head></head><body><h1></h1><my-element>'
|
||||
const postFragment = '</body></html>'
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('my-element')
|
||||
})
|
||||
|
||||
it('correctly closes tags containing attributes', () => {
|
||||
const preFragment = '<html><head></head><body class="foo bar"><div>'
|
||||
const postFragment = '</body></html>'
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('div')
|
||||
})
|
||||
|
||||
it('correctly closes tags containing an XML namespace', () => {
|
||||
const preFragment = '<html><head></head><body><custom:tag>'
|
||||
const postFragment = '</body></html>'
|
||||
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 = '<html><head></head><body><custom:custom2:tag>'
|
||||
const postFragment = '</body></html>'
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('custom:custom2:tag')
|
||||
})
|
||||
|
||||
it('correctly closes tags in the present of JSX tags containing member accesses', () => {
|
||||
const preFragment = '<Foo><Bar.Baz></Bar.Baz>'
|
||||
const postFragment = ''
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo')
|
||||
})
|
||||
|
||||
it('correctly closes JSX tags containing member accesses', () => {
|
||||
const preFragment = '<Foo.Bar><div></div>'
|
||||
const postFragment = ''
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo.Bar')
|
||||
})
|
||||
|
||||
it('correctly closes JSX tags containing deep member accesses', () => {
|
||||
const preFragment = '<Foo.Bar.Baz><div></div>'
|
||||
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 = '<thead><th>'
|
||||
const postFragment = '</thead>'
|
||||
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('th')
|
||||
})
|
||||
})
|
||||
})
|
6
packages/bracket-matcher/spec/fixtures/multiplecursor.md
vendored
Normal file
6
packages/bracket-matcher/spec/fixtures/multiplecursor.md
vendored
Normal file
@ -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
|
15
packages/bracket-matcher/spec/fixtures/sample.html
vendored
Normal file
15
packages/bracket-matcher/spec/fixtures/sample.html
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<p><a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
13
packages/bracket-matcher/spec/fixtures/sample.js
vendored
Normal file
13
packages/bracket-matcher/spec/fixtures/sample.js
vendored
Normal file
@ -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));
|
||||
};
|
17
packages/bracket-matcher/spec/fixtures/sample.xml
vendored
Normal file
17
packages/bracket-matcher/spec/fixtures/sample.xml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<html>
|
||||
<body id="bod">
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
hello
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
world
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -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;
|
||||
}
|
@ -2659,9 +2659,8 @@ 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"
|
||||
|
Loading…
Reference in New Issue
Block a user