diff --git a/packages/language-css/grammars/modern-tree-sitter-css.cson b/packages/language-css/grammars/modern-tree-sitter-css.cson index 101696e18e..7cf94ea10c 100644 --- a/packages/language-css/grammars/modern-tree-sitter-css.cson +++ b/packages/language-css/grammars/modern-tree-sitter-css.cson @@ -8,7 +8,7 @@ fileTypes: [ ] treeSitter: - parserSource: 'github:tree-sitter/tree-sitter-css#9af0bdd9d225edee12f489cfa8284e248321959b' + parserSource: 'github:tree-sitter/tree-sitter-css#5b24cbe3301a81b00c85d6c38db3bf2561e9ca41' grammar: 'tree-sitter/tree-sitter-css.wasm' highlightsQuery: 'tree-sitter/queries/highlights.scm' foldsQuery: 'tree-sitter/queries/folds.scm' diff --git a/packages/language-css/grammars/tree-sitter/queries/highlights.scm b/packages/language-css/grammars/tree-sitter/queries/highlights.scm index 4caea08958..6380491c5e 100644 --- a/packages/language-css/grammars/tree-sitter/queries/highlights.scm +++ b/packages/language-css/grammars/tree-sitter/queries/highlights.scm @@ -1,23 +1,46 @@ -; NOTE: `tree-sitter-css` recovers poorly from invalidity inside a block when -; you're adding a new property-value pair above others in a list. When the user -; is typing and the file is temporarily invalid, it will make incorrect guesses -; about tokens that occur between the cursor and the end of the block. +; WORKAROUNDS +; =========== + +; Mark `ERROR` nodes that occur inside blocks. We are much more cautious about +; inferences inside blocks because we're often wrong. +( + (ERROR) @_IGNORE_ + (#is? test.childOfType "block") + (#set! isErrorInsideBlock true) +) + +; (stylesheet (ERROR)) can't be queried directly, but it's important that we be +; able to detect it. +( + (ERROR) @_IGNORE_ + (#is? test.childOfType "stylesheet") + (#set! isErrorAtTopLevel true) +) + +; This selector captures an empty file with (e.g.) the word `div` typed. +(ERROR + (identifier) @entity.name.tag.css + (#set! capture.final) + (#is? test.descendantOfNodeWithData isErrorAtTopLevel) +) + +; When there's a parsing error inside a `block` node, too many things get +; incorrectly interpreted as `tag_name`s. Ignore all `tag_name` nodes until the +; error is resolved. ; -; The fix here is for `tree-sitter-css` to get better at recovering from its -; parsing error, but parser authors don't currently have much control over -; that. In the meantime, this query is a decent mitigation: it colors the -; affected tokens like plain text instead of assuming (nearly always -; incorrectly) them to be tag names. +; This should be fixed upstream because it has undesirable effects on nested +; selectors, but in the meantime this workaround is better than doing nothing. ; -; Ideally, this is temporary, and we can remove it soon. Until then, it makes -; syntax highlighting less obnoxious. +; Keep an eye on https://github.com/tree-sitter/tree-sitter-css/issues/65 to +; know when this workaround might no longer be necessary. ((tag_name) @_IGNORE_ - (#is? test.descendantOfType "ERROR") + (#is? test.descendantOfNodeWithData isErrorInsideBlock) (#set! capture.final)) + (ERROR (attribute_name) @_IGNORE_ (#set! capture.final)) @@ -26,8 +49,6 @@ (attribute_name) @invalid.illegal) (#set! capture.final)) -; WORKAROUND: -; ; In `::after`, the "after" has a node type of `tag_name`. Unclear whether this ; is a bug or intended behavior. We want to catch it here so that it doesn't ; get scoped like an HTML tag name in a selector. @@ -38,7 +59,7 @@ (#set! adjust.startAt lastChild.previousSibling.startPosition) (#set! adjust.endAt lastChild.endPosition)) -; Claim this range and block it from being scoped as a tag name. +; Claim the `tag_name` range and block it from being scoped as a tag name. (pseudo_element_selector (tag_name) @_IGNORE_ (#is? test.last true) diff --git a/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm b/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm index 6399b8a0b5..a958382d2e 100755 Binary files a/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm and b/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm differ diff --git a/packages/language-css/spec/.eslintrc.js b/packages/language-css/spec/.eslintrc.js new file mode 100644 index 0000000000..8cc26374a1 --- /dev/null +++ b/packages/language-css/spec/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + env: { jasmine: true }, + globals: { + waitsForPromise: true, + runGrammarTests: true, + runFoldsTests: true + }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/packages/language-css/spec/fixtures/ends-in-tag-name.css b/packages/language-css/spec/fixtures/ends-in-tag-name.css new file mode 100644 index 0000000000..e7653ac9d2 --- /dev/null +++ b/packages/language-css/spec/fixtures/ends-in-tag-name.css @@ -0,0 +1,5 @@ + +/* Ensure that a file that ends in a bare tag name scopes the tag name properly. */ + +div +/* <- entity.name.tag.css */ diff --git a/packages/language-css/spec/fixtures/sample.css b/packages/language-css/spec/fixtures/sample.css new file mode 100644 index 0000000000..40cfde3f35 --- /dev/null +++ b/packages/language-css/spec/fixtures/sample.css @@ -0,0 +1,10 @@ +div { +/* <- entity.name.tag.css */ +/* ^ punctuation.section.property-list.begin.bracket.curly.css */ +} + +/* Next test verifies that selectors are properly inferred at the end of a file before the `{` is typed. */ + +div.foo +/* <- entity.name.tag.css */ +/* ^ entity.other.attribute-name.class.css */ diff --git a/packages/language-css/spec/tree-sitter-grammar-spec.js b/packages/language-css/spec/tree-sitter-grammar-spec.js new file mode 100644 index 0000000000..971e6f8cda --- /dev/null +++ b/packages/language-css/spec/tree-sitter-grammar-spec.js @@ -0,0 +1,21 @@ +const path = require('path'); + +describe('WASM Tree-sitter CSS grammar', () => { + + beforeEach(async () => { + await atom.packages.activatePackage('language-css'); + }); + + it('passes grammar tests', async () => { + await runGrammarTests( + path.join(__dirname, 'fixtures', 'sample.css'), + /\/\*/, + /\*\// + ); + await runGrammarTests( + path.join(__dirname, 'fixtures', 'ends-in-tag-name.css'), + /\/\*/, + /\*\// + ); + }); +}); diff --git a/packages/language-python/grammars/ts/indents.scm b/packages/language-python/grammars/ts/indents.scm index 9753893079..2a46fe1a38 100644 --- a/packages/language-python/grammars/ts/indents.scm +++ b/packages/language-python/grammars/ts/indents.scm @@ -1,4 +1,8 @@ -; Excluding dictionary key/value separators… + +; IGNORE NON-BLOCK-STARTING COLONS +; ================================ + +; First, exclude dictionary key/value separators… (dictionary (pair ":" @_IGNORE_ (#set! capture.final))) @@ -7,14 +11,164 @@ ((lambda ":" @_IGNORE_) (#set! capture.final)) -; …and type annotations on function parameters/class members… +; …list subscript syntax… +(slice ":" @_IGNORE_ + (#set! capture.final)) + +; …and type annotations on function parameters/class members. (":" @_IGNORE_ . (type) (#set! capture.final)) -; …all other colons we encounter hint at upcoming indents. +; IGNORE BLOCK-STARTING COLONS BEFORE ONE-LINERS +; ============================================== + +; Now that we've done that, all block-starting colons that have their +; consequence block start and end on the same line should be filtered out. +; +; We also test for `lastTextOnRow` to ensure we're not followed by an _empty_ +; consequence block, which is surprisingly common. Probably a bug, but it's got +; to be worked around in the meantime. +; +; We check for adjacency between the `:` and the `block` because otherwise we +; might incorrectly match cases like +; +; if 2 > 1: # some comment +; +; since those comments can also be followed by an empty `block` node on the same +; line. +; +(if_statement + ":" @_IGNORE_ + . + consequence: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(elif_clause + ":" @_IGNORE_ + . + consequence: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(else_clause + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(match_statement + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(case_clause + ":" @_IGNORE_ + . + consequence: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(while_statement + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(for_statement + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(try_statement + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(except_clause + ":" @_IGNORE_ + . + (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +; Special case for try/except statements, since they don't seem to be valid +; until they're fully intact. If we don't do this, `except` doesn't dedent. +; +; This is like the `elif`/`else` problem below, but it's trickier because an +; identifier could plausibly begin with the string `except` and we don't want +; to make an across-the-board assumption. +(ERROR + "try" + ":" @indent + (block + (expression_statement + (identifier) @dedent + (#match? @dedent "except") + ) + ) +) + +(function_definition + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + +(class_definition + ":" @_IGNORE_ + . + body: (block) + (#is-not? test.lastTextOnRow) + (#is? test.startsOnSameRowAs "nextSibling.endPosition") + (#set! capture.final) +) + + +; REMAINING COLONS +; ================ + +; Now that we've done this work, all other colons we encounter hint at upcoming +; indents. +; +; TODO: Based on the stuff we're doing above, it's arguable that the +; exclude-all-counterexamples approach is no longer useful and we should +; instead be opting into indentation. Revisit this! ":" @indent +; MISCELLANEOUS +; ============= + ; When typing out "else" after an "if" statement, tree-sitter-python won't -; acknowlege it as an `else` statement until it's indented properly, which is +; acknowledge it as an `else` statement until it's indented properly, which is ; quite the dilemma for us. Before that happens, it's an identifier named ; "else". This has a chance of spuriously dedenting if you're typing out a ; variable called `elsewhere` or something, but I'm OK with that. @@ -22,7 +176,21 @@ ; This also means that we _should not_ mark an actual `else` keyword with ; `@dedent`, because if it's recognized as such, that's a sign that it's ; already indented correctly and we shouldn't touch it. +; +; All this also applies to `elif`. ((identifier) @dedent (#match? @dedent "^(elif|else)$")) +; Likewise, typing `case` at the beginning of a line within a match block — in +; cases where it's interpreted as an identifier — strongly suggests that we +; should dedent one level so it's properly recognized as a new `case` keyword. +( + (identifier) @dedent + (#equals? @dedent "case") + (#is? test.descendantOfType "case_clause") +) + + +; All instances of brackets/braces should be indented if they span multiple +; lines. ["(" "[" "{"] @indent [")" "]" "}"] @dedent diff --git a/packages/language-python/spec/.eslintrc.js b/packages/language-python/spec/.eslintrc.js new file mode 100644 index 0000000000..8cc26374a1 --- /dev/null +++ b/packages/language-python/spec/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + env: { jasmine: true }, + globals: { + waitsForPromise: true, + runGrammarTests: true, + runFoldsTests: true + }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/packages/language-python/spec/indentation-spec.js b/packages/language-python/spec/indentation-spec.js new file mode 100644 index 0000000000..fa4d058704 --- /dev/null +++ b/packages/language-python/spec/indentation-spec.js @@ -0,0 +1,222 @@ +const dedent = require('dedent'); + +describe('Python indentation (modern Tree-sitter)', () => { + let editor; + let languageMode; + let grammar; + + async function insertNewline() { + editor.getLastSelection().insertText('\n', { autoIndent: true, autoIndentNewline: true }) + await languageMode.atTransactionEnd(); + } + + async function expectToAutoIndentAfter(text, assert = true) { + editor.setText(text); + await languageMode.atTransactionEnd(); + editor.setCursorBufferPosition([Infinity, Infinity]); + let currentRow = editor.getLastCursor().getBufferPosition().row; + insertNewline(); + if (assert) { + expect(editor.lineTextForBufferRow(currentRow + 1)).toEqual(' '); + } else { + expect(editor.lineTextForBufferRow(currentRow + 1)).toEqual(''); + } + } + + beforeEach(async () => { + atom.config.set('core.useTreeSitterParsers', true); + + editor = await atom.workspace.open(); + await atom.packages.activatePackage('language-python'); + grammar = atom.grammars.grammarForScopeName('source.python'); + editor.setGrammar(grammar); + languageMode = editor.languageMode; + await languageMode.ready; + }); + + it('indents blocks properly', async () => { + await expectToAutoIndentAfter(`if 1 > 2:`) + await expectToAutoIndentAfter(`if 1 > 2: # test`) + await expectToAutoIndentAfter(`if 1 > 2: pass`, false) + + await expectToAutoIndentAfter(`def f(x):`) + await expectToAutoIndentAfter(`def f(x): # test`) + await expectToAutoIndentAfter(`def f(x): pass`, false) + + await expectToAutoIndentAfter(`class Fx(object):`) + await expectToAutoIndentAfter(`class Fx(object): # test`) + await expectToAutoIndentAfter(`class Fx(object): pass`, false) + + await expectToAutoIndentAfter(`while True:`) + await expectToAutoIndentAfter(`while True: # test`) + await expectToAutoIndentAfter(`while True: pass`, false) + + await expectToAutoIndentAfter(`for _ in iter(x):`) + await expectToAutoIndentAfter(`for _ in iter(x): # test`) + await expectToAutoIndentAfter(`for _ in iter(x): pass`, false) + + await expectToAutoIndentAfter(dedent` + if 1 > 2: + pass + elif 2 > 3: + `) + + await expectToAutoIndentAfter(dedent` + if 1 > 2: + pass + elif 2 > 3: # test + `) + + await expectToAutoIndentAfter(dedent` + if 1 > 2: + pass + elif 2 > 3: pass + `, false) + + await expectToAutoIndentAfter(dedent` + if 1 > 2: + pass + elif 2 > 3: + pass + else: + `) + + await expectToAutoIndentAfter(dedent` + if 1 > 2: + pass + elif 2 > 3: + pass + else: # test + `) + + await expectToAutoIndentAfter(dedent` + if 1 > 2: + pass + elif 2 > 3: + pass + else: pass + `, false) + + await expectToAutoIndentAfter(`try:`) + + // The assertions below don't work because `tree-sitter-python` doesn't + // parse them correctly unless they occur within an already intact + // `try/except` block. This needs to be fixed upstream. + // await expectToAutoIndentAfter(`try: # test`) + // await expectToAutoIndentAfter(`try: pass`, false) + + await expectToAutoIndentAfter(dedent` + try: + do_something() + except: + `) + await expectToAutoIndentAfter(dedent` + try: + do_something() + except: # test + `) + await expectToAutoIndentAfter(dedent` + try: + do_something() + except: pass + `, false) + }); + + it('indents blocks properly (complex cases)', async () => { + editor.setText(dedent` + try: pass + except: pass + `); + await languageMode.atTransactionEnd(); + editor.setCursorBufferPosition([0, Infinity]); + await insertNewline(); + expect(editor.lineTextForBufferRow(1)).toBe('') + + editor.setText(dedent` + try: #foo + except: pass + `) + await languageMode.atTransactionEnd(); + editor.setCursorBufferPosition([0, Infinity]); + await insertNewline(); + expect(editor.lineTextForBufferRow(1)).toBe(' ') + }) + + it(`does not indent for other usages of colons`, async () => { + await expectToAutoIndentAfter(`x = lambda a : a + 10`, false) + await expectToAutoIndentAfter(`x = list[:2]`, false) + await expectToAutoIndentAfter(`x = { foo: 2 }`, false) + }); + + it('indents braces properly', async () => { + let pairs = [ + ['[', ']'], + ['{', '}'], + ['(', ')'] + ]; + for (let [a, b] of pairs) { + editor.setText(`x = ${a} + + ${b}`) + await languageMode.atTransactionEnd(); + editor.setCursorBufferPosition([0, Infinity]); + await insertNewline(); + expect(editor.lineTextForBufferRow(1)).toBe(' ') + } + + editor.setText(`x = < + + >`) + await languageMode.atTransactionEnd(); + editor.setCursorBufferPosition([0, Infinity]); + await insertNewline(); + expect(editor.lineTextForBufferRow(1)).toBe('') + }); + + it('dedents properly', async () => { + editor.setText(dedent` + if 1 > 2: + pass + eli + `); + editor.setCursorBufferPosition([Infinity, Infinity]); + await languageMode.atTransactionEnd(); + editor.getLastSelection().insertText('f', { + autoIndent: true, + autoDecreaseIndent: true + }); + await languageMode.atTransactionEnd(); + expect(editor.lineTextForBufferRow(2)).toBe('elif'); + + editor.setText(dedent` + if 1 > 2: + pass + els + `); + editor.setCursorBufferPosition([Infinity, Infinity]); + await languageMode.atTransactionEnd(); + editor.getLastSelection().insertText('e', { + autoIndent: true, + autoDecreaseIndent: true + }); + await languageMode.atTransactionEnd(); + expect(editor.lineTextForBufferRow(2)).toBe('else'); + + + editor.setText(dedent` + match x: + case "a": + pass + cas + `); + editor.setCursorBufferPosition([Infinity, Infinity]); + await languageMode.atTransactionEnd(); + editor.getLastSelection().insertText('e', { + autoIndent: true, + autoDecreaseIndent: true + }); + await languageMode.atTransactionEnd(); + expect(editor.lineTextForBufferRow(3)).toBe(' case'); + }); + +}); diff --git a/src/scope-resolver.js b/src/scope-resolver.js index 8f6eb8c379..951bc46793 100644 --- a/src/scope-resolver.js +++ b/src/scope-resolver.js @@ -745,8 +745,20 @@ ScopeResolver.TESTS = { return false; }, - // Passes if there's an ancestor, but fails if the ancestor type matches - // the second,third,etc argument + // Passes if this node's parent is of the given type(s). + // + // Only rarely needed, but may be useful when dealing with ERROR nodes. + childOfType(node, type) { + if (!node.parent) return false; + let multiple = type.includes(' '); + let target = multiple ? type.split(/\s+/) : type; + return multiple ? target.includes(node.parent.type) : node.parent.type === type; + }, + + // Takes at least two node types (separated by spaces) and starts traversing + // up the node's parent chain. Passes if the first node type is encountered + // before any of the rest; fails if any of the rest are reached before the + // first. ancestorTypeNearerThan(node, types) { let [target, ...rejected] = types.split(/\s+/); rejected = new Set(rejected) @@ -766,9 +778,21 @@ ScopeResolver.TESTS = { return descendants.length > 0; }, + // Passes if this node has at least one child of the given type(s). + // + // Only rarely needed, but may be useful when dealing with ERROR nodes. + parentOfType(node, type) { + if (node.childCount === 0) return false; + let multiple = type.includes(' '); + let target = multiple ? type.split(/\s+/) : type; + return node.children.some(c => { + return multiple ? target.includes(c.type) : c.type === target; + }); + }, + // Passes if this range (after adjustments) has previously had data stored at // the given key. - rangeWithData(node, rawValue, existingData) { + rangeWithData(_node, rawValue, existingData) { if (existingData === undefined) { return false; } let [key, value] = interpretPossibleKeyValuePair(rawValue, false); @@ -782,7 +806,7 @@ ScopeResolver.TESTS = { // Passes if one of this node's ancestors has stored data at the given key // for its inherent range (ignoring adjustments). - descendantOfNodeWithData(node, rawValue, existingData, instance) { + descendantOfNodeWithData(node, rawValue, _existingData, instance) { let current = node; let [key, value] = interpretPossibleKeyValuePair(rawValue, false); @@ -818,7 +842,7 @@ ScopeResolver.TESTS = { // Passes only when a given config option is present and truthy. Accepts // either (a) a configuration key or (b) a configuration key and value // separated by a space. - config(node, rawValue, existingData, instance) { + config(_node, rawValue, _existingData, instance) { let [key, value] = interpretPossibleKeyValuePair(rawValue, true); // Invalid predicates should be ignored. diff --git a/vendor/jasmine.js b/vendor/jasmine.js index d69b33d2d8..af1c499adb 100644 --- a/vendor/jasmine.js +++ b/vendor/jasmine.js @@ -46,7 +46,7 @@ jasmine.DEFAULT_UPDATE_INTERVAL = 250; jasmine.MAX_PRETTY_PRINT_DEPTH = 40; /** - * Default timeout interval in milliseconds for waitsFor() blocks. + * Default timeout interval in milliseconds for waitsfor () blocks. */ jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; @@ -546,7 +546,7 @@ if (isCommonJS) exports.runs = runs; /** * Waits a fixed time period before moving to the next block. * - * @deprecated Use waitsFor() instead + * @deprecated Use waitsfor () instead * @param {Number} timeout milliseconds to wait */ var waits = function(timeout) { @@ -2286,7 +2286,7 @@ jasmine.Spec.prototype.expect = function(actual) { /** * Waits a fixed time period before moving to the next block. * - * @deprecated Use waitsFor() instead + * @deprecated Use waitsfor () instead * @param {Number} timeout milliseconds to wait */ jasmine.Spec.prototype.waits = function(timeout) { @@ -2716,17 +2716,22 @@ jasmine.Matchers.prototype.toSatisfy = function(fn) { // position of the editor `expect` should be satisfied, and `testPosition`, that // is where in file the test actually happened. This makes it easier for us // to construct an error showing where EXACTLY was the assertion that failed -function normalizeTreeSitterTextData(editor, commentRegex) { +function normalizeTreeSitterTextData(editor, commentRegex, trailingCommentRegex) { let allMatches = [], lastNonComment = 0 const checkAssert = new RegExp('^\\s*' + commentRegex.source + '\\s*[\\<\\-|\\^]') editor.getBuffer().getLines().forEach((row, i) => { const m = row.match(commentRegex) - if(m) { + if (m) { + if (trailingCommentRegex) { + row = row.replace(trailingCommentRegex, '') + } + // Strip extra space at the end of the line (but not the beginning!) + row = row.replace(/\s+$/, '') // const scope = editor.scopeDescriptorForBufferPosition([i, m.index]) // FIXME: use editor.scopeDescriptorForBufferPosition when it works const scope = editor.tokensForScreenRow(i) const scopes = scope.flatMap(e => e.scopes) - if(scopes.find(s => s.match(/comment/)) && row.match(checkAssert)) { + if (scopes.find(s => s.match(/comment/)) && row.match(checkAssert)) { allMatches.push({row: lastNonComment, text: row, col: m.index, testRow: i}) return } @@ -2735,7 +2740,7 @@ function normalizeTreeSitterTextData(editor, commentRegex) { }) return allMatches.map(({text, row, col, testRow}) => { const exactPos = text.match(/\^\s+(.*)/) - if(exactPos) { + if (exactPos) { const expected = exactPos[1] return { expected, @@ -2744,7 +2749,7 @@ function normalizeTreeSitterTextData(editor, commentRegex) { } } else { const pos = text.match(/\<-\s+(.*)/) - if(!pos) throw new Error(`Can't match ${text}`) + if (!pos) throw new Error(`Can't match ${text}`) return { expected: pos[1], editorPosition: {row, column: col}, @@ -2761,10 +2766,10 @@ async function openDocument(fullPath) { return editor; } -async function runGrammarTests(fullPath, commentRegex) { +async function runGrammarTests(fullPath, commentRegex, trailingCommentRegex = null) { const editor = await openDocument(fullPath); - const normalized = normalizeTreeSitterTextData(editor, commentRegex) + const normalized = normalizeTreeSitterTextData(editor, commentRegex, trailingCommentRegex) expect(normalized.length).toSatisfy((n, reason) => { reason("Tokenizer didn't run correctly - could not find any comment") return n > 0 @@ -2773,7 +2778,7 @@ async function runGrammarTests(fullPath, commentRegex) { expect(editor.scopeDescriptorForBufferPosition(editorPosition).scopes).toSatisfy((scopes, reason) => { const dontFindScope = expected.startsWith("!"); expected = expected.replace(/^!/, "") - if(dontFindScope) { + if (dontFindScope) { reason(`Expected to NOT find scope "${expected}" but found it\n` + ` at ${fullPath}:${testPosition.row+1}:${testPosition.column+1}` ); @@ -2785,7 +2790,7 @@ async function runGrammarTests(fullPath, commentRegex) { const normalized = expected.replace(/([\.\-])/g, '\\$1'); const scopeRegex = new RegExp('^' + normalized + '(\\..+)?$'); let result = scopes.find(e => e.match(scopeRegex)) !== undefined; - if(dontFindScope) result = !result; + if (dontFindScope) result = !result; return result }) }) @@ -2797,25 +2802,25 @@ async function runFoldsTests(fullPath, commentRegex) { let grouped = {} const normalized = normalizeTreeSitterTextData(editor, commentRegex).forEach(test => { const [kind, id] = test.expected.split('.') - if(!kind || !id) { + if (!kind || !id) { throw new Error(`Folds must be in the format fold_end.some-id\n` + ` at ${test.testPosition.row+1}:${test.testPosition.column+1}`) } grouped[id] ||= {} grouped[id][kind] = test }) - for(const k in grouped) { + for (const k in grouped) { const v = grouped[k] const keys = Object.keys(v) - if(keys.indexOf('fold_begin') === -1) + if (keys.indexOf('fold_begin') === -1) throw new Error(`Fold ${k} must contain fold_begin`) - if(keys.indexOf('fold_end') === -1) + if (keys.indexOf('fold_end') === -1) throw new Error(`Fold ${k} must contain fold_end`) - if(keys.indexOf('fold_new_position') === -1) + if (keys.indexOf('fold_new_position') === -1) throw new Error(`Fold ${k} must contain fold_new_position`) } - for(const k in grouped) { + for (const k in grouped) { const fold = grouped[k] const begin = fold['fold_begin'] const end = fold['fold_end']