diff --git a/.gitignore b/.gitignore index 415c8794d..0dbc7f064 100644 --- a/.gitignore +++ b/.gitignore @@ -27,8 +27,6 @@ luac.out # Shared objects (inc. Windows DLLs) *.dll -*.so -*.so.* *.dylib # Executables diff --git a/DOCS.md b/DOCS.md index ac3540cc6..330395d18 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1676,12 +1676,9 @@ More optimized version would be to create a lua file that has only necessary plu -- ~/.config/nvim/lua/partials/org_cron.lua -- If you are using lazy.vim do this: -local treesitter = vim.fn.stdpath('data') .. '/lazy/nvim-treesitter' local orgmode = vim.fn.stdpath('data') .. '/lazy/orgmode' vim.opt.runtimepath:append(orgmode) -vim.opt.runtimepath:append(treesitter) -- If you are using Packer or any other package manager that uses built-in package manager, do this: -vim.cmd('packadd nvim-treesitter') vim.cmd('packadd orgmode') -- Run the orgmode cron diff --git a/README.md b/README.md index 366287597..7312a5c43 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ ### Requirements * Neovim 0.9.2 or later -* [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) ### Installation @@ -33,39 +32,31 @@ Use your favourite package manager: ```lua { 'nvim-orgmode/orgmode', - dependencies = { - { 'nvim-treesitter/nvim-treesitter', lazy = true }, - }, event = 'VeryLazy', config = function() - -- Load treesitter grammar for org - require('orgmode').setup_ts_grammar() - - -- Setup treesitter - require('nvim-treesitter.configs').setup({ - highlight = { - enable = true, - }, - ensure_installed = { 'org' }, - }) - -- Setup orgmode require('orgmode').setup({ org_agenda_files = '~/orgfiles/**/*', org_default_notes_file = '~/orgfiles/refile.org', }) + + -- NOTE: If you are using nvim-treesitter with `ensure_installed = "all"` option + -- add `org` to ignore_install + -- require('nvim-treesitter.configs').setup({ + -- ensure_installed = 'all', + -- ignore_install = { 'org' }, + -- }) end, } ``` -
+
packer.nvim
```lua -use {'nvim-treesitter/nvim-treesitter'} use {'nvim-orgmode/orgmode', config = function() require('orgmode').setup{} end @@ -79,7 +70,6 @@ end
```vim -Plug 'nvim-treesitter/nvim-treesitter' Plug 'nvim-orgmode/orgmode' ``` @@ -90,7 +80,6 @@ Plug 'nvim-orgmode/orgmode'
```vim -call dein#add('nvim-treesitter/nvim-treesitter') call dein#add('nvim-orgmode/orgmode') ``` @@ -104,29 +93,27 @@ since instructions above covers full setup ```lua -- init.lua --- Load custom treesitter grammar for org filetype -require('orgmode').setup_ts_grammar() - --- Treesitter configuration -require('nvim-treesitter.configs').setup { - highlight = { - enable = true, - }, - ensure_installed = {'org'}, -- Or run :TSUpdate org -} - require('orgmode').setup({ org_agenda_files = {'~/Dropbox/org/*', '~/my-orgs/**/*'}, org_default_notes_file = '~/Dropbox/org/refile.org', }) -``` + +-- NOTE: If you are using nvim-treesitter with `ensure_installed = "all"` option +-- add `org` to ignore_install +-- require('nvim-treesitter.configs').setup({ +-- ensure_installed = 'all', +-- ignore_install = { 'org' }, +-- }) Or if you are using `init.vim`, wrap the above snippet like so: ```vim " init.vim lua << EOF -require('orgmode').setup_ts_grammar() ... +require('orgmode').setup({ + org_agenda_files = {'~/Dropbox/org/*', '~/my-orgs/**/*'}, + org_default_notes_file = '~/Dropbox/org/refile.org', +}) EOF ``` @@ -191,29 +178,17 @@ or a hands-on [tutorial](https://github.com/nvim-orgmode/orgmode/wiki/Getting-St ## Treesitter Info The built-in treesitter parser is used for parsing the org files. -Highlights are experimental and partially supported. - -### Advantages of treesitter over built in parsing/syntax: -* More reliable, since parsing is done with a proper parsing tool -* Better highlighting (Experimental, still requires improvements) -* Future features will be easier to implement because the grammar already parses some things that were not parsed before (tables, latex, etc.) -* Allows for easier hacking (custom motions that can work with TS nodes, etc.) ### Known highlighting issues and limitations * LaTex is still highlighted through syntax file -### Improvements over Vim's syntax highlighting -* Better highlighting of certain parts (tags, deadline/schedule/closed dates) -* [Treesitter highlight injections](https://github.com/nvim-treesitter/nvim-treesitter/blob/4f2265632becabcd2c5b1791fa31ef278f1e496c/CONTRIBUTING.md#injections) through `#BEGIN_SRC filetype` blocks -* Headline markup highlighting (https://github.com/nvim-orgmode/orgmode/issues/67) - ## Troubleshoot ### Indentation is not working Make sure you are not overriding indentexpr in Org buffers with [nvim-treesitter indentation](https://github.com/nvim-treesitter/nvim-treesitter#indentation) ### I get `treesitter/query.lua` errors when opening agenda/capture prompt or org files -Make sure you are using latest changes from [tree-sitter-org](https://github.com/milisims/tree-sitter-org) grammar.
-by running `:TSUpdate org` and restarting the editor. +Tree-sitter parser might not be installed. +Try running `:lua require('orgmode.config'):reinstall_grammar()` to reinstall it. ### Dates are not in English Dates are generated with Lua native date support, and it reads your current locale when creating them.
diff --git a/ftplugin/org.lua b/ftplugin/org.lua index 60ebbcc4d..361763c60 100644 --- a/ftplugin/org.lua +++ b/ftplugin/org.lua @@ -9,6 +9,8 @@ local utils = require('orgmode.utils') vim.b.org_bufnr = vim.api.nvim_get_current_buf() +vim.treesitter.start() + config:setup_mappings('org', vim.b.org_bufnr) config:setup_mappings('text_objects', vim.b.org_bufnr) config:setup_foldlevel() @@ -21,7 +23,7 @@ require('orgmode.org.indent').setup_virtual_indent() vim.bo.modeline = false vim.opt_local.fillchars:append('fold: ') vim.opt_local.foldmethod = 'expr' -vim.opt_local.foldexpr = 'nvim_treesitter#foldexpr()' +vim.opt_local.foldexpr = 'v:lua.require("orgmode.org.fold").foldexpr()' if utils.has_version_10() then vim.opt_local.foldtext = '' else diff --git a/lua/orgmode/config/init.lua b/lua/orgmode/config/init.lua index 12ca9771c..653e0d63e 100644 --- a/lua/orgmode/config/init.lua +++ b/lua/orgmode/config/init.lua @@ -28,6 +28,19 @@ function Config:__index(key) return rawget(getmetatable(self), key) end +function Config:install_grammar() + local ok = pcall(vim.treesitter.language.add, 'org') + if ok then + return + end + require('orgmode.utils.treesitter.install').run() +end + +---@param url? string +function Config:reinstall_grammar(url) + return require('orgmode.utils.treesitter.install').run(url) +end + ---@param opts table ---@return OrgConfig function Config:extend(opts) diff --git a/lua/orgmode/files/init.lua b/lua/orgmode/files/init.lua index 3dccc55c9..0819f960c 100644 --- a/lua/orgmode/files/init.lua +++ b/lua/orgmode/files/init.lua @@ -25,18 +25,17 @@ function OrgFiles:new(opts) } setmetatable(data, self) self.__index = self - data:load_sync() return data end ---@param force? boolean Force reload all files ----@return OrgPromise +---@return OrgPromise function OrgFiles:load(force) if not force and self.load_state then if self.load_state == 'loading' then self:ensure_loaded() end - return Promise.resolve(self.files) + return Promise.resolve(self) end self.load_state = 'loading' @@ -51,7 +50,7 @@ function OrgFiles:load(force) return Promise.all(actions):next(function() self.load_state = 'loaded' - return self.files + return self end) end diff --git a/lua/orgmode/init.lua b/lua/orgmode/init.lua index d687df1d7..6146b6173 100644 --- a/lua/orgmode/init.lua +++ b/lua/orgmode/init.lua @@ -1,6 +1,4 @@ _G.orgmode = _G.orgmode or {} -local ts_revision = 'f8c6b1e72f82f17e41004e04e15f62a83ecc27b0' -local setup_ts_grammar_used = false ---@type Org | nil local instance = nil @@ -48,9 +46,11 @@ function Org:init() require('orgmode.events').init() self.highlighter = require('orgmode.colors.highlighter'):new() require('orgmode.colors.highlights').define_highlights() - self.files = require('orgmode.files'):new({ - paths = require('orgmode.config').org_agenda_files, - }) + self.files = require('orgmode.files') + :new({ + paths = require('orgmode.config').org_agenda_files, + }) + :load_sync() self.agenda = require('orgmode.agenda'):new({ files = self.files, }) @@ -96,52 +96,18 @@ function Org:setup_autocmds() }) end ---- @param revision string? -function Org.setup_ts_grammar(revision) - setup_ts_grammar_used = true - local parser_config = require('nvim-treesitter.parsers').get_parser_configs() - ---@diagnostic disable-next-line: inject-field - parser_config.org = { - install_info = { - url = 'https://github.com/nvim-orgmode/tree-sitter-org', - revision = revision or ts_revision, - files = { 'src/parser.c', 'src/scanner.c' }, - }, - filetype = 'org', - } -end - ----@private -function Org._check_ts_grammar() - vim.defer_fn(function() - if setup_ts_grammar_used then - return - end - local parser_config = require('nvim-treesitter.parsers').get_parser_configs() - if parser_config and parser_config.org and parser_config.org.install_info.revision then - if parser_config.org.install_info.revision ~= ts_revision then - require('orgmode.utils').echo_error({ - 'You are using outdated version of tree-sitter grammar for Orgmode.', - 'To use latest version, replace current grammar installation with "require(\'orgmode\').setup_ts_grammar()" and run :TSUpdate org.', - 'More info in setup section of readme: https://github.com/nvim-orgmode/orgmode#setup', - }) - end - else - require('orgmode.utils').echo_error({ - 'Cannot detect parser revision.', - "Please check your org grammar's install info.", - 'Maybe you forgot to call "require(\'orgmode\').setup_ts_grammar()" before setup.', - }) - end - end, 200) +function Org.setup_ts_grammar() + require('orgmode.utils').echo_info( + 'calling require("orgmode").setup_ts_grammar() is no longer necessary. Dependency on nvim-treesitter was removed' + ) end ---@param opts? OrgDefaultConfig ---@return Org function Org.setup(opts) opts = opts or {} - Org._check_ts_grammar() local config = require('orgmode.config'):extend(opts) + config:install_grammar() instance = Org:new() vim.defer_fn(function() if config.notifications.enabled and #vim.api.nvim_list_uis() > 0 then diff --git a/lua/orgmode/org/fold.lua b/lua/orgmode/org/fold.lua new file mode 100644 index 000000000..440023137 --- /dev/null +++ b/lua/orgmode/org/fold.lua @@ -0,0 +1,108 @@ +-- Taken from https://github.com/nvim-treesitter/nvim-treesitter + +local api = vim.api +local ts_utils = require('orgmode.utils.treesitter') + +---@type vim.treesitter.Query +local query = nil + +local M = {} + +-- This is cached on buf tick to avoid computing that multiple times +-- Especially not for every line in the file when `zx` is hit +local folds_levels = ts_utils.memoize_by_buf_tick(function(bufnr) + local max_fold_level = api.nvim_get_option_value('foldnestmax', { win = 0 }) + local trim_level = function(level) + if level > max_fold_level then + return max_fold_level + end + return level + end + + query = query or vim.treesitter.query.get('org', 'folds') + local trees = vim.treesitter.get_parser(bufnr):parse() + local root = trees[1]:root() + + local matches = {} + for _, node in query:iter_captures(root, bufnr) do + table.insert(matches, node) + end + + ---@type table + local start_counts = {} + ---@type table + local stop_counts = {} + + local prev_start = -1 + local prev_stop = -1 + + local min_fold_lines = api.nvim_get_option_value('foldminlines', { win = 0 }) + + for _, match in ipairs(matches) do + local start, _, stop, stop_col = match:range() ---@type integer, integer, integer, integer + + if stop_col == 0 then + stop = stop - 1 + end + + local fold_length = stop - start + 1 + local should_fold = fold_length > min_fold_lines + + -- Fold only multiline nodes that are not exactly the same as previously met folds + -- Checking against just the previously found fold is sufficient if nodes + -- are returned in preorder or postorder when traversing tree + if should_fold and not (start == prev_start and stop == prev_stop) then + start_counts[start] = (start_counts[start] or 0) + 1 + stop_counts[stop] = (stop_counts[stop] or 0) + 1 + prev_start = start + prev_stop = stop + end + end + + ---@type string[] + local levels = {} + local current_level = 0 + + -- We now have the list of fold opening and closing, fill the gaps and mark where fold start + for lnum = 0, api.nvim_buf_line_count(bufnr) do + local prefix = '' + + local last_trimmed_level = trim_level(current_level) + current_level = current_level + (start_counts[lnum] or 0) + local trimmed_level = trim_level(current_level) + current_level = current_level - (stop_counts[lnum] or 0) + local next_trimmed_level = trim_level(current_level) + + -- Determine if it's the start/end of a fold + -- NB: vim's fold-expr interface does not have a mechanism to indicate that + -- two (or more) folds start at this line, so it cannot distinguish between + -- ( \n ( \n )) \n (( \n ) \n ) + -- versus + -- ( \n ( \n ) \n ( \n ) \n ) + -- If it did have such a mechanism, (trimmed_level - last_trimmed_level) + -- would be the correct number of starts to pass on. + if trimmed_level - last_trimmed_level > 0 then + prefix = '>' + elseif trimmed_level - next_trimmed_level > 0 then + -- Ending marks tend to confuse vim more than it helps, particularly when + -- the fold level changes by at least 2; we can uncomment this if + -- vim's behavior gets fixed. + -- prefix = "<" + prefix = '' + end + + levels[lnum + 1] = prefix .. tostring(trimmed_level) + end + + return levels +end) + +---@return string +function M.foldexpr() + local buf = api.nvim_get_current_buf() + local levels = folds_levels(buf) or {} + + return levels[vim.v.lnum] or '0' +end + +return M diff --git a/lua/orgmode/org/indent.lua b/lua/orgmode/org/indent.lua index 6ce26713d..71ccab8ef 100644 --- a/lua/orgmode/org/indent.lua +++ b/lua/orgmode/org/indent.lua @@ -223,47 +223,6 @@ local get_matches = ts_utils.memoize_by_buf_tick(function(bufnr) return matches end) -local prev_section = nil -local function foldexpr() - query = query or vim.treesitter.query.get('org', 'org_indent') - local matches = get_matches(0) - local match = matches[vim.v.lnum] - local next_match = matches[vim.v.lnum + 1] - if not match and not next_match then - return '=' - end - match = match or {} - - if match.type == 'headline' then - prev_section = match - if - match.parent:parent():type() ~= 'section' - and match.stars > 1 - and match.parent:named_child_count('section') == 0 - then - return 0 - end - return '>' .. match.stars - end - - if match.type == 'drawer' or match.type == 'property_drawer' or match.type == 'block' then - if match.line_nr == vim.v.lnum then - return 'a1' - end - if match.line_end_nr == vim.v.lnum then - return 's1' - end - end - - if next_match and next_match.type == 'headline' and prev_section then - if next_match.stars <= prev_section.stars then - return '<' .. prev_section.stars - end - end - - return '=' -end - -- Some explanation as to the caching insanity inside of this function. The `get_matches` function -- is memoized, but that only goes so far. When a user wants to indent a large region, say with -- `norm! 0gg=G` every indent operation will call `get_matches` and get *new* matches. For the most @@ -344,7 +303,6 @@ end return { setup_virtual_indent = setup_virtual_indent, - foldexpr = foldexpr, indentexpr = indentexpr, foldtext = foldtext, } diff --git a/lua/orgmode/parser/files.lua b/lua/orgmode/parser/files.lua index 890d34908..4975d851e 100644 --- a/lua/orgmode/parser/files.lua +++ b/lua/orgmode/parser/files.lua @@ -26,7 +26,7 @@ end function Files.load(callback) Files.loader():load():next(function(files) - Files.orgfiles = files + Files.orgfiles = Files.loader().files Files._build_tags() if callback then callback() diff --git a/lua/orgmode/utils/treesitter.lua b/lua/orgmode/utils/treesitter/init.lua similarity index 97% rename from lua/orgmode/utils/treesitter.lua rename to lua/orgmode/utils/treesitter/init.lua index 2e12cc890..0d110c735 100644 --- a/lua/orgmode/utils/treesitter.lua +++ b/lua/orgmode/utils/treesitter/init.lua @@ -5,7 +5,8 @@ local query_cache = {} -- Reload treesitter highlighter without triggering FileType autocommands that include reloading entire file function M.restart_highlights(bufnr) bufnr = bufnr or 0 - require('nvim-treesitter.configs').reattach_module('highlight', bufnr, 'org') + vim.treesitter.stop(bufnr) + vim.treesitter.start(bufnr) end function M.parse_current_file() diff --git a/lua/orgmode/utils/treesitter/install.lua b/lua/orgmode/utils/treesitter/install.lua new file mode 100644 index 000000000..e8f08d2a0 --- /dev/null +++ b/lua/orgmode/utils/treesitter/install.lua @@ -0,0 +1,142 @@ +local Promise = require('orgmode.utils.promise') +local utils = require('orgmode.utils') +local ts_revision = 'f8c6b1e72f82f17e41004e04e15f62a83ecc27b0' +local uv = vim.loop +local M = { + compilers = { vim.fn.getenv('CC'), 'cc', 'gcc', 'clang', 'cl', 'zig' }, +} + +function M.get_package_path() + -- Path to this source file, removing the leading '@' + local source = string.sub(debug.getinfo(1, 'S').source, 2) + + -- Path to the package root + return vim.fn.fnamemodify(source, ':p:h:h:h:h:h') +end + +function M.select_compiler_args(compiler) + if string.match(compiler, 'cl$') or string.match(compiler, 'cl.exe$') then + return { + '/Fe:', + 'parser.so', + '/Isrc', + 'src/parser.c', + 'src/scanner.c', + '-Os', + '/LD', + } + elseif string.match(compiler, 'zig$') or string.match(compiler, 'zig.exe$') then + return { + 'c++', + '-o', + 'parser.so', + 'src/parser.c', + 'src/scanner.c', + '-lc', + '-Isrc', + '-shared', + '-Os', + } + else + local args = { + '-o', + 'parser.so', + '-I./src', + 'src/parser.c', + 'src/scanner.c', + '-Os', + } + if vim.fn.has('mac') == 1 then + table.insert(args, '-bundle') + else + table.insert(args, '-shared') + end + if vim.fn.has('win32') == 0 then + table.insert(args, '-fPIC') + end + return args + end +end + +function M.exe(cmd, opts) + return Promise.new(function(resolve) + local stdin = uv.new_pipe() + local stdout = uv.new_pipe() + local stderr = uv.new_pipe() + opts.stdio = { stdin, stdout, stderr } + uv.spawn(cmd, opts, function(code) + resolve(code) + end) + end) +end + +function M.get_path(url) + local local_path = vim.fn.expand(url) + local is_local_path = vim.fn.isdirectory(local_path) == 1 + + if is_local_path then + return Promise.resolve(local_path) + end + + local path = ('%s/tree-sitter-org'):format(vim.fn.stdpath('cache')) + vim.fn.delete(path, 'rf') + + utils.echo_info('Installing tree-sitter grammar...') + return M.exe('git', { + args = { 'clone', '--filter=blob:none', url, path }, + }) + :next(function(code) + if code ~= 0 then + error('[orgmode] Failed to clone tree-sitter-org') + end + return M.exe('git', { + args = { 'checkout', ts_revision }, + cwd = path, + }) + end) + :next(function(code) + if code ~= 0 then + error('[orgmode] Failed to checkout to correct revision on tree-sitter-org') + end + return path + end) +end + +---@param url? string +function M.run(url) + url = url or 'https://github.com/nvim-orgmode/tree-sitter-org' + local compiler = vim.tbl_filter(function(exe) + return exe ~= vim.NIL and vim.fn.executable(exe) == 1 + end, M.compilers)[1] + + if not compiler then + error('[orgmode] No C compiler found for installing tree-sitter grammar') + end + + local compiler_args = M.select_compiler_args(compiler) + local package_path = M.get_package_path() + local path = nil + + return M.get_path(url) + :next(function(directory) + path = directory + return M.exe(compiler, { + args = compiler_args, + cwd = directory, + }) + end) + :next(vim.schedule_wrap(function(code) + if code ~= 0 then + error('[orgmode] Failed to compile parser') + end + local renamed = vim.fn.rename(path .. '/parser.so', package_path .. '/parser/org.so') + if renamed ~= 0 then + error('[orgmode] Failed to move generated tree-sitter parser to runtime folder') + end + utils.echo_info('Done!') + return true + end)) + :wait(60000) +end + +return M diff --git a/parser/.gitignore b/parser/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/parser/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/queries/org/folds.scm b/queries/org/folds.scm index ebd61aa87..c701f44b3 100644 --- a/queries/org/folds.scm +++ b/queries/org/folds.scm @@ -4,5 +4,4 @@ (drawer) (property_drawer) (block) - ] @fold - (#trim! @fold)) + ] @fold) diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua index dc7b71bfb..13df49eba 100644 --- a/scripts/minimal_init.lua +++ b/scripts/minimal_init.lua @@ -19,25 +19,11 @@ vim.opt.rtp:prepend(lazypath) require('lazy').setup({ { 'nvim-orgmode/orgmode', - dependencies = { - { 'nvim-treesitter/nvim-treesitter', lazy = true }, - }, event = 'VeryLazy', + branch = 'feat/no-nvim-ts', config = function() - -- Load treesitter grammar for org - require('orgmode').setup_ts_grammar() - - -- Setup treesitter - require('nvim-treesitter.configs').setup({ - highlight = { - enable = true, - }, - ensure_installed = { 'org' }, - }) - - -- Setup orgmode require('orgmode').setup() - end, + end }, }, { root = lazy_root, diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 5c26c9976..8ef83de9d 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -85,7 +85,6 @@ end M.setup({ plenary = 'https://github.com/nvim-lua/plenary.nvim.git', - treesitter = 'https://github.com/nvim-treesitter/nvim-treesitter', }) -- WARN: Do all plugin setup, test runs, reproductions, etc. AFTER calling setup with a list of plugins! -- Basically, do all that stuff AFTER this line. @@ -125,12 +124,6 @@ if vim.env.CI == 'true' then } end -require('orgmode').setup_ts_grammar() -require('nvim-treesitter.configs').setup({ - ensure_installed = { 'org' }, - sync_install = true, -}) - require('orgmode').setup({ org_agenda_files = { base_root_path .. '/plenary/fixtures/*' }, org_default_notes_file = base_root_path .. '/plenary/fixtures/refile.org',