diff --git a/lib/misc/blocks.js b/lib/misc/blocks.js index 46417203..86bd7f67 100644 --- a/lib/misc/blocks.js +++ b/lib/misc/blocks.js @@ -1,16 +1,39 @@ 'use babel' // TODO: docstrings +// TODO: Fix RangeCompatible types import { forLines } from './scopes' +/** + * interface LineInfo { + * scope: readonly string[] + * line: string + * } + */ -export function getLine (ed, l) { + +/** + * + * @param {TextEditor} editor + * @param {number} l + * @returns {LineInfo} + */ +export function getLine(editor, l) { return { - scope: ed.scopeDescriptorForBufferPosition([l, 0]).scopes, - line: ed.getTextInBufferRange([[l, 0], [l, Infinity]]) + scope: editor.scopeDescriptorForBufferPosition([l, 0]).getScopesArray(), + line: editor.getTextInBufferRange([ + [l, 0], + [l, Infinity], + ]), } } -function isBlank ({line, scope}, allowDocstrings = false) { +/** + * + * @param {LineInfo.line} line + * @param {LineInfo.scope} scope + * @param {boolean} allowDocstrings + */ +function isBlank({ line, scope }, allowDocstrings = false) { for (const s of scope) { if (/\bcomment\b/.test(s) || (!allowDocstrings && /\bdocstring\b/.test(s))) { return true @@ -18,43 +41,70 @@ function isBlank ({line, scope}, allowDocstrings = false) { } return /^\s*(#.*)?$/.test(line) } -function isEnd ({ line, scope }) { - if (isStringEnd({ line, scope })) { + +/** + * + * @param {LineInfo} lineInfo + */ +function isEnd(lineInfo) { + if (isStringEnd(lineInfo)) { return true } - return /^(end\b|\)|\]|\})/.test(line) + return /^(end\b|\)|]|})/.test(lineInfo.line) } -function isStringEnd ({ line, scope }) { - scope = scope.join(' ') - return /\bstring\.multiline\.end\b/.test(scope) || - (/\bstring\.end\b/.test(scope) && /\bbacktick\b/.test(scope)) + +/** + * + * @param {LineInfo} lineInfo + */ +function isStringEnd(lineInfo) { + const scope = lineInfo.scope.join(' ') + return /\bstring\.multiline\.end\b/.test(scope) || (/\bstring\.end\b/.test(scope) && /\bbacktick\b/.test(scope)) } -function isCont ({ line, scope }) { - scope = scope.join(' ') - if (/\bstring\b/.test(scope) && !(/\bpunctuation\.definition\.string\b/.test(scope))) { + +/** + * + * @param {LineInfo} lineInfo + */ +function isCont(lineInfo) { + const scope = lineInfo.scope.join(' ') + if (/\bstring\b/.test(scope) && !/\bpunctuation\.definition\.string\b/.test(scope)) { return true } - - return line.match(/^(else|elseif|catch|finally)\b/) + return lineInfo.line.match(/^(else|elseif|catch|finally)\b/) } -function isStart (lineInfo) { + +/** + * + * @param {LineInfo} lineInfo + */ +function isStart(lineInfo) { return !(/^\s/.test(lineInfo.line) || isBlank(lineInfo) || isEnd(lineInfo) || isCont(lineInfo)) } -function walkBack(ed, row) { - while ((row > 0) && !isStart(getLine(ed, row))) { +/** + * + * @param {TextEditor} editor + * @param {number} row + */ +function walkBack(editor, row) { + while (row > 0 && !isStart(getLine(editor, row))) { row-- } return row } -function walkForward (ed, start) { +/** + * + * @param {TextEditor} editor + * @param {number} start + */ +function walkForward(editor, start) { let end = start let mark = start - while (mark < ed.getLastBufferRow()) { + while (mark < editor.getLastBufferRow()) { mark++ - const lineInfo = getLine(ed, mark) - + const lineInfo = getLine(editor, mark) if (isStart(lineInfo)) { break } @@ -62,10 +112,7 @@ function walkForward (ed, start) { // An `end` only counts when there still are unclosed blocks (indicated by `forLines` // returning a non-empty array). // If the line closes a multiline string we also take that as ending the block. - if ( - !(forLines(ed, start, mark-1).length === 0) || - isStringEnd(lineInfo) - ) { + if (!(forLines(editor, start, mark - 1).length === 0) || isStringEnd(lineInfo)) { end = mark } } else if (!(isBlank(lineInfo) || isStart(lineInfo))) { @@ -75,74 +122,119 @@ function walkForward (ed, start) { return end } -function getRange (ed, row) { - const start = walkBack(ed, row) - const end = walkForward(ed, start) +/** + * + * @param {TextEditor} editor + * @param {number} row + * @returns {[[number, number], [number, number]] | undefined} + */ +function getRange(editor, row) { + const start = walkBack(editor, row) + const end = walkForward(editor, start) if (start <= row && row <= end) { - return [[start, 0], [end, Infinity]] + return [ + [start, 0], + [end, Infinity], + ] + } else { + return undefined // TODO: make sure returned range from getRanges is not undefined } } -function getSelection (ed, sel) { - const {start, end} = sel.getBufferRange() - const range = [[start.row, start.column], [end.row, end.column]] - while (isBlank(getLine(ed, range[0][0]), true) && (range[0][0] <= range[1][0])) { +/** + * + * @param {TextEditor} editor + * @param {Selection} selection + */ +function getSelection(editor, selection) { + const { start, end } = selection.getBufferRange() + const range = [ + [start.row, start.column], + [end.row, end.column], + ] + while (isBlank(getLine(editor, range[0][0]), true) && range[0][0] <= range[1][0]) { range[0][0]++ range[0][1] = 0 } - while (isBlank(getLine(ed, range[1][0]), true) && (range[1][0] >= range[0][0])) { + while (isBlank(getLine(editor, range[1][0]), true) && range[1][0] >= range[0][0]) { range[1][0]-- range[1][1] = Infinity } return range } -export function moveNext (ed, sel, range) { +/** + * + * @param {TextEditor} editor + * @param {Selection} selection + * @param {[[number, number], [number, number]]} range + */ +export function moveNext(editor, selection, range) { // Ensure enough room at the end of the buffer const row = range[1][0] let last - while ((last = ed.getLastBufferRow()) < (row+2)) { - if ((last !== row) && !isBlank(getLine(ed, last))) { + while ((last = editor.getLastBufferRow()) < row + 2) { + if (last !== row && !isBlank(getLine(editor, last))) { break } - sel.setBufferRange([[last, Infinity], [last, Infinity]]) - sel.insertText('\n') + selection.setBufferRange([ + [last, Infinity], + [last, Infinity], + ]) + selection.insertText('\n') } // Move the cursor let to = row + 1 - while ((to < ed.getLastBufferRow()) && isBlank(getLine(ed, to))) { + while (to < editor.getLastBufferRow() && isBlank(getLine(editor, to))) { to++ } - to = walkForward(ed, to) - return sel.setBufferRange([[to, Infinity], [to, Infinity]]) + to = walkForward(editor, to) + return selection.setBufferRange([ + [to, Infinity], + [to, Infinity], + ]) } -function getRanges (ed) { - const ranges = ed.getSelections().map(sel => { +/** + * + * @param {TextEditor} editor + */ +function getRanges(editor) { + const ranges = editor.getSelections().map((selection) => { return { - selection: sel, - range: sel.isEmpty() ? - getRange(ed, sel.getHeadBufferPosition().row) : - getSelection(ed, sel) + selection: selection, + range: selection.isEmpty() + ? getRange(editor, selection.getHeadBufferPosition().row) + : getSelection(editor, selection), } + // TODO: replace with getBufferRowRange? (getHeadBufferPosition isn't a public API) }) return ranges.filter(({ range }) => { - return range && ed.getTextInBufferRange(range).trim() + return range && editor.getTextInBufferRange(range).trim() }) } -export function get (ed) { - return getRanges(ed).map(({ range, selection }) => { +/** + * + * @param {TextEditor} editor + */ +export function get(editor) { + return getRanges(editor).map(({ range, selection }) => { return { range, selection, line: range[0][0], - text: ed.getTextInBufferRange(range) + text: editor.getTextInBufferRange(range), } }) } -export function getLocalContext (editor, row) { +/** + * + * @param {TextEditor} editor + * @param {number} row + */ +export function getLocalContext(editor, row) { const range = getRange(editor, row) const context = range ? editor.getTextInBufferRange(range) : '' // NOTE: @@ -152,16 +244,21 @@ export function getLocalContext (editor, row) { const startRow = range ? range[0][0] : 0 return { context, - startRow + startRow, } } -export function select (ed = atom.workspace.getActiveTextEditor()) { - if (!ed) return - return ed.mutateSelectedText(selection => { - const range = getRange(ed, selection.getHeadBufferPosition().row) +/** + * + * @param {TextEditor | undefined} editor + */ +export function select(editor = atom.workspace.getActiveTextEditor()) { + if (!editor) return + return editor.mutateSelectedText((selection) => { + const range = getRange(editor, selection.getHeadBufferPosition().row) if (range) { selection.setBufferRange(range) } }) + // TODO: replace with getBufferRowRange? (getHeadBufferPosition isn't a public API) }