Merge remote-tracking branch 'origin/master' into clojure-grammar-enhancements

This commit is contained in:
Maurício Szabo 2024-04-11 21:53:18 -03:00
commit 1027a51811
386 changed files with 22338 additions and 8694 deletions

View File

@ -1,6 +1,6 @@
env:
PYTHON_VERSION: 3.12
GITHUB_TOKEN: ENCRYPTED[!b0ff4671044672be50914a3a10b49af642bd8e0e681a6f4e5855ec5230a5cf9afbc53d9e90239b8d2c79455f014f383f!]
GITHUB_TOKEN: ENCRYPTED[!c394f11378a8bc92ff1b05662ee3e574fc662692e45f0a048aa8cab42fb072b039d83f68fd6953f470af51846063ce46!]
# 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[690950798401ec3715e9d20ac29a0859d3c58097038081ff6afeaf4721e661672d34eb952d8a6442bc7410821ab8545a]
ROLLING_UPLOAD_TOKEN: ENCRYPTED[f935c396a9f4bca108ec2fdedb00dbc9be2f4c411f100d577acdab42db59ea134be059ce8535396db8222a2b1eb68c27]
prepare_script:
- apt-get update
- export DEBIAN_FRONTEND="noninteractive"
@ -85,6 +85,7 @@ arm_linux_task:
libasound2-dev
libnss3
xvfb
- gem install dotenv -v '~> 2.8'
- gem install fpm
- git submodule init
- git submodule update
@ -122,7 +123,7 @@ silicon_mac_task:
only_if: $CIRRUS_CRON != "" || $CIRRUS_TAG != ""
skip: $CIRRUS_CHANGE_IN_REPO == $CIRRUS_LAST_GREEN_CHANGE
macos_instance:
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
memory: 8G
env:
CSC_LINK: ENCRYPTED[0078015a03bb6cfdbd80113ae5bbb6f448fd4bbbc40efd81bf2cb1554373046b475a4d7c77e3e3e82ac1ce2f7e3d2da5]
@ -130,39 +131,45 @@ silicon_mac_task:
APPLEID: ENCRYPTED[549ce052bd5666dba5245f4180bf93b74ed206fe5e6e7c8f67a8596d3767c1f682b84e347b326ac318c62a07c8844a57]
APPLEID_PASSWORD: ENCRYPTED[774c3307fd3b62660ecf5beb8537a24498c76e8d90d7f28e5bc816742fd8954a34ffed13f9aa2d1faf66ce08b4496e6f]
TEAM_ID: ENCRYPTED[11f3fedfbaf4aff1859bf6c105f0437ace23d84f5420a2c1cea884fbfa43b115b7834a463516d50cb276d4c4d9128b49]
ROLLING_UPLOAD_TOKEN: ENCRYPTED[690950798401ec3715e9d20ac29a0859d3c58097038081ff6afeaf4721e661672d34eb952d8a6442bc7410821ab8545a]
ROLLING_UPLOAD_TOKEN: ENCRYPTED[f935c396a9f4bca108ec2fdedb00dbc9be2f4c411f100d577acdab42db59ea134be059ce8535396db8222a2b1eb68c27]
prepare_script:
- brew update
- brew install node@16 yarn git python@$PYTHON_VERSION
- python3 -m pip install setuptools
- brew uninstall node
- brew install git python@$PYTHON_VERSION python-setuptools
- git submodule init
- git submodule update
- ln -s /opt/homebrew/bin/python$PYTHON_VERSION /opt/homebrew/bin/python
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- mkdir tj_n && cd tj_n
- curl -L https://github.com/tj/n/archive/0ce85771fdff8f4b3e09ade700461b4f58a64444.tar.gz -O
- tar xf 0ce85771fdff8f4b3e09ade700461b4f58a64444.tar.gz
- sudo bash ./n-0ce85771fdff8f4b3e09ade700461b4f58a64444/bin/n 16
- cd ..
- sudo npm install -g yarn
- sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
install_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- yarn install --ignore-engines || yarn install --ignore-engines
build_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- yarn build
- yarn run build:apm
build_binary_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- yarn dist || yarn dist
rename_binary_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- node script/rename.js "Silicon.Mac"
binary_artifacts:
path: ./binaries/*
test_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- rm -R node_modules/electron; yarn install --check-files
- hdiutil mount binaries/*Pulsar*dmg
- export BINARY_NAME=`ls /Volumes/Pulsar*/Pulsar.app/Contents/MacOS/Pulsar`
- PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
rolling_upload_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- export PATH="/opt/homebrew/bin:$PATH"
- cd ./script/rolling-release-scripts
- npm install
- node ./rolling-release-binary-upload.js cirrus
@ -177,7 +184,7 @@ silicon_mac_task:
# intel_mac_task:
# alias: mac
# macos_instance:
# image: ghcr.io/cirruslabs/macos-monterey-xcode:14
# image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
# memory: 8G
# env:
# CSC_LINK: ENCRYPTED[0078015a03bb6cfdbd80113ae5bbb6f448fd4bbbc40efd81bf2cb1554373046b475a4d7c77e3e3e82ac1ce2f7e3d2da5]
@ -190,30 +197,32 @@ silicon_mac_task:
# - echo A | softwareupdate --install-rosetta
# - arch -x86_64 xcode-select --install
# - arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
# - export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
# - export PATH="/usr/local/bin:$PATH"
# - arch -x86_64 brew update
# - arch -x86_64 brew install node@16 yarn git python@$PYTHON_VERSION
# - arch -x86_64 brew uninstall node
# - arch -x86_64 brew install node@16 git python@$PYTHON_VERSION python-setuptools
# - ln -s /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python
# - npm install -g yarn
# - git submodule init
# - git submodule update
# - sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
# install_script:
# - export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
# - export PATH="/usr/local/bin:$PATH"
# - arch -x86_64 npx yarn install --ignore-engines || arch -x86_64 npx yarn install --ignore-engines
# build_script:
# - export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
# - export PATH="/usr/local/bin:$PATH"
# - arch -x86_64 npx yarn build
# - arch -x86_64 yarn run build:apm
# build_binary_script:
# - export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
# - export PATH="/usr/local/bin:$PATH"
# - arch -x86_64 npx yarn dist || arch -x86_64 npx yarn dist
# rename_binary_script:
# - export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
# - export PATH="/usr/local/bin:$PATH"
# - node script/rename.js "Intel.Mac"
# binary_artifacts:
# path: ./binaries/*
# test_script:
# - export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
# - export PATH="/usr/local/bin:$PATH"
# - rm -R node_modules/electron; yarn install --check-files
# - hdiutil mount binaries/*Pulsar*dmg
# - export BINARY_NAME=`ls /Volumes/Pulsar*/Pulsar.app/Contents/MacOS/Pulsar`

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
*.ts
vendor

View File

@ -19,6 +19,20 @@ module.exports = {
asyncArrow: "always",
named: "never"
}],
"no-constant-condition": "off",
"no-unused-vars": [
"warn",
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_'
}
],
"node/no-missing-require": [
"error",
{
allowModules: ["atom"]
}
],
"node/no-unpublished-require": [
"error",
{

View File

@ -24,11 +24,21 @@ jobs:
build:
strategy:
matrix:
os: [ ubuntu-20.04, windows-latest, macos-latest ]
os: [ ubuntu-latest, macos-latest, windows-latest ]
include:
- os: ubuntu-latest
image: "debian:10"
fail-fast: false
runs-on: ${{ matrix.os }}
container: ${{ matrix.image }}
outputs:
timestamp: ${{ steps.linux-binary-version.outputs.timestamp }}
steps:
- name: Install build dependencies - Linux
if: ${{ runner.os == 'Linux' }}
run: apt-get update && apt-get install -y git python3 python3-pip make gcc g++ libx11-dev libxkbfile-dev pkg-config libsecret-1-dev rpm xvfb ffmpeg zstd
- name: Checkout the latest code
uses: actions/checkout@v3
@ -38,6 +48,10 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
- name: Setup Python
# actions/setup-python's copy of Python is compiled for too new glibc,
# which won't work in a Debian 10 Docker image.
# Get Python from apt repos instead on Linux.
if: ${{ runner.os != 'Linux' }}
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@ -48,11 +62,30 @@ jobs:
# out of the box. 'setuptools' package provides 'distutils'.
run: python3 -m pip install setuptools
- name: Install Yarn - Linux
if: ${{ runner.os == 'Linux' }}
run: npm install -g yarn
- name: Setup Git Submodule
if: ${{ runner.os != 'Linux' }}
run: |
git submodule init
git submodule update
- name: Setup Git Submodule - Linux
if: ${{ runner.os == 'Linux' }}
run: |
git config --global --add safe.directory /__w/pulsar/pulsar
git submodule init
git submodule update
- name: Set Timestamp for Binary Version - Linux
id: linux-binary-version
if: ${{ runner.os == 'Linux' }}
# This output is currently only set for the sake of the Rolling binary upload script.
# See the "test-and-upload-Linux" job below.
run: echo "timestamp=`date -u +%Y%m%d%H`" >> "$GITHUB_OUTPUT"
- name: Check Pulsar Version
if: ${{ runner.os != 'Windows' }}
run: sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
@ -86,6 +119,15 @@ jobs:
yarn build
yarn run build:apm
- name: Cache Pulsar dependencies - Linux
if: ${{ runner.os == 'Linux' }}
uses: actions/cache/save@v3
with:
path: |
node_modules
packages
key: Linux-dependencies-${{ github.sha }}-${{ github.workflow }}
# macOS Signing Stuff
- name: Build Pulsar Binaries (macOS) (Signed)
if: ${{ runner.os == 'macOS' && github.event_name == 'push' }}
@ -135,21 +177,19 @@ jobs:
if: ${{ runner.os == 'Windows' }}
run: node ./script/rename.js "Windows"
- name: Cache Pulsar Binaries - Linux
if: ${{ runner.os == 'Linux' }}
uses: actions/cache/save@v3
with:
path: ./binaries
key: pulsar-Linux-Binaries-${{ github.sha }}-${{ github.workflow }}
- name: Upload Binary Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }} Binaries
path: ./binaries/*
- name: Test Binary - Linux
if: ${{ (runner.os == 'Linux') && env.RUN_LINUX_VT }}
run: |
rm -R node_modules/electron; yarn install --check-files
./binaries/*AppImage --appimage-extract
export BINARY_NAME='squashfs-root/pulsar'
mkdir -p ./tests/videos
Xvfb -screen 0 1024x768x24+32 :99 & nohup ffmpeg -video_size 1024x768 -f x11grab -i :99.0 ./tests/videos/out.mpg & DISPLAY=:99 PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
- name: Test Binary - Windows
if: runner.os == 'Windows' && env.RUN_WINDOWS_VT == true
# TODO: Convert script to PowerShell
@ -168,7 +208,7 @@ jobs:
PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml arch -x86_64 npx playwright test --reporter=junit,list
- name: Add binaries to Rolling Release Repo
if: ${{ github.event_name == 'push' }}
if: ${{ github.event_name == 'push' && runner.os != 'Linux' }}
# We only want to upload rolling binaries if they are a commit to master
# Otherwise we want to not upload if it's a PR or manually triggered build
run: |
@ -177,6 +217,74 @@ jobs:
node ./rolling-release-binary-upload.js
- name: Upload Video Artifacts
# Run whether this job passed or failed, unless explicitly cancelled.
if: ${{ !cancelled() && runner.os != 'Linux' }}
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }} Videos
path: ./tests/videos/**
test-and-upload-Linux:
# I couldn't figure out how to get these visual tests to actually run in the
# Debian Docker environment. If anyone can make it work, feel free to
# re-combine visual testing on Linux back into the main build job above!
# - DeeDeeG
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout the latest code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Restore Cached Pulsar Binaries - Linux
if: ${{ runner.os == 'Linux' }}
uses: actions/cache/restore@v3
with:
path: ./binaries
key: pulsar-Linux-Binaries-${{ github.sha }}-${{ github.workflow }}
- name: Restore Cached Pulsar dependencies - Linux
if: ${{ runner.os == 'Linux' }}
uses: actions/cache/restore@v3
with:
path: |
node_modules
packages
key: Linux-dependencies-${{ github.sha }}-${{ github.workflow }}
- name: Test Binary - Linux
if: ${{ (runner.os == 'Linux') && env.RUN_LINUX_VT }}
run: |
rm -R node_modules/electron; yarn install --check-files
./binaries/*AppImage --appimage-extract
export BINARY_NAME='squashfs-root/pulsar'
mkdir -p ./tests/videos
Xvfb -screen 0 1024x768x24+32 :99 & nohup ffmpeg -video_size 1024x768 -f x11grab -i :99.0 ./tests/videos/out.mpg & DISPLAY=:99 PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
- name: Check Pulsar Version
if: ${{ runner.os != 'Windows' }}
run: sed -i -e "s/[0-9]*-dev/${TIMESTAMP}/g" package.json
env:
TIMESTAMP: ${{needs.build.outputs.timestamp}}
- name: Add binaries to Rolling Release Repo - Linux
if: ${{ github.event_name == 'push' && runner.os == 'Linux' }}
# We only want to upload rolling binaries if they are a commit to master
# Otherwise we want to not upload if it's a PR or manually triggered build
run: |
cd ./script/rolling-release-scripts
npm install
node ./rolling-release-binary-upload.js
- name: Upload Video Artifacts - Linux
# Run whether this job passed or failed, unless explicitly cancelled.
if: ${{ !cancelled() && runner.os == 'Linux' }}
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }} Videos

View File

@ -116,7 +116,9 @@ jobs:
- package: "spell-check"
- package: "status-bar"
- package: "styleguide"
# - package: "symbols-view"
- package: "symbol-provider-ctags"
- package: "symbol-provider-tree-sitter"
- package: "symbols-view"
- package: "tabs"
- package: "timecop"
- package: "tree-view"

View File

@ -18,6 +18,183 @@
better support for "def" elements (example - don't syntax `default` or
`definition` as a `def`, but highlights `p/defresolver`)
## 1.115.0
- Fixed some folds in Ruby like `unless`, some blocks, multiline comments, function calls, and different array syntaxes for strings and keywords.
- Improved the accuracy of indentation hinting in modern Tree-sitter grammars, especially in multi-cursor scenarios.
- Improved the ability of the user to opt into a specific kind of grammar for a specific language.
- Changed the behavior of the `grammar-selector` package so that it will show the user's preferred grammar for a specific language.
- Updated to version `0.20.9` of `web-tree-sitter`.
- Improved syntax highlighting, indentation, and code folding in various languages, including TypeScript, shell scripts, Ruby, and C/C++.
### Pulsar
- Fixed: Fixed folds for Ruby [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/956)
- Fixed: Tree-sitter fixes: 1.115 edition [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/941)
- Updated: cirrus: Update Rolling upload token again [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/960)
- Fixed: cirrus: Various fixes for macOS Cirrus CI [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/961)
- Fixed: Fix(fuzzy-finder) fs.lstatSync throws Exception if not a file or dir [@schadomi7](https://github.com/pulsar-edit/pulsar/pull/944)
- Updated: CI: Update Rolling upload token for Cirrus CI [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/936)
- Updated: Cirrus: Install older dotenv gem version ~> 2.8 (< 3) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/937)
## 1.114.0
- Prevented an exception raised in the command palette in certain unusual filtering scenarios.
- Refrain from rendering anchor icons when showing a package's README file in `settings-view`.
- Build Linux binaries on Debian 10, for older glibc and compatibility with older Linux distros.
- Fixed a rendering error in `atom.ui.markdown.render` when `disableMode` was set to `"strict"` and the input contained HTML line breaks.
- Added support for the semanticolor package in modern tree-sitter grammars.
- Added new `--force` flag to `ppm link` command that will uninstall any conflicting package already installed.
- Added language entity colors to `syntax-variables.less`.
- Numerous Tree-Sitter Grammar syntax highlighting fixes.
- Bumped dugite to make the github package compatible with ARM Linux.
### Pulsar
- Fixed: fix(tree-sitter): pass node text to grammar [@claytonrcarter](https://github.com/pulsar-edit/pulsar/pull/860)
- Fixed: Fix issue with Markdown rendering after line break in strict mode [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/889)
- Updated: Update README badges [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/891)
- Updated: Update copyright year to 2024 [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/870)
- Added: CI: build Linux x86-64 binaries on older Linux [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/858)
- Fixed: Tree-sitter rolling fixes (January edition) [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/859)
- Fixed: Fix failing spec [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/902)
- Fixed: [settings-view] Don't display heading anchor icons within a README [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/905)
- Updated: ppm: Update ppm to commit 241d794f326b63b5abdb9769 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/908)
- Fixed: script: Update version check in Rolling release binary upload script to exclude '-dev' versions [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/903)
- Fixed: CI: Fix tag Linux binaries are uploaded to for Rolling [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/901)
- Fixed: [command-palette] Guard against failure to highlight a match [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/913)
- Fixed: `symbols-view` rolling fixes [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/861)
- Fixed: Tree-sitter rolling fixes (February) [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/906)
- Updated: [meta] Update Cirrus `GITHUB_TOKEN` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/924)
- Updated: deps: Update github to v0.36.20-pretranspiled to bump dugite [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/925)
- Fixed: [symbols-view] Remap go-to-declaration commands on Windows/Linux [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/926)
### PPM
- Fixed: Fix test failure due to missing atom command [@toddy15](https://github.com/pulsar-edit/ppm/pull/124)
- Updated: Update syntax-variables.less to include language entity colors [@savetheclocktower](https://github.com/pulsar-edit/ppm/pull/123)
- Added: feat(link): add --force flag [@claytonrcarter](https://github.com/pulsar-edit/ppm/pull/122)
### github
- Updated: Bump dugite to 2.5.2 [@DeeDeeG](https://github.com/pulsar-edit/github/pull/39)
## 1.113.0
- Enabled Modern Tree-sitter Grammars by default
- Added a modern Tree-sitter grammar for PHP.
- Fix a measurement issue that was causing visual glitches in the `github` package's diff views.
- Enabled the core `symbols-view` package to accept symbols from a number of sources, including Tree-sitter grammars and IDE packages.
- Switch default to false for converting ASCII emoticons to emoji when rendering Markdown.
- Fix certain find-and-replace scenarios when the "Preserve Case During Replace" setting is enabled.
- Fix an issue in `symbols-view` when returning from visiting a symbol declaration.
### Pulsar
- Fixed: Tree-sitter fixes for December (including a PHP grammar!) [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/852)
- Added: Make `useExperimentalModernTreeSitter` the default... [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/855)
- Fixed: Ensure editor is visible before measuring block decorations [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/854)
- Added: Overhaul `symbols-view` [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/829)
- Added: Default to no emoji when rendering Markdown [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/850)
### find-and-replace
- Fixed: [find-and-replace] Fix `capitalize` utility [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/849)
### symbols-view
- Fixed: [symbols-view] Fix issue with returning from a declaration [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/864)
## 1.112.1
- Fixed a bug in PPM that prevented package publishing.
### Pulsar
- Bumped: ppm: Update ppm to commit 0bc207133b26de82aa28500e [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/845)
- Bumped: ppm: Update ppm to commit 7dfd9ca8cf877391fc6ef1d5 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/842)
### PPM
- Fixed: Fix placement of `setTimeout` [@savetheclocktower](https://github.com/pulsar-edit/ppm/pull/118)
- Fixed: Fix bugs found in `publish` after 1.112 release [@savetheclocktower](https://github.com/pulsar-edit/ppm/pull/116)
## 1.112.0
- Fixed github package not giving feedback when a token with the wrong scopes was entered, tweak scope-checking logic to match expectations, and log incorrect scopes.
- Various cleanups, maintenance and upkeep of the PPM repo.
- Added options for a user to control when to automatically show or hide the wrap-guide; "Always", "When soft wrap is enabled", and "When soft wrap at preferred line length is enabled".
- Updated network handling in PPM to something newer and more secure.
- Updated most of PPM's code to use async/await and promises internally.
- Created `atom.ui.fuzzyMatcher` API, moving the Pulsar `fuzzy-finder` module into the core of the editor for community packages to utilize.
- Fixed an issue that prevented Pulsar from inheriting the directory from which the `pulsar` binary was run.
### Pulsar
- Added: [tree-sitter] Share config caches between `ScopeResolver`s [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/836)
- Bumped: deps: Update github to v0.36.19-pretranspiled (fix silent failure when inputting a token with incorrect scopes) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/838)
- Bumped: ppm: Update ppm to commit 957acbd90cfc9f361c183b3c [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/837)
- Added: Return to original logic for `ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT` [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/831)
- Added: Moving fuzzy-native to core [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/774)
- Fixed: Tree-sitter rolling fixes for November [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/819)
- Fixed: CI: Update Rolling upload token for Cirrus [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/812)
- Bumped: ppm: Update to commit 13fb2845e00d7e04c2461f93 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/809)
- Added: Ability to indicate when to automatically show or hide the wrap-guide [@Trigan2025](https://github.com/pulsar-edit/pulsar/pull/780)
### PPM
- Bumped: fix(deps): update dependency semver to v7.5.2 [security] [@renovate](https://github.com/pulsar-edit/ppm/pull/114)
- Added: Configure Renovate [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/110)
- Added: Migrate from `rimraf` to NodeJS `fs` [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/108)
- Added: Implement Codacy Recommendations [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/113)
- Removed: Prune outdated Deps [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/109)
- Removed: Remove unused Variables [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/112)
- Added: Add Codacy and Friends Configuration [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/111)
- Removed: src: Delete unused code in uninstall.js [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/104)
- Fixed: src: Fix usage/help text (and error message) for -b/-t flags for ppm install [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/105)
- Added: Repository Cleanup [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/107)
- Fixed: Fix Newer NodeJS CI [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/106)
- Fixed: src: Stop pinging backend during package uninstalls [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/103)
- Added: Asyncify without topmost interface [@2colours](https://github.com/pulsar-edit/ppm/pull/95)
- Fixed: CI: Work around a weird bug in Yarn v1.x [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/101)
- Fixed: src: Rebrand two lines of "ppm --version" output [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/100)
- Bumped: deps: Bump nan for compatibility with newer NodeJS [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/97)
- Fixed: Fix Error Handling [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/99)
- Removed: Remove `request` Migrate to `superagent` && Fix CI [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/87)
### github
- Added: lib: Allow parent scopes when checking if each required scope is set [@DeeDeeG](https://github.com/pulsar-edit/github/pull/38)
## 1.111.0
- Added a new "UI" API to `atom`, accessible via `atom.ui`. This exposes a `markdown` object, allowing community packages to offload Markdown handling to the core editor.
- Fine-tuned/deduped dependencies to remove ~35.5 MB from Pulsar's installed size.
- Fixed an issue that sometimes caused text to shift or disappear after an editor pane regains focus.
- Fixed scoping/highlighting of single-quoted (`'...'`) and C-style (`$'...'`) strings in shell scripts.
- Fixed an issue with the "Dismiss this Version" button (in the `pulsar-updater` package).
- Fixed an issue with how Linux Pulsar binaries were built, to ensure compatibility with non-bleeding edge glibc versions. (Compatibility with even older glibc versions is still being looked into, for the folks on older or RHEL-compatible distros.)
### Pulsar
- Fixed: meta: Update CirrusCI GitHub Token [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/804)
- Bumped: deps: Update `github`, for `dugite` deduping purposes [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/799)
- Fixed: Tree-sitter running fixes (October) [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/789)
- Fixed: Prevent "half screen" bug by resetting scroll position when editor regains focus [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/798)
- Added: [core] New `UI` API [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/763)
- Fixed: CI: Build binaries for tag pushes (GitHub Actions) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/782)
- Added: [DOCS] Add non-macOS keybindings for fuzzy-finder readme [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/796)
- Removed: Remove Teletype from Welcome guide [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/793)
- Fixed: CI: Python 3.12-related fixes on Cirrus CI [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/794)
- Fixed: CI: Work around missing 'distutils' for Python 3.12+ (GHA round two) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/795)
- Added: [meta] Create Workflow to validate WASM Grammar Changes [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/740)
- Fixed: 🐛 ✅ Fix caching for "Dismiss this Version" in pulsar-updater [@kiskoza](https://github.com/pulsar-edit/pulsar/pull/785)
- Fixed: [tree-sitter] Fix proliferation of extra injection layers [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/783)
- Added: CI: Increase timeout length for macOS binary builds [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/781)
- Fixed: Fix the matching of `$'...'` strings. [@danfuzz](https://github.com/pulsar-edit/pulsar/pull/776)
- Fixed: [meta] Install Python package `setuptools` && Use Python `3.12.x` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/779)
- Fixed: Update `web-tree-sitter` to include `isalnum` builtin [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/770)
- Fixed: [meta] Build x86 Linux binaries on Ubuntu 20.04, for older (more compatible) glibc [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/760)
- Bumped: [core] Bump `git-utils`: `5.7.1` => `^5.7.3` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/772)
- Removed: [core] Cleanup Unused Deps [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/771)
### github
- Bumped: deps: Update `whats-my-line` to bump `dugite` to 2.1.0 [@DeeDeeG](https://github.com/pulsar-edit/github/pull/36)
### whats-my-line
- Bumped: Pin `dugite` to `2.1.0` [@confused-Techie](https://github.com/pulsar-edit/whats-my-line/pull/7)
- Bumped: Bump dugite && Bump `package.json` version [@confused-Techie](https://github.com/pulsar-edit/whats-my-line/pull/2)
- Added: Add dugite tests [@confused-Techie](https://github.com/pulsar-edit/whats-my-line/pull/4)
- Removed: Remove TypeScript [@confused-Techie](https://github.com/pulsar-edit/whats-my-line/pull/3)
- Added: Setup Tests and Modernize [@confused-Techie](https://github.com/pulsar-edit/whats-my-line/pull/1)
## 1.110.0
- Made the modification of `editor.preferredLineLength` configurable within `wrap-guide` when changing `wrap-guide.columns`

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022-2023 Pulsar-Edit
Copyright (c) 2022-2024 Pulsar-Edit
Original work copyright (c) 2011-2022 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -5,7 +5,9 @@
[![Badge Discussions]][Discussions]
[![Badge Discord]][Discord]
[![Badge Reddit]][Reddit]
[![Badge Reddit]][Reddit]
[![Badge Mastodon]][Mastodon]
[![Badge Lemmy]][Lemmy]
[![Badge Status]][Status]
[![Badge License]][License]
@ -51,6 +53,8 @@
[Status]: https://cirrus-ci.com/github/pulsar-edit/pulsar/master
[Codacy]: https://app.codacy.com/gh/pulsar-edit/pulsar
[Reddit]: https://www.reddit.com/r/pulsaredit/
[Mastodon]: https://fosstodon.org/@pulsaredit/
[Lemmy]: https://lemmy.ml/c/pulsaredit/
[#]: #
@ -72,14 +76,18 @@
<!---------------------------{ Badges }--------------------------->
[Badge OpenCollective]: https://opencollective.com/pulsar-edit/tiers/badge.svg
[Badge Discussions]: https://img.shields.io/github/discussions/pulsar-edit/.github?style=for-the-badge&labelColor=78af9f&color=5a8377
[Badge Discussions]: https://img.shields.io/github/discussions/pulsar-edit/.github?style=for-the-badge&logo=GitHub&labelColor=50555b&color=35393d
[Badge Upstream]: https://img.shields.io/badge/Upstream_Status-Sunset-966227.svg?style=for-the-badge&labelColor=c38033
[Badge Discord]: https://img.shields.io/badge/Discord-4b7494.svg?style=for-the-badge&labelColor=6399c4&logoColor=white&logo=Discord
[Badge License]: https://img.shields.io/badge/License-MIT-ac7f31.svg?style=for-the-badge&labelColor=e5ab42
[Badge Crowdin]: https://badges.crowdin.net/pulsar-edit/localized.svg
[Badge Codacy]: https://app.codacy.com/project/badge/Grade/24873ecb93dc4c1d865202ce5b24efc1
[Badge Reddit]: https://img.shields.io/reddit/subreddit-subscribers/pulsaredit?style=for-the-badge&label=Reddit&logoColor=white&logo=Reddit&labelColor=e05d44&color=b14835
<!-- Reddit subscribers badge broken. See: https://github.com/badges/shields/issues/9817 & https://github.com/badges/shields/issues/9256 -->
<!-- [Badge Reddit]: https://img.shields.io/reddit/subreddit-subscribers/pulsaredit?style=for-the-badge&label=Reddit&logoColor=white&logo=Reddit&labelColor=e05d44&color=b14835 -->
[Badge Reddit]: https://img.shields.io/badge/%2Fr%2Fpulsaredit-e05d44?style=for-the-badge&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835
[Badge Status]: https://img.shields.io/cirrus/github/pulsar-edit/pulsar?style=for-the-badge&labelColor=c77b7f&label=Build%20Status&color=8d575a
[Badge Mastodon]: https://img.shields.io/mastodon/follow/109416671848539153?domain=https%3A%2F%2Ffosstodon.org%2F&style=for-the-badge&logo=Mastodon&logoColor=white&label=Mastodon&labelColor=a0a0d8&color=7c7cc1
[Badge Lemmy]: https://img.shields.io/lemmy/pulsaredit%40lemmy.ml?style=for-the-badge&logo=Lemmy&logoColor=white&label=Lemmy&labelColor=64ad82&color=1d9b52
<!--------------------------{ Buttons }--------------------------->

View File

@ -7,6 +7,23 @@
</dd>
<dt><a href="#Clipboard">Clipboard</a></dt>
<dd></dd>
<dt><a href="#Container">Container</a></dt>
<dd><p>A container capture. When another capture&#39;s node is contained by the
definition capture&#39;s node, it gets added to this instance.</p>
</dd>
<dt><a href="#CaptureOrganizer">CaptureOrganizer</a></dt>
<dd><p>Keeps track of @definition.* captures and the captures they may contain.</p>
</dd>
<dt><a href="#InvalidProviderError">InvalidProviderError</a><code>Error</code></dt>
<dd><p>An error thrown when a newly added symbol provider does not conform to its
contract.</p>
</dd>
<dt><a href="#ListController">ListController</a></dt>
<dd><p>A class for setting various UI properties on a symbol list palette. This is a
privilege given to the “main” (or <em>exclusive</em>) provider for a given task.</p>
<p>This is how we allow a provider to communicate its state to the UI without
giving it full control over the <code>SelectListView</code> used to show results.</p>
</dd>
</dl>
## Constants
@ -173,6 +190,9 @@ style: Exclusively used for the <code>style</code> attribute</p>
## Functions
<dl>
<dt><a href="#renderMarkdown">renderMarkdown(content, givenOpts)</a><code>string</code></dt>
<dd><p>Takes a Markdown document and renders it as HTML.</p>
</dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
@ -199,6 +219,25 @@ style: Exclusively used for the <code>style</code> attribute</p>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#isIterable">isIterable(obj)</a><code>Boolean</code></dt>
<dd><p>Ensures an object can be iterated over.</p>
<p>The contract with the symbol providers is that they return an object that
gives us symbol objects when we iterate over it. It&#39;ll probably be an array,
but we&#39;re cool with anything iterable.</p>
</dd>
<dt><a href="#timeout">timeout(ms)</a><code>Promise.&lt;true&gt;</code></dt>
<dd><p>Returns a promise that resolves after a given number of milliseconds.</p>
</dd>
<dt><a href="#getBadgeTextVariant">getBadgeTextVariant(text)</a><code>String</code></dt>
<dd><p>Given a string of text, returns a hexadecimal character from <code>0</code> to <code>f</code> to
represent a classification “bucket.” This is used when assigning colors to
various symbol badges.</p>
</dd>
<dt><a href="#badge">badge(text, options)</a><code>Element</code></dt>
<dd><p>Return a DOM element for a badge for a given symbol tag name.</p>
</dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
@ -355,6 +394,37 @@ atom.clipboard.write('hello');
console.log(atom.clipboard.read());
```
<a name="Container"></a>
## Container
A container capture. When another capture's node is contained by the
definition capture's node, it gets added to this instance.
**Kind**: global class
<a name="CaptureOrganizer"></a>
## CaptureOrganizer
Keeps track of @definition.* captures and the captures they may contain.
**Kind**: global class
<a name="InvalidProviderError"></a>
## InvalidProviderError ⇐ <code>Error</code>
An error thrown when a newly added symbol provider does not conform to its
contract.
**Kind**: global class
**Extends**: <code>Error</code>
<a name="ListController"></a>
## ListController
A class for setting various UI properties on a symbol list palette. This is a
privilege given to the “main” (or _exclusive_) provider for a given task.
This is how we allow a provider to communicate its state to the UI without
giving it full control over the `SelectListView` used to show results.
**Kind**: global class
<a name="etch"></a>
## etch
@ -625,6 +695,68 @@ This file aims to run some short simple tests against `update.js`. Focusing
**Babel**:
<a name="beforeEach"></a>
## beforeEach()
**Kind**: global function
**Babel**:
<a name="isIterable"></a>
## isIterable(obj) ⇒ <code>Boolean</code>
Ensures an object can be iterated over.
The contract with the symbol providers is that they return an object that
gives us symbol objects when we iterate over it. It'll probably be an array,
but we're cool with anything iterable.
**Kind**: global function
**Returns**: <code>Boolean</code> - Whether the item will respond correctly to a `for..of`
loop.
| Param | Type | Description |
| --- | --- | --- |
| obj | <code>?</code> | Anything. |
<a name="timeout"></a>
## timeout(ms) ⇒ <code>Promise.&lt;true&gt;</code>
Returns a promise that resolves after a given number of milliseconds.
**Kind**: global function
**Returns**: <code>Promise.&lt;true&gt;</code> - A promise that resolves with `true` as its argument.
| Param | Type | Description |
| --- | --- | --- |
| ms | <code>Number</code> | Number of milliseconds after which to resolve. |
<a name="getBadgeTextVariant"></a>
## getBadgeTextVariant(text) ⇒ <code>String</code>
Given a string of text, returns a hexadecimal character from `0` to `f` to
represent a classification “bucket.” This is used when assigning colors to
various symbol badges.
**Kind**: global function
**Returns**: <code>String</code> - A single character that represents a hexadecimal digit.
| Param | Type | Description |
| --- | --- | --- |
| text | <code>String</code> | The text of the badge. |
<a name="badge"></a>
## badge(text, options) ⇒ <code>Element</code>
Return a DOM element for a badge for a given symbol tag name.
**Kind**: global function
**Returns**: <code>Element</code> - An element for adding to an `atom-select-view` entry.
| Param | Type | Description |
| --- | --- | --- |
| text | <code>String</code> | The text of the tag. |
| options | <code>Object</code> | Options. Defaults to an empty object. |
| options.variant | <code>Boolean</code> | Whether to add a class name for the badge's “variant.” If enabled, this will attempt to assign a different badge color for each kind of tag. Optional; defaults to `false`. |
<a name="beforeEach"></a>
## beforeEach()
**Kind**: global function
**Babel**:

View File

@ -7,6 +7,23 @@
</dd>
<dt><a href="#Clipboard">Clipboard</a></dt>
<dd></dd>
<dt><a href="#Container">Container</a></dt>
<dd><p>A container capture. When another capture&#39;s node is contained by the
definition capture&#39;s node, it gets added to this instance.</p>
</dd>
<dt><a href="#CaptureOrganizer">CaptureOrganizer</a></dt>
<dd><p>Keeps track of @definition.* captures and the captures they may contain.</p>
</dd>
<dt><a href="#InvalidProviderError">InvalidProviderError</a><code>Error</code></dt>
<dd><p>An error thrown when a newly added symbol provider does not conform to its
contract.</p>
</dd>
<dt><a href="#ListController">ListController</a></dt>
<dd><p>A class for setting various UI properties on a symbol list palette. This is a
privilege given to the “main” (or <em>exclusive</em>) provider for a given task.</p>
<p>This is how we allow a provider to communicate its state to the UI without
giving it full control over the <code>SelectListView</code> used to show results.</p>
</dd>
</dl>
## Constants
@ -173,6 +190,9 @@ style: Exclusively used for the <code>style</code> attribute</p>
## Functions
<dl>
<dt><a href="#renderMarkdown">renderMarkdown(content, givenOpts)</a><code>string</code></dt>
<dd><p>Takes a Markdown document and renders it as HTML.</p>
</dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
@ -199,6 +219,25 @@ style: Exclusively used for the <code>style</code> attribute</p>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#isIterable">isIterable(obj)</a><code>Boolean</code></dt>
<dd><p>Ensures an object can be iterated over.</p>
<p>The contract with the symbol providers is that they return an object that
gives us symbol objects when we iterate over it. It&#39;ll probably be an array,
but we&#39;re cool with anything iterable.</p>
</dd>
<dt><a href="#timeout">timeout(ms)</a><code>Promise.&lt;true&gt;</code></dt>
<dd><p>Returns a promise that resolves after a given number of milliseconds.</p>
</dd>
<dt><a href="#getBadgeTextVariant">getBadgeTextVariant(text)</a><code>String</code></dt>
<dd><p>Given a string of text, returns a hexadecimal character from <code>0</code> to <code>f</code> to
represent a classification “bucket.” This is used when assigning colors to
various symbol badges.</p>
</dd>
<dt><a href="#badge">badge(text, options)</a><code>Element</code></dt>
<dd><p>Return a DOM element for a badge for a given symbol tag name.</p>
</dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
<dd></dd>
<dt><a href="#beforeEach">beforeEach()</a></dt>
@ -363,6 +402,37 @@ atom.clipboard.write('hello');
console.log(atom.clipboard.read());
```
<a name="Container"></a>
## Container
A container capture. When another capture's node is contained by the
definition capture's node, it gets added to this instance.
**Kind**: global class
<a name="CaptureOrganizer"></a>
## CaptureOrganizer
Keeps track of @definition.* captures and the captures they may contain.
**Kind**: global class
<a name="InvalidProviderError"></a>
## InvalidProviderError ⇐ <code>Error</code>
An error thrown when a newly added symbol provider does not conform to its
contract.
**Kind**: global class
**Extends**: <code>Error</code>
<a name="ListController"></a>
## ListController
A class for setting various UI properties on a symbol list palette. This is a
privilege given to the “main” (or _exclusive_) provider for a given task.
This is how we allow a provider to communicate its state to the UI without
giving it full control over the `SelectListView` used to show results.
**Kind**: global class
<a name="etch"></a>
## etch
@ -633,6 +703,68 @@ This file aims to run some short simple tests against `update.js`. Focusing
**Babel**:
<a name="beforeEach"></a>
## beforeEach()
**Kind**: global function
**Babel**:
<a name="isIterable"></a>
## isIterable(obj) ⇒ <code>Boolean</code>
Ensures an object can be iterated over.
The contract with the symbol providers is that they return an object that
gives us symbol objects when we iterate over it. It'll probably be an array,
but we're cool with anything iterable.
**Kind**: global function
**Returns**: <code>Boolean</code> - Whether the item will respond correctly to a `for..of`
loop.
| Param | Type | Description |
| --- | --- | --- |
| obj | <code>?</code> | Anything. |
<a name="timeout"></a>
## timeout(ms) ⇒ <code>Promise.&lt;true&gt;</code>
Returns a promise that resolves after a given number of milliseconds.
**Kind**: global function
**Returns**: <code>Promise.&lt;true&gt;</code> - A promise that resolves with `true` as its argument.
| Param | Type | Description |
| --- | --- | --- |
| ms | <code>Number</code> | Number of milliseconds after which to resolve. |
<a name="getBadgeTextVariant"></a>
## getBadgeTextVariant(text) ⇒ <code>String</code>
Given a string of text, returns a hexadecimal character from `0` to `f` to
represent a classification “bucket.” This is used when assigning colors to
various symbol badges.
**Kind**: global function
**Returns**: <code>String</code> - A single character that represents a hexadecimal digit.
| Param | Type | Description |
| --- | --- | --- |
| text | <code>String</code> | The text of the badge. |
<a name="badge"></a>
## badge(text, options) ⇒ <code>Element</code>
Return a DOM element for a badge for a given symbol tag name.
**Kind**: global function
**Returns**: <code>Element</code> - An element for adding to an `atom-select-view` entry.
| Param | Type | Description |
| --- | --- | --- |
| text | <code>String</code> | The text of the tag. |
| options | <code>Object</code> | Options. Defaults to an empty object. |
| options.variant | <code>Boolean</code> | Whether to add a class name for the badge's “variant.” If enabled, this will attempt to assign a different badge color for each kind of tag. Optional; defaults to `false`. |
<a name="beforeEach"></a>
## beforeEach()
**Kind**: global function
**Babel**:

View File

@ -14,7 +14,7 @@ const languages = [
// {language: "gfm", code: '10', checks: {numeric: '10'}},
// {language: "git", code: '10', checks: {numeric: '10'}},
{language: "Go", code: '10', checks: {numeric: '10'}},
{language: "HTML", code: '<foo type="10"></foo>', checks: {string: '10'}},
{language: "HTML", code: '<foo type="10"></foo>', checks: {quoted: '"10"'}},
{language: "XML", code: '<foo type="10"></foo>', checks: {string: '"10"'}},
// {language: "hyperlink", code: '10', checks: {numeric: '10'}},
{language: "JSON", code: '10', checks: {numeric: '10'}},
@ -23,7 +23,7 @@ const languages = [
// {language: "mustache", code: '10', checks: {numeric: '10'}},
{language: "Objective C", code: '10', checks: {numeric: '10'}},
{language: "Perl", code: '10', checks: {numeric: '10'}},
{language: "PHP", code: '<? 10 %>', checks: {numeric: '10'}},
{language: "PHP", code: '<? $foo ?>', checks: {variable: '$foo'}},
// {language: "property-list", code: '10', checks: {numeric: '10'}},
{language: "Python", code: '10', checks: {numeric: '10'}},
{language: "Ruby on Rails", code: '10', checks: {numeric: '10'}},

View File

@ -2,7 +2,7 @@
"name": "pulsar",
"author": "Pulsar-Edit <admin@pulsar-edit.dev>",
"productName": "Pulsar",
"version": "1.110.0-dev",
"version": "1.115.0-dev",
"description": "A Community-led Hyper-Hackable Text Editor",
"branding": {
"id": "pulsar",
@ -30,6 +30,7 @@
"dependencies": {
"@atom/source-map-support": "^0.3.4",
"@babel/core": "7.18.6",
"@pulsar-edit/fuzzy-native": "https://github.com/pulsar-edit/fuzzy-native.git#c6ddd2e0ace7b3cfe8082fcbe5985c49f76da5b8",
"about": "file:packages/about",
"archive-view": "file:packages/archive-view",
"async": "3.2.4",
@ -62,6 +63,7 @@
"deprecation-cop": "file:packages/deprecation-cop",
"dev-live-reload": "file:packages/dev-live-reload",
"document-register-element": "https://github.com/pulsar-edit/document-register-element.git#1f5868f",
"dompurify": "^3.0.6",
"encoding-selector": "file:packages/encoding-selector",
"etch": "0.14.1",
"event-kit": "^2.5.3",
@ -75,7 +77,7 @@
"fuzzy-finder": "file:packages/fuzzy-finder",
"git-diff": "file:packages/git-diff",
"git-utils": "^5.7.3",
"github": "https://codeload.github.com/pulsar-edit/github/tar.gz/refs/tags/v0.36.17-pretranspiled",
"github": "https://github.com/pulsar-edit/github/archive/refs/tags/v0.36.20-pretranspiled.tar.gz",
"go-to-line": "file:packages/go-to-line",
"grammar-selector": "file:packages/grammar-selector",
"grim": "2.0.3",
@ -124,6 +126,10 @@
"line-ending-selector": "file:packages/line-ending-selector",
"line-top-index": "0.3.1",
"link": "file:packages/link",
"markdown-it": "^13.0.2",
"markdown-it-emoji": "^2.0.2",
"markdown-it-github-headings": "^2.0.1",
"markdown-it-task-checkbox": "^1.0.6",
"markdown-preview": "file:./packages/markdown-preview",
"minimatch": "^3.0.3",
"mocha": "6.2.3",
@ -153,14 +159,16 @@
"service-hub": "^0.7.4",
"settings-view": "file:packages/settings-view",
"sinon": "9.2.1",
"snippets": "github:pulsar-edit/snippets#ba70705",
"snippets": "github:pulsar-edit/snippets#6b9163415270b8757b262f5dfaafaa05d47324a9",
"solarized-dark-syntax": "file:packages/solarized-dark-syntax",
"solarized-light-syntax": "file:packages/solarized-light-syntax",
"spell-check": "file:packages/spell-check",
"status-bar": "file:packages/status-bar",
"styleguide": "file:./packages/styleguide",
"superstring": "^2.4.4",
"symbols-view": "https://codeload.github.com/atom/symbols-view/legacy.tar.gz/refs/tags/v0.118.4",
"symbol-provider-ctags": "file:./packages/symbol-provider-ctags",
"symbol-provider-tree-sitter": "file:./packages/symbol-provider-tree-sitter",
"symbols-view": "file:./packages/symbols-view",
"tabs": "file:packages/tabs",
"temp": "0.9.4",
"text-buffer": "^13.18.6",
@ -176,6 +184,7 @@
"whitespace": "file:./packages/whitespace",
"winreg": "^1.2.1",
"wrap-guide": "file:./packages/wrap-guide",
"yaml-front-matter": "^4.1.1",
"yargs": "17.6.2"
},
"packageDependencies": {
@ -211,7 +220,7 @@
"exception-reporting": "file:./packages/exception-reporting",
"find-and-replace": "file:./packages/find-and-replace",
"fuzzy-finder": "file:packages/fuzzy-finder",
"github": "0.36.17",
"github": "0.36.20",
"git-diff": "file:./packages/git-diff",
"go-to-line": "file:./packages/go-to-line",
"grammar-selector": "file:./packages/grammar-selector",
@ -230,7 +239,9 @@
"spell-check": "file:./packages/spell-check",
"status-bar": "file:./packages/status-bar",
"styleguide": "file:./packages/styleguide",
"symbols-view": "0.118.4",
"symbol-provider-ctags": "file:./packages/symbol-provider-ctags",
"symbol-provider-tree-sitter": "file:./packages/symbol-provider-tree-sitter",
"symbols-view": "file:./packages/symbols-view",
"tabs": "file:./packages/tabs",
"timecop": "file:./packages/timecop",
"tree-view": "file:./packages/tree-view",

View File

@ -9,3 +9,4 @@
@import "styles/syntax/base.less";
@import "styles/syntax/css.less";
@import "styles/syntax/html.less";
@import "styles/syntax/json.less";

View File

@ -229,6 +229,12 @@
&.syntax--italic {
font-style: italic;
}
// Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity,
// we assign the color of a comment to this new scope.
&.syntax--horizontal-rule {
color: #8A8A8A;
}
}
// /* comment */

View File

@ -0,0 +1,11 @@
.syntax--source.syntax--json {
// Color JSON keys differently from other strings.
.syntax--meta.syntax--structure.syntax--key {
.syntax--string.syntax--quoted.syntax--double {
color: #96CBFE;
}
}
}

View File

@ -8,3 +8,4 @@
@import "styles/syntax/base.less";
@import "styles/syntax/css.less";
@import "styles/syntax/json.less";

View File

@ -198,6 +198,12 @@
&.syntax--italic {
font-style: italic;
}
// Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity,
// we assign the color of a comment to this new scope.
&.syntax--horizontal-rule {
color: #999988;
}
}
// /* comment */

View File

@ -0,0 +1,11 @@
.syntax--source.syntax--json {
// Color JSON keys differently from other strings.
.syntax--meta.syntax--structure.syntax--key {
.syntax--string.syntax--quoted.syntax--double {
color: #008080;
}
}
}

View File

@ -33,7 +33,7 @@ const packagesToTest = {
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const whenEditorReady = function(editor) {
const whenEditorReady = function (editor) {
const languageMode = editor.getBuffer().getLanguageMode();
if (!languageMode.constructor.name.includes('TreeSitter')) {
return Promise.resolve();
@ -105,8 +105,6 @@ describe("CSS property name and value autocompletions", async () => {
await atom.workspace.open(packagesToTest[packageLabel].file);
editor = atom.workspace.getActiveTextEditor();
await whenEditorReady(editor);
console.warn('USING TREE SITTER?!?', packageLabel, meta.useTreeSitter);
atom.config.set('core.useExperimentalModernTreeSitter', meta.useTreeSitter ?? false);
atom.config.set('core.useTreeSitterParsers', meta.useTreeSitter ?? false);
});
@ -738,7 +736,7 @@ div:nth {
})
);
Object.keys(packagesToTest).forEach(function(packageLabel) {
Object.keys(packagesToTest).forEach(function (packageLabel) {
if (packagesToTest[packageLabel].name !== 'language-css') {
describe(`${packageLabel} files`, async () => {
beforeEach(async () => {

View File

@ -9,7 +9,8 @@ const provider = {
getSuggestions (request) {
try {
if (request.editor.getBuffer().getLanguageMode().tree) {
let languageMode = request.editor.getBuffer().getLanguageMode();
if (languageMode.constructor.name === 'TreeSitterLanguageMode') {
return getSuggestionsWithTreeSitter(request)
} else {
return getSuggestionsWithTextMate(request)

View File

@ -50,7 +50,7 @@ function isTagStart ({prefix, scopeDescriptor, bufferPosition, editor}) {
function isAttributeStart ({prefix, scopeDescriptor, bufferPosition, editor}) {
const scopes = scopeDescriptor.getScopesArray()
if (!getPreviousAttribute(editor, bufferPosition) && prefix && !prefix.trim()) {
return hasTagScope(scopes)
return hasTagScope(scopes) || afterTagScope(editor, bufferPosition)
}
const previousBufferPosition = [bufferPosition.row, Math.max(0, bufferPosition.column - 1)]
@ -68,6 +68,25 @@ function isAttributeStart ({prefix, scopeDescriptor, bufferPosition, editor}) {
)
}
// This fixes the
//
// <div |
//
// scenario in Tree-sitter grammars — no closing `>` on the tag, so we should
// move back to the nearest text and try to read the scopes from there.
// Designed to work no matter how many spaces there are between the end of the
// tag name and the cursor.
function afterTagScope (editor, bufferPosition) {
let cursor = editor.getCursors().find(cursor => {
return cursor.getBufferPosition().isEqual(bufferPosition)
})
if (!cursor) return false;
let position = cursor.getPreviousWordBoundaryBufferPosition();
position = position.translate([0, -1]);
let scopes = editor.scopeDescriptorForBufferPosition(position);
return scopes.getScopesArray().some(t => t.startsWith('entity.name.tag'));
}
function isAttributeValueStart ({scopeDescriptor, bufferPosition, editor}) {
const scopes = scopeDescriptor.getScopesArray()
@ -75,6 +94,11 @@ function isAttributeValueStart ({scopeDescriptor, bufferPosition, editor}) {
const previousScopes = editor.scopeDescriptorForBufferPosition(previousBufferPosition)
const previousScopesArray = previousScopes.getScopesArray()
// This is an unambiguous case — if the cursor is on the right side of the
// opening quote, then we must be in the right place.
if (previousScopesArray.includes('punctuation.definition.string.begin.html'))
return true
// autocomplete here: attribute="|"
// not here: attribute=|""
// or here: attribute=""|

View File

@ -0,0 +1,13 @@
module.exports = {
env: { jasmine: true },
globals: {
"waitsForPromise": true,
"advanceClock": true
},
rules: {
"node/no-unpublished-require": "off",
"node/no-extraneous-require": "off",
"no-unused-vars": "off",
"no-empty": "off"
}
};

View File

@ -1,5 +1,5 @@
describe('HTML autocompletions', () => {
let editor, provider
let editor, provider, languageMode
function getCompletions () {
const cursor = editor.getLastCursor()
@ -32,51 +32,66 @@ describe('HTML autocompletions', () => {
return -1;
}
beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage('autocomplete-html'))
waitsForPromise(() => atom.packages.activatePackage('language-html'))
waitsForPromise(() => atom.workspace.open('test.html'))
beforeEach(async () => {
await atom.packages.activatePackage('autocomplete-html')
await atom.packages.activatePackage('language-html')
await atom.workspace.open('test.html')
editor = atom.workspace.getActiveTextEditor()
languageMode = editor.getBuffer().getLanguageMode()
languageMode.useAsyncParsing = false
languageMode.useAsyncIndent = false
await languageMode.ready
runs(() => provider = atom.packages.getActivePackage('autocomplete-html').mainModule.getProvider())
runs(() => editor = atom.workspace.getActiveTextEditor())
provider = atom.packages.getActivePackage('autocomplete-html').mainModule.getProvider()
})
it('returns no completions when not at the start of a tag', () => {
afterEach(async () => {
await languageMode.atTransactionEnd()
})
it('returns no completions when not at the start of a tag', async () => {
editor.setText('')
await languageMode.atTransactionEnd()
expect(getCompletions().length).toBe(0)
editor.setText('d')
editor.setCursorBufferPosition([0, 0])
await languageMode.atTransactionEnd()
expect(getCompletions().length).toBe(0)
editor.setCursorBufferPosition([0, 1])
expect(getCompletions().length).toBe(0)
})
it('returns no completions in style tags', () => {
it('returns no completions in style tags', async () => {
editor.setText(`\
<style>
<
</style>\
`
)
await languageMode.atTransactionEnd()
editor.setCursorBufferPosition([1, 1])
expect(getCompletions().length).toBe(0)
})
it('returns no completions in script tags', () => {
it('returns no completions in script tags', async () => {
editor.setText(`\
<script>
<
</script>\
`
)
await languageMode.atTransactionEnd()
editor.setCursorBufferPosition([1, 1])
expect(getCompletions().length).toBe(0)
})
it('autcompletes tag names without a prefix', () => {
it('autcompletes tag names without a prefix', async () => {
editor.setText('<')
editor.setCursorBufferPosition([0, 1])
await languageMode.atTransactionEnd()
const completions = getCompletions()
expect(completions.length).toBeGreaterThan(113) // Fun Fact last check this was 232
@ -90,9 +105,10 @@ describe('HTML autocompletions', () => {
}
})
it('autocompletes tag names with a prefix', () => {
it('autocompletes tag names with a prefix', async () => {
editor.setText('<d')
editor.setCursorBufferPosition([0, 2])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBeGreaterThan(9) // Fun fact last check was 14
@ -126,24 +142,27 @@ describe('HTML autocompletions', () => {
expect(isValueInCompletions('dt', completions)).toBe(true)
})
it("does not autocomplete tag names if there's a space after the <", () => {
it("does not autocomplete tag names if there's a space after the <", async () => {
editor.setText('< ')
editor.setCursorBufferPosition([0, 2])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBe(0)
editor.setText('< h')
editor.setCursorBufferPosition([0, 2])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(0)
})
it('does not provide a descriptionMoreURL if the tag does not have a unique description', () => {
it('does not provide a descriptionMoreURL if the tag does not have a unique description', async () => {
// isindex does not have an associated MDN page as of March 25, 2023
editor.setText('<i')
editor.setCursorBufferPosition([0, 2])
await languageMode.atTransactionEnd()
const completions = getCompletions()
const loc = getValueInCompletionsIndex('isindex', completions)
@ -153,9 +172,10 @@ describe('HTML autocompletions', () => {
expect(completions[loc].descriptionMoreURL).toBeNull()
})
it('autocompletes attribute names without a prefix', () => {
it('autocompletes attribute names without a prefix', async () => {
editor.setText('<div ')
editor.setCursorBufferPosition([0, 5])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBeGreaterThan(86) // Fun fact last check this was 264
@ -171,6 +191,7 @@ describe('HTML autocompletions', () => {
editor.setText('<marquee ')
editor.setCursorBufferPosition([0, 9])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBeGreaterThan(98) // Last check 274
@ -186,6 +207,7 @@ describe('HTML autocompletions', () => {
editor.setText('<div >')
editor.setCursorBufferPosition([0, 5])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBeGreaterThan(0)
@ -193,6 +215,7 @@ describe('HTML autocompletions', () => {
editor.setText('<div >')
editor.setCursorBufferPosition([0, 5])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBeGreaterThan(0)
@ -201,9 +224,10 @@ describe('HTML autocompletions', () => {
}
})
it('autocompletes attribute names with a prefix', () => {
it('autocompletes attribute names with a prefix', async () => {
editor.setText('<div c')
editor.setCursorBufferPosition([0, 6])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBeGreaterThan(3) // Last check 9
@ -218,6 +242,7 @@ describe('HTML autocompletions', () => {
editor.setText('<div C')
editor.setCursorBufferPosition([0, 6])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBeGreaterThan(3) // Last check 9
@ -228,6 +253,7 @@ describe('HTML autocompletions', () => {
editor.setText('<div c>')
editor.setCursorBufferPosition([0, 6])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBeGreaterThan(3)
@ -238,6 +264,7 @@ describe('HTML autocompletions', () => {
editor.setText('<div c></div>')
editor.setCursorBufferPosition([0, 6])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBeGreaterThan(3)
@ -248,6 +275,7 @@ describe('HTML autocompletions', () => {
editor.setText('<marquee di')
editor.setCursorBufferPosition([0, 12])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(isValueInCompletions('direction', completions, 'displayText'))
@ -255,65 +283,73 @@ describe('HTML autocompletions', () => {
editor.setText('<marquee dI')
editor.setCursorBufferPosition([0, 12])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(isValueInCompletions('direction', completions, 'displayText'))
expect(isValueInCompletions('dir', completions, 'displayText'))
})
it('autocompletes attribute names without a prefix surrounded by whitespace', () => {
it('autocompletes attribute names without a prefix surrounded by whitespace', async () => {
editor.setText('<select autofocus')
editor.setCursorBufferPosition([0, 8])
await languageMode.atTransactionEnd()
const completions = getCompletions()
for (let completion of completions) { expect(completion.type).toBe('attribute') }
expect(isValueInCompletions('autofocus', completions, 'displayText'))
})
it('autocompletes attribute names with a prefix surrounded by whitespace', () => {
it('autocompletes attribute names with a prefix surrounded by whitespace', async () => {
editor.setText('<select o autofocus')
editor.setCursorBufferPosition([0, 9])
await languageMode.atTransactionEnd()
const completions = getCompletions()
for (let completion of completions) { expect(completion.type).toBe('attribute') }
expect(isValueInCompletions('onabort', completions, 'displayText'))
})
it("respects the 'flag' type when autocompleting attribute names", () => {
it("respects the 'flag' type when autocompleting attribute names", async () => {
editor.setText('<select ')
editor.setCursorBufferPosition([0, 8])
await languageMode.atTransactionEnd()
const completions = getCompletions()
expect(isValueInCompletions('autofocus', completions, 'snippet'))
})
it('does not autocomplete attribute names in between an attribute name and value', () => {
it('does not autocomplete attribute names in between an attribute name and value', async () => {
editor.setText('<select autofocus=""')
editor.setCursorBufferPosition([0, 18])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBe(0)
editor.setText('<select autofocus= ""')
editor.setCursorBufferPosition([0, 18])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(0)
editor.setText('<select autofocus= ""')
editor.setCursorBufferPosition([0, 19])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(0)
editor.setText('<select autofocus= ""')
editor.setCursorBufferPosition([0, 19])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(0)
})
it('does not autocomplete attribute names outside of a tag', () => {
it('does not autocomplete attribute names outside of a tag', async () => {
editor.setText('<kbd>')
editor.setCursorBufferPosition([0, 0])
@ -324,18 +360,20 @@ describe('HTML autocompletions', () => {
expect(getCompletions().length).toBe(0)
})
it('does not throw when a local attribute is not in the attributes list', () => {
it('does not throw when a local attribute is not in the attributes list', async () => {
// Some tags, like body, have local attributes that are not present in the top-level attributes array
editor.setText('<body ')
editor.setCursorBufferPosition([0, 6])
await languageMode.atTransactionEnd()
const completions = getCompletions()
expect(isValueInCompletions('onafterprint', completions, 'displayText'))
})
it('does not provide a descriptionMoreURL if the attribute does not have a unique description', () => {
it('does not provide a descriptionMoreURL if the attribute does not have a unique description', async () => {
editor.setText('<input on')
editor.setCursorBufferPosition([0, 9])
await languageMode.atTransactionEnd()
const completions = getCompletions()
@ -346,9 +384,10 @@ describe('HTML autocompletions', () => {
expect(completions[loc].descriptionMoreURL).toBeNull()
})
it('autocompletes attribute values without a prefix', () => {
it('autocompletes attribute values without a prefix', async () => {
editor.setText('<marquee behavior=""')
editor.setCursorBufferPosition([0, 19])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBe(3)
@ -360,28 +399,37 @@ describe('HTML autocompletions', () => {
expect(completions[1].text).toBe('slide')
expect(completions[2].text).toBe('alternate')
editor.setText('<marquee behavior="')
editor.setCursorBufferPosition([0, 19])
completions = getCompletions()
expect(completions.length).toBe(3)
expect(completions[0].text).toBe('scroll')
expect(completions[1].text).toBe('slide')
expect(completions[2].text).toBe('alternate')
editor.setText('<marquee behavior=\'')
editor.setCursorBufferPosition([0, 19])
completions = getCompletions()
expect(completions.length).toBe(3)
expect(completions[0].text).toBe('scroll')
expect(completions[1].text).toBe('slide')
expect(completions[2].text).toBe('alternate')
// NOTE: The Tree-sitter parser goes absolutely mental in this scenario. It
// presents a much more reasonable tree when the closing quote is present,
// but it'd be incredibly hard to salvage something from this mess.
//
// This isn't ideal, but most users will have enabled smart typing pairs,
// so we'll let this slide.
// editor.setText('<marquee behavior="')
// editor.setCursorBufferPosition([0, 19])
// await languageMode.atTransactionEnd()
//
// completions = getCompletions()
// expect(completions.length).toBe(3)
//
// expect(completions[0].text).toBe('scroll')
// expect(completions[1].text).toBe('slide')
// expect(completions[2].text).toBe('alternate')
//
// editor.setText('<marquee behavior=\'')
// editor.setCursorBufferPosition([0, 19])
// await languageMode.atTransactionEnd()
//
// completions = getCompletions()
// expect(completions.length).toBe(3)
//
// expect(completions[0].text).toBe('scroll')
// expect(completions[1].text).toBe('slide')
// expect(completions[2].text).toBe('alternate')
editor.setText('<marquee behavior=\'\'')
editor.setCursorBufferPosition([0, 19])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(3)
@ -391,9 +439,10 @@ describe('HTML autocompletions', () => {
expect(completions[2].text).toBe('alternate')
})
it('autocompletes attribute values with a prefix', () => {
it('autocompletes attribute values with a prefix', async () => {
editor.setText('<html behavior="" lang="e"')
editor.setCursorBufferPosition([0, 25])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBe(6)
@ -408,6 +457,7 @@ describe('HTML autocompletions', () => {
editor.setText('<html behavior="" lang="E"')
editor.setCursorBufferPosition([0, 25])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(6)
@ -421,6 +471,7 @@ describe('HTML autocompletions', () => {
editor.setText('<html behavior="" lang=\'e\'')
editor.setCursorBufferPosition([0, 25])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(6)
@ -433,9 +484,10 @@ describe('HTML autocompletions', () => {
expect(completions[5].text).toBe('es')
})
it('autocompletes ambiguous attribute values', () => {
it('autocompletes ambiguous attribute values', async () => {
editor.setText('<button type=""')
editor.setCursorBufferPosition([0, 14])
await languageMode.atTransactionEnd()
let completions = getCompletions()
expect(completions.length).toBe(3)
@ -449,6 +501,7 @@ describe('HTML autocompletions', () => {
editor.setText('<link rel=""')
editor.setCursorBufferPosition([0, 11])
await languageMode.atTransactionEnd()
completions = getCompletions()
expect(completions.length).toBe(13)
@ -460,9 +513,10 @@ describe('HTML autocompletions', () => {
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/link#attributes')).toBe(true)
})
it("provides 'true' and 'false' suggestions when autocompleting boolean attributes", () => {
it("provides 'true' and 'false' suggestions when autocompleting boolean attributes", async () => {
editor.setText('<html contenteditable=""')
editor.setCursorBufferPosition([0, 23])
await languageMode.atTransactionEnd()
const completions = getCompletions()
expect(completions.length).toBe(2)
@ -470,50 +524,55 @@ describe('HTML autocompletions', () => {
expect(completions[1].text).toBe('false')
})
it('does not attempt to autocomplete values before the beginning of a string', () => {
it('does not attempt to autocomplete values before the beginning of a string', async () => {
editor.setText('<button type=""')
editor.setCursorBufferPosition([0, 13])
await languageMode.atTransactionEnd()
let completions = []
expect(() => completions = getCompletions()).not.toThrow()
expect(completions.length).toBe(0)
})
it('does not attempt to autocomplete values after the end of a string', () => {
it('does not attempt to autocomplete values after the end of a string', async () => {
editor.setText('<button type=""')
editor.setCursorBufferPosition([0, 15])
await languageMode.atTransactionEnd()
let completions = []
expect(() => completions = getCompletions()).not.toThrow()
expect(completions.length).toBe(0)
})
it('does not throw when quotes are in the attribute value', () => {
it('does not throw when quotes are in the attribute value', async () => {
editor.setText('<button type="\'"')
editor.setCursorBufferPosition([0, 15])
await languageMode.atTransactionEnd()
expect(() => getCompletions()).not.toThrow()
})
it("does not autocomplete attribute values if there isn't a corresponding attribute", () => {
it("does not autocomplete attribute values if there isn't a corresponding attribute", async () => {
editor.setText('<button type="""')
editor.setCursorBufferPosition([0, 16])
await languageMode.atTransactionEnd()
let completions = []
expect(() => completions = getCompletions()).not.toThrow()
expect(completions.length).toBe(0)
})
it('does not throw when attempting to autocomplete values for nonexistent attributes', () => {
it('does not throw when attempting to autocomplete values for nonexistent attributes', async () => {
editor.setText('<button typ=""')
editor.setCursorBufferPosition([0, 13])
await languageMode.atTransactionEnd()
let completions = []
expect(() => completions = getCompletions()).not.toThrow()
expect(completions.length).toBe(0)
})
it('triggers autocomplete when an attibute has been inserted', () => {
it('triggers autocomplete when an attibute has been inserted', async () => {
spyOn(atom.commands, 'dispatch')
const suggestion = {type: 'attribute', text: 'whatever'}
provider.onDidInsertSuggestion({editor, suggestion})
@ -526,20 +585,16 @@ describe('HTML autocompletions', () => {
expect(args[1]).toBe('autocomplete-plus:activate')
})
it('does not error in EJS documents', () => {
it('does not error in EJS documents', async () => {
waitsForPromise(async () => {
await atom.workspace.open('test.html.ejs')
editor = atom.workspace.getActiveTextEditor()
editor.setText('<span><% a = ""; %></span>')
return languageMode.atTransactionEnd()
})
waitsForPromise(() => {
return atom.packages.activatePackage('language-javascript')
})
runs(() => {
editor.setCursorBufferPosition([0, editor.getText().indexOf('""') + 1])
expect(() => getCompletions()).not.toThrow()
})
await atom.packages.activatePackage('language-javascript')
editor.setCursorBufferPosition([0, editor.getText().indexOf('""') + 1])
expect(() => getCompletions()).not.toThrow()
})
})

View File

@ -1,7 +1,5 @@
const {CompositeDisposable, Disposable, Point, Range} = require('atom')
const path = require('path')
const fuzzaldrin = require('fuzzaldrin')
const fuzzaldrinPlus = require('fuzzaldrin-plus')
const ProviderManager = require('./provider-manager')
const SuggestionList = require('./suggestion-list')
@ -190,7 +188,6 @@ class AutocompleteManager {
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableAutoActivation', (value) => { this.autoActivationEnabled = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableAutoConfirmSingleSuggestion', (value) => { this.autoConfirmSingleSuggestionEnabled = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.consumeSuffix', (value) => { this.consumeSuffix = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.useAlternateScoring', (value) => { this.useAlternateScoring = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.fileBlacklist', (value) => {
if (value) {
this.fileBlacklist = value.map((s) => { return s.trim() })
@ -365,7 +362,6 @@ class AutocompleteManager {
filterSuggestions (suggestions, {prefix}) {
const results = []
const fuzzaldrinProvider = this.useAlternateScoring ? fuzzaldrinPlus : fuzzaldrin
for (let i = 0; i < suggestions.length; i++) {
// sortScore mostly preserves in the original sorting. The function is
// chosen such that suggestions with a very high match score can break out.
@ -382,7 +378,7 @@ class AutocompleteManager {
results.push(suggestion)
} else {
const keepMatching = suggestion.ranges || suggestionPrefix[0].toLowerCase() === text[0].toLowerCase()
if (keepMatching && (score = fuzzaldrinProvider.score(text, suggestionPrefix)) > 0) {
if (keepMatching && (score = atom.ui.fuzzyMatcher.score(text, suggestionPrefix)) > 0) {
suggestion.score = score * suggestion.sortScore
results.push(suggestion)
}

View File

@ -1,9 +1,6 @@
const {CompositeDisposable} = require('atom')
const SnippetParser = require('./snippet-parser')
const {isString} = require('./type-helpers')
const fuzzaldrinPlus = require('fuzzaldrin-plus')
const {marked} = require('marked')
const createDOMPurify = require('dompurify')
const createSuggestionFrag = () => {
const frag = document.createDocumentFragment()
@ -83,9 +80,6 @@ module.exports = class SuggestionListElement {
this.subscriptions.add(atom.config.observe('autocomplete-plus.maxVisibleSuggestions', maxVisibleSuggestions => {
this.maxVisibleSuggestions = maxVisibleSuggestions
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.useAlternateScoring', useAlternateScoring => {
this.useAlternateScoring = useAlternateScoring
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.moveToCancel', moveToCancel => {
this.moveToCancel = moveToCancel
}))
@ -137,15 +131,13 @@ module.exports = class SuggestionListElement {
if (item.descriptionMarkdown && item.descriptionMarkdown.length > 0) {
this.descriptionContainer.style.display = 'block'
this.descriptionContent.innerHTML = createDOMPurify().sanitize(
marked(item.descriptionMarkdown, {
gfm: true,
this.descriptionContent.innerHTML = atom.ui.markdown.render(
item.descriptionMarkdown,
{
breaks: true,
sanitize: false,
mangle: false,
headerIds: false
})
)
renderMode: "fragment"
}
);
this.setDescriptionMoreLink(item)
} else if (item.description && item.description.length > 0) {
this.descriptionContainer.style.display = 'block'
@ -544,12 +536,8 @@ module.exports = class SuggestionListElement {
if (!characterMatchIndices) {
characterMatchIndices = this.findCharacterMatchIndices(replacementText, replacementPrefix)
} else {
characterMatchIndices = characterMatchIndices.reduce((matches, index) => {
matches[index] = true
return matches
}, {})
}
characterMatchIndices = new Set(characterMatchIndices);
const appendNonMatchChars = (el, nonMatchChars) => {
if (nonMatchChars) {
@ -572,7 +560,7 @@ module.exports = class SuggestionListElement {
workingEl = s
}
if (characterMatchIndices && characterMatchIndices[index]) {
if (characterMatchIndices && characterMatchIndices.has(index)) {
appendNonMatchChars(workingEl, nonMatchChars)
nonMatchChars = ''
@ -668,26 +656,11 @@ module.exports = class SuggestionListElement {
//
// Returns an {Object}
findCharacterMatchIndices (text, replacementPrefix) {
if (!text || !text.length || !replacementPrefix || !replacementPrefix.length) { return }
if (!text?.length || !replacementPrefix?.length) { return }
const matches = {}
if (this.useAlternateScoring) {
const matchIndices = fuzzaldrinPlus.match(text, replacementPrefix)
for (const i of matchIndices) {
matches[i] = true
}
} else {
let wordIndex = 0
for (let i = 0; i < replacementPrefix.length; i++) {
const ch = replacementPrefix[i]
while (wordIndex < text.length && text[wordIndex].toLowerCase() !== ch.toLowerCase()) {
wordIndex += 1
}
if (wordIndex >= text.length) { break }
matches[wordIndex] = true
wordIndex += 1
}
}
return matches
return atom.ui.fuzzyMatcher.match(
text, replacementPrefix, {recordMatchIndexes: true}
)?.matchIndexes || [];
}
dispose () {

View File

@ -10,11 +10,7 @@
},
"dependencies": {
"atom-slick": "^2.0.0",
"dompurify": "^3.0.4",
"fuzzaldrin": "^2.1.0",
"fuzzaldrin-plus": "^0.6.0",
"grim": "^2.0.1",
"marked": "^5.1.1",
"minimatch": "^3.0.3",
"selector-kit": "^0.1.0",
"stable": "^0.1.8",
@ -200,12 +196,6 @@
"default": true,
"order": 18
},
"useAlternateScoring": {
"description": "Prefers runs of consecutive characters, acronyms and start of words. (Experimental)",
"type": "boolean",
"default": true,
"order": 19
},
"useLocalityBonus": {
"description": "Gives words near the cursor position a higher score than those far away",
"type": "boolean",

View File

@ -1,6 +1,7 @@
/* eslint-env jasmine */
/* eslint-disable no-template-curly-in-string */
const SuggestionListElement = require('../lib/suggestion-list-element')
const { conditionPromise } = require('./spec-helper')
const fragmentToHtml = fragment => {
const el = document.createElement('span')
@ -63,11 +64,12 @@ describe('Suggestion List Element', () => {
})
describe('itemChanged', () => {
beforeEach(() => jasmine.attachToDOM(suggestionListElement.element))
beforeEach(() => {
jasmine.useRealClock()
jasmine.attachToDOM(suggestionListElement.element)
})
it('updates the list item', async () => {
jasmine.useRealClock()
const suggestion = {text: 'foo'}
const newSuggestion = {text: 'foo', description: 'foobar', rightLabel: 'foo'}
suggestionListElement.model = {items: [newSuggestion]}
@ -77,6 +79,9 @@ describe('Suggestion List Element', () => {
suggestionListElement.itemChanged({suggestion: newSuggestion, index: 0})
await conditionPromise(() =>
suggestionListElement.element.querySelector('.right-label').innerText
)
expect(suggestionListElement.element.querySelector('.right-label').innerText)
.toBe('foo')
@ -190,14 +195,15 @@ describe('Suggestion List Element', () => {
let snippets = suggestionListElement.snippetParser.findSnippets(text)
text = suggestionListElement.removeSnippetsFromText(snippets, text)
let matches = suggestionListElement.findCharacterMatchIndices(text, replacementPrefix)
matches = new Set(matches)
for (var i = 0; i <= text.length; i++) {
if (truthyIndices.indexOf(i) !== -1) {
expect(matches[i]).toBeTruthy()
expect(matches.has(i)).toBeTruthy()
} else {
let m = matches
if (m) {
m = m[i]
m = m.has(i)
}
expect(m).toBeFalsy()
}
@ -208,14 +214,14 @@ describe('Suggestion List Element', () => {
assertMatches('hello', '', [])
assertMatches('hello', 'h', [0])
assertMatches('hello', 'hl', [0, 2])
assertMatches('hello', 'hlo', [0, 2, 4])
assertMatches('hello', 'hlo', [0, 3, 4])
})
it('finds matches when snippets exist', () => {
assertMatches('${0:hello}', '', [])
assertMatches('${0:hello}', 'h', [0])
assertMatches('${0:hello}', 'hl', [0, 2])
assertMatches('${0:hello}', 'hlo', [0, 2, 4])
assertMatches('${0:hello}', 'hlo', [0, 3, 4])
assertMatches('${0:hello}world', '', [])
assertMatches('${0:hello}world', 'h', [0])
assertMatches('${0:hello}world', 'hw', [0, 5])

View File

@ -267,6 +267,12 @@
&.syntax--raw {
color: @green;
}
// Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity,
// we assign the color of a comment to this new scope.
&.syntax--horizontal-rule {
color: @gray;
}
}
.syntax--source.syntax--gfm {

View File

@ -8,6 +8,13 @@
}
}
// Color JSON keys differently from other strings.
.syntax--meta.syntax--structure.syntax--key {
.syntax--string.syntax--quoted.syntax--double {
color: @red;
}
}
.syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json {
& > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json,
& > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation {

View File

@ -267,6 +267,12 @@
&.syntax--raw {
color: @green;
}
// Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity,
// we assign the color of a comment to this new scope.
&.syntax--horizontal-rule {
color: @gray;
}
}
.syntax--source.syntax--gfm {

View File

@ -8,6 +8,13 @@
}
}
// Color JSON keys differently from other strings.
.syntax--meta.syntax--structure.syntax--key {
.syntax--string.syntax--quoted.syntax--double {
color: @red;
}
}
.syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json {
& > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json,
& > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation {

View File

@ -0,0 +1,10 @@
module.exports = {
env: { jasmine: true },
rules: {
"semi": ["error", "never"],
"node/no-unpublished-require": "off",
"node/no-extraneous-require": "off",
"no-unused-vars": "off",
"no-empty": "off"
}
}

View File

@ -264,7 +264,7 @@ class BracketMatcherView {
firstChild.childCount > 2 &&
firstChild.firstChild.type === '<'
) {
if (lastChild === firstChild && firstChild.lastChild.type === '/>') {
if (lastChild.id === firstChild.id && firstChild.lastChild.type === '/>') {
startTag = firstChild
endTag = firstChild
} else if (

View File

@ -0,0 +1,12 @@
module.exports = {
env: { jasmine: true },
globals: {
waitsForPromise: true,
},
rules: {
"node/no-unpublished-require": "off",
"node/no-extraneous-require": "off",
"no-unused-vars": "off",
"no-empty": "off"
}
};

View File

@ -3,8 +3,16 @@ const {Point, TextBuffer} = require('atom')
const HAS_NEW_TEXT_BUFFER_VERSION = (new TextBuffer()).getLanguageMode().bufferDidFinishTransaction
const path = require('path')
function editorReady (languageMode) {
if (languageMode.atTransactionEnd) {
return languageMode.atTransactionEnd()
} else {
return Promise.resolve()
}
}
describe('bracket matching', () => {
let editorElement, editor, buffer
let editorElement, editor, buffer, languageMode
beforeEach(() => {
atom.config.set('bracket-matcher.autocompleteBrackets', true)
@ -17,10 +25,17 @@ describe('bracket matching', () => {
waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.js')))
waitsForPromise(() => {
let editor = atom.workspace.getActiveTextEditor()
let languageMode = editor.getBuffer().getLanguageMode()
return languageMode.ready
})
runs(() => {
editor = atom.workspace.getActiveTextEditor()
editorElement = atom.views.getView(editor)
buffer = editor.getBuffer()
languageMode = buffer.getLanguageMode()
})
})
@ -112,9 +127,10 @@ describe('bracket matching', () => {
})
describe('when there are unpaired brackets', () => {
it('highlights the correct start/end pairs', () => {
it('highlights the correct start/end pairs', async () => {
editor.setText('(()')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectNoHighlights()
editor.setCursorBufferPosition([0, 1])
@ -125,6 +141,7 @@ describe('bracket matching', () => {
editor.setText(('())'))
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 1])
editor.setCursorBufferPosition([0, 1])
@ -139,9 +156,10 @@ describe('bracket matching', () => {
})
describe('when there are commented brackets', () => {
it('highlights the correct start/end pairs', () => {
it('highlights the correct start/end pairs', async () => {
editor.setText('(//)')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectNoHighlights()
editor.setCursorBufferPosition([0, 2])
@ -152,6 +170,7 @@ describe('bracket matching', () => {
editor.setText('{/*}*/')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectNoHighlights()
editor.setCursorBufferPosition([0, 2])
@ -162,6 +181,7 @@ describe('bracket matching', () => {
editor.setText('[/*]*/]')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 6])
editor.setCursorBufferPosition([0, 6])
@ -173,9 +193,10 @@ describe('bracket matching', () => {
})
describe('when there are quoted brackets', () => {
it('highlights the correct start/end pairs', () => {
it('highlights the correct start/end pairs', async () => {
editor.setText("(')')")
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 4])
editor.setCursorBufferPosition([0, 5])
@ -186,6 +207,7 @@ describe('bracket matching', () => {
editor.setText('["]"]')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 4])
editor.setCursorBufferPosition([0, 5])
@ -197,9 +219,10 @@ describe('bracket matching', () => {
})
describe('when there are brackets inside code embedded in a string', () => {
it('highlights the correct start/end pairs', () => {
it('highlights the correct start/end pairs', async () => {
editor.setText('(`${(1+1)}`)')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 11])
editor.setCursorBufferPosition([0, 12])
@ -211,9 +234,10 @@ describe('bracket matching', () => {
})
describe('when there are brackets inside a string inside code embedded in a string', () => {
it('highlights the correct start/end pairs', () => {
it('highlights the correct start/end pairs', async () => {
editor.setText("(`${('(1+1)')}`)")
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 15])
editor.setCursorBufferPosition([0, 16])
@ -225,9 +249,10 @@ describe('bracket matching', () => {
})
describe('when there are brackets in regular expressions', () => {
it('highlights the correct start/end pairs', () => {
it('highlights the correct start/end pairs', async () => {
editor.setText('(/[)]/)')
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 0], [0, 6])
editor.setCursorBufferPosition([0, 7])
@ -239,9 +264,10 @@ describe('bracket matching', () => {
})
describe('when the start character and end character of the pair are equivalent', () => {
it('does not attempt to highlight pairs', () => {
it('does not attempt to highlight pairs', async () => {
editor.setText("'hello'")
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectNoHighlights()
})
})
@ -257,12 +283,13 @@ describe('bracket matching', () => {
})
describe('when the pair moves', () => {
it('repositions the highlights', () => {
it('repositions the highlights', async () => {
editor.moveToEndOfLine()
editor.moveLeft()
expectHighlights([0, 28], [12, 0])
editor.deleteToBeginningOfLine()
await editorReady(languageMode)
expectHighlights([0, 0], [12, 0])
})
})
@ -313,21 +340,20 @@ describe('bracket matching', () => {
forEachLanguageWithTags(scopeName => {
describe(`${scopeName} tag matching`, () => {
beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage('language-html'))
waitsForPromise(() => atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml')))
runs(() => {
editor = atom.workspace.getActiveTextEditor()
editorElement = atom.views.getView(editor)
buffer = editor.buffer
atom.grammars.assignLanguageMode(buffer, scopeName)
buffer.getLanguageMode().syncOperationLimit = Infinity
})
beforeEach( async () => {
await atom.packages.activatePackage('language-html')
await atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.xml'))
editor = atom.workspace.getActiveTextEditor()
editorElement = atom.views.getView(editor)
buffer = editor.buffer
atom.grammars.assignLanguageMode(buffer, scopeName)
languageMode = buffer.getLanguageMode()
languageMode.syncOperationLimit = Infinity
await languageMode.ready
})
describe('when on an opening tag', () => {
it('highlights the opening and closing tag', () => {
it('highlights the opening and closing tag', async () => {
buffer.setText(`\
<test>
<test>text</test>
@ -337,6 +363,7 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([0, 0])
await editorReady(languageMode)
expectHighlights([0, 1], [3, 2])
editor.setCursorBufferPosition([0, 1])
@ -345,7 +372,7 @@ describe('bracket matching', () => {
})
describe('when on a closing tag', () => {
it('highlights the opening and closing tag', () => {
it('highlights the opening and closing tag', async () => {
buffer.setText(`\
<test>
<!-- <test> -->
@ -355,6 +382,7 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([3, 0])
await editorReady(languageMode)
expectHighlights([3, 2], [0, 1])
editor.setCursorBufferPosition([3, 2])
@ -369,13 +397,14 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([1, Infinity])
await editorReady(languageMode)
expectHighlights([1, 14], [1, 3])
editor.setCursorBufferPosition([2, Infinity])
expectHighlights([2, 14], [2, 3])
})
it('highlights the correct opening tag, skipping self-closing tags', () => {
it('highlights the correct opening tag, skipping self-closing tags', async () => {
buffer.setText(`\
<test>
<test />
@ -384,12 +413,13 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([2, Infinity])
await editorReady(languageMode)
expectHighlights([2, 2], [0, 1])
})
})
describe('when on a self-closing tag', () => {
it('highlights only the self-closing tag', () => {
it('highlights only the self-closing tag', async () => {
buffer.setText(`\
<test>
<test />
@ -398,10 +428,11 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([1, Infinity])
await editorReady(languageMode)
expectHighlights([1, 3], [1, 3])
})
it('highlights a self-closing tag without a space', () => {
it('highlights a self-closing tag without a space', async () => {
buffer.setText(`\
<test>
<test/>
@ -410,10 +441,11 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([1, Infinity])
await editorReady(languageMode)
expectHighlights([1, 3], [1, 3])
})
it('highlights a self-closing tag with many spaces', () => {
it('highlights a self-closing tag with many spaces', async () => {
buffer.setText(`\
<test>
<test />
@ -422,10 +454,11 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([1, Infinity])
await editorReady(languageMode)
expectHighlights([1, 3], [1, 3])
})
it('does not catastrophically backtrack when many attributes are present (regression)', () => {
it('does not catastrophically backtrack when many attributes are present (regression)', async () => {
// https://github.com/atom/bracket-matcher/issues/303
buffer.setText(`\
@ -438,6 +471,7 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([0, 6])
await editorReady(languageMode)
expectHighlights([0, 1], [4, 2])
editor.setCursorBufferPosition([1, 6])
@ -455,7 +489,7 @@ describe('bracket matching', () => {
})
describe('when the tag spans multiple lines', () => {
it('highlights the opening and closing tag', () => {
it('highlights the opening and closing tag', async () => {
buffer.setText(`\
<div>
<div class="test"
@ -469,6 +503,7 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([0, 1])
await editorReady(languageMode)
expectHighlights([0, 1], [6, 2])
editor.setCursorBufferPosition([6, 2])
expectHighlights([6, 2], [0, 1])
@ -476,7 +511,7 @@ describe('bracket matching', () => {
})
describe('when the tag has attributes', () => {
it('highlights the opening and closing tags', () => {
it('highlights the opening and closing tags', async () => {
buffer.setText(`\
<test a="test">
text
@ -485,6 +520,7 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([2, 2])
await editorReady(languageMode)
expectHighlights([2, 2], [0, 1])
editor.setCursorBufferPosition([0, 7])
@ -493,7 +529,7 @@ describe('bracket matching', () => {
})
describe("when the tag has an attribute with a value of '/'", () => {
it('highlights the opening and closing tags', () => {
it('highlights the opening and closing tags', async () => {
buffer.setText(`\
<test a="/">
text
@ -502,6 +538,7 @@ describe('bracket matching', () => {
)
editor.setCursorBufferPosition([2, 2])
await editorReady(languageMode)
expectHighlights([2, 2], [0, 1])
editor.setCursorBufferPosition([0, 7])
@ -510,10 +547,11 @@ describe('bracket matching', () => {
})
describe('when the opening and closing tags are on the same line', () => {
it('highlight the opening and closing tags', () => {
it('highlight the opening and closing tags', async () => {
buffer.setText('<test>text</test>')
editor.setCursorBufferPosition([0, 2])
await editorReady(languageMode)
expectHighlights([0, 1], [0, 12])
editor.setCursorBufferPosition([0, 12])
@ -522,17 +560,19 @@ describe('bracket matching', () => {
})
describe('when the closing tag is missing', () => {
it('does not highlight anything', () => {
it('does not highlight anything', async () => {
buffer.setText('<test>\ntext\n')
editor.setCursorBufferPosition([0, 10])
await editorReady(languageMode)
expectNoHighlights()
})
})
describe('when between the opening and closing tag', () => {
it('does not highlight anything', () => {
it('does not highlight anything', async () => {
buffer.setText('<div>\nhi\n</div>\n')
editor.setCursorBufferPosition([1, 0])
await editorReady(languageMode)
expectNoHighlights()
})
})
@ -986,14 +1026,16 @@ describe('bracket matching', () => {
})
describe('matching bracket insertion', () => {
beforeEach(() => {
beforeEach(async () => {
editor.buffer.setText('')
await editorReady(languageMode)
atom.config.set('editor.autoIndent', true)
})
describe('when more than one character is inserted', () => {
it('does not insert a matching bracket', () => {
it('does not insert a matching bracket', async () => {
editor.insertText('woah(')
await editorReady(languageMode)
expect(editor.buffer.getText()).toBe('woah(')
})
})
@ -1467,18 +1509,23 @@ describe('bracket matching', () => {
})
describe('when return is pressed inside a matching pair', () => {
it('puts the cursor on the indented empty line', () => {
it('puts the cursor on the indented empty line', async () => {
editor.insertText('void main() ')
editor.insertText('{')
await editorReady(languageMode)
expect(editor.getText()).toBe('void main() {}')
editor.insertNewline()
expect(editor.getCursorBufferPosition()).toEqual([1, 2])
expect(buffer.lineForRow(1)).toBe(' ')
expect(buffer.lineForRow(2)).toBe('}')
editor.setText(' void main() ')
editor.setText('void main() ')
editor.insertText('{')
await editorReady(languageMode)
expect(editor.getText()).toBe('void main() {}')
editor.indentSelectedRows()
expect(editor.getText()).toBe(' void main() {}')
await editorReady(languageMode)
editor.insertNewline()
expect(editor.getCursorBufferPosition()).toEqual([1, 4])
expect(buffer.lineForRow(1)).toBe(' ')
@ -1514,13 +1561,15 @@ describe('bracket matching', () => {
describe('when in language specific scope', () => {
describe('string interpolation', () => {
beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
runs(() => buffer.setPath('foo.rb'))
beforeEach(async () => {
await atom.packages.activatePackage('language-ruby')
buffer.setPath('foo.rb')
let languageMode = buffer.getLanguageMode()
languageMode.useAsyncParsing = false
await languageMode.ready
})
it('should insert curly braces inside doubly quoted string', () => {
it('should insert curly braces inside double-quoted string', () => {
editor.insertText('foo = ')
editor.insertText('"')
editor.insertText('#')
@ -1529,7 +1578,7 @@ describe('bracket matching', () => {
expect(editor.getText()).toBe('foo = ""')
})
it('should not insert curly braces inside singly quoted string', () => {
it('should not insert curly braces inside single-quoted string', () => {
editor.insertText('foo = ')
editor.insertText("'")
editor.insertText('#')

View File

@ -15,9 +15,6 @@ class CommandPalettePackage {
this.commandPaletteView.show(true)
}
}))
this.disposables.add(atom.config.observe('command-palette.useAlternateScoring', (newValue) => {
this.commandPaletteView.update({useAlternateScoring: newValue})
}))
this.disposables.add(atom.config.observe('command-palette.preserveLastSearch', (newValue) => {
this.commandPaletteView.update({preserveLastSearch: newValue})
}))

View File

@ -2,8 +2,6 @@
import SelectListView from 'atom-select-list'
import {humanizeKeystroke} from 'underscore-plus'
import fuzzaldrin from 'fuzzaldrin'
import fuzzaldrinPlus from 'fuzzaldrin-plus'
export default class CommandPaletteView {
constructor (initiallyVisibleItemCount = 10) {
@ -55,7 +53,7 @@ export default class CommandPaletteView {
if (Array.isArray(item.tags)) {
const matchingTags = item.tags
.map(t => [t, this.fuzz.score(t, query)])
.map(t => [t, atom.ui.fuzzyMatcher.score(t, query)])
.filter(([t, s]) => s > 0)
.sort((a, b) => a.s - b.s)
.map(([t, s]) => t)
@ -132,21 +130,14 @@ export default class CommandPaletteView {
if (props.hasOwnProperty('preserveLastSearch')) {
this.preserveLastSearch = props.preserveLastSearch
}
if (props.hasOwnProperty('useAlternateScoring')) {
this.useAlternateScoring = props.useAlternateScoring
}
}
get fuzz () {
return this.useAlternateScoring ? fuzzaldrinPlus : fuzzaldrin
}
highlightMatchesInElement (text, query, el) {
const matches = this.fuzz.match(text, query)
const matches = atom.ui.fuzzyMatcher.match(text, query, {recordMatchIndexes: true})
let matchedChars = []
let lastIndex = 0
for (const matchIndex of matches) {
let lastIndex = 0;
const matchIndexes = matches ? (matches.matchIndexes ?? []) : []
matchIndexes.forEach(matchIndex => {
const unmatched = text.substring(lastIndex, matchIndex)
if (unmatched) {
if (matchedChars.length > 0) {
@ -162,7 +153,7 @@ export default class CommandPaletteView {
matchedChars.push(text[matchIndex])
lastIndex = matchIndex + 1
}
})
if (matchedChars.length > 0) {
const matchSpan = document.createElement('span')
@ -184,15 +175,15 @@ export default class CommandPaletteView {
const scoredItems = []
for (const item of items) {
let score = this.fuzz.score(item.displayName, query)
let score = atom.ui.fuzzyMatcher.score(item.displayName, query)
if (item.tags) {
score += item.tags.reduce(
(currentScore, tag) => currentScore + this.fuzz.score(tag, query),
(currentScore, tag) => currentScore + atom.ui.fuzzyMatcher.score(tag, query),
0
)
}
if (item.description) {
score += this.fuzz.score(item.description, query)
score += atom.ui.fuzzyMatcher.score(item.description, query)
}
if (score > 0) {

View File

@ -16,8 +16,6 @@
"atomTestRunner": "atom-mocha-test-runner",
"dependencies": {
"atom-select-list": "^0.7.1",
"fuzzaldrin": "^2.1.0",
"fuzzaldrin-plus": "^0.6.0",
"underscore-plus": "^1.0.0"
},
"devDependencies": {
@ -27,11 +25,6 @@
"sinon": "^3.2.1"
},
"configSchema": {
"useAlternateScoring": {
"type": "boolean",
"default": true,
"description": "Use an alternative scoring approach which prefers run of consecutive characters, acronyms and start of words."
},
"preserveLastSearch": {
"type": "boolean",
"default": false,

View File

@ -6,7 +6,6 @@ import { CompositeDisposable } from 'atom';
import etch from 'etch';
import fs from 'fs-plus';
import Grim from 'grim';
import { marked } from 'marked';
import path from 'path';
import { shell } from 'electron';
@ -138,7 +137,7 @@ export default class DeprecationCopView {
<span className="text-warning icon icon-alert" />
<div
className="list-item deprecation-message"
innerHTML={marked(deprecation.getMessage())}
innerHTML={atom.ui.markdown.render(deprecation.getMessage())}
/>
{this.renderIssueURLIfNeeded(
packageName,
@ -218,7 +217,7 @@ export default class DeprecationCopView {
<span className="text-warning icon icon-alert" />
<div
className="list-item deprecation-message"
innerHTML={marked(deprecation.message)}
innerHTML={atom.ui.markdown.render(deprecation.message)}
/>
{this.renderSelectorIssueURLIfNeeded(
packageName,

View File

@ -12,7 +12,6 @@
"etch": "0.9.0",
"fs-plus": "^3.0.0",
"grim": "^2.0.1",
"marked": "5.0.3",
"underscore-plus": "^1.7.0"
},
"consumedServices": {

View File

@ -36,7 +36,10 @@ showIf = (condition) ->
else
{display: 'none'}
capitalize = (str) -> str[0].toUpperCase() + str.toLowerCase().slice(1)
capitalize = (str) ->
return '' if str == ''
str[0].toUpperCase() + str.toLowerCase().slice(1)
titleize = (str) -> str.toLowerCase().replace(/(?:^|\s)\S/g, (capital) -> capital.toUpperCase())
preserveCase = (text, reference) ->

View File

@ -139,7 +139,7 @@
"preserveCaseOnReplace": {
"type": "boolean",
"default": false,
"title": "Preserve case during replace.",
"title": "Preserve Case During Replace",
"description": "Keep the replaced text case during replace: replacing 'user' with 'person' will replace 'User' with 'Person' and 'USER' with 'PERSON'."
}
}

View File

@ -1,6 +1,5 @@
const {Point, CompositeDisposable} = require('atom')
const fs = require('fs')
const NativeFuzzy = require('@pulsar-edit/fuzzy-native')
const path = require('path')
const SelectListView = require('atom-select-list')
@ -79,10 +78,12 @@ module.exports = class FuzzyFinderView {
elementForItem: ({filePath, label, ownerGitHubUsername}) => {
const filterQuery = this.selectListView.getFilterQuery()
this.nativeFuzzyForResults.setCandidates([0], [label])
atom.ui.fuzzyMatcher.setCandidates(
this.nativeFuzzyForResults, [label]
);
const items = this.nativeFuzzyForResults.match(
filterQuery,
{maxResults: 1, recordMatchIndexes: true}
{maxResults: 1, recordMatchIndexes: true, algorithm: 'command-t'}
)
const matches = items.length ? items[0].matchIndexes : []
const repository = repositoryForPath(filePath)
@ -125,14 +126,13 @@ module.exports = class FuzzyFinderView {
})
if (!this.nativeFuzzy) {
this.nativeFuzzy = new NativeFuzzy.Matcher(
indexArray(this.items.length),
this.nativeFuzzy = atom.ui.fuzzyMatcher.setCandidates(
this.items.map(el => el.label)
)
);
// We need a separate instance of the fuzzy finder to calculate the
// matched paths only for the returned results. This speeds up considerably
// the filtering of items.
this.nativeFuzzyForResults = new NativeFuzzy.Matcher([], [])
this.nativeFuzzyForResults = atom.ui.fuzzyMatcher.setCandidates([]);
}
this.selectListView.update({ filter: this.filterFn })
}
@ -166,10 +166,14 @@ module.exports = class FuzzyFinderView {
this.moveToCaretPosition(caretPosition)
} else if (!uri) {
this.cancel()
} else if (fs.lstatSync(uri).isDirectory()) {
this.selectListView.update({errorMessage: 'Selected path is a directory'})
setTimeout(() => { this.selectListView.update({errorMessage: null}) }, 2000)
} else {
try {
if (fs.lstatSync(uri).isDirectory()) {
this.selectListView.update({errorMessage: 'Selected path is a directory'})
setTimeout(() => { this.selectListView.update({errorMessage: null}) }, 2000)
return
}
} catch (e) {}
const caretPosition = this.getCaretPosition()
this.cancel()
this.openURI(uri, caretPosition, openOptions)
@ -290,10 +294,10 @@ module.exports = class FuzzyFinderView {
setItems (items) {
this.items = items
this.nativeFuzzy.setCandidates(
indexArray(this.items.length),
atom.ui.fuzzyMatcher.setCandidates(
this.nativeFuzzy,
this.items.map(item => item.label)
)
);
if (this.isQueryALineJump()) {
this.selectListView.update({
@ -338,7 +342,7 @@ module.exports = class FuzzyFinderView {
filterFn(items, query) {
if (!query) return items
return this.nativeFuzzy.match(query, {maxResults: MAX_RESULTS})
return this.nativeFuzzy.match(query, {maxResults: MAX_RESULTS, algorithm: 'command-t'})
.map(({id}) => this.items[id])
}
}
@ -382,14 +386,6 @@ function highlight (path, matches, offsetIndex) {
return fragment
}
function indexArray (length) {
const array = []
for (let i = 0; i < length; i++) {
array[i] = i
}
return array
}
class FuzzyFinderItem {
constructor ({filePath, label, ownerGitHubUsername, filterQuery, matches, repository}) {
this.filePath = filePath

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,6 @@
"repository": "https://github.com/pulsar-edit/fuzzy-finder",
"license": "MIT",
"dependencies": {
"@pulsar-edit/fuzzy-native": "https://github.com/pulsar-edit/fuzzy-native.git#13d2e5f",
"atom-select-list": "^0.7.0",
"fs-plus": "^3.0.0",
"minimatch": "~3.0.3",

View File

@ -4,6 +4,13 @@ module.exports = class GrammarListView {
constructor() {
this.autoDetect = { name: 'Auto Detect' };
this.configSubscription = atom.config.observe(
'grammar-selector.hideDuplicateTextMateGrammars',
(value) => {
this.hideDuplicateGrammars = value
}
);
this.selectListView = new SelectListView({
itemsClassList: ['mark-active'],
items: [],
@ -14,24 +21,20 @@ module.exports = class GrammarListView {
if (grammar === this.currentGrammar) {
element.classList.add('active');
}
element.classList.add('grammar-item');
element.textContent = grammarName;
element.dataset.grammar = grammarName;
const div = document.createElement('div');
div.classList.add('pull-right');
if (isTreeSitter(grammar)) {
if (!this.hideDuplicateGrammars) {
// When we show all grammars, we should add a badge to each grammar
// to distinguish them from one another in the list.
const parser = document.createElement('span');
let badgeColor = 'badge-success';
let badgeText = 'Tree-sitter';
if (isExperimentalTreeSitterMode()) {
badgeColor = isModernTreeSitter(grammar) ?
'badge-success' : 'badge-warning';
badgeText = isModernTreeSitter(grammar) ?
'Tree-sitter' : 'Legacy Tree-sitter';
}
let badgeText = getBadgeTextForGrammar(grammar);
let badgeColor = getBadgeColorForGrammar(grammar);
parser.classList.add(
'grammar-selector-parser',
@ -39,10 +42,12 @@ module.exports = class GrammarListView {
badgeColor
);
parser.textContent = badgeText;
parser.setAttribute(
'title',
'(Recommended) A faster parser with improved syntax highlighting & code navigation support.'
);
if (isModernTreeSitter(grammar)) {
parser.setAttribute(
'title',
'(Recommended) A faster parser with improved syntax highlighting & code navigation support.'
);
}
div.appendChild(parser);
}
@ -98,6 +103,16 @@ module.exports = class GrammarListView {
this.selectListView.reset();
}
getAllDisplayableGrammars() {
let allGrammars = atom.grammars
.getGrammars({ includeTreeSitter: true })
.filter(grammar => {
return grammar !== atom.grammars.nullGrammar && grammar.name;
});
return allGrammars;
}
async toggle() {
if (this.panel != null) {
this.cancel();
@ -112,29 +127,7 @@ module.exports = class GrammarListView {
this.currentGrammar = this.autoDetect;
}
let grammars = atom.grammars
.getGrammars({ includeTreeSitter: true })
.filter(grammar => {
return grammar !== atom.grammars.nullGrammar && grammar.name;
});
// Don't show modern tree-sitter grammars in the selector unless the user
// has opted into it.
if (!isExperimentalTreeSitterMode()) {
grammars = grammars.filter(grammar => !isModernTreeSitter(grammar));
}
if (atom.config.get('grammar-selector.hideDuplicateTextMateGrammars')) {
const blacklist = new Set();
grammars.forEach(grammar => {
if (isTreeSitter(grammar)) {
blacklist.add(grammar.name);
}
});
grammars = grammars.filter(
grammar => isTreeSitter(grammar) || !blacklist.has(grammar.name)
);
}
let grammars = this.getAllDisplayableGrammars();
grammars.sort((a, b) => {
if (a.scopeName === 'text.plain') {
@ -146,6 +139,20 @@ module.exports = class GrammarListView {
}
return a.name.localeCompare(b.name);
});
if (this.hideDuplicateGrammars) {
let displayedGrammars = [];
let seenIds = new Set();
for (let grammar of grammars) {
if (seenIds.has(grammar.scopeName)) continue;
seenIds.add(grammar.scopeName);
displayedGrammars.push(grammar);
}
grammars = displayedGrammars;
}
grammars.unshift(this.autoDetect);
await this.selectListView.update({ items: grammars });
this.attach();
@ -153,26 +160,21 @@ module.exports = class GrammarListView {
}
};
// We look up global settings here, but it's just to determine the badge
// colors. Otherwise we should be looking up these values in a scope-specific
// manner.
function getLanguageModeConfig() {
let isTreeSitterMode = atom.config.get('core.useTreeSitterParsers');
let isExperimental = atom.config.get('core.useExperimentalModernTreeSitter');
let isLegacy = atom.config.get('core.useLegacyTreeSitter');
if (!isTreeSitterMode) return 'textmate';
return isExperimental ? 'wasm-tree-sitter' : 'node-tree-sitter';
}
function isExperimentalTreeSitterMode() {
return getLanguageModeConfig() === 'wasm-tree-sitter';
}
function isTreeSitter(grammar) {
return isOldTreeSitter(grammar) || isModernTreeSitter(grammar);
return isLegacy ? 'node-tree-sitter' : 'web-tree-sitter';
}
function isModernTreeSitter(grammar) {
return grammar.constructor.name === 'WASMTreeSitterGrammar';
}
function isOldTreeSitter(grammar) {
function isLegacyTreeSitter(grammar) {
return grammar.constructor.name === 'TreeSitterGrammar';
}
@ -180,9 +182,69 @@ function compareGrammarType(a, b) {
return getGrammarScore(a) - getGrammarScore(b);
}
// Given a scope name, determines the user's preferred parser type for that
// language.
function getParserPreferenceForScopeName(scopeName) {
let useTreeSitterParsers = atom.config.get(
'core.useTreeSitterParsers',
{ scope: [scopeName] }
);
let useLegacyTreeSitter = atom.config.get(
'core.useLegacyTreeSitter',
{ scope: [scopeName] }
);
if (!useTreeSitterParsers) {
return 'textmate';
} else if (useLegacyTreeSitter) {
return 'node-tree-sitter';
} else {
return 'web-tree-sitter';
}
}
function getBadgeTextForGrammar(grammar) {
switch (grammar.constructor.name) {
case 'Grammar':
return 'TextMate';
case 'WASMTreeSitterGrammar':
return 'Modern Tree-sitter';
case 'TreeSitterGrammar':
return 'Legacy Tree-sitter';
}
}
const BADGE_COLORS_BY_LANGUAGE_MODE_CONFIG = {
'textmate': {
'Grammar': 'badge-success',
'TreeSitterGrammar': 'badge-info',
'WASMTreeSitterGrammar': 'badge-info'
},
'web-tree-sitter': {
'WASMTreeSitterGrammar': 'badge-success',
'TreeSitterGrammar': 'badge-warning',
'Grammar': 'badge-info'
},
'node-tree-sitter': {
'TreeSitterGrammar': 'badge-success',
'WASMTreeSitterGrammar': 'badge-warning',
'Grammar': 'badge-info'
}
};
function getBadgeColorForGrammar(grammar) {
let languageModeConfig = getLanguageModeConfig();
let classNameMap = BADGE_COLORS_BY_LANGUAGE_MODE_CONFIG[languageModeConfig];
return classNameMap[grammar.constructor.name];
}
function getGrammarScore(grammar) {
let languageParser = getLanguageModeConfig();
if (isModernTreeSitter(grammar)) { return -2; }
if (isOldTreeSitter(grammar)) { return -1; }
let languageParser = getParserPreferenceForScopeName(grammar.scopeName);
if (isModernTreeSitter(grammar)) {
return languageParser === 'node-tree-sitter' ? -1 : -2;
}
if (isLegacyTreeSitter(grammar)) {
return languageParser === 'node-tree-sitter' ? -2 : -1;
}
return languageParser === 'textmate' ? -3 : 0;
}

View File

@ -22,12 +22,13 @@
"showOnRightSideOfStatusBar": {
"type": "boolean",
"default": true,
"description": "Show the active pane item's language on the right side of Pulsar's status bar, instead of the left."
"description": "Show the active pane items language on the right side of Pulsars status bar, instead of the left."
},
"hideDuplicateTextMateGrammars": {
"type": "boolean",
"default": true,
"description": "Hides the TextMate grammar when there is an existing Tree-sitter grammar"
"title": "Hide Duplicate Grammars",
"description": "Hides non-preferred grammars when there is more than one grammar. When checked, whichever grammar is preferred for a given scope name (TextMate or Tree-sitter) will be the only one shown. When unchecked, all grammars will always be shown in the list, regardless of the users settings."
}
}
}

View File

@ -0,0 +1,8 @@
module.exports = {
env: { jasmine: true },
rules: {
"node/no-unpublished-require": "off",
"no-unused-vars": "off",
"no-empty": "off"
}
};

View File

@ -3,9 +3,9 @@ const SelectListView = require('atom-select-list');
function setConfigForLanguageMode(mode) {
let useTreeSitterParsers = mode !== 'textmate';
let useExperimentalModernTreeSitter = mode === 'wasm-tree-sitter';
let useLegacyTreeSitter = mode === 'node-tree-sitter';
atom.config.set('core.useTreeSitterParsers', useTreeSitterParsers);
atom.config.set('core.useExperimentalModernTreeSitter', useExperimentalModernTreeSitter);
atom.config.set('core.useLegacyTreeSitter', useLegacyTreeSitter);
}
describe('GrammarSelector', () => {
@ -39,12 +39,14 @@ describe('GrammarSelector', () => {
it('displays a list of all the available grammars', async () => {
const grammarView = (await getGrammarView(editor)).element;
let allGrammars = atom.grammars
.getGrammars({ includeTreeSitter: true })
.filter(g => g.name)
// -1 for removing nullGrammar, +1 for adding "Auto Detect"
// Tree-sitter names the regex and JSDoc grammars
expect(grammarView.querySelectorAll('li').length).toBe(
atom.grammars
.getGrammars({ includeTreeSitter: true })
.filter(g => g.name).length
allGrammars.length
);
expect(grammarView.querySelectorAll('li')[0].textContent).toBe(
'Auto Detect'
@ -55,7 +57,9 @@ describe('GrammarSelector', () => {
.forEach(li =>
expect(li.textContent).not.toBe(atom.grammars.nullGrammar.name)
);
expect(grammarView.textContent.includes('Tree-sitter')).toBe(true); // check we are showing and labelling Tree-sitter grammars
if (!atom.config.get('grammar-selector.hideDuplicateTextMateGrammars')) {
expect(grammarView.textContent.includes('Tree-sitter')).toBe(true); // check we are showing and labelling Tree-sitter grammars
}
}));
describe('when a grammar is selected', () =>
@ -204,6 +208,11 @@ describe('GrammarSelector', () => {
}));
describe('when toggling hideDuplicateTextMateGrammars', () => {
// For continuity reasons, the name of the setting won't be changed; but
// this should now be construed as “hide duplicate grammars” — with the
// grammar selector showing whatever the user-indicated preference is for
// a given grammar.
it('shows only the Tree-sitter if true and both exist', async () => {
// the main JS grammar has both a TextMate and Tree-sitter implementation
atom.config.set('grammar-selector.hideDuplicateTextMateGrammars', true);
@ -224,7 +233,7 @@ describe('GrammarSelector', () => {
}
});
it('shows both if false (in proper order when language parser is node-tree-sitter)', async () => {
it('shows all three if false (in proper order when language parser is node-tree-sitter)', async () => {
await atom.packages.activatePackage('language-c'); // punctuation making it sort wrong
setConfigForLanguageMode('node-tree-sitter');
atom.config.set(
@ -239,10 +248,14 @@ describe('GrammarSelector', () => {
const grammar = listItems[i];
const name = grammar.name;
if (cppCount === 0 && name === 'C++') {
// first C++ entry should be Tree-sitter
// first C++ entry should be legacy Tree-sitter
expect(grammar.constructor.name).toBe('TreeSitterGrammar');
cppCount++;
} else if (cppCount === 1) {
// next C++ entry should be modern Tree-sitter
expect(grammar.constructor.name).toBe('WASMTreeSitterGrammar');
cppCount++;
} else if (cppCount === 2) {
// immediate next grammar should be the TextMate version
expect(name).toBe('C++');
expect(grammar.constructor.name).toBe('Grammar');
@ -252,10 +265,44 @@ describe('GrammarSelector', () => {
}
}
expect(cppCount).toBe(2); // ensure we actually saw both grammars
expect(cppCount).toBe(3); // ensure we actually saw all three grammars
});
it('shows both if false (in proper order when language parser is textmate)', async () => {
it('shows all three if false (in proper order when language parser is wasm-tree-sitter)', async () => {
await atom.packages.activatePackage('language-c'); // punctuation making it sort wrong
setConfigForLanguageMode('wasm-tree-sitter');
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
false
);
await getGrammarView(editor);
let cppCount = 0;
const listItems = atom.workspace.getModalPanels()[0].item.items;
for (let i = 0; i < listItems.length; i++) {
const grammar = listItems[i];
const name = grammar.name;
if (cppCount === 0 && name === 'C++') {
// first C++ entry should be legacy Tree-sitter
expect(grammar.constructor.name).toBe('WASMTreeSitterGrammar');
cppCount++;
} else if (cppCount === 1) {
// next C++ entry should be modern Tree-sitter
expect(grammar.constructor.name).toBe('TreeSitterGrammar');
cppCount++;
} else if (cppCount === 2) {
// immediate next grammar should be the TextMate version
expect(name).toBe('C++');
expect(grammar.constructor.name).toBe('Grammar');
cppCount++;
} else {
expect(name).not.toBe('C++'); // there should not be any other C++ grammars
}
}
expect(cppCount).toBe(3); // ensure we actually saw three grammars
});
it('shows all three if false (in proper order when language parser is textmate)', async () => {
await atom.packages.activatePackage('language-c'); // punctuation making it sort wrong
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
@ -269,11 +316,15 @@ describe('GrammarSelector', () => {
const grammar = listItems[i];
const name = grammar.name;
if (cppCount === 0 && name === 'C++') {
// first C++ entry should be TextMate
// first C++ entry should be legacy Tree-sitter
expect(grammar.constructor.name).toBe('Grammar');
cppCount++;
} else if (cppCount === 1) {
// immediate next grammar should be the Tree-sitter version
// next C++ entry should be modern Tree-sitter
expect(grammar.constructor.name).toBe('WASMTreeSitterGrammar');
cppCount++;
} else if (cppCount === 2) {
// immediate next grammar should be the TextMate version
expect(name).toBe('C++');
expect(grammar.constructor.name).toBe('TreeSitterGrammar');
cppCount++;
@ -282,7 +333,7 @@ describe('GrammarSelector', () => {
}
}
expect(cppCount).toBe(2); // ensure we actually saw both grammars
expect(cppCount).toBe(3); // ensure we actually saw three grammars
});
});
@ -352,40 +403,85 @@ describe('GrammarSelector', () => {
.forEach(li =>
expect(li.textContent).not.toBe(atom.grammars.nullGrammar.name)
);
// Ensure we're showing and labelling tree-sitter grammars…
if (!atom.config.get('grammar-selector.hideDuplicateTextMateGrammars')) {
// Ensure we're showing and labelling tree-sitter grammars.
expect(grammarView.textContent.includes('Tree-sitter')).toBe(true);
// …and old tree-sitter grammars.
expect(grammarView.textContent.includes('Legacy Tree-sitter')).toBe(true);
}
});
});
describe('when toggling hideDuplicateTextMateGrammars', () => {
it('shows only the Tree-sitter if true and both exist', async () => {
// the main JS grammar has both a TextMate and Tree-sitter implementation
it('shows only the preferred if true and several exist (and preferred is default)', async () => {
atom.config.set('grammar-selector.hideDuplicateTextMateGrammars', true);
const grammarView = await getGrammarView(editor);
const observedNames = new Map();
// Show a maximum of two different kinds of grammar (both tree-sitter
// variants).
// Show a maximum of one grammar (the modern tree-sitter variant).
grammarView.element.querySelectorAll('li').forEach(li => {
const name = li.getAttribute('data-grammar');
if (!observedNames.has(name)) {
observedNames.set(name, 0);
}
observedNames.set(name, observedNames.get(name) + 1);
expect(observedNames.get(name) < 3).toBe(true, `found ${observedNames.get(name)} of ${name}`);
expect(observedNames.get(name) < 2).toBe(true, `found ${observedNames.get(name)} of ${name}`);
});
// check the seen JS is actually the Tree-sitter one
const list = atom.workspace.getModalPanels()[0].item;
for (const item of list.items) {
if (item.name === 'JavaScript') {
expect(item.constructor.name.includes('TreeSitterGrammar'));
expect(item.constructor.name).toBe('WASMTreeSitterGrammar');
}
}
});
it('shows both if false', async () => {
it('shows only the preferred if true and several exist (and preferred is node-tree-sitter)', async () => {
atom.config.set('grammar-selector.hideDuplicateTextMateGrammars', true);
setConfigForLanguageMode('node-tree-sitter', { scopeSelector: '.source.js' })
const grammarView = await getGrammarView(editor);
const observedNames = new Map();
grammarView.element.querySelectorAll('li').forEach(li => {
const name = li.getAttribute('data-grammar');
if (!observedNames.has(name)) {
observedNames.set(name, 0);
}
observedNames.set(name, observedNames.get(name) + 1);
expect(observedNames.get(name) < 2).toBe(true, `found ${observedNames.get(name)} of ${name}`);
});
// check the seen JS is actually the Tree-sitter one
const list = atom.workspace.getModalPanels()[0].item;
for (const item of list.items) {
if (item.name === 'JavaScript') {
expect(item.constructor.name).toBe('TreeSitterGrammar');
}
}
});
it('shows only the preferred if true and several exist (and preferred is textmate)', async () => {
atom.config.set('grammar-selector.hideDuplicateTextMateGrammars', true);
setConfigForLanguageMode('textmate', { scopeSelector: '.source.js' })
const grammarView = await getGrammarView(editor);
const observedNames = new Map();
grammarView.element.querySelectorAll('li').forEach(li => {
const name = li.getAttribute('data-grammar');
if (!observedNames.has(name)) {
observedNames.set(name, 0);
}
observedNames.set(name, observedNames.get(name) + 1);
expect(observedNames.get(name) < 2).toBe(true, `found ${observedNames.get(name)} of ${name}`);
});
// check the seen JS is actually the Tree-sitter one
const list = atom.workspace.getModalPanels()[0].item;
for (const item of list.items) {
if (item.name === 'JavaScript') {
expect(item.constructor.name).toBe('Grammar');
}
}
});
it('shows three if false (in the proper order when language parser is web-tree-sitter)', async () => {
await atom.packages.activatePackage('language-c'); // punctuation making it sort wrong
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
@ -403,14 +499,14 @@ describe('GrammarSelector', () => {
cppCount++;
} else if (cppCount === 1) {
expect(name).toBe('C++');
expect(grammar.constructor.name).toBe('TreeSitterGrammar'); // second C++ entry should be Legacy Tree-sitter
expect(grammar.constructor.name).toBe('TreeSitterGrammar'); // first C++ entry should be Modern Tree-sitter
cppCount++;
} else if (cppCount === 2) {
expect(name).toBe('C++');
expect(grammar.constructor.name).toBe('Grammar'); // immediate next grammar should be the TextMate version
cppCount++;
} else {
expect(name).not.toBe('C++'); // there should not be any other C++ grammars
expect(name).not.toBe('C++');
}
}
@ -420,6 +516,10 @@ describe('GrammarSelector', () => {
describe('for every Tree-sitter grammar', () => {
it('adds a label to identify it as Tree-sitter', async () => {
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
false
);
const grammarView = await getGrammarView(editor);
const elements = grammarView.element.querySelectorAll('li');
const listItems = atom.workspace.getModalPanels()[0].item.items;
@ -448,6 +548,164 @@ describe('GrammarSelector', () => {
});
// We will not need these tests for long.
describe('when language parser is "node-tree-sitter"', () => {
beforeEach(() => {
setConfigForLanguageMode('node-tree-sitter');
});
describe('when grammar-selector:show is triggered', () => {
it('displays a list of all the available grammars ', async () => {
const grammarView = (await getGrammarView(editor)).element;
// -1 for removing nullGrammar, +1 for adding "Auto Detect"
// Tree-sitter names the regex and JSDoc grammars
expect(grammarView.querySelectorAll('li').length).toBe(
atom.grammars
.getGrammars({ includeTreeSitter: true })
.filter(g => g.name).length
);
expect(grammarView.querySelectorAll('li')[0].textContent).toBe(
'Auto Detect'
);
expect(grammarView.textContent.includes('source.a')).toBe(false);
grammarView
.querySelectorAll('li')
.forEach(li =>
expect(li.textContent).not.toBe(atom.grammars.nullGrammar.name)
);
// Ensure we're showing and labelling tree-sitter grammars…
expect(grammarView.textContent.includes('Tree-sitter')).toBe(true);
// …and also old tree-sitter grammars.
expect(grammarView.textContent.includes('Legacy Tree-sitter')).toBe(true);
});
});
describe('when toggling hideDuplicateTextMateGrammars', () => {
it('shows only the Tree-sitter if true and several exist', async () => {
// the main JS grammar has both a TextMate and Tree-sitter implementation
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
true
);
const grammarView = await getGrammarView(editor);
const observedNames = new Map();
grammarView.element.querySelectorAll('li').forEach(li => {
const name = li.getAttribute('data-grammar');
if (!observedNames.has(name)) {
observedNames.set(name, 0);
}
observedNames.set(name, observedNames.get(name) + 1);
expect(observedNames.get(name) < 2).toBe(true, `found ${observedNames.get(name)} of ${name}`);
});
// check the seen JS is actually the Tree-sitter one
const list = atom.workspace.getModalPanels()[0].item;
for (const item of list.items) {
if (item.name === 'JavaScript') {
expect(item.constructor.name).toBe('TreeSitterGrammar');
}
}
});
it('shows all if false', async () => {
await atom.packages.activatePackage('language-c');
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
false
);
await getGrammarView(editor);
let cppCount = 0;
const listItems = atom.workspace.getModalPanels()[0].item.items;
for (let i = 0; i < listItems.length; i++) {
const grammar = listItems[i];
const name = grammar.name;
if (cppCount === 0 && name === 'C++') {
expect(grammar.constructor.name).toBe('TreeSitterGrammar'); // first C++ entry should be Modern Tree-sitter
cppCount++;
} else if (cppCount === 1) {
expect(grammar.constructor.name).toBe('WASMTreeSitterGrammar'); // next C++ entry should be legacy Tree-sitter
cppCount++;
} else if (cppCount === 2) {
expect(name).toBe('C++');
expect(grammar.constructor.name).toBe('Grammar'); // immediate next grammar should be the TextMate version
cppCount++;
} else {
expect(name).not.toBe('C++'); // there should not be any other C++ grammars
}
}
expect(cppCount).toBe(3); // ensure we actually saw three grammars
});
});
describe('for every Tree-sitter grammar', () => {
it('adds a label to identify it as Tree-sitter (when showing duplicate grammars)', async () => {
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
false
);
const grammarView = await getGrammarView(editor);
const elements = grammarView.element.querySelectorAll('li');
const listItems = atom.workspace.getModalPanels()[0].item.items;
for (let i = 0; i < listItems.length; i++) {
let item = listItems[i];
let element = elements[i];
if (item.constructor.name.includes('TreeSitterGrammar')) {
expect(
element.childNodes[1].childNodes[0].className.startsWith(
'grammar-selector-parser'
)
).toBe(true);
}
if (item.constructor.name === 'TreeSitterGrammar') {
expect(
element.childNodes[1].childNodes[0].classList.contains('badge-success')
).toBe(true);
} else if (item.constructor.name === 'WASMTreeSitterGrammar') {
expect(
element.childNodes[1].childNodes[0].classList.contains('badge-warning')
).toBe(true);
}
}
});
it('does not add a label to identify it as Tree-sitter (when hiding duplicate grammars)', async () => {
atom.config.set(
'grammar-selector.hideDuplicateTextMateGrammars',
true
);
const grammarView = await getGrammarView(editor);
const elements = grammarView.element.querySelectorAll('li');
const listItems = atom.workspace.getModalPanels()[0].item.items;
for (let i = 0; i < listItems.length; i++) {
let item = listItems[i];
let element = elements[i];
if (item.constructor.name.includes('TreeSitterGrammar')) {
expect(
element.childNodes[1].childNodes[0].className.startsWith(
'grammar-selector-parser'
)
).toBe(false);
}
if (item.constructor.name === 'TreeSitterGrammar') {
expect(
element.childNodes[1].childNodes[0].classList.contains('badge-success')
).toBe(false);
} else if (item.constructor.name === 'WASMTreeSitterGrammar') {
expect(
element.childNodes[1].childNodes[0].classList.contains('badge-warning')
).toBe(false);
}
}
});
});
});
});
function getTooltipText(element) {

View File

@ -1,5 +1,13 @@
@import "ui-variables";
.grammar-item {
&:after {
content: "";
display: table;
clear: both;
}
}
.grammar-status a,
.grammar-status a:hover {
color: @text-color;

View File

@ -3,9 +3,11 @@ scopeName: 'source.c'
type: 'modern-tree-sitter'
parser: 'tree-sitter-c'
firstLineRegex: '-\\*-[^*]*(Mode:\\s*)?C(\\s*;.*?)?\\s*-\\*-'
injectionRegex: '^(c|C)$'
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-c#212a80f86452bb1316324fa0db730cf52f29e05a'
grammar: 'tree-sitter-c/tree-sitter-c.wasm'
highlightsQuery: 'tree-sitter-c/highlights.scm'
tagsQuery: 'tree-sitter-c/tags.scm'
@ -17,3 +19,6 @@ fileTypes: [
'c'
'h.in'
]
comments:
start: '// '

View File

@ -6,6 +6,7 @@ parser: 'tree-sitter-cpp'
injectionRegex: '^(c|C)(\\+\\+|pp|PP)$'
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-cpp#a71474021410973b29bfe99440d57bcd750246b1'
grammar: 'tree-sitter-cpp/tree-sitter-cpp.wasm'
highlightsQuery: 'tree-sitter-cpp/highlights.scm'
tagsQuery: 'tree-sitter-cpp/tags.scm'
@ -33,3 +34,6 @@ fileTypes: [
]
contentRegex: '\n\\s*(namespace|class|template)\\s+'
comments:
start: '// '

View File

@ -3,6 +3,7 @@ scopeName: 'source.c'
type: 'tree-sitter'
parser: 'tree-sitter-c'
firstLineRegex: '-\\*-[^*]*(Mode:\\s*)?C(\\s*;.*?)?\\s*-\\*-'
injectionRegex: 'c|C'
fileTypes: [

View File

@ -13,20 +13,51 @@
"#define" @keyword.control.directive.define.c
"#include" @keyword.control.directive.include.c
; This will match if the more specific rules above haven't matched. The
; anonymous nodes will match under ideal conditions, but might not be present
; if the parser is flummoxed.
(["#if" "#ifdef" "#ifndef" "#endif" "#elif" "#else" "#define" "#include"] @punctuation.definition.directive.c
(#set! adjust.endAfterFirstMatchOf "^#"))
; `preproc_directive` will be used when the parser doesn't recognize the
; directive as one of the above. It's permissive; `#afdfafsdfdfad` would be
; parsed as a `preproc_directive`.
;
; Hence this rule will match if the more specific rules above haven't matched.
; The anonymous nodes will match under ideal conditions, but might not be
; present even when they ought to be _if_ the parser is flummoxed; so this'll
; sometimes catch `#ifdef` and others.
((preproc_directive) @keyword.control.directive.c
(#set! capture.shy true))
((preproc_ifdef
(identifier) @entity.name.function.preprocessor.c
(#match? @entity.name.function.preprocessor.c "[a-zA-Z_$][\\w$]*")))
((preproc_directive) @punctuation.definition.directive.c
(#set! capture.shy true)
(#set! adjust.endAfterFirstMatchOf "^#"))
; Macro functions are definitely entities.
(preproc_function_def
(identifier) @entity.name.function.preprocessor.c
(#set! capture.final true))
; Identifiers in macro definitions are definitely constants.
((preproc_def
name: (identifier) @constant.preprocessor.c))
; We can also safely treat identifiers as constants in `#ifdef`…
((preproc_ifdef
(identifier) @constant.preprocessor.c))
; …and `#if` and `#elif`…
(preproc_if
(binary_expression
(identifier) @constant.preprocessor.c))
(preproc_elif
(binary_expression
(identifier) @constant.preprocessor.c))
; …and `#undef`.
((preproc_call
directive: (preproc_directive) @_IGNORE_
argument: (preproc_arg) @constant.preprocessor.c)
(#eq? @_IGNORE_ "#undef"))
(system_lib_string) @string.quoted.other.lt-gt.include.c
((system_lib_string) @punctuation.definition.string.begin.c
(#set! adjust.endAfterFirstMatchOf "^<"))
@ -43,30 +74,50 @@
(type_identifier) @_IGNORE_
(#set! capture.final true))
(primitive_type) @support.type.builtin.c
(type_identifier) @support.type.other.c
(primitive_type) @support.storage.type.builtin.c
; When the user has typed `#define FOO`, the macro injection thinks that `FOO`
; is a type declaration (for some reason). This node structure seems to exist
; only in that unusual and incorrect scenario, so we'll stop it from happening
; so that it doesn't override the underlying `constant.other.c` scope.
(translation_unit
(type_identifier) @_IGNORE_
(#set! capture.final))
(type_identifier) @support.other.storage.type.c
; These types are all reserved words; if we see an identifier with this name,
; it must be a type.
((identifier) @support.type.builtin.c
(#match? @support.type.builtin.c "^(char|int|float|double|long)$"))
((identifier) @support.storage.type.builtin.c
(#match? @support.storage.type.builtin.c "^(char|int|float|double|long)$"))
; Assume any identifier that ends in `_t` is a type. This convention is not
; always followed, but it's a very strong indicator when it's present.
((identifier) @support.type.other.c
(#match? @support.type.other.c "_t$"))
((identifier) @support.other.storage.type.c
(#match? @support.other.storage.type.c "_t$"))
; These refer to language constructs and remain in the `storage` namespace.
[
"enum"
"long"
"short"
"signed"
"struct"
"typedef"
"union"
"unsigned"
] @storage.type.c
; These refer to value types and go under `support`.
[
"long"
"short"
] @support.storage.type.builtin.c
; These act as modifiers to value types and also go under `support`.
[
"signed"
"unsigned"
] @support.storage.modifier.builtin.c
; These act as general language modifiers and remain in the `storage`
; namespace.
[
"const"
"extern"
@ -75,10 +126,10 @@
"restrict"
"static"
"volatile"
] @storage.modifier.c
] @storage.modifier._TYPE_.c
((primitive_type) @support.type.stdint.c
(#match? @support.type.stdint.c "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$"))
((primitive_type) @support.storage.type.stdint.c
(#match? @support.storage.type.stdint.c "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$"))
(enum_specifier
name: (type_identifier) @variable.other.declaration.type.c)
@ -116,32 +167,69 @@
; Declarations and assignments
; ----------------------------
; The "x" in `int x`;
; The "x" in `int x;`
(declaration
declarator: (identifier) @variable.declaration.c)
declarator: (identifier) @variable.other.declaration.c)
; The "x" in `int x = y`;
; The "x" in `int x = y;`
(init_declarator
declarator: (identifier) @variable.declaration.c)
declarator: (identifier) @variable.other.declaration.c)
; The "x" in `SomeType *x;`
; (Should work no matter how many pointers deep we are.)
(pointer_declarator
declarator: [(identifier) (field_identifier)] @variable.other.declaration.pointer.c
(#is? test.descendantOfType "declaration field_declaration"))
; An array declarator: the "table" in `int table[4];`
(array_declarator
declarator: (identifier) @variable.other.declaration.c)
; A member of a struct.
(field_declaration
(field_identifier) @entity.other.attribute-name.c)
(field_identifier) @variable.other.declaration.member.c)
; An attribute in a C99 struct designated initializer:
; the "foo" in `MY_TYPE a = { .foo = true };
(initializer_pair
(field_designator
(field_identifier) @variable.other.declaration.member.c))
; (and the associated ".")
(initializer_pair
(field_designator
"." @keyword.operator.accessor.c))
(field_declaration
(pointer_declarator
(field_identifier) @entity.other.attribute-name.c))
(field_identifier) @variable.other.declaration.member.c))
(field_declaration
(array_declarator
(field_identifier) @entity.other.attribute-name.c))
(field_identifier) @variable.other.declaration.member.c))
(init_declarator
(pointer_declarator
(identifier) @entity.other.attribute-name.c))
(identifier) @variable.other.declaration.member.c))
; The "x" in `x = y;`
(assignment_expression
left: (identifier) @variable.other.assignment.c)
; The "foo" in `something->foo = "bar";`
(assignment_expression
left: (field_expression
field: (field_identifier) @variable.other.member.assignment.c)
(#set! capture.final))
; Goto label definitions: the "foo" in `foo:` before a statement.
(labeled_statement
label: (statement_identifier) @entity.name.label.c)
; Goto statements — the "foo" in `goto foo;`
(goto_statement
label: (statement_identifier) @support.other.label.c)
; Function parameters
; -------------------
@ -154,9 +242,10 @@
declarator: (identifier) @variable.parameter.c)
; The "foo" in `const char *foo` within a parameter list.
(parameter_declaration
declarator: (pointer_declarator
declarator: (identifier) @variable.parameter.c))
; (Should work no matter how many pointers deep we are.)
(pointer_declarator
declarator: [(identifier) (field_identifier)] @variable.parameter.pointer.c
(#is? test.descendantOfType "parameter_declaration"))
; The "foo" in `const char foo[]` within a parameter list.
(parameter_declaration
@ -172,7 +261,13 @@
; The "size" in `finfo->size`.
(field_expression
"->"
field: (field_identifier) @support.other.property.c)
field: (field_identifier) @variable.other.member.c)
; The "bar" in `foo.bar`.
(field_expression
operator: "."
field: (field_identifier) @variable.other.member.c)
; FUNCTIONS
@ -212,8 +307,19 @@
(false)
] @constant.language._TYPE_.c
((identifier) @constant.c
(#match? @constant.c "[_A-Z][_A-Z0-9]*$"))
; Don't try to scope (e.g.) `int FOO = 1` as a constant when the user types `=`
; but has not typed the value yet.
(ERROR
(identifier) @_IGNORE_
(#set! capture.final))
; In most languages we wouldn't be making the assumption that an all-caps
; identifier should be treated as a constant. But those languages don't have
; macro preprocessors. The convention is decently strong in C/C++ that all-caps
; identifiers will refer to `#define`d things.
((identifier) @constant.other.c
(#match? @constant.other.c "^[_A-Z][_A-Z0-9]*$")
(#set! capture.shy))
; COMMENTS
@ -309,8 +415,10 @@
";" @punctuation.terminator.statement.c
"," @punctuation.separator.comma.c
"->" @punctuation.separator.pointer-access.c
("," @punctuation.separator.comma.c
(#set! capture.shy))
("->" @keyword.operator.accessor.pointer-access.c
(#set! capture.shy))
(parameter_list
"(" @punctuation.definition.parameters.begin.bracket.round.c
@ -335,6 +443,22 @@
"[" @punctuation.definition.array.begin.bracket.square.c
"]" @punctuation.definition.array.end.bracket.square.c
; META
; ====
((compound_statement) @meta.block.c
(#set! adjust.startAt firstChild.endPosition)
(#set! adjust.endAt lastChild.startPosition))
((enumerator_list) @meta.block.enum.c
(#set! adjust.startAt firstChild.endPosition)
(#set! adjust.endAt lastChild.startPosition))
((field_declaration_list) @meta.block.field.c
(#set! adjust.startAt firstChild.endPosition)
(#set! adjust.endAt lastChild.startPosition))
; TODO:
;
; * TM-style grammar has a lot of `mac-classic` scopes. I doubt they'd be

View File

@ -13,29 +13,55 @@
"#define" @keyword.control.directive.define.cpp
"#include" @keyword.control.directive.include.cpp
; This will match if the more specific rules above haven't matched. The
; anonymous nodes will match under ideal conditions, but might not be present
; if the parser is flummoxed.
((preproc_directive) @keyword.control.directive.c
(["#if" "#ifdef" "#ifndef" "#endif" "#elif" "#else" "#define" "#include"] @punctuation.definition.directive.cpp
(#set! adjust.endAfterFirstMatchOf "^#"))
; `preproc_directive` will be used when the parser doesn't recognize the
; directive as one of the above. It's permissive; `#afdfafsdfdfad` would be
; parsed as a `preproc_directive`.
;
; Hence this rule will match if the more specific rules above haven't matched.
; The anonymous nodes will match under ideal conditions, but might not be
; present even when they ought to be _if_ the parser is flummoxed; so this'll
; sometimes catch `#ifdef` and others.
((preproc_directive) @keyword.control.directive.cpp
(#set! capture.shy true))
((preproc_ifdef
(identifier) @entity.name.function.preprocessor.c
(#match? @entity.name.function.preprocessor.c "[a-zA-Z_$][\\w$]*")))
(preproc_function_def
(identifier) @entity.name.function.preprocessor.c
(#set! capture.final true))
((preproc_directive) @punctuation.definition.directive.cpp
(#set! capture.shy true)
(#set! adjust.endAfterFirstMatchOf "^#"))
; Macro functions are definitely entities.
(preproc_function_def
(identifier) @entity.name.function.preprocessor.cpp
(#set! capture.final true)
)
(#set! capture.final true))
(system_lib_string) @string.quoted.other.lt-gt.include.c
((system_lib_string) @punctuation.definition.string.begin.c
; Identifiers in macro definitions are definitely constants.
((preproc_def
name: (identifier) @constant.preprocessor.cpp))
; We can also safely treat identifiers as constants in `#ifdef`…
((preproc_ifdef
(identifier) @constant.preprocessor.cpp))
; …and `#if` and `#elif`…
(preproc_if
(binary_expression
(identifier) @constant.preprocessor.cpp))
(preproc_elif
(binary_expression
(identifier) @constant.preprocessor.cpp))
; …and `#undef`.
((preproc_call
directive: (preproc_directive) @_IGNORE_
argument: (preproc_arg) @constant.preprocessor.cpp)
(#eq? @_IGNORE_ "#undef"))
(system_lib_string) @string.quoted.other.lt-gt.include.cpp
((system_lib_string) @punctuation.definition.string.begin.cpp
(#set! adjust.endAfterFirstMatchOf "^<"))
((system_lib_string) @punctuation.definition.string.end.c
((system_lib_string) @punctuation.definition.string.end.cpp
(#set! adjust.startBeforeFirstMatchOf ">$"))
@ -48,15 +74,24 @@
(type_identifier) @_IGNORE_
(#set! capture.final true))
; When the user has typed `#define FOO`, the macro injection thinks that `FOO`
; is a type declaration (for some reason). This node structure seems to exist
; only in that unusual and incorrect scenario, so we'll stop it from happening
; so that it doesn't override the underlying `constant.other.c` scope.
(translation_unit
(type_identifier) @_IGNORE_
(#set! capture.final))
(primitive_type) @support.type.builtin.cpp
; Type parameters
(template_argument_list
(type_descriptor
type: (type_identifier) @variable.parameter.type.cpp
(#set! capture.final true)))
; Mark all function definition types with data…
(function_definition
type: (_) @_IGNORE_
(#set! functionDefinitionType true))
; …so that we can detect when a type identifier is part of a template/generic.
((type_identifier) @variable.parameter.type.cpp
(#is? test.descendantOfNodeWithData functionDefinitionType))
(class_specifier
(type_identifier) @entity.name.class.cpp
@ -67,28 +102,38 @@
; These types are all reserved words; if we see an identifier with this name,
; it must be a type.
((identifier) @support.type.builtin.cpp
(#match? @support.type.builtin.cpp "^(char|int|float|double|long)$"))
((identifier) @support.storage.type.builtin.cpp
(#match? @support.storage.type.builtin.cpp "^(char|int|float|double|long)$"))
; Assume any identifier that ends in `_t` is a type. This convention is not
; always followed, but it's a very strong indicator when it's present.
((identifier) @support.type.other.cpp
(#match? @support.type.other.cpp "_t$"))
((identifier) @support.other.storage.type.cpp
(#match? @support.other.storage.type.cpp "_t$"))
; These refer to language constructs and remain in the `storage` namespace.
[
"enum"
"long"
"short"
"signed"
"struct"
"typedef"
"union"
"unsigned"
"template"
] @storage.type.cpp
; These refer to value types and go under `support`.
[
"long"
"short"
] @support.storage.type.builtin.cpp
; These act as modifiers to value types and also go under `support`.
[
"signed"
"unsigned"
] @support.storage.modifier.builtin.cpp
; These act as general language modifiers and remain in the `storage`
; namespace.
[
"const"
"extern"
@ -108,15 +153,15 @@
"override"
"final"
"noexcept"
] @storage.modifier.cpp
"typename"
] @storage.modifier._TYPE_.cpp
(
(primitive_type) @support.type.stdint.cpp
(#match? @support.type.stdint.cpp "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$")
(primitive_type) @support.storage.type.stdint.cpp
(#match? @support.storage.type.stdint.cpp "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$")
)
"typename" @storage.modifier.typename.cpp
; FUNCTIONS
; =========
@ -124,9 +169,21 @@
(function_declarator
(identifier) @entity.name.function.cpp)
(function_declarator
(destructor_name
(identifier) @entity.name.function.cpp))
; The "foo" in `void Bar::foo () {`.
(function_declarator
declarator: (qualified_identifier
name: (identifier) @entity.name.function.cpp))
(function_declarator
(field_identifier) @entity.name.function.method.cpp)
(field_initializer
(field_identifier) @entity.name.function.cpp)
(call_expression
(identifier) @support.function.c99.cpp
; Regex copied from the TM grammar.
@ -139,6 +196,19 @@
field: (field_identifier) @support.other.function.cpp)
(#set! capture.final true))
; The "foo" in `troz::foo(...)`.
(call_expression
function: (qualified_identifier
name: (identifier) @support.other.function.cpp)
(#set! capture.final true))
; The "foo" in `troz::foo<SomeType>(...)`.
(call_expression
function: (qualified_identifier
name: (template_function
name: (identifier) @support.other.function.cpp))
(#set! capture.final true))
(call_expression
(identifier) @support.other.function.cpp
(#set! capture.final true))
@ -180,34 +250,67 @@
; Declarations and assignments
; ----------------------------
; The "x" in `int x`;
; The "x" in `int x;`
(declaration
declarator: (identifier) @variable.declaration.cpp)
; The "x" in `int x = y`;
; The "x" in `int x = y;`
(init_declarator
declarator: (identifier) @variable.declaration.cpp)
; The "x" in `SomeType *x;`
; (Should work no matter how many pointers deep we are.)
(pointer_declarator
declarator: [(identifier) (field_identifier)] @variable.declaration.pointer.cpp
(#is? test.descendantOfType "declaration field_declaration"))
; A member of a struct.
(field_declaration
(field_identifier) @variable.declaration.cpp)
(field_identifier) @variable.declaration.member.cpp)
; An attribute in a C99 struct designated initializer:
; the "foo" in `MY_TYPE a = { .foo = true };
(initializer_pair
(field_designator
(field_identifier) @variable.declaration.member.cpp))
; (and the associated ".")
(initializer_pair
(field_designator
"." @keyword.operator.accessor.cpp))
(field_declaration
(pointer_declarator
(field_identifier) @variable.declaration.cpp))
(field_identifier) @variable.declaration.member.cpp))
(field_declaration
(array_declarator
(field_identifier) @variable.declaration.cpp))
(field_identifier) @variable.declaration.member.cpp))
(init_declarator
(pointer_declarator
(identifier) @variable.declaration.cpp))
(identifier) @variable.declaration.member.cpp))
; The "x" in `x = y;`
(assignment_expression
left: (identifier) @variable.other.assignment.cpp)
(reference_declarator
; The "foo" in `something->foo = "bar";`
(assignment_expression
left: (field_expression
field: (field_identifier) @variable.other.member.assignment.cpp)
(#set! capture.final))
((reference_declarator
(identifier) @variable.declaration.cpp)
(#is-not? test.descendantOfType parameter_declaration))
; Goto label definitions like `foo:` before a statement.
(labeled_statement
label: (statement_identifier) @entity.name.label.cpp)
(goto_statement
label: (statement_identifier) @support.other.label.cpp)
; Function parameters
; -------------------
@ -215,12 +318,19 @@
(preproc_params
(identifier) @variable.parameter.preprocessor.cpp)
; The "foo" in `const char foo` within a parameter list.
(parameter_declaration
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.)
(pointer_declarator
declarator: [(identifier) (field_identifier)] @variable.parameter.pointer.cpp
(#is? test.descendantOfType "parameter_declaration"))
(parameter_declaration
declarator: (pointer_declarator
declarator: (identifier) @variable.parameter.cpp))
declarator: (reference_declarator
(identifier) @variable.parameter.cpp))
; The "foo" in `const char foo[]` within a parameter list.
(parameter_declaration
@ -238,6 +348,11 @@
"->"
field: (field_identifier) @variable.other.member.cpp)
; The "bar" in `foo.bar`.
(field_expression
operator: "."
field: (field_identifier) @variable.other.member.cpp)
; Common naming idiom for C++ instanced vars: "fMemberName"
; ((identifier) @variable.other.readwrite.member.cpp
; (#match? @variable.other.readwrite.member.cpp "^(f|m)[A-Z]\\w*$"))
@ -256,11 +371,21 @@
(null)
(true)
(false)
(nullptr)
] @constant.language._TYPE_.cpp
((identifier) @constant.cpp
(#match? @constant.cpp "[_A-Z][_A-Z0-9]*$"))
; Don't try to scope (e.g.) `int FOO = 1` as a constant when the user types `=`
; but has not typed the value yet.
(ERROR
(identifier) @_IGNORE_
(#set! capture.final))
; In most languages we wouldn't be making the assumption that an all-caps
; identifier should be treated as a constant. But those languages don't have
; macro preprocessors. The convention is decently strong in C/C++ that all-caps
; identifiers will refer to `#define`d things.
((identifier) @constant.other.cpp
(#match? @constant.other.cpp "^[_A-Z][_A-Z0-9]*$")
(#set! capture.shy))
; COMMENTS
@ -315,6 +440,7 @@
"throw"
"using"
"namespace"
"class"
] @keyword.control._TYPE_.cpp
; OPERATORS
@ -324,6 +450,7 @@
(abstract_pointer_declarator "*" @keyword.operator.pointer.cpp)
(pointer_expression "*" @keyword.operator.pointer.cpp)
(destructor_name "~" @keyword.operator.destructor.cpp)
"sizeof" @keyword.operator.sizeof.cpp
(pointer_expression "&" @keyword.operator.pointer.cpp)
@ -381,8 +508,10 @@
";" @punctuation.terminator.statement.cpp
"," @punctuation.separator.comma.cpp
"->" @keyword.operator.accessor.cpp
("," @punctuation.separator.comma.cpp
(#set! capture.shy))
("->" @keyword.operator.accessor.pointer-access.cpp
(#set! capture.shy))
(parameter_list
"(" @punctuation.definition.parameters.begin.bracket.round.cpp
@ -407,6 +536,22 @@
"[" @punctuation.definition.array.begin.bracket.square.cpp
"]" @punctuation.definition.array.end.bracket.square.cpp
; META
; ====
((compound_statement) @meta.block.cpp
(#set! adjust.startAt firstChild.endPosition)
(#set! adjust.endAt lastChild.startPosition))
((enumerator_list) @meta.block.enum.cpp
(#set! adjust.startAt firstChild.endPosition)
(#set! adjust.endAt lastChild.startPosition))
((field_declaration_list) @meta.block.field.cpp
(#set! adjust.startAt firstChild.endPosition)
(#set! adjust.endAt lastChild.startPosition))
; TODO:
;
; * TM-style grammar has a lot of `mac-classic` scopes. I doubt they'd be

View File

@ -1,16 +1,30 @@
exports.activate = function() {
exports.activate = function () {
// Highlight macro bodies as C/C++
for (const language of ['c', 'cpp']) {
for (const nodeType of ['preproc_def', 'preproc_function_def']) {
atom.grammars.addInjectionPoint(`source.${language}`, {
type: nodeType,
language(node) {
language() {
return language;
},
content(node) {
return node.lastNamedChild;
return node.descendantsOfType('preproc_arg');
}
});
}
}
};
exports.consumeHyperlinkInjection = (hyperlink) => {
for (const language of ['c', 'cpp']) {
hyperlink.addInjectionPoint(`source.${language}`, {
types: ['comment', 'string_literal']
});
}
};
exports.consumeTodoInjection = (todo) => {
for (const language of ['c', 'cpp']) {
todo.addInjectionPoint(`source.${language}`, { types: ['comment'] });
}
};

View File

@ -10,10 +10,22 @@
"license": "MIT",
"engines": {
"atom": "*",
"node": "*"
"node": ">=12"
},
"dependencies": {
"tree-sitter-c": "0.20.2",
"tree-sitter-cpp": "0.20.0"
},
"consumedServices": {
"hyperlink.injection": {
"versions": {
"0.1.0": "consumeHyperlinkInjection"
}
},
"todo.injection": {
"versions": {
"0.1.0": "consumeTodoInjection"
}
}
}
}

View File

@ -1,6 +1,11 @@
'.source.c, .source.cpp, .source.objc, .source.objcpp':
'editor':
'commentStart': '// '
# Technically, line comments aren't universally valid in C, but all modern
# C compilers support them.
'commentDelimiters':
'block': ['/*', '*/']
'line': '//'
'increaseIndentPattern': '(?x)
^ .* \\{ [^}"\']* $
|^ .* \\( [^\\)"\']* $

View File

@ -39,6 +39,7 @@ parser: 'tree-sitter-clojure'
)
'''
treeSitter:
parserSource: 'github:mauricioszabo/tree-sitter-clojure#inner-strings'
grammar: 'ts/grammar.wasm'
highlightsQuery: [
'ts/highlights.scm'

View File

@ -7,6 +7,7 @@ parser: 'tree-sitter-clojure'
fileTypes: [
]
treeSitter:
parserSource: 'github:mauricioszabo/tree-sitter-clojure#inner-strings'
grammar: 'ts/grammar.wasm'
highlightsQuery: [
'ts/edn-only-highlights.scm'

View File

@ -74,9 +74,12 @@
((list_lit
"(" @punctuation.section.expression.begin (#is-not? test.descendantOfNodeWithData "clojure.dismissTag")
.
(sym_lit) @meta.definition.global @keyword.control (#eq? @meta.definition.global "ns")
(sym_lit) @meta.definition.global @keyword.control
(#eq? @meta.definition.global "ns")
.
(sym_lit) @meta.definition.global @entity.global
; We need to distinguish this `@meta.definition.global` from the one above or
; else this query will fail.
(sym_lit) @meta.definition.global.__TEXT__ @entity.global
")" @punctuation.section.expression.end)
@meta.namespace.clojure
(#set! isNamespace true))

View File

@ -5,7 +5,7 @@
"main": "lib/main",
"engines": {
"atom": "*",
"node": "*"
"node": ">=12"
},
"repository": "https://github.com/pulsar-edit/pulsar",
"license": "MIT",

View File

@ -2,5 +2,7 @@
'editor':
'commentStart': '; '
'nonWordCharacters': '/\\()"\':,;~^[]{}`'
'commentDelimiters':
'line': ';'
'autocomplete':
'extraWordCharacters': '-'

View File

@ -0,0 +1,9 @@
module.exports = {
env: { jasmine: true },
rules: {
"node/no-unpublished-require": "off",
"node/no-extraneous-require": "off",
"no-unused-vars": "off",
"no-empty": "off"
}
};

View File

@ -4,7 +4,6 @@ describe("Clojure grammar", function() {
beforeEach(function() {
atom.config.set('core.useTreeSitterParsers', false);
atom.config.set('core.useExperimentalModernTreeSitter', false);
waitsForPromise(() => atom.packages.activatePackage("language-clojure"));

View File

@ -3,9 +3,7 @@ const path = require('path');
function setConfigForLanguageMode(mode) {
let useTreeSitterParsers = mode !== 'textmate';
let useExperimentalModernTreeSitter = mode === 'modern-tree-sitter';
atom.config.set('core.useTreeSitterParsers', useTreeSitterParsers);
atom.config.set('core.useExperimentalModernTreeSitter', useExperimentalModernTreeSitter);
}
describe('Clojure grammars', () => {

View File

@ -1,6 +1,8 @@
'.source.coffee, .source.litcoffee, .source.coffee.md':
'editor':
'commentStart': '# '
'commentDelimiters':
'line': '#'
'.source.coffee':
'editor':
'autoIndentOnPaste': false

View File

@ -1,5 +1,8 @@
'.source.cs':
'editor':
'commentStart': '// '
'commentDelimiters':
'line': '//'
'block': ['/*', '*/']
'increaseIndentPattern': '(?x)\n\t\t^ .* \\{ [^}"\']* $\n\t| ^ \\s* \\{ \\} $\n\t'
'decreaseIndentPattern': '(?x)\n\t\t^ (.*\\*/)? \\s* \\} ( [^}{"\']* \\{ | \\s* while \\s* \\( .* )? [;\\s]* (//.*|/\\*.*\\*/\\s*)? $\n\t'

View File

@ -8,6 +8,7 @@ fileTypes: [
]
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-css#98c7b3dceb24f1ee17f1322f3947e55638251c37'
grammar: 'tree-sitter/tree-sitter-css.wasm'
highlightsQuery: 'tree-sitter/queries/highlights.scm'
foldsQuery: 'tree-sitter/queries/folds.scm'

View File

@ -1,36 +1,36 @@
; WORKAROUND:
; 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.
;
; When you're typing a new property name inside of a list, tree-sitter-css will
; assume the thing you're typing is a descendant selector tag name until you
; get to the colon. This prevents it from highlighting the incomplete line like
; a selector tag name.
; 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.
;
; Ideally, this is temporary, and we can remove it soon. Until then, it makes
; syntax highlighting less obnoxious.
(descendant_selector
(tag_name) @_IGNORE_
(#set! capture.final true))
((tag_name) @_IGNORE_
(#is? test.descendantOfType "ERROR")
(#set! capture.final))
(ERROR
(attribute_name) @_IGNORE_
(#set! capture.final true))
(#set! capture.final))
((ERROR
(attribute_name) @invalid.illegal)
(#set! capture.final true))
(#set! capture.final))
; WORKAROUND:
;
; `:hover` and other pseudo-classes don't highlight correctly inside a media
; query (https://github.com/tree-sitter/tree-sitter-css/issues/28)
(
(ERROR) @entity.other.attribute-name.pseudo-class.css
(#match? @entity.other.attribute-name.pseudo-class.css "^:[\\w-]+$")
)
; WORKAROUND:
;
; In `::after`, the "after" has a node type of `tag_name`. We want to catch it
; here so that it doesn't get scoped like an HTML tag name in a selector.
; 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.
; Scope the entire `::after` range as one unit.
((pseudo_element_selector)
@ -61,9 +61,6 @@
; (selectors "," @punctuation.separator.list.comma.css)
; The "div" in `div.foo {`.
(tag_name) @entity.name.tag.css
; The "foo" in `div[attr=foo] {`.
(attribute_selector (plain_value) @string.unquoted.css)
@ -77,10 +74,30 @@
(id_selector
"#" @punctuation.definition.entity.id.css) @entity.other.attribute-name.id.css
; KNOWN ISSUE: Namespace selectors like `svg|link` are not supported. See:
; https://github.com/tree-sitter/tree-sitter-css/issues/33
; Declaration of a namespace:
; The "svg" in `@namespace svg url(http://www.w3.org/2000/svg);`
(namespace_name) @entity.other.namespace-prefix.css
;(namespace_name) @entity.other.namespace-prefix.css
; A namespaced tag name:
; The "svg" in `svg|a {}`.
(namespace_selector
. (tag_name) @entity.other.namespace-prefix.css
"|" @punctuation.separator.namespace.css
(#set! capture.final))
; Not sure if this is intended, but a namespaced attribute in an attribute
; selector is construed as two tag-name children of the `attribute_name`.
; The "xl" in `[xl|href] {}`.
(attribute_name
. (tag_name) @entity.other.namespace-prefix.css
"|" @punctuation.separator.namespace.css
(tag_name) @entity.other.attribute_name.css
(#set! capture.final)) @_IGNORE_
; The "div" in `div.foo {`.
(tag_name) @entity.name.tag.css
; The "*" in `*[foo="bar"]`.
(universal_selector) @entity.name.tag.universal.css
; The '.' in `.foo`.
(class_selector
@ -101,27 +118,41 @@
(#set! adjust.startAt lastChild.previousSibling.startPosition)
(#set! adjust.endAt lastChild.endPosition))
; Punctuation around the arguments of a pseudo-class or a function.
(arguments
"(" @punctuation.definition.arguments.begin.bracket.round.css
")" @punctuation.definition.arguments.end.bracket.round.css)
; Punctuation around an attribute selector.
(attribute_selector
"[" @punctuation.definition.entity.begin.bracket.square.css
(attribute_name) @entity.other.attribute-name.css
"]" @punctuation.definition.entity.end.bracket.square.css)
; Operators inside attribute selectors.
(attribute_selector
["=" "^=" "$=" "~=" "|="] @keyword.operator.pattern.css)
; The "foo" in `@keyframes foo {`.
(keyframes_name) @entity.name.keyframes.css
; VARIABLES
; =========
; Variable declaration:
; The "--link-visited" in `--link-visited: #039;`.
(declaration
(property_name) @variable.other.assignment.css
(#match? @variable.other.assignment.css "^--" )
(#set! capture.final true))
; Variable usage:
; The ""--link--visited" in `color: var(--link-visited);`.
((function_name) @support.function.var.css
(arguments (plain_value) @variable.css)
(#eq? @support.function.var.css "var"))
; PROPERTIES
; ==========
@ -145,9 +176,9 @@
(#match? @string.quoted.single.css "^'")
(#match? @string.quoted.single.css "'$"))
; The punctuation around quoted strings.
((string_value) @punctuation.definition.string.begin.css
(#set! adjust.startAndEndAroundFirstMatchOf "^[\"']"))
((string_value) @punctuation.definition.string.end.css
(#set! adjust.startAndEndAroundFirstMatchOf "[\"']$"))
@ -206,10 +237,6 @@
; (#eq? @support.function.var.css "var")
; )
((function_name) @support.function.var.css
(arguments (plain_value) @variable.css)
(#eq? @support.function.var.css "var"))
((function_name) @support.function._TEXT_.css
; Because we just handled it above.
(#not-eq? @support.function._TEXT_.css "var"))
@ -286,16 +313,19 @@
((rule_set) @meta.selector.css
(#set! adjust.endBeforeFirstMatchOf "{"))
; Scope the inside of a media query block so that tooling doesn't mistake it
; for a property/value pair.
(keyword_query) @meta.media-query.css
((feature_query) @meta.media-query.css
(#set! adjust.startAfterFirstMatchOf "^\\(")
(#set! adjust.endBeforeFirstMatchOf "\\)$"))
(parenthesized_query) @meta.media-query.css
; META
; ====
[
(plain_value)
(integer_value)
(string_value)
] @meta.property-value.css
; `!important` starts out as an ERROR node as it's being typed, but we need it
; to be recognized as a possible property value for `autocomplete-css` to be
; able to complete it. This should match only when it comes at the end of a

View File

@ -1 +1,5 @@
(selectors) @name
((selectors) @name
(#set! symbol.tag "Selector"))
((keyframes_name) @name
(#set! symbol.tag "Keyframes"))

View File

@ -1,21 +1,7 @@
exports.activate = () => {
atom.grammars.addInjectionPoint('source.css', {
type: 'comment',
language: () => 'todo',
content: (node) => node
});
atom.grammars.addInjectionPoint('source.css', {
type: 'comment',
language: () => 'hyperlink',
content: (node) => node
});
atom.grammars.addInjectionPoint('source.css', {
type: 'string_value',
language: () => 'hyperlink',
content: (node) => node
exports.consumeHyperlinkInjection = (hyperlink) => {
hyperlink.addInjectionPoint('source.css', {
types: ['comment', 'string_value']
});
// Catch things like
@ -23,15 +9,18 @@ exports.activate = () => {
// @import url(https://www.example.com/style.css);
//
// where the URL is unquoted.
atom.grammars.addInjectionPoint('source.css', {
type: 'call_expression',
hyperlink.addInjectionPoint('source.css', {
types: ['call_expression'],
language: () => 'hyperlink',
content: (node) => {
content(node) {
let functionName = node.descendantsOfType('function_value')[0]?.text;
if (!functionName === 'url') { return null; }
return node.descendantsOfType('plain_value');
}
});
};
exports.consumeTodoInjection = (todo) => {
todo.addInjectionPoint('source.css', { types: ['comment'] });
};

View File

@ -14,5 +14,17 @@
"license": "MIT",
"dependencies": {
"tree-sitter-css": "^0.19.0"
},
"consumedServices": {
"hyperlink.injection": {
"versions": {
"0.1.0": "consumeHyperlinkInjection"
}
},
"todo.injection": {
"versions": {
"0.1.0": "consumeTodoInjection"
}
}
}
}

View File

@ -4,6 +4,8 @@
'editor':
'commentStart': '/*'
'commentEnd': '*/'
'commentDelimiters':
'block': ['/*', '*/']
'foldEndPattern': '(?<!\\*)\\*\\*/|^\\s*\\}|\\/*\\s*@end\\s*\\*\\/'
'autocomplete':
'extraWordCharacters': '-'

View File

@ -39,7 +39,7 @@
(paragraph) @markup.paragraph.gfm
(thematic_break) @punctuation.definition.horizontal-rule.gfm
(thematic_break) @markup.horizontal-rule.gfm
(block_quote) @markup.quote.blockquote.gfm
((block_quote) @punctuation.definition.blockquote.gfm
@ -140,8 +140,9 @@
(code_span) @meta.embedded.line.inline-code.gfm @markup.raw.inline.gfm
(info_string) @storage.modifier.language._TEXT_.gfm
(fenced_code_block) @markup.code.fenced.gfm @meta.embedded.block.fenced-code.gfm
(indented_code_block) @markup.code.indented.gfm @meta.embedded.block.indented-code.gfm
(fenced_code_block
(code_fence_content) @markup.raw.block.fenced.gfm) @meta.embedded.block.fenced-code.gfm
(indented_code_block) @markup.raw.block.indented.gfm @meta.embedded.block.indented-code.gfm
; BOLD/ITALIC/OTHER

View File

@ -3,46 +3,54 @@
(atx_h1_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "· "))
(#set! symbol.prepend "· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h2_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "·· "))
(#set! symbol.prepend "·· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h3_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "··· "))
(#set! symbol.prepend "··· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h4_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "···· "))
(#set! symbol.prepend "···· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h5_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "····· "))
(#set! symbol.prepend "····· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h6_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "······ "))
(#set! symbol.prepend "······ ")
(#set! symbol.icon "chevron-right"))
((setext_heading
(heading_content) @name) @definition.heading
(setext_h1_underline)
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "· "))
(#set! symbol.prepend "· ")
(#set! symbol.icon "chevron-right"))
((setext_heading
(heading_content) @name) @definition.heading
(setext_h2_underline)
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "·· "))
(#set! symbol.prepend "·· ")
(#set! symbol.icon "chevron-right"))

View File

@ -38,30 +38,31 @@ exports.activate = () => {
includeChildren: true
});
// Highlight inline HTML within paragraphs.
atom.grammars.addInjectionPoint('source.gfm.embedded', {
type: 'paragraph',
language(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
if (html.length === 0) { return null; }
return 'html';
},
for (let nodeType of ['paragraph', 'table_cell']) {
atom.grammars.addInjectionPoint('source.gfm.embedded', {
type: nodeType,
language(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
if (html.length === 0) { return null; }
return 'html';
},
content(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
return html;
},
content(node) {
let html = node.descendantsOfType([
'html_open_tag',
'html_close_tag',
'html_self_closing_tag'
]);
return html;
},
includeChildren: true
});
includeChildren: true
});
}
// All code blocks of the form
//
@ -87,3 +88,40 @@ exports.activate = () => {
includeChildren: true
});
};
// Since this parser isn't guaranteed to detect all URLs in paragraphs (see
// https://github.com/pulsar-edit/pulsar/issues/885), we'll inject the
// `hyperlink` parser into `text` nodes in paragraphs when there appear to be
// URLs in them.
exports.consumeHyperlinkInjection = (hyperlink) => {
function textChildren(node) {
let results = [];
for (let i = 0; i < node.namedChildCount; i++) {
let child = node.child(i);
if (child.type === 'text') {
results.push(child);
}
}
return results;
}
hyperlink.addInjectionPoint('source.gfm.embedded', {
types: ['paragraph'],
// Override the language callback so that it doesn't test URLs that are
// already handled in `uri_autolink` nodes.
language(node) {
for (let child of textChildren(node)) {
if (hyperlink.test(child)) {
return 'hyperlink';
}
}
return null;
},
content(node) {
return textChildren(node);
}
});
};

View File

@ -10,5 +10,12 @@
},
"devDependencies": {
"coffeescript": "^1.7.0"
}
},
"consumedServices": {
"hyperlink.injection": {
"versions": {
"0.1.0": "consumeHyperlinkInjection"
}
}
}
}

View File

@ -3,3 +3,5 @@
'softWrap': true
'commentStart': '<!-- '
'commentEnd': ' -->'
'commentDelimiters':
'block': ['<!--', '-->']

View File

@ -0,0 +1,12 @@
module.exports = {
env: { jasmine: true },
globals: {
waitsForPromise: true
},
rules: {
"node/no-unpublished-require": "off",
"node/no-extraneous-require": "off",
"no-unused-vars": "off",
"no-empty": "off"
}
};

View File

@ -3,6 +3,7 @@ describe("GitHub Flavored Markdown grammar", function() {
let grammar = null;
beforeEach(function() {
atom.config.set('core.useTreeSitterParsers', false);
waitsForPromise(() => atom.packages.activatePackage("language-gfm"));
runs(() => grammar = atom.grammars.grammarForScopeName("source.gfm"));

View File

@ -11,6 +11,7 @@ comments:
start: '// '
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-go#ff86c7f1734873c8c4874ca4dd95603695686d7a'
grammar: 'tree-sitter-go/tree-sitter-go.wasm'
highlightsQuery: 'tree-sitter-go/highlights.scm'
foldsQuery: 'tree-sitter-go/folds.scm'

View File

@ -47,6 +47,8 @@
[
"struct"
"interface"
"map"
] @storage.type._TYPE_.go
(struct_type
@ -55,13 +57,14 @@
(field_identifier) @entity.other.attribute-name.go)))
(keyed_element
(field_identifier) @entity.other.attribute-name.go
.
":" @punctuation.separator.key-value.go)
. (literal_element) @entity.other.attribute-name.go)
(keyed_element ":" @punctuation.separator.key-value.go)
[
"break"
"case"
"chan"
"continue"
"default"
"defer"
@ -78,7 +81,10 @@
] @keyword.control._TYPE_.go
; Function names: the "foo" in `func foo() {`
(function_declaration (identifier) @entity.name.function.go)
; Method names: the "Foo" in `func (x Bar) Foo {`
(method_declaration (field_identifier) @entity.name.function.method.go)
(call_expression
(identifier) @support.function.builtin.go
@ -252,7 +258,8 @@
";" @punctuation.terminator.go
"," @punctuation.separator.comma.go
":" @punctuation.separator.colon.go
(":" @punctuation.separator.colon.go
(#set! capture.shy))
(parameter_list
"(" @punctuation.definition.parameters.begin.bracket.round.go

View File

@ -0,0 +1,10 @@
exports.consumeHyperlinkInjection = (hyperlink) => {
hyperlink.addInjectionPoint('source.go', {
types: ['comment', 'interpreted_string_literal', 'raw_string_literal']
});
};
exports.consumeTodoInjection = (todo) => {
todo.addInjectionPoint('source.go', { types: ['comment'] });
};

View File

@ -1,6 +1,7 @@
{
"name": "language-go",
"description": "Go language support in Atom",
"main": "lib/main",
"keywords": [
"tree-sitter"
],
@ -13,5 +14,17 @@
"repository": "https://github.com/pulsar-edit/pulsar",
"dependencies": {
"tree-sitter-go": "0.19.1"
},
"consumedServices": {
"hyperlink.injection": {
"versions": {
"0.1.0": "consumeHyperlinkInjection"
}
},
"todo.injection": {
"versions": {
"0.1.0": "consumeTodoInjection"
}
}
}
}

View File

@ -1,6 +1,9 @@
'.source.go':
'editor':
'commentStart': '// '
'commentDelimiters':
'block': ['/*', '*/']
'line': '//'
'increaseIndentPattern': '^.*(\\bcase\\b.*:|\\bdefault\\b:|(\\b(func|if|else|switch|select|for|struct)\\b.*)?{[^}]*|\\([^)]*)$'
'decreaseIndentPattern': '^\\s*(\\bcase\\b.*:|\\bdefault\\b:|}[),]?|\\)[,]?)$'
'decreaseNextIndentPattern': '^\\s*[^\\s()}]+(?<m>[^()]*\\((?:\\g<m>[^()]*|[^()]*)\\))*[^()]*\\)[,]?$'

View File

@ -11,6 +11,7 @@ fileTypes: [
injectionRegex: '^(ejs|EJS)$'
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-embedded-template#203f7bd3c1bbfbd98fc19add4b8fcb213c059205'
grammar: 'tree-sitter-embedded-template/tree-sitter-embedded-template.wasm'
highlightsQuery: 'tree-sitter-embedded-template/ejs/highlights.scm'
foldsQuery: 'tree-sitter-embedded-template/ejs/folds.scm'

View File

@ -11,6 +11,7 @@ fileTypes: [
injectionRegex: '^(erb|ERB)$'
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-embedded-template#203f7bd3c1bbfbd98fc19add4b8fcb213c059205'
grammar: 'tree-sitter-embedded-template/tree-sitter-embedded-template.wasm'
highlightsQuery: 'tree-sitter-embedded-template/erb/highlights.scm'
foldsQuery: 'tree-sitter-embedded-template/erb/folds.scm'

View File

@ -6,6 +6,7 @@ parser: 'tree-sitter-html'
injectionRegex: '(HTML|html|Html)$'
treeSitter:
parserSource: 'github:tree-sitter/tree-sitter-html#d742025fa2d8e6100f134a6ea990443aa1f074b3'
grammar: 'tree-sitter-html/tree-sitter-html.wasm'
highlightsQuery: 'tree-sitter-html/highlights.scm'
foldsQuery: 'tree-sitter-html/folds.scm'
@ -20,3 +21,4 @@ fileTypes: [
comments:
start: '<!-- '
end: ' -->'
block: ['<!--', '-->']

View File

@ -1,10 +1,74 @@
; When dealing with a self-closing element that spans multiple lines, this lets
; us fold the attribute list.
;
; This query captures elements that happen to be self-closing but don't end
; with an XHTML-style ` />`. Because `tree-sitter-html` doesn't distinguish
; these from elements that can have content, we have to check the tag name to
; know how to treat these.
((element
(start_tag
(tag_name) @_IGNORE_) @fold)
(#match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$")
)
; This one captures the XHTML-style nodes.
(self_closing_tag) @fold
; TODO: Right now, the fold cache doesn't work properly when a given range
; satisfies more than one fold. We should employ `ScopeResolver` to fix this.
; Fold up all of
;
; <div
; foo="bar"
; baz="thud">
;
; </div>
;
; with the fold indicator appearing on whichever line has the `>` that closes
; the opening tag.
;
; Usually this'll be the same line on which the tag opened; but when it isn't,
; this allows for the attribute list of the opening element to be folded
; separately from the element's contents.
;
(element
(start_tag
(tag_name) @_IGNORE_
">" @fold)
(#not-match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$")
(#set! fold.endAt parent.parent.lastNamedChild.startPosition)
(#set! fold.adjustToEndOfPreviousRow true)
)
; When we have…
;
; <div
; foo="bar"
; baz="thud"
; >
;
; </div>
;
; …we can put a fold indicator on the line with `<div` and use it to fold up
; all of a start tag's attributes.
;
; We keep the end of the fold on a separate line because otherwise we lose the
; ability to independently toggle the folding of the element's contents.
;
(element
(start_tag
(tag_name) @_IGNORE_) @fold
(#not-match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$")
(#set! fold.endAt lastChild.startPosition)
(#set! fold.adjustToEndOfPreviousRow true))
; (element) @fold
; (style_element) @fold
; (script_element) @fold
[
(element)
(script_element)
(style_element)
] @fold

View File

@ -105,7 +105,14 @@
; ----------
(attribute "=" @punctuation.separator.key-value.html)
(attribute_name) @entity.other.attribute-name
(attribute_name) @entity.other.attribute-name.html
; If this matches, the value is double-quoted.
(quoted_attribute_value "\"") @string.quoted.double.html
; If this matches, the value is single-quoted.
(quoted_attribute_value "'") @string.quoted.single.html
; Single- and double-quotes around attribute values.
((quoted_attribute_value ["\"" "'"] @punctuation.definition.string.begin.html)
@ -114,11 +121,6 @@
((quoted_attribute_value ["\"" "'"] @punctuation.definition.string.end.html)
(#is? test.last true))
; If this matches, the value is double-quoted.
(quoted_attribute_value "\"") @string.quoted.double.html
; If this matches, the value is single-quoted.
(quoted_attribute_value "'") @string.quoted.single.html
; Prevent quoted attribute values from having `string.unquoted` applied.
(quoted_attribute_value
@ -135,3 +137,8 @@
; ====
(entity) @constant.character.entity.html
; Helpers for autocomplete-html
((element) @meta.tag.incomplete.html
(#not-match? @meta.tag.incomplete.html ">$"))

View File

@ -1,7 +1,7 @@
((start_tag) @indent
; Only indent if this isn't a self-closing tag.
(#not-match? @indent "^<(?:area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)\\s"))
(#not-match? @indent "^<(?:area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)(?=\\s|>)"))
; `end_tag` will still match when only `</div` is present. Without enforcing
; the presence of `>`, the dedent happens too soon.

View File

@ -1,4 +1,4 @@
exports.activate = function() {
exports.activate = function () {
atom.grammars.addInjectionPoint('text.html.basic', {
type: 'script_element',
language() {
@ -19,38 +19,6 @@ exports.activate = function() {
}
});
const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/;
const HYPERLINK_PATTERN = /\bhttps?:/
atom.grammars.addInjectionPoint('text.html.basic', {
type: 'comment',
language: (node) => {
return TODO_PATTERN.test(node.text) ? 'todo' : undefined;
},
content: (node) => node,
languageScope: null
});
atom.grammars.addInjectionPoint('text.html.basic', {
type: 'comment',
language: (node) => {
return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined;
},
content: (node) => node,
languageScope: null
});
atom.grammars.addInjectionPoint('text.html.basic', {
type: 'attribute_value',
language: (node) => {
return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined;
},
content: (node) => node,
languageScope: null
});
// TODO: Inject hyperlink grammar into plain text?
// EMBEDDED
atom.grammars.addInjectionPoint('text.html.ejs', {
@ -95,3 +63,14 @@ exports.activate = function() {
}
});
};
exports.consumeHyperlinkInjection = (hyperlink) => {
// TODO: Inject hyperlink grammar into plain text?
hyperlink.addInjectionPoint('text.html.basic', {
types: ['comment', 'attribute_value']
});
};
exports.consumeTodoInjection = (todo) => {
todo.addInjectionPoint('text.html.basic', { types: ['comment'] });
};

View File

@ -19,5 +19,17 @@
},
"devDependencies": {
"dedent": "^0.7.0"
},
"consumedServices": {
"hyperlink.injection": {
"versions": {
"0.1.0": "consumeHyperlinkInjection"
}
},
"todo.injection": {
"versions": {
"0.1.0": "consumeTodoInjection"
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More