diff --git a/.cirrus.yml b/.cirrus.yml index 274494de0e..58d5fff145 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,6 +1,6 @@ env: PYTHON_VERSION: 3.12 - GITHUB_TOKEN: ENCRYPTED[!06df1c93af902fc78fbd143724c5d11733607c6e743665859073de9aa2a317a5ac67b302f5ed92b232f6d28fae602e92!] + GITHUB_TOKEN: ENCRYPTED[!587e64c0e1412de985721fd9a82605b5b359a3565854235f9b092824dd50c847e5ed3fcf34acf5ead8b2707cd50ec895!] # The above token, is a GitHub API Token, that allows us to download RipGrep without concern of API limits # linux_task: @@ -63,7 +63,7 @@ arm_linux_task: memory: 8G env: USE_SYSTEM_FPM: 'true' - ROLLING_UPLOAD_TOKEN: ENCRYPTED[50debc954fd1b46513fa44d6270491edaba4c9c0ab6fbf0a3fb8e3bdfe9e380d203bd90857ae720b5af31d8a02c6389f] + ROLLING_UPLOAD_TOKEN: ENCRYPTED[bc554e7208b73b37e2b776c70905b142a5023cea4a9b89e1370b8815adea18f8436e8b075e6cf1bcb62133d659b5d198] prepare_script: - apt-get update - export DEBIAN_FRONTEND="noninteractive" @@ -135,7 +135,7 @@ silicon_mac_task: APPLEID: ENCRYPTED[549ce052bd5666dba5245f4180bf93b74ed206fe5e6e7c8f67a8596d3767c1f682b84e347b326ac318c62a07c8844a57] APPLEID_PASSWORD: ENCRYPTED[774c3307fd3b62660ecf5beb8537a24498c76e8d90d7f28e5bc816742fd8954a34ffed13f9aa2d1faf66ce08b4496e6f] TEAM_ID: ENCRYPTED[11f3fedfbaf4aff1859bf6c105f0437ace23d84f5420a2c1cea884fbfa43b115b7834a463516d50cb276d4c4d9128b49] - ROLLING_UPLOAD_TOKEN: ENCRYPTED[50debc954fd1b46513fa44d6270491edaba4c9c0ab6fbf0a3fb8e3bdfe9e380d203bd90857ae720b5af31d8a02c6389f] + ROLLING_UPLOAD_TOKEN: ENCRYPTED[bc554e7208b73b37e2b776c70905b142a5023cea4a9b89e1370b8815adea18f8436e8b075e6cf1bcb62133d659b5d198] prepare_script: - brew update - brew uninstall node@20 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7fb479842..2c44b9c3ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -238,6 +238,10 @@ jobs: needs: build steps: + - name: Install build dependencies - Linux + if: ${{ runner.os == 'Linux' }} + run: sudo apt-get update && sudo apt-get install -y git python3 python3-pip make gcc g++ libx11-dev libxkbfile-dev pkg-config libsecret-1-dev rpm xvfb ffmpeg zstd wget squashfs-tools + - name: Checkout the latest code uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae8ba3ad6..4abc279fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,45 @@ ## [Unreleased] +## 1.124.0 + +- Enhanced spellcheck to allow spellchecking on sections of a buffer. Making it possible to spellcheck comments within code, which has been enabled by default. +- Tree-sitter fixes and enhancements for `language-c`. +- Updated error message received when deleting a file in Linux to be more accurate. +- Fixed error that could cause some keymaps to not appear under a package in `settings-view`. + +### Pulsar + +- CI: Add build dependencies for Linux 'test bins' job [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1165) +- Tree-sitter rolling fixes, 1.124 edition [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/1148) +- Fix Linux trash error message [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/1151) +- electron-builder: Don't create differential update blockmaps [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1139) +- CI: Update Cirrus Rolling release upload token [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1141) + +#### spell-check + +- [spell-check] Allow the user to whitelist sections of a buffer for spellchecking on a per-language basis. [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/1147) + +#### settings-view + +- [settings-view] Fix Package keymap compatibility check [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/1161) + +## 1.123.0 + - Fixed SQL State Storage not loading when starting Pulsar from a self-contained binary like appImage, tar.gz, etc. +- [symbols-view] Allow project-wide symbol search to consider results from more than one provider. +- Tree-sitter fixes and enhancements for hyperlinks, C, and shell scripts. +- Restore use of `shell.moveItemToTrash` API in tree-view, for Electron 12 compatibility. + +### Pulsar + +- Tree-sitter rolling fixes, 1.123 edition [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/1118) +- [symbols-view] Allow project-wide symbol searches to consider multiple providers [@savetheclocktower](github.com/pulsar-edit/pulsar/pull/1133) +- electron-builder: Fix race condition when preparing to copy binaries [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1137) +- [ci] Update GitHub Token in CirrusCI config [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/1134) +- Fixing requiring of better-sqlite3 [@mauricioszabo](github.com/pulsar-edit/pulsar/pull/1122) +- Revert removal of `shell.moveItemToTrash` [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/1125) +- CI: Bump macOS runner images from macos-12 to macos-13 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/1120) ## 1.122.0 diff --git a/package.json b/package.json index 956b3fd49d..acef8795b5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pulsar", "author": "Pulsar-Edit ", "productName": "Pulsar", - "version": "1.122.0-dev", + "version": "1.124.0-dev", "description": "A Community-led Hyper-Hackable Text Editor", "branding": { "id": "pulsar", @@ -311,4 +311,4 @@ "random-seed": "0.3.0", "webdriverio": "7.20.9" } -} +} \ No newline at end of file diff --git a/packages/language-c/grammars/tree-sitter-cpp/highlights.scm b/packages/language-c/grammars/tree-sitter-cpp/highlights.scm index c819911905..09f02e7f66 100644 --- a/packages/language-c/grammars/tree-sitter-cpp/highlights.scm +++ b/packages/language-c/grammars/tree-sitter-cpp/highlights.scm @@ -143,6 +143,12 @@ ; VARIABLES ; ========= +; The "x" in `FSEvent& x`; +(reference_declarator + [(identifier) (field_identifier)] @variable.other.declaration._LANG_ + (#is? test.descendantOfType "declaration field_declaration")) + + ; Function parameters ; ------------------- @@ -150,11 +156,10 @@ ; ((identifier) @variable.other.readwrite.member.cpp ; (#match? @variable.other.readwrite.member.cpp "^(f|m)[A-Z]\\w*$")) -; The "foo" in `int &foo` with in a parameter list. -(parameter_declaration - declarator: (reference_declarator - (identifier) @variable.parameter.cpp)) - +; The "foo" in `const char *foo` within a parameter list. +; (Should work no matter how many pointers deep we are.) +(reference_declarator (identifier) @variable.parameter.cpp + (#is? test.descendantOfType "parameter_declaration")) ; KEYWORDS ; ======== diff --git a/packages/language-hyperlink/grammars/modern-tree-sitter-hyperlink.cson b/packages/language-hyperlink/grammars/modern-tree-sitter-hyperlink.cson index 7977a53691..ebd8738380 100644 --- a/packages/language-hyperlink/grammars/modern-tree-sitter-hyperlink.cson +++ b/packages/language-hyperlink/grammars/modern-tree-sitter-hyperlink.cson @@ -5,6 +5,6 @@ parser: 'tree-sitter-hyperlink' injectionRegex: 'hyperlink' treeSitter: - parserSource: 'github:savetheclocktower/tree-sitter-hyperlink#04c3a667ba432236578ac99bbacd0412f88d6fac' + parserSource: 'github:savetheclocktower/tree-sitter-hyperlink#0704b3e5a72892495dd13b85a5064582414cb39a' grammar: 'ts/tree-sitter-hyperlink.wasm' highlightsQuery: 'ts/highlights.scm' diff --git a/packages/language-hyperlink/grammars/ts/tree-sitter-hyperlink.wasm b/packages/language-hyperlink/grammars/ts/tree-sitter-hyperlink.wasm index 723c2ae6bd..21a144fb89 100755 Binary files a/packages/language-hyperlink/grammars/ts/tree-sitter-hyperlink.wasm and b/packages/language-hyperlink/grammars/ts/tree-sitter-hyperlink.wasm differ diff --git a/packages/language-shellscript/grammars/tree-sitter/highlights.scm b/packages/language-shellscript/grammars/tree-sitter/highlights.scm index 031507a766..8c8549738f 100644 --- a/packages/language-shellscript/grammars/tree-sitter/highlights.scm +++ b/packages/language-shellscript/grammars/tree-sitter/highlights.scm @@ -134,7 +134,15 @@ (binary_expression ["&&" "||"] @keyword.operator.logical.shell) (pipeline "|" @keyword.operator.pipe.shell) -(expansion operator: "#" @keyword.operator.expansion.shell) + +; Any expansion operator, including all `#`s and `%`s in the following examples: +; +; foo="${bar#*.}" +; foo="${bar##*.}" +; foo="${bar%*.}" +; foo="${bar%%*.}" +; +(expansion operator: _ @keyword.operator.expansion.shell) ; "*" @keyword.operator.glob.shell diff --git a/packages/settings-view/lib/package-keymap-view.js b/packages/settings-view/lib/package-keymap-view.js index 47236b0f8a..b1938727ca 100644 --- a/packages/settings-view/lib/package-keymap-view.js +++ b/packages/settings-view/lib/package-keymap-view.js @@ -11,7 +11,6 @@ import KeybindingsPanel from './keybindings-panel' export default class PackageKeymapView { constructor (pack) { this.pack = pack - this.otherPlatformPattern = new RegExp(`\\.platform-(?!${_.escapeRegExp(process.platform)}\\b)`) this.namespace = this.pack.name this.disposables = new CompositeDisposable() etch.initialize(this) @@ -128,8 +127,8 @@ export default class PackageKeymapView { continue } - if (this.otherPlatformPattern.test(selector)) { - continue + if (!this.selectorIsCompatibleWithPlatform(selector)) { + continue; } const keyBindingRow = document.createElement('tr') @@ -183,4 +182,11 @@ export default class PackageKeymapView { atom.clipboard.write(content) } + + selectorIsCompatibleWithPlatform(selector, platform = process.platform) { + const otherPlatformPattern = new RegExp(`\\.platform-(?!${_.escapeRegExp(platform)}\\b)`); + const currentPlatformPattern = new RegExp(`\\.platform-(${_.escapeRegExp(platform)}\\b)`); + + return !(otherPlatformPattern.test(selector) && !currentPlatformPattern.test(selector)); + } } diff --git a/packages/settings-view/spec/package-keymap-view-spec.js b/packages/settings-view/spec/package-keymap-view-spec.js new file mode 100644 index 0000000000..0f8e1a60bf --- /dev/null +++ b/packages/settings-view/spec/package-keymap-view-spec.js @@ -0,0 +1,35 @@ + +const PackageKeymapView = require("../lib/package-keymap-view.js"); +let view; + +describe("PackageKeymapView", () => { + + beforeEach(() => { + // Just prevent this stuff from calling through, it doesn't matter for this test + spyOn(atom.packages, "getLoadedPackage").andReturn({ keymaps: [] }); + + view = new PackageKeymapView({ + name: "test-package" + }); + }); + + it("should say a selector with no platform listed is compatible with the current one", () => { + expect(view.selectorIsCompatibleWithPlatform("atom-text-editor", "win32")).toBe(true); + }); + + it("should say a selector with a platform other than the current is not compatible", () => { + expect(view.selectorIsCompatibleWithPlatform(".platform-darwin", "linux")).toBe(false); + expect(view.selectorIsCompatibleWithPlatform(".platform-win32", "darwin")).toBe(false); + }); + + it("should say a selector with the current platform listed is compatible", () => { + expect(view.selectorIsCompatibleWithPlatform(".platform-linux", "linux")).toBe(true); + expect(view.selectorIsCompatibleWithPlatform(".platform-win32", "win32")).toBe(true); + expect(view.selectorIsCompatibleWithPlatform(".platform-darwin", "darwin")).toBe(true); + }); + + it("should say a selector with the current platform and others listed is compatible", () => { + expect(view.selectorIsCompatibleWithPlatform(".platform-linux, .platform-win32", "win32")).toBe(true); + expect(view.selectorIsCompatibleWithPlatform(".platform-linux, .platform-win32", "linux")).toBe(true); + }); +}); diff --git a/packages/spell-check/lib/main.js b/packages/spell-check/lib/main.js index df66c98767..8803a23c25 100644 --- a/packages/spell-check/lib/main.js +++ b/packages/spell-check/lib/main.js @@ -42,23 +42,9 @@ module.exports = { // Hook up changes to the configuration settings. this.excludedScopeRegexLists = []; this.subs.add( - atom.config.observe( - 'spell-check.excludedScopes', - (excludedScopes) => { - this.excludedScopeRegexLists = excludedScopes.map( - (excludedScope) => - excludedScope - .split(/\s+/)[0] - .split('.') - .filter((className) => className) - .map( - (className) => - new RegExp(`\\b${className}\\b`) - ) - ); - return this.updateViews(); - } - ) + atom.config.onDidChange('spell-check.excludedScopes', () => { + return this.updateViews(); + }) ); this.subs.add( diff --git a/packages/spell-check/lib/scope-helper.js b/packages/spell-check/lib/scope-helper.js new file mode 100644 index 0000000000..f43135e607 --- /dev/null +++ b/packages/spell-check/lib/scope-helper.js @@ -0,0 +1,155 @@ +function normalizeSegment(segment) { + if (!segment.startsWith('.')) return segment; + return segment.substring(1); +} + +function segmentsMatch( + descriptorSegment, + selectorSegment, + { enforceSegmentOrder = false } = {} +) { + let descriptorParts = normalizeSegment(descriptorSegment).split('.'); + let selectorParts = normalizeSegment(selectorSegment).split('.'); + + if (selectorParts.length > descriptorParts.length) { + return false; + } + + // Remove all parts from the descriptor scope name that aren't present in the + // selector scope name. + for (let i = descriptorParts.length - 1; i >= 0; i--) { + let part = descriptorParts[i]; + if (!selectorParts.includes(part)) { + descriptorParts.splice(i, 1); + } + } + // Does order matter? It would if this were a TextMate scope, but Atom has + // broadly treated `.function.entity` as equivalent to `.entity.function`, + // even though it causes headaches in some places. + // + // We'll assume that order doesn't matter, but the user can opt into strict + // ordering if they want. + if (!enforceSegmentOrder) { + descriptorParts.sort(); + selectorParts.sort(); + } + return descriptorParts.join('.') === selectorParts.join('.'); +} + +class ScopeSelector { + static create(stringOrScopeSelector) { + if (typeof stringOrScopeSelector === 'string') { + return new ScopeSelector(stringOrScopeSelector); + } else if (stringOrScopeSelector instanceof ScopeSelector) { + return stringOrScopeSelector; + } else { + throw new TypeError(`Invalid argument`); + } + } + + constructor(selectorString) { + this.selectorString = selectorString; + this.variations = selectorString.split(/,\s*/); + } + + matches(scopeDescriptorOrArray, rawOptions = {}) { + let options = { + // Whether to treat (e.g.) `.function.entity` as distinct from + // `.entity.function`. Defaults to `false` to match prevailing Atom + // behavior. + enforceSegmentOrder: false, + ...rawOptions, + }; + console.log(this, 'matches', scopeDescriptorOrArray); + let scopeList; + if (Array.isArray(scopeDescriptorOrArray)) { + scopeList = scopeDescriptorOrArray; + } else { + scopeList = scopeDescriptorOrArray.getScopesArray(); + } + + return this.variations.some((variation) => + this.matchesVariation(scopeList, variation, options) + ); + } + + matchesVariation(scopeList, selectorString, options) { + let parts = selectorString.split(/\s+/); + if (parts.length > scopeList.length) return false; + + let lastIndex = -1; + + outer: for (let selectorPart of parts) { + // Find something in the descriptor that matches this selector part. + for (let [i, descriptorPart] of scopeList.entries()) { + // Ignore everything before our index cursor; this is what enforces the + // ordering of the scope selector. + if (i <= lastIndex) continue; + let doesMatch = segmentsMatch( + descriptorPart, + selectorPart, + options + ); + if (doesMatch) { + lastIndex = i; + continue outer; + } + } + // If we get this far, we searched the entire descriptor list for a + // selector part and failed to find it; hence this variation doesn't + // match. + return false; + } + // If we get this far, we made it through the entire gauntlet without + // hitting the early return. This variation matches! + return true; + } +} + +// Private: A candidate for possible addition to the {ScopeDescriptor} API. +// +// Tests whether the given scope descriptor matches the given scope selector. +// +// A subset of the full TextMate scope selector syntax is supported: +// +// * Descendant scopes (e.g., `source.python string`); this function will +// enforce the ordering of segments. +// * Variations (e.g., `comment.block, string.quoted`); this function will +// return true if any of the variations match. +// +// Not supported: +// +// * Subtraction syntax (e.g., `comment - block`). +// * Left/right edge syntax (e.g., `L:comment.block`). +// +// For example: given the scope descriptor… +// +// [ +// 'source.js', +// 'meta.block.function.js', +// 'string.quoted.single.js' +// ] +// +// …here are outcomes of various tests: +// +// scopeDescriptorMatchesSelector(descriptor, `source`) // -> true +// scopeDescriptorMatchesSelector(descriptor, `text`) // -> false +// scopeDescriptorMatchesSelector(descriptor, `source.js`) // -> true +// scopeDescriptorMatchesSelector(descriptor, `source.python`) // -> false +// scopeDescriptorMatchesSelector(descriptor, `source.js meta.block.function.js`) // -> true +// scopeDescriptorMatchesSelector(descriptor, `source meta`) // -> true +// scopeDescriptorMatchesSelector(descriptor, `source meta.block.class`) // -> false +// scopeDescriptorMatchesSelector(descriptor, `source meta string`) // -> true +// scopeDescriptorMatchesSelector(descriptor, `source string`) // -> true +// scopeDescriptorMatchesSelector(descriptor, `source string meta`) // -> false +// +// - `scopeDescriptor` A {ScopeDescriptor} or a scope descriptor {Array}. +// - `selector` A {String} representing a scope selector. +function scopeDescriptorMatchesSelector(scopeDescriptor, selector) { + let scopeSelector = ScopeSelector.create(selector); + return scopeSelector.matches(scopeDescriptor); +} + +module.exports = { + scopeDescriptorMatchesSelector, +}; diff --git a/packages/spell-check/lib/spell-check-view.js b/packages/spell-check/lib/spell-check-view.js index c6545bf810..f995c5e8e5 100644 --- a/packages/spell-check/lib/spell-check-view.js +++ b/packages/spell-check/lib/spell-check-view.js @@ -2,6 +2,18 @@ let SpellCheckView; const _ = require('underscore-plus'); const { CompositeDisposable } = require('atom'); const SpellCheckTask = require('./spell-check-task'); +const { scopeDescriptorMatchesSelector } = require('./scope-helper'); + +// Tests whether a grammar's root scope matches a scope specified in the +// `grammars` setting. Allows for a more generic name in the setting (e.g., +// `source` to match all `source.[x]` grammars). +function topLevelScopeMatches(grammar, scope) { + if (scope === grammar) return true; + if (grammar.startsWith(`${scope}.`)) { + return true; + } + return false; +} let CorrectionsView = null; @@ -73,6 +85,15 @@ module.exports = SpellCheckView = class SpellCheckView { }) ); + this.disposables.add( + atom.config.observe( + 'spell-check.excludedScopes', + (excludedScopes) => { + this.excludedScopes = excludedScopes; + } + ) + ); + this.subscribeToBuffer(); this.disposables.add(this.editor.onDidDestroy(this.destroy.bind(this))); @@ -118,6 +139,7 @@ module.exports = SpellCheckView = class SpellCheckView { this.unsubscribeFromBuffer(); if (this.spellCheckCurrentGrammar()) { + this.scopesToSpellCheck = this.getSpellCheckScopesForCurrentGrammar(); this.buffer = this.editor.getBuffer(); this.bufferDisposable = new CompositeDisposable( this.buffer.onDidStopChanging( @@ -131,7 +153,69 @@ module.exports = SpellCheckView = class SpellCheckView { spellCheckCurrentGrammar() { const grammar = this.editor.getGrammar().scopeName; - return _.contains(atom.config.get('spell-check.grammars'), grammar); + let grammars = atom.config.get('spell-check.grammars'); + let topLevelScopes = grammars.map((rawScope) => { + if (!rawScope.includes(' ')) return rawScope; + return rawScope.substring(0, rawScope.indexOf(' ')); + }); + return topLevelScopes.some((scope) => { + return topLevelScopeMatches(grammar, scope); + }); + // return topLevelScopes.includes(grammar); + } + + // Returns: + // + // * `true` if the entire buffer should be checked; + // * `false` if none of the buffer should be checked; or, if only certain + // parts of the buffer should be checked, + // * an {Array} of scope names matching regions of the buffer that should + // be checked. + getSpellCheckScopesForCurrentGrammar() { + const grammar = this.editor.getGrammar().scopeName; + let grammars = atom.config.get('spell-check.grammars'); + let scopeList = []; + // Despite the name of this setting, spell-checking is no longer all or + // nothing on a per-grammar basis; we now allow users to opt into + // checking subsections of a buffer by adding descendant scopes. Each + // segment must begin with all or part of a root scope name (e.g., + // `source.js`, `text.html`, but otherwise any valid scope selector is + // accepted here.) + // + // Examples: + // + // * `source.js comment.block` + // * `source comment, source string.quoted` + // * `text` + // + // The first example targets just JS block comments; the second targets + // all comments and quoted strings in _all_ source files; and the third + // targets any text format, whether HTML or Markdown or plaintext. + // + // This allows for more granular spell-checking than was possible + // before, even if the `excludeScopes` setting was utilized. + for (let rawScope of grammars) { + if (!rawScope.includes(' ')) { + // Any value that's just the bare root scope of the language + // (like `source.python`) means that we're spell-checking the + // entire buffer. This applies even if there's a later match + // for this grammar that's more restrictive. + if (topLevelScopeMatches(grammar, rawScope)) { + return true; + } + } else { + // If the value also includes a descendant scope, it means we're + // spell-checking some subset of the buffer. + let index = rawScope.indexOf(' '); + let rootScope = rawScope.substring(0, index); + if (topLevelScopeMatches(grammar, rootScope)) { + // There could be multiple of these — e.g., `source.python string, + // source.python comment` — so we won't return early. + scopeList.push(rawScope); + } + } + } + return scopeList.length > 0 ? scopeList : false; } destroyMarkers() { @@ -147,6 +231,9 @@ module.exports = SpellCheckView = class SpellCheckView { const scope = this.editor.scopeDescriptorForBufferPosition( misspelling[0] ); + // Under the hood, we spell-check the entire document; but we + // might end up ignoring some of the misspellings based on + // their scope descriptor. if (!this.scopeIsExcluded(scope)) { result.push( this.markerLayer.markBufferRange(misspelling, { @@ -308,11 +395,31 @@ module.exports = SpellCheckView = class SpellCheckView { return (this.spellCheckModule.contextMenuEntries = []); } - scopeIsExcluded(scopeDescriptor, excludedScopes) { - return this.spellCheckModule.excludedScopeRegexLists.some((regexList) => - scopeDescriptor.scopes.some((scopeName) => - regexList.every((regex) => regex.test(scopeName)) - ) - ); + scopeIsExcluded(scopeDescriptor) { + // Practically speaking, `this.scopesToSpellCheck` will either be `true` + // or an array of scope selectors. If it's the latter, then we should + // apply whitelisting and exclude anything that doesn't match. + if (Array.isArray(this.scopesToSpellCheck)) { + // If we know none of the subscopes match this region, we can + // exclude it even before we get to the `excludedScopes` setting. + let someMatch = this.scopesToSpellCheck.some( + (scopeToSpellCheck) => { + return scopeDescriptorMatchesSelector( + scopeDescriptor, + scopeToSpellCheck + ); + } + ); + if (!someMatch) return true; + } + // Whether or not we applied whitelisting above, excluded scopes take + // precedence; anything that doesn't make it through this gauntlet + // gets excluded. + return this.excludedScopes.some((excludedScope) => { + return scopeDescriptorMatchesSelector( + scopeDescriptor, + excludedScope + ); + }); } }; diff --git a/packages/spell-check/package.json b/packages/spell-check/package.json index 61ac11aa6d..6d8d48193b 100644 --- a/packages/spell-check/package.json +++ b/packages/spell-check/package.json @@ -20,7 +20,8 @@ "repository": "https://github.com/pulsar-edit/pulsar", "license": "MIT", "engines": { - "atom": "*" + "atom": "*", + "node": ">=14" }, "scripts": { "format": "prettier --write \"spec/*.js\" \"lib/**/*.js\" \"script/*.js\" --loglevel warn" @@ -29,6 +30,7 @@ "grammars": { "type": "array", "default": [ + "source comment", "source.asciidoc", "source.gfm", "text.git-commit", @@ -37,7 +39,7 @@ "source.rst", "text.restructuredtext" ], - "description": "List of scopes for languages which will be checked for misspellings. See [the README](https://github.com/pulsar-edit/pulsar/blob/master/packages/spell-check/README.md) for more information on finding the correct scope for a specific language.", + "description": "List of scopes for languages which will be checked for misspellings. See [the README](https://github.com/pulsar-edit/pulsar/blob/master/packages/spell-check/README.md) for more information on finding the correct scope for a specific language. If you want to spell-check only parts of some languages, you may specify a more detailed scope selector (like `source.js comment.block` or `source string`); but the first part of the selector must match the language scope.", "order": 1 }, "excludedScopes": { diff --git a/packages/spell-check/spec/spell-check-spec.js b/packages/spell-check/spec/spell-check-spec.js index cdc32c18f9..4c74200dfd 100644 --- a/packages/spell-check/spec/spell-check-spec.js +++ b/packages/spell-check/spec/spell-check-spec.js @@ -47,8 +47,8 @@ describe('Spell check', function () { }); afterEach(async () => { - await languageMode.atTransactionEnd(); - SpellCheckTask.clear(); + await languageMode.atTransactionEnd(); + SpellCheckTask.clear(); }); it('decorates all misspelled words', async function () { @@ -77,9 +77,124 @@ describe('Spell check', function () { expect(textForMarker(misspellingMarkers[1])).toEqual('bok'); }); + it('allows certain sub-scopes to be whitelisted into spell checking, implicitly excluding anything that does not match', async () => { + editor.setText( + `speledWrong = 5; +function speledWrong() {} +// We only care about mispelings in comments and strings! +let foo = "this is speled wrong" +class SpeledWrong {}` + ); + + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + 'source.js comment', + 'source.js string', + 'text.plain.null-grammar', + ]); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [2, 22], + [2, 32], + ], + [ + [3, 19], + [3, 25], + ], + ]); + } + }); + + it('interprets a bare root scope as opting out of scope whitelisting, even when other more specific segments are present', async () => { + editor.setText( + `speledWrong = 5; +function speledWrong() {} +// We only care about mispelings in comments and strings! +let foo = "this is speled wrong" +class SpeledWrong {}` + ); + + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + // Exactly as above, but with an extra `'source.js'` listing; this will + // supersede the more specific settings below. + 'source.js', + 'source.js comment', + 'source.js string', + 'text.plain.null-grammar', + ]); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [0, 0], + [0, 11], + ], + [ + [1, 9], + [1, 20], + ], + [ + [2, 22], + [2, 32], + ], + [ + [3, 4], + [3, 7], + ], + [ + [3, 19], + [3, 25], + ], + [ + [4, 6], + [4, 17], + ], + ]); + } + }); + + it('allows a generic root scope like "source"', async () => { + editor.setText( + `speledWrong = 5; +function speledWrong() {} +// We only care about mispelings in comments and strings! +let foo = "this is speled wrong" +class SpeledWrong {}` + ); + + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + 'source comment', + 'source string', + 'text.plain.null-grammar', + ]); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [2, 22], + [2, 32], + ], + [ + [3, 19], + [3, 25], + ], + ]); + } + }); + it('allows certain scopes to be excluded from spell checking', async function () { editor.setText( -`speledWrong = 5; + `speledWrong = 5; function speledWrong() {} class SpeledWrong {}` ); @@ -126,7 +241,9 @@ class SpeledWrong {}` } { - atom.config.set('spell-check.excludedScopes', ['.entity.name.type.class']); + atom.config.set('spell-check.excludedScopes', [ + '.entity.name.type.class', + ]); await conditionPromise(() => getMisspellingMarkers().length === 2); const markers = getMisspellingMarkers(); expect(markers.map((marker) => marker.getBufferRange())).toEqual([ diff --git a/packages/symbols-view/lib/project-view.js b/packages/symbols-view/lib/project-view.js index 7988cdb3d2..04d06894cd 100644 --- a/packages/symbols-view/lib/project-view.js +++ b/packages/symbols-view/lib/project-view.js @@ -143,7 +143,11 @@ module.exports = class ProjectView extends SymbolsView { // longer need the symbols we asked for. let signal = this.abortController.signal; - let providers = await this.broker.select(meta); + // A user would probably expect this search to return symbols from all + // files in the project, regardless of their language. Instead of picking a + // “winning” provider as we usually do, we should instead consult _all_ + // providers that consider themselves up to the task. + let providers = await this.broker.select(meta, { enforceExclusivity: false }); if (providers?.length === 0) { console.warn('No providers found!'); return null; diff --git a/packages/symbols-view/lib/provider-broker.js b/packages/symbols-view/lib/provider-broker.js index f702336272..0c28739f42 100644 --- a/packages/symbols-view/lib/provider-broker.js +++ b/packages/symbols-view/lib/provider-broker.js @@ -175,7 +175,7 @@ module.exports = class ProviderBroker { * @returns {Promise} A promise that resolves with a list * of symbol providers. */ - async select(meta) { + async select(meta, { enforceExclusivity = true } = {}) { let shouldLog = Config.get('enableDebugLogging'); let exclusivesByScore = []; let results = []; @@ -210,7 +210,14 @@ module.exports = class ProviderBroker { let { value: score } = outcome; let name = provider.name ?? 'unknown'; let packageName = provider?.packageName ?? 'unknown'; - let isExclusive = provider?.isExclusive ?? false; + + // When `enforceExclusivity` is `false`, we'll treat all providers as + // non-exclusive, even the ones that indicate otherwise. + // + // It still falls on a provider to know whether to provide symbols or + // not. Any provider is free to inspect the metadata and return an empty + // set if it thinks it's inappropriate for its results to be considered. + let isExclusive = enforceExclusivity ? (provider?.isExclusive ?? false) : false; if (shouldLog) console.debug('Score for', provider.name, 'is:', score); diff --git a/packages/symbols-view/spec/fixtures/providers/second-dummy-provider.js b/packages/symbols-view/spec/fixtures/providers/second-dummy-provider.js new file mode 100644 index 0000000000..5f5cd9e8fb --- /dev/null +++ b/packages/symbols-view/spec/fixtures/providers/second-dummy-provider.js @@ -0,0 +1,51 @@ +const { Point } = require('atom'); + +function last(arr) { + return arr[arr.length - 1]; +} + +const ICONS = [ + 'icon-package', + 'icon-key', + 'icon-gear', + 'icon-tag', + null +]; + +module.exports = { + packageName: 'symbol-provider-dummy-second', + name: 'Dummy (Second)', + isExclusive: true, + canProvideSymbols() { + return true; + }, + getSymbols(meta) { + let { editor, type } = meta; + let results = []; + if (type === 'file') { + let count = editor.getLineCount(); + // Put a symbol on every third line. + for (let i = 0; i < count; i += 3) { + results.push({ + position: new Point(i, 0), + name: `(Second) Symbol on Row ${i + 1}`, + icon: ICONS[(i / 3) % (ICONS.length + 1)] + }); + } + } else if (type === 'project') { + let root = last(atom.project.getPaths()); + let count = editor.getLineCount(); + // Put a symbol on every third line. + for (let i = 0; i < count; i += 3) { + results.push({ + position: new Point(i, 0), + name: `(Second) Symbol on Row ${i + 1}`, + directory: root, + file: 'other-file.js', + icon: ICONS[i % (ICONS.length + 1)] + }); + } + } + return results; + } +}; diff --git a/packages/symbols-view/spec/symbols-view-spec.js b/packages/symbols-view/spec/symbols-view-spec.js index ef73f3b599..9001bd62ee 100644 --- a/packages/symbols-view/spec/symbols-view-spec.js +++ b/packages/symbols-view/spec/symbols-view-spec.js @@ -7,6 +7,7 @@ const SymbolsView = require('../lib/symbols-view'); const { migrateOldConfigIfNeeded } = require('../lib/util'); const DummyProvider = require('./fixtures/providers/dummy-provider'); +const SecondDummyProvider = require('./fixtures/providers/second-dummy-provider'); const AsyncDummyProvider = require('./fixtures/providers/async-provider'); const ProgressiveProjectProvider = require('./fixtures/providers/progressive-project-provider.js'); const QuicksortProvider = require('./fixtures/providers/quicksort-provider.js'); @@ -696,6 +697,27 @@ describe('SymbolsView', () => { expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText(`${relative}:13`); }); + it('includes results from all providers, even if they claim to be exclusive', async () => { + registerProvider(DummyProvider); + registerProvider(SecondDummyProvider); + + await dispatchAndWaitForChoices('symbols-view:toggle-project-symbols'); + symbolsView = atom.workspace.getModalPanels()[0].item; + + expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined(); + expect(document.body.contains(symbolsView.element)).toBe(true); + expect(symbolsView.element.querySelectorAll('li').length).toBe(10); + + let root = atom.project.getPaths()[1]; + let resolved = directory.resolve('other-file.js'); + let relative = `${path.basename(root)}${resolved.replace(root, '')}`; + + expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 1'); + expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText(`${relative}:1`); + expect(symbolsView.element.querySelector('li:last-child .primary-line')).toHaveText('(Second) Symbol on Row 13'); + expect(symbolsView.element.querySelector('li:last-child .secondary-line')).toHaveText(`${relative}:13`); + }); + it('does not prefill the query field if `prefillSelectedText` is `false`', async () => { atom.config.set('symbols-view.prefillSelectedText', false); registerProvider(DummyProvider); diff --git a/packages/tree-view/lib/tree-view.js b/packages/tree-view/lib/tree-view.js index 2fdc4b4071..1b356eaa33 100644 --- a/packages/tree-view/lib/tree-view.js +++ b/packages/tree-view/lib/tree-view.js @@ -954,7 +954,7 @@ class TreeView { formatTrashEnabledMessage() { switch (process.platform) { case 'linux': - return 'Is `gvfs-trash` installed?'; + return 'Do you have permission to delete, and Trash is enabled on the volume where the files are stored?'; case 'darwin': return 'Is Trash enabled on the volume where the files are stored?'; case 'win32': diff --git a/packages/welcome/lib/changelog-view.js b/packages/welcome/lib/changelog-view.js index 8f47460936..bfba6364ff 100644 --- a/packages/welcome/lib/changelog-view.js +++ b/packages/welcome/lib/changelog-view.js @@ -50,34 +50,17 @@ export default class ChangeLogView {

Feel free to read our Full Change Log.

diff --git a/script/electron-builder.js b/script/electron-builder.js index a49fd52f40..9a15598285 100644 --- a/script/electron-builder.js +++ b/script/electron-builder.js @@ -1,6 +1,7 @@ const path = require('path') const normalizePackageData = require('normalize-package-data'); const fs = require("fs/promises"); +const {mkdirSync} = require("fs"); const generateMetadata = require('./generate-metadata-for-builder') const macBundleDocumentTypes = require("./mac-bundle-document-types.js"); @@ -198,7 +199,8 @@ let options = { }, }, "dmg": { - "sign": false + "sign": false, + "writeUpdateInfo": false }, "win": { "icon": icoIcon, @@ -235,7 +237,8 @@ let options = { // the AppID 'dev.pulsar-edit.pulsar'. If this value ever changes, // A PR to GitHub Desktop must be made with the updated value "include": "resources/win/installer.nsh", - "warningsAsErrors": false + "warningsAsErrors": false, + "differentialPackage": false }, "extraMetadata": { }, @@ -289,7 +292,13 @@ async function main() { config: options }).then((result) => { console.log("Built binaries") - fs.mkdir('binaries').catch(() => "") + try { + mkdirSync('binaries', {recursive: true}) + } catch (err) { + console.warn("Warning: error encountered when making the 'binaries' dir.") + console.warn("(HINT: If the 'binaries' folder already exists, then this error message is probably fine to ignore!)") + console.warn(err) + } Promise.all(result.map(r => fs.copyFile(r, path.join('binaries', path.basename(r))))) }).catch((error) => { console.error("Error building binaries") diff --git a/src/config-file.js b/src/config-file.js index cee35b442d..eb67876aa1 100644 --- a/src/config-file.js +++ b/src/config-file.js @@ -79,7 +79,7 @@ module.exports = class ConfigFile { this.requestLoad(); } }) - watcher.start(); + await watcher.start(); return { dispose: () => watcher.stop() }; } catch (error) { //TODO_PULSAR: Find out why the atom global variable isn't available at this point diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index 9374495940..bdc514afee 100644 --- a/src/wasm-tree-sitter-language-mode.js +++ b/src/wasm-tree-sitter-language-mode.js @@ -3529,7 +3529,7 @@ class LanguageLayer { // might be stale, pass `force: false` as an option. // // In certain circumstances, the new tree might be promoted to the canonical - // tree for this layer. To prevent this, pass `anonymous: false` as an option. + // tree for this layer. To prevent this, pass `anonymous: true` as an option. // // All trees returned by this method are managed by this language layer and // will be deleted when the next transaction is complete. Retaining a @@ -3900,10 +3900,10 @@ class LanguageLayer { } } -// An injection `LanguageLayer` may need to parse and highlight a strange -// subset of its stated range — for instance, all the descendants within a -// parent that are of a particular type. A `NodeRangeSet` is how that strange -// subset is expressed. +// Private: An injection `LanguageLayer` may need to parse and highlight a +// strange subset of its stated range — for instance, all the descendants +// within a parent that are of a particular type. A `NodeRangeSet` is how that +// strange subset is expressed. class NodeRangeSet { constructor(previous, nodes, injectionPoint) { this.previous = previous; @@ -3920,9 +3920,13 @@ class NodeRangeSet { } } + // Extracts the information we need from fresh tree nodes so that it's + // guaranteed to survive even if the tree is destroyed. getNodeSpec(node, getChildren) { let { startIndex, endIndex, startPosition, endPosition, id } = node; let result = { startIndex, endIndex, startPosition, endPosition, id }; + // `children` is a getter, so checking `childCount` is cheaper than + // checking `children.length`. if (getChildren && node.childCount > 0) { result.children = []; for (let child of node.children) { @@ -4104,8 +4108,8 @@ class NodeRangeSet { } } -// A subclass of map that associates a set of scope names with the editor -// locations at which they are opened. +// Private: A subclass of `Map` that associates a set of scope names with the +// editor locations at which they are opened. // // In some complicated scenarios, we need to know where a scope was opened when // deciding how to handle it. @@ -4142,12 +4146,15 @@ class OpenScopeMap extends Map { } } -// Like a map, but expects each key to have multiple values. +// Private: A subclass of `Map` that anticipates multiple values at each key. +// The main way to add a value at a given key is via a new `Index::add` method. class Index extends Map { constructor() { super(); } + // Like `Map::set`, but adds one or more values at a given key. Initializes + // the key's value to be an empty array if necessary. add(key, ...values) { let existing = this.get(key); if (!existing) { @@ -4159,11 +4166,13 @@ class Index extends Map { } -// A class designed to aggregate and normalize a set of ranges. Each time a -// buffer range is added, it's compared to the existing list; if there are -// intersections with range already in the list, those intersections are +// Private: A class designed to aggregate and normalize a set of ranges. Each +// time a buffer range is added, it's compared to the existing list; if there +// are intersections with range already in the list, those intersections are // combined into one larger range. // +// The ranges can be iterated via `for..of`. +// // Assumes all ranges are instances of `Range` rather than Tree-sitter range // specs. class RangeList { @@ -4175,6 +4184,11 @@ class RangeList { this.ranges.length = 0; } + // Add a new `Range` to the list. + // + // If this range intersects with a range already in the list, it will merge + // with the existing range. Otherwise it'll insert itself such that the list + // maintains buffer ordering. add(newRange) { let intersecting = []; for (let range of this.ranges) { @@ -4235,15 +4249,28 @@ class IndentResolver { if (!root || !root.tree || !root.ready) { return null; } let { languageMode } = this; let options = { + // Whether to skip emitting the `did-suggest-indent` event. skipEvent: false, + // Whether to skip blank lines when finding a comparison row. skipBlankLines: true, + // Whether to skip the second (dedent) phase of indentation hinting. skipDedentCheck: false, + // Whether to account for the leading whitespace that already exists on + // the row when returning an indentation level. preserveLeadingWhitespace: false, + // A cache of existing indentation levels to reduce work when resuming + // an indentation hint started earlier. Takes the form of a `Map` whose + // keys are line numbers and whose values are indentation levels. indentationLevels: null, + // Whether to force a re-parse of the tree if we think the tree is dirty. forceTreeParse: false, ...rawOptions }; + // We can also pass a `tree` option to tell this method to re-use a + // specific tree. In those cases, we also include a `controllingLayer` + // option as a sanity check; the tree can only be reused if the controlling + // layer is still the one we expect. let originalControllingLayer = options.controllingLayer; // Indentation hinting is a two-phase process. @@ -4253,6 +4280,9 @@ class IndentResolver { // // In phase 2, we consider `row`’s own content to see if any of it suggests // an alteration from the phase 1 value. + // + // To start, we check the previous row (typically the nearest row with text + // on it) to know what our indentation “baseline” ought to be. let comparisonRow = options.comparisonRow ?? this.getComparisonRow(row, options); let existingIndent = 0; @@ -4265,8 +4295,9 @@ class IndentResolver { // we return later on. // // Sadly, if the row is _more_ indented than we need it to be, we won't - // be able to dedent it into the correct position. This option probably - // needs to be revisited. + // be able to dedent it into the correct position when + // `preserveLeadingWhitespace` is `true`. This option probably needs to + // be revisited. existingIndent = this.indentLevelForLine( this.buffer.lineForRow(row), tabLength); } @@ -4327,7 +4358,15 @@ class IndentResolver { // There's no layer with an indents query to help us out. The default // behavior in this situation with any grammar — even plain text — is to // match the previous line's indentation. - return comparisonRowIndent - existingIndent; + let finalIndent = comparisonRowIndent - existingIndent; + if (!options.skipEvent) { + this.emitter.emit('did-suggest-indent', { + currentRow: row, + comparisonRow, + finalIndent + }) + } + return finalIndent; } let { queries: { indentsQuery }, scopeResolver } = controllingLayer; @@ -4343,6 +4382,29 @@ class IndentResolver { indentTree = options.tree; } + // In practice, we want to use synchronous hinting whenever we can. Here we + // opt into synchronous hinting when + // + // * we don't have to re-parse the tree; + // * we are explicitly told to re-parse the tree; + // * we think we can afford to spend the time to re-parse the tree. + // + // Indentation hinting can be expensive because it runs with every + // individual change, even within transactions! And since each individual + // change changes the tree, triggering hinting in the middle of a + // transaction forces a re-parse that otherwise wouldn't have happened + // until the transaction was finished. It's cheaper to wait until the end + // of a transaction and invoke auto-indentation over the entire transaction + // extent, but this can easily produce a different (and less accurate) + // outcome than synchronous hinting. + // + // We still need asynchronous hinting for edge cases. A re-parse costs + // time, and any package can programmaticaly create a buffer transaction + // that triggers indentation hinting an arbitrary number of times, so we + // must guard against those scenarios no matter how rare they are. The + // `shouldUseAsyncIndent` method on the language mode manages that; it + // tells us whether we can spare the time we'll spend to do a tree + // re-parse. if (!indentTree) { if (!controllingLayer.treeIsDirty || options.forceTreeParse || !languageMode.shouldUseAsyncIndent()) { // If we're in this code path, it either means the tree is clean (the @@ -4359,8 +4421,8 @@ class IndentResolver { // preliminary indent level and then follow up later with a more // accurate one. It's a bit disorienting that the editor falls back to // an indent level of `0` when a newline is inserted. - let comparisonRowText = this.buffer.lineForRow(comparisonRow) - let rowText = this.buffer.lineForRow(row) + let comparisonRowText = this.buffer.lineForRow(comparisonRow); + let rowText = this.buffer.lineForRow(row); return languageMode.atTransactionEnd().then(({ changeCount }) => { let shouldFallback = false; // If this was the only change in the transaction, then we can @@ -4383,21 +4445,10 @@ class IndentResolver { } } if (shouldFallback) { - // We're now revisiting this indentation question at the end of the - // transaction. Other changes may have taken place since we were - // first asked what the indent level should be for this line. So - // how do we know if the question is still relevant? After all, the - // text that was on this row earlier might be on some other row - // now. - // - // So we compare the text that was on the row when we were first - // called… to the text that is on the row now that the transaction - // is over. If they're the same, that's a _strong_ indicator that - // the result we return will still be relevant. - // - // If not, as is the case in this code path, we return `undefined`, - // signalling to the `TextEditor` that its only recourse is to - // auto-indent the whole extent of the transaction instead. + // When we think the buffer has changed too much for our hint to be + // relevant, we return `undefined`, signalling to the `TextEditor` + // that its only recourse is to auto-indent the whole extent of the + // transaction instead. return undefined; } @@ -4463,7 +4514,7 @@ class IndentResolver { // the form `(#is? test.foo)`. if (!scopeResolver.store(capture)) { continue; } // Apply indentation-specific scope tests and skip this capture if any - // tests fail. + // tests fail. This applies all tests of the form `(#is? indent.foo)`. let passed = this.applyTests(capture, { currentRow: row, comparisonRow, @@ -4478,6 +4529,7 @@ class IndentResolver { positionSet.add(key); if (name === 'indent') { + // This capture hints at an increase in indentation level. if (indentCapturePosition === null) { indentCapturePosition = node.endPosition; } @@ -4632,6 +4684,21 @@ class IndentResolver { // should run this layer's indents query against its own tree. (If _no_ // layers qualify at this position, we won't hit this code path, so // we'll reluctantly still use the original layer and tree.) + // + // NOTE: This strange edge case bypasses all of the heuristics we + // defined above that govern synchronous vs. asynchronous hinting. + // + // In our defense, the cost of this reparse is still accounted for in + // the reparse budget. Also, it's not clear that such a tree would even + // need a re-parse, since the buffer change that leads to this edge + // case will often happen outside of this language layer. + // + // Still, if we find an edge case in which this might be a problem, we + // should decide what to do here. It would feel a bit weird to go async + // this late in the hinting process, so one option might be to + // determine `dedentControllingLayer` at the same time as + // `controllingLayer` so that it can be considered when making the + // initial decision between sync/async hinting. indentsQuery = dedentControllingLayer.queries.indentsQuery; indentTree = dedentControllingLayer.getOrParseTree(); } @@ -4811,10 +4878,12 @@ class IndentResolver { // `true`). // // - `callback` A {Function} that takes one parameter: - // - `meta` An {Object} that consisting of _some subset_ of the following + // - `meta` An {Object} consisting of _some subset_ of the following // properties: // - `captureMode` A {String} describing one of several different modes - // which influence a capture: + // which influence a capture; when this property is absent, it means + // that indentation level was determined in a simpler manner that + // did not use any Tree-sitter features. // - A value of `normal` means that an indentation level was determined // through the normal two-phase process. // - A value of `match` means that an indentation level was determined @@ -4831,7 +4900,8 @@ class IndentResolver { // - `comparisonRow` The {Number} of the row that was consulted to // determine the baseline indentation of the target row. This is // often the row directly above `row`, but can be an earlier row if - // the target row was preceded by whitespace. + // the target row was preceded by whitespace. (Zero-indexed just like + // `currentRow`.) // - `comparisonRowIndent` {Number} The indentation level of the // comparison row. // - `indentDelta` {Number} The amount of indentation (in increments) @@ -4856,18 +4926,15 @@ class IndentResolver { // overrides the conventional indentation logic. // - `finalIndent` {Number} A number representing the final value that // will shortly be returned from a call to - // `suggestedIndentForBufferRow`. This value accounts for the possible - // presence of the `preserveLeadingWhitespace` option. For instance, - // if `suggestedIndentForBufferRow` would return `5`, but the target - // row already has an indent level of `3`, `finalIndent` will instead - // be `2`. - // - `adjustedIndent` {Number} A `finalIndent`, but takes existing + // `suggestedIndentForBufferRow`. This value does not account for the + // `preserveLeadingWhitespace` option; it represents what the actual + // indentation level of the line is going to be. + // - `adjustedIndent` {Number} Like `finalIndent`, but takes existing // indentation level into account if the `preserveLeadingWhitespace` - // option was enabled. For instance, if `suggestedIndentForBufferRow` - // would return `5`, but the target row already has an indent level of - // `3`, `adjustedIndent` will instead be `2`. If - // `preserveLeadingWhitespace` is `false`, `finalIndent` and - // `adjustedIndent` will be identical. + // option was enabled. For instance, if `finalIndent` is `5`, but the + // target row already has an indent level of `3`, `adjustedIndent` will + // instead be `2`. If `preserveLeadingWhitespace` is `false`, + // `finalIndent` and `adjustedIndent` will always be identical. // onDidSuggestIndent(callback) { return this.emitter.on('did-suggest-indent', callback); @@ -5034,6 +5101,9 @@ class IndentResolver { // re-parse the tree in order to make an accurate indents query. let indentTree = options.tree; if (!indentTree) { + // Unlike `suggestedIndentForBufferRow`, this method is not something + // that can run in the middle of a transaction. That means we don't need + // to consult the reparse budget. if (!controllingLayer.treeIsDirty || options.forceTreeParse || !this.useAsyncIndent || !this.useAsyncParsing) { indentTree = controllingLayer.getOrParseTree(); } else {