Skip to content

Commit

Permalink
fix!: lookaround assertions (#39)
Browse files Browse the repository at this point in the history
BREAKING CHANGES:
- Depends on tree-sitter-regex [v0.20.0](https://github.com/tree-sitter/tree-sitter-regex/pull/15/files)

* chore: simplify test runner, ci, and makefile

* test: fixup lookbehind cases

* fix: lookbehind
  • Loading branch information
bennypowers authored Aug 3, 2023
1 parent 49ade10 commit aeacf14
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 89 deletions.
8 changes: 1 addition & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,12 @@ jobs:
- name: checkout
uses: actions/checkout@v2

- name: Install Packages
run: |
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
git clone --depth 1 https://github.com/MunifTanjim/nui.nvim ~/.local/share/nvim/site/pack/vendor/start/nui.nvim
git clone --depth 1 https://github.com/nvim-treesitter/nvim-treesitter ~/.local/share/nvim/site/pack/vendor/start/nvim-treesitter.nvim
- name: Install Neovim
uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: ${{ matrix.nvim-versions }}

- name: Test
run: make ci
run: make test

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
vendor
.tests

36 changes: 9 additions & 27 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,21 @@ SHELL:=/usr/bin/env bash
.PHONY: test run_tests watch ci unload

clean:
@rm -rf vendor/plenary.nvim
@rm -rf vendor/nvim-treesitter
@rm -rf vendor/nui.nvim

unload:
@pgrep -f 'nvim --headless' | xargs kill -s KILL;

run_tests:
@REGEXPLAINER_DEBOUNCE=false \
nvim \
--headless \
-u tests/mininit.lua \
-c "lua require'plenary.test_harness'.test_directory('tests/regexplainer/', {minimal_init='tests/mininit.lua',sequential=true})"\
-c "qa!"
@rm -rf vendor

watch:
@echo "Testing..."
@find . \
-type f \
-name '*.lua' \
-o -name '*.js' \
! -path "./vendor/**/*" | entr -d make run_tests
! -path "./.tests/**/*" | entr -d make run_tests

test:
@make unload
@make run_tests

ci:
@nvim --version
@nvim \
--headless \
--noplugin \
-u tests/mininit.lua \
-c "TSUpdateSync javascript typescript regex" \
-c "qa!"
@make run_tests
@REGEXPLAINER_DEBOUNCE=false \
nvim \
--headless \
--noplugin \
-u tests/mininit.lua \
-c "lua require'plenary.test_harness'.test_directory('tests/regexplainer/', {minimal_init='tests/mininit.lua',sequential=true})"\
-c "qa!"
24 changes: 17 additions & 7 deletions lua/regexplainer/component/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_n
---@field zero_or_more? boolean # a regexp component marked with `*`
---@field one_or_more? boolean # a regexp component marked with `+`
---@field lazy? boolean # a regexp quantifier component marked with `?`
---@field negative? boolean # when it's a negative lookbehind
---@field negative? boolean # when it's a negative lookaround
---@field direction? 'ahead'|'behind' # when it's a lookaround, is it a lookahead or a lookbehind
---@field error? any # parsing error

---@class RegexplainerParentComponent : RegexplainerBaseComponent
Expand All @@ -34,7 +35,7 @@ local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_n
---| 'control_escape',
---| 'decimal_escape',
---| 'identity_escape',
---| 'lookahead_assertion'
---| 'lookaround_assertion'
---| 'pattern'
---| 'pattern_character'
---| 'term'
Expand All @@ -55,7 +56,7 @@ local component_types = {
'control_escape',
'decimal_escape',
'identity_escape',
'lookahead_assertion',
'lookaround_assertion',
'pattern',
'pattern_character',
'term',
Expand Down Expand Up @@ -113,10 +114,18 @@ end
---@param component RegexplainerComponent
---@return boolean
--
function M.is_look_assertion(component)
return component.type:find('^look%a+_assertion') ~= nil
function M.is_lookaround_assertion(component)
return component.type:find('^lookaround_assertion') ~= nil
end

---@param component RegexplainerComponent
---@return boolean
--
function M.is_lookbehind_assertion(component)
return component.type:find('^lookaround_assertion') ~= nil
end


-- Does a container component contain nothing by pattern_characters?
---@param component RegexplainerComponent
---@return boolean
Expand Down Expand Up @@ -370,11 +379,12 @@ function M.make_components(node, parent, root_regex_node)
end
end

if node_pred.is_look_assertion(child) then
local _, _, sign = string.find(text, '%(%?<?([=!])')
if node_pred.is_lookaround_assertion(child) then
local _, _, behind, sign = string.find(text, '%(%?(<?)([=!])')
component.type = type
component.negative = sign == '!'
component.depth = (parent and parent.depth or 0) + 1
component.direction = behind == '<' and 'behind' or 'ahead'
end

-- once state has been set above, process the children
Expand Down
13 changes: 8 additions & 5 deletions lua/regexplainer/renderers/narrative/narrative.lua
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,16 @@ local function get_narrative_clause(component, options, state)

end

if comp.is_look_assertion(component) then
if component.type == 'lookbehind_assertion' then
if comp.is_lookaround_assertion(component) then
if comp.direction == 'behind' then
state.lookbehind_found = true
end

local negation = component.negative and 'NOT ' or ''
local direction = comp.is_lookahead_assertion(component) and 'followed by' or 'preceeding'
local direction = 'followed by'
if component.direction == 'behind' then
direction = 'preceeding'
end
prefix = '**' .. negation .. direction .. ' ' .. '**'

local sublines, sep = get_sublines(component, options, state)
Expand All @@ -209,7 +212,7 @@ local function get_narrative_clause(component, options, state)
end

if not comp.is_capture_group(component)
and not comp.is_look_assertion(component) then
and not comp.is_lookaround_assertion(component) then
suffix = get_suffix(component)
end

Expand Down Expand Up @@ -249,7 +252,7 @@ function M.recurse(components, options, state)
last = last,
}))

if comp.is_lookahead_assertion(component) then
if comp.is_lookaround_assertion(component) then
if not clauses[#clauses] then
table.insert(clauses, next_clause)
else
Expand Down
18 changes: 5 additions & 13 deletions lua/regexplainer/utils/treesitter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ local node_types = {
'document',
'group_name',
'identity_escape',
'lookahead_assertion',
'lookbehind_assertion',
'lookaround_assertion',
'named_capturing_group',
'non_capturing_group',
'one_or_more',
Expand Down Expand Up @@ -74,8 +73,7 @@ function M.is_container(node)
return type == 'anonymous_capturing_group'
or type == 'alternation'
or type == 'character_class'
or type == 'lookahead_assertion'
or type == 'lookbehind_assertion'
or type == 'lookaround_assertion'
or type == 'named_capturing_group'
or type == 'non_capturing_group'
or type == 'pattern'
Expand Down Expand Up @@ -135,15 +133,9 @@ function M.is_upwards_stop(node)
return node and node:type() == 'pattern' or M.is_document(node)
end

-- Is it a lookahead or lookbehind assertion?
function M.is_look_assertion(node)
---@see https://github.com/tree-sitter/tree-sitter-regex/issues/13
if node:type() == 'ERROR' then
local text = get_node_text(node, 0)
return text:match [[^%(%<]]
else
return require 'regexplainer.component'.is_look_assertion { type = node:type() }
end
-- Is it a lookaround assertion?
function M.is_lookaround_assertion(node)
return require 'regexplainer.component'.is_lookaround_assertion { type = node:type() }
end

function M.is_modifier(node)
Expand Down
19 changes: 1 addition & 18 deletions tests/fixtures/narrative/09 Lookaround.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,14 @@
/@(?=g(?:raph)?ql)@/;

/**
* ⚠️ **Lookbehinds are poorly supported**
* ⚠️ results may not be accurate
* ⚠️ See https://github.com/tree-sitter/tree-sitter-regex/issues/13
*
* **preceeding **:
* `it's the `
* `attack of the killer tomatos`
*/
/(?<=it's the )attack of the killer tomatos/;

/**
* ⚠️ **Lookbehinds are poorly supported**
* ⚠️ results may not be accurate
* ⚠️ See https://github.com/tree-sitter/tree-sitter-regex/issues/13
*
* `x`
* **NOT preceeding **:
* `x` **NOT preceeding **:
* `u`
* `@`
*/
Expand All @@ -65,10 +56,6 @@


/**
* ⚠️ **Lookbehinds are poorly supported**
* ⚠️ results may not be accurate
* ⚠️ See https://github.com/tree-sitter/tree-sitter-regex/issues/13
*
* **preceeding **:
* `g`
* `\``
Expand All @@ -79,10 +66,6 @@
/(?<=g)`(.*)`/mg;

/**
* ⚠️ **Lookbehinds are poorly supported**
* ⚠️ results may not be accurate
* ⚠️ See https://github.com/tree-sitter/tree-sitter-regex/issues/13
*
* **preceeding **:
* `g`
* non-capturing group (_optional_):
Expand Down
76 changes: 64 additions & 12 deletions tests/mininit.lua
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
vim.cmd([[
set rtp+=.
set noswapfile
filetype on
packloadall
runtime plugin/regexplainer.vim
]])

local did, configs = pcall(require, 'nvim-treesitter.configs')
if not did then print(configs) end

configs.setup {}
local M = {}

function M.root(root)
local f = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (root or "")
end

---@param plugin string
function M.load(plugin)
local name = plugin:match(".*/(.*)")
local package_root = M.root(".tests/site/pack/deps/start/")
if not vim.loop.fs_stat(package_root .. name) then
print("Installing " .. plugin)
vim.fn.mkdir(package_root, "p")
vim.fn.system({
"git",
"clone",
"--depth=1",
"https://github.com/" .. plugin .. ".git",
package_root .. "/" .. name,
})
end
end

local langs = {
'html',
'javascript',
'typescript',
'regex',
}

function M.setup()
vim.cmd([[
set noswapfile
filetype on
set runtimepath=$VIMRUNTIME
runtime plugin/regexplainer.vim
]])

vim.opt.runtimepath:append(M.root())
vim.opt.packpath = { M.root(".tests/site") }

M.load("MunifTanjim/nui.nvim")
M.load("nvim-lua/plenary.nvim")
M.load("nvim-treesitter/nvim-treesitter")

local parser_install_dir = M.root(".tests/share/treesitter");
vim.opt.runtimepath:append(parser_install_dir)

vim.cmd[[packloadall]]

require 'nvim-treesitter.configs'.setup {
parser_install_dir = parser_install_dir,
}

for _, lang in ipairs(langs) do
if not require'nvim-treesitter.parsers'.has_parser(lang) then
vim.cmd('TSInstallSync ' .. lang)
end
end
end

M.setup()

0 comments on commit aeacf14

Please sign in to comment.