From adfdd05472dec99d3b49eb437b26aa86a619bde8 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 27 Jan 2024 00:41:41 -0800 Subject: [PATCH] =?UTF-8?q?Allow=20=E2=80=9Chalf-covering=E2=80=9D=C2=A0of?= =?UTF-8?q?=20scopes=20at=20the=20edges=20of=20injections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's a hard-to-grok setting for language injections that allows a deeper layer to monopolize the scope application for a range. In most cases, an injection is placed into a node that the parser doesn't know much about (like a `script` block in HTML); but Rust and C parsers needed a way to inject themselves _into themselves_ so that they could add syntax highlighting to macros. Because they were applying highlighting to a range that the base layer _already_ had plans to highlight, they needed a way to block the shallower layer from acting. This mode has never worked briliantly, but it's been made smarter in several ways since the invention of modern Tree-sitter. And here's another one: if the highlight iterator is at a position where an injection range is _about_ to begin, it shouldn't be able to stop any other layer from _closing_ a scope; and if the highlight iterator is at a position where an injection range has just _finished_, it shouldn't be able to stop any other layer from _opening_ a scope. Because of this, we can now fix a bug that I think might've been present for a while in the application of scopes to rust macros like `println!` — the position after the exclamation point is one of those injection-layer boundaries, to the effect that a scope name was opened that would persist until at least the end of the screen line. --- .../tree-sitter-rust/queries/highlights.scm | 4 +- src/wasm-tree-sitter-language-mode.js | 76 ++++++++++++++----- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm b/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm index 1193475a04..3a74b20738 100644 --- a/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm +++ b/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm @@ -32,8 +32,8 @@ ; ----------- ; Wrap the "foo" and "!" of `foo!()`. -((macro_invocation (identifier) @support.other.function.rust)) - ; (#set! adjust.endAt firstChild.nextSibling.endPosition)) +((macro_invocation (identifier)) @support.other.function.rust + (#set! adjust.endAt firstChild.nextSibling.endPosition)) (call_expression function: (identifier) @support.other.function.rust) diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index eb4b26a202..3b0588018b 100644 --- a/src/wasm-tree-sitter-language-mode.js +++ b/src/wasm-tree-sitter-language-mode.js @@ -2582,7 +2582,7 @@ class HighlightIterator { getCloseScopeIds() { let iterator = last(this.iterators); - // if (this.currentScopeIsCovered) { + // if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'close') { // console.log( // iterator.name, // iterator.depth, @@ -2606,8 +2606,12 @@ class HighlightIterator { // ); // } if (iterator) { - if (this.currentScopeIsCovered) { - return iterator.getOpenScopeIds().filter(id => { + // If this iterator is covered completely, or if it's covered in a + // position that prevents us from closing scopes… + if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'close') { + // …then the only closing scope we're allowed to apply is one that ends + // the base scope of an injection range. + return iterator.getCloseScopeIds().filter(id => { return iterator.languageLayer.languageScopeId === id; }); } else { @@ -2620,7 +2624,7 @@ class HighlightIterator { getOpenScopeIds() { let iterator = last(this.iterators); // let ids = iterator.getOpenScopeIds(); - // if (this.currentScopeIsCovered) { + // if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'open') { // console.log( // iterator.name, // iterator.depth, @@ -2644,7 +2648,11 @@ class HighlightIterator { // ); // } if (iterator) { - if (this.currentScopeIsCovered) { + // If this iterator is covered completely, or if it's covered in a + // position that prevents us from opening scopes… + if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'open') { + // …then the only opening scope we're allowed to apply is one that ends + // the base scope of an injection range. return iterator.getOpenScopeIds().filter(id => { return iterator.languageLayer.languageScopeId === id; }); @@ -2662,20 +2670,22 @@ class HighlightIterator { if (layerCount > 1) { const rest = [...this.iterators]; const leader = rest.pop(); - let covered = rest.some(it => { - return it.coversIteratorAtPosition( - leader, - leader.getPosition() - ); - }); + let covers = false; + for (let it of rest) { + let iteratorCovers = it.coversIteratorAtPosition(leader, leader.getPosition()); + if (iteratorCovers !== false) { + covers = iteratorCovers; + break; + } + } - if (covered) { - this.currentScopeIsCovered = true; + if (covers) { + this.currentIteratorIsCovered = covers; return; } } - this.currentScopeIsCovered = false; + this.currentIteratorIsCovered = false; } logPosition() { @@ -2743,16 +2753,40 @@ class LayerHighlightIterator { // …and this iterator is deeper than the other… if (iterator.depth > this.depth) { return false; } - // …and this iterator's ranges actually include this position. + // …and one of this iterator's content ranges actually includes this + // position. (With caveats!) let ranges = this.languageLayer.getCurrentRanges(); if (ranges) { - return ranges.some(range => range.containsPoint(position)); + // A given layer's content ranges aren't allowed to overlap each other. + // So only a single range from this list can possibly match. + let overlappingRange = ranges.find(range => range.containsPoint(position)) + if (!overlappingRange) return false; + + // If the current position is right in the middle of an injection's + // range, then it should cover all attempts to apply scopes. But what if + // we're on one of its edges? Since closing scopes act before opening + // scopes, + // + // * if this iterator _starts_ a range at position X, it doesn't get to + // prevent another iterator from _ending_ a scope at position X; + // * if this iterator _ends_ a range at position X, it doesn't get to + // prevent another iterator from _starting_ a scope at position X. + // + // So at a given position, `currentIteratorIsCovered` can be `true` (all + // scopes suppressed), `false` (none suppressed), `"close"` (only closing + // scopes suppressed), or `"open"` (only opening scopes suppressed). + if (overlappingRange.end.compare(position) === 0) { + // We're at the right edge of the injection range. We want to prevent + // iterators from closing scopes, but not from opening them. + return 'close'; + } else if (overlappingRange.start.compare(position) === 0) { + // We're at the left edge of the injection range. We want to prevent + // iterators from opening scopes, but not from closing them. + return 'open'; + } else { + return true; + } } - - // TODO: Despite all this, we may want to allow parent layers to apply - // scopes at the very edges of this layer's ranges/extent; or perhaps to - // apply ending scopes at starting positions and vice-versa; or at least to - // make it a configurable behavior. } seek(start, endRow) {