Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion browser/components/CodeEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ export default class CodeEditor extends React.Component {
if (cm.somethingSelected()) cm.indentSelection('add')
else {
const tabs = cm.getOption('indentWithTabs')
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
if (snippetManager.moveToNextTabStop(cm)) {
return
} else if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
cm.execCommand('goLineStart')
if (tabs) {
cm.execCommand('insertTab')
Expand Down
194 changes: 161 additions & 33 deletions browser/lib/SnippetManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class SnippetManager {
}
]
this.snippets = []
this.activeTabStops = null
this.expandSnippet = this.expandSnippet.bind(this)
this.moveToNextTabStop = this.moveToNextTabStop.bind(this)
this.init = this.init.bind(this)
this.assignSnippets = this.assignSnippets.bind(this)
}
Expand Down Expand Up @@ -42,45 +44,171 @@ class SnippetManager {
this.snippets = snippets
}

getDollarTabStop(content, index) {
if (content[index] !== '$' || content[index - 1] === '\\') {
return null
}

const final = content[index + 1] === '$'
const length = final ? 2 : 1
const previousChar = content[index - 1]
const nextChar = content[index + length]

if (previousChar && previousChar.match(/[A-Za-z0-9_]/)) {
return null
}

if (nextChar && nextChar.match(/[A-Za-z0-9_{(]/)) {
return null
}

return { final, length }
}

parseSnippetContent(content) {
const legacyCursorString = ':{}'
const tabStops = []
let parsedContent = ''
let line = 0
let ch = 0

const appendChar = char => {
parsedContent += char

if (char === '\n') {
line++
ch = 0
} else {
ch++
}
}

for (let i = 0; i < content.length; i++) {
if (content.substr(i, legacyCursorString.length) === legacyCursorString) {
tabStops.push({ line, ch, final: true })
i += legacyCursorString.length - 1
continue
}

const dollarTabStop = this.getDollarTabStop(content, i)

if (dollarTabStop) {
tabStops.push({ line, ch, final: dollarTabStop.final })
i += dollarTabStop.length - 1
continue
}

appendChar(content[i])
}

return { content: parsedContent, tabStops }
}

getAbsoluteTabStop(tabStop, from) {
return {
line: from.line + tabStop.line,
ch: tabStop.line === 0 ? from.ch + tabStop.ch : tabStop.ch,
final: tabStop.final
}
}

clearActiveTabStops() {
if (!this.activeTabStops) {
return
}

this.activeTabStops.bookmarks.forEach(tabStop => {
if (tabStop.bookmark && typeof tabStop.bookmark.clear === 'function') {
tabStop.bookmark.clear()
}
})
this.activeTabStops = null
}

setTabStops(cm, tabStops) {
if (tabStops.length === 0) {
return
}

const bookmarks = tabStops.map(tabStop => {
const position = { line: tabStop.line, ch: tabStop.ch }

return {
position,
final: tabStop.final,
bookmark:
typeof cm.setBookmark === 'function' ? cm.setBookmark(position) : null
}
})

cm.setCursor(bookmarks[0].position)

if (bookmarks.length > 1 && !bookmarks[0].final) {
this.activeTabStops = {
bookmarks,
index: 0,
cycle: bookmarks.every(tabStop => !tabStop.final)
}
}
}

moveToNextTabStop(cm) {
if (!this.activeTabStops) {
return false
}

const activeTabStops = this.activeTabStops
let nextIndex = activeTabStops.index + 1

if (nextIndex >= activeTabStops.bookmarks.length) {
if (!activeTabStops.cycle) {
this.clearActiveTabStops()
return false
}
nextIndex = 0
}

const nextTabStop = activeTabStops.bookmarks[nextIndex]
const position =
nextTabStop.bookmark && typeof nextTabStop.bookmark.find === 'function'
? nextTabStop.bookmark.find()
: nextTabStop.position

if (!position) {
this.clearActiveTabStops()
return false
}

activeTabStops.index = nextIndex
cm.setCursor(position)

if (nextTabStop.final) {
this.clearActiveTabStops()
}

return true
}

expandSnippet(wordBeforeCursor, cursor, cm) {
const templateCursorString = ':{}'
for (let i = 0; i < this.snippets.length; i++) {
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {
continue
}
if (this.snippets[i].content.indexOf(templateCursorString) !== -1) {
const snippetLines = this.snippets[i].content.split('\n')
let cursorLineNumber = 0
let cursorLinePosition = 0

let cursorIndex
for (let j = 0; j < snippetLines.length; j++) {
cursorIndex = snippetLines[j].indexOf(templateCursorString)

if (cursorIndex !== -1) {
cursorLineNumber = j
cursorLinePosition = cursorIndex

break
}
}

cm.replaceRange(
this.snippets[i].content.replace(templateCursorString, ''),
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
const snippet = this.parseSnippetContent(this.snippets[i].content)
const tabStops = snippet.tabStops.map(tabStop => {
return this.getAbsoluteTabStop(
tabStop,
wordBeforeCursor.range.from || cursor
)
cm.setCursor({
line: cursor.line + cursorLineNumber,
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
})
} else {
cm.replaceRange(
this.snippets[i].content,
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
}
})

this.clearActiveTabStops()
cm.replaceRange(
snippet.content,
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
this.setTabStops(cm, tabStops)
return true
}

Expand Down