From 959110b9d1c9fe483c84c35c34d4edd5e2372e73 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Tue, 5 Dec 2023 21:41:51 +0100 Subject: [PATCH] feat(module): snippets for import of child modules (#17) --- .luacheckrc | 1 + CHANGELOG.md | 6 + README.md | 14 +++ doc/haskell-snippets.txt | 166 ++++++++++++++++++++++----- flake.nix | 9 +- lua/haskell-snippets/data.lua | 15 ++- lua/haskell-snippets/expressions.lua | 13 +-- lua/haskell-snippets/functions.lua | 9 +- lua/haskell-snippets/module.lua | 112 +++++++++++++----- lua/haskell-snippets/pragmas.lua | 11 +- lua/haskell-snippets/quasiquotes.lua | 7 +- lua/haskell-snippets/util.lua | 5 +- 12 files changed, 281 insertions(+), 87 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 70eb8ac..ae80cc3 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,6 +1,7 @@ ignore = { "631", -- max_line_length "122", -- read-only field of global variable + "512", -- loop executed at most once } read_globals = { "vim", diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d0a94e..bbf62b3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2023-12-05 + +- Module imports: + - `qualc` snippet for qualified import of child modules. + - `impc` snippet for unqualified import of child modules. + ## [1.3.0] - 2023-12-05 - Expressions: Add multi-way if snippet (`ifmw`) [[#16](https://github.com/mrcjkb/haskell-snippets.nvim/pull/16)]. diff --git a/README.md b/README.md index 12c3e91..f2481de 100755 --- a/README.md +++ b/README.md @@ -119,6 +119,20 @@ ls.add_snippets('haskell', haskell_snippets, { key = 'haskell' }) ![tty](https://github.com/mrcjkb/haskell-snippets.nvim/assets/12857160/c62eac49-e06c-4ed9-9a9c-b987c1ad498c) +#### `haskell-snippets.module.impc` + +- Trigger: `impc` +- Requires a tree-sitter parser for Haskell. + +![tty](https://github.com/mrcjkb/haskell-snippets.nvim/assets/12857160/6ffc5f2f-c734-4e48-ac13-99ba1f9a0b18) + +#### `haskell-snippets.module.qualc` + +- Trigger: `qualc` +- Requires a tree-sitter parser for Haskell. + +![tty](https://github.com/mrcjkb/haskell-snippets.nvim/assets/12857160/43cef52f-eeda-48ad-bb2d-86241ac715b2) + ### Data and typeclasses #### `haskell-snippets.data.adt` diff --git a/doc/haskell-snippets.txt b/doc/haskell-snippets.txt index 92e8c15..0e83f41 100644 --- a/doc/haskell-snippets.txt +++ b/doc/haskell-snippets.txt @@ -29,11 +29,29 @@ HaskellSnippetCollection *HaskellSnippetCollection* PragmaSnippetCollection *PragmaSnippetCollection* - Fields: ~ - {prag} (Snippet) Compiler pragma - {lang} (Snippet) Language pragma - {discover} (Snippet) Hspec/Sydtest discover GHC option - {nowarn} (Snippet) GHC option + +pragmas.prag *pragmas.prag* + + Type: ~ + (Snippet) Compiler pragma + + +pragmas.lang *pragmas.lang* + + Type: ~ + (Snippet) Language pragma + + +pragmas.discover *pragmas.discover* + + Type: ~ + (Snippet) Hspec/Sydtest discover GHC option + + +pragmas.nowarn *pragmas.nowarn* + + Type: ~ + (Snippet) GHC option ============================================================================== @@ -43,9 +61,29 @@ PragmaSnippetCollection *PragmaSnippetCollection* ModuleSnippetCollection *ModuleSnippetCollection* - Fields: ~ - {mod} (Snippet) Module declaration - {qual} (Snippet) Qualified import + +module.mod *module.mod* + + Type: ~ + (Snippet) Module declaration + + +module.qual *module.qual* + + Type: ~ + (Snippet) Qualified import + + +module.impc *module.impc* + + Type: ~ + (Snippet|nil) Import (child module) + + +module.qualc *module.qualc* + + Type: ~ + (Snippet|nil) Qualified import (child module) ============================================================================== @@ -55,13 +93,41 @@ ModuleSnippetCollection *ModuleSnippetCollection* DataSnippetCollection *DataSnippetCollection* - Fields: ~ - {adt} (Snippet) Algebraic data type - {newtype} (Snippet) newtype - {rec} (Snippet) Record - {cls} (Snippet) Typeclass - {ins} (Snippet) Typeclass instance - {constraint} (Snippet) Typeclass constraint + +data.adt *data.adt* + + Type: ~ + (Snippet) Algebraic data type + + +data.newtype *data.newtype* + + Type: ~ + (Snippet) newtype + + +data.rec *data.rec* + + Type: ~ + (Snippet) Record + + +data.cls *data.cls* + + Type: ~ + (Snippet) Typeclass + + +data.ins *data.ins* + + Type: ~ + (Snippet) Typeclass instance + + +data.constraint *data.constraint* + + Type: ~ + (Snippet) Typeclass constraint ============================================================================== @@ -71,10 +137,23 @@ DataSnippetCollection *DataSnippetCollection* FunctionSnippetCollection *FunctionSnippetCollection* - Fields: ~ - {fun} (Snippet) Function and type signature - {func} (Snippet) Function and type signature (multi-line) - {lambda} (Snippet) Lambda + +functions.fn *functions.fn* + + Type: ~ + (Snippet) Function and type signature + + +functions.func *functions.func* + + Type: ~ + (Snippet) Function and type signature (multi-line) + + +functions.lambda *functions.lambda* + + Type: ~ + (Snippet) Lambda ============================================================================== @@ -84,12 +163,35 @@ FunctionSnippetCollection *FunctionSnippetCollection* ExpressionSnippetCollection *ExpressionSnippetCollection* - Fields: ~ - {if_expr} (Snippet) if expression - {if_expr_multiline} (Snippet) if expression (multi-line) - {if_expr_multiway} (Snippet) if expression (multi-way) - {case} (Snippet) case expression (pattern match) - {lambdacase} (Snippet) lambda case (pattern match) + +expressions.if_expr *expressions.if_expr* + + Type: ~ + (Snippet) if expression + + +expressions.if_expr_multiline *expressions.if_expr_multiline* + + Type: ~ + (Snippet) if expression (multi-line) + + +expressions.if_expr_multiway *expressions.if_expr_multiway* + + Type: ~ + (Snippet) if expression (multi-way) + + +expressions.case *expressions.case* + + Type: ~ + (Snippet) case expression (pattern match) + + +expressions.lambdacase *expressions.lambdacase* + + Type: ~ + (Snippet) lambda case (pattern match) ============================================================================== @@ -99,9 +201,17 @@ ExpressionSnippetCollection *ExpressionSnippetCollection* QuasiQuoteSnippetCollection *QuasiQuoteSnippetCollection* - Fields: ~ - {qq} (Snippet) QuasiQuote - {sql} (Snippet) postgres-simple [sql||] QuasiQuote + +quasiquotes.qq *quasiquotes.qq* + + Type: ~ + (Snippet) QuasiQuote + + +quasiquotes.sql *quasiquotes.sql* + + Type: ~ + (Snippet) postgres-simple [sql||] QuasiQuote vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/flake.nix b/flake.nix index c00990f..0903ac3 100755 --- a/flake.nix +++ b/flake.nix @@ -82,8 +82,13 @@ devShell = pkgs.mkShell { name = "haskell-snippets-devShell"; inherit (pre-commit-check) shellHook; - buildInputs = with pkgs; [ - zlib + buildInputs = with pre-commit-hooks.packages.${system}; [ + alejandra + lua-language-server + stylua + luacheck + editorconfig-checker + markdownlint-cli ]; }; in { diff --git a/lua/haskell-snippets/data.lua b/lua/haskell-snippets/data.lua index 5202ba2..b134539 100644 --- a/lua/haskell-snippets/data.lua +++ b/lua/haskell-snippets/data.lua @@ -5,15 +5,8 @@ ---@brief ]] ---@class DataSnippetCollection ----@field adt Snippet Algebraic data type ----@field newtype Snippet newtype ----@field rec Snippet Record ----@field cls Snippet Typeclass ----@field ins Snippet Typeclass instance ----@field constraint Snippet Typeclass constraint - ----@type DataSnippetCollection local data = { + ---@type Snippet[] All data-related snippets all = {}, } @@ -44,6 +37,7 @@ adt_constructor_choice = function() }) end +---@type Snippet Algebraic data type data.adt = s({ trig = 'adt', dscr = 'Algebraic data type', @@ -71,6 +65,7 @@ data.adt = s({ }) table.insert(data.all, data.adt) +---@type Snippet newtype data.newtype = s({ trig = 'new', dscr = 'newtype', @@ -116,6 +111,7 @@ record_field_choice = function() }) end +---@type Snippet Record data.rec = s({ trig = 'rec', dscr = 'Record', @@ -160,6 +156,7 @@ data.rec = s({ }) table.insert(data.all, data.rec) +---@type Snippet Typeclass data.cls = s({ trig = 'cls', dscr = 'Typeclass', @@ -173,6 +170,7 @@ data.cls = s({ }) table.insert(data.all, data.cls) +---@type Snippet Typeclass instance data.ins = s({ trig = 'ins', dscr = 'Typeclass instance', @@ -188,6 +186,7 @@ data.ins = s({ }) table.insert(data.all, data.ins) +---@type Snippet Typeclass constraint data.constraint = s({ trig = '=>', descr = 'Typeclass constraint', diff --git a/lua/haskell-snippets/expressions.lua b/lua/haskell-snippets/expressions.lua index 7a981f9..b36a7f4 100644 --- a/lua/haskell-snippets/expressions.lua +++ b/lua/haskell-snippets/expressions.lua @@ -5,14 +5,8 @@ ---@brief ]] ---@class ExpressionSnippetCollection ----@field if_expr Snippet if expression ----@field if_expr_multiline Snippet if expression (multi-line) ----@field if_expr_multiway Snippet if expression (multi-way) ----@field case Snippet case expression (pattern match) ----@field lambdacase Snippet lambda case (pattern match) - ----@type ExpressionSnippetCollection local expressions = { + ---@type Snippet[] All expression-related snippets all = {}, } @@ -24,6 +18,7 @@ local text = ls.text_node local insert = ls.insert_node local dynamic = ls.dynamic_node +---@type Snippet if expression expressions.if_expr = s({ trig = 'if', dscr = 'If expression (single line)', @@ -37,6 +32,7 @@ expressions.if_expr = s({ }) table.insert(expressions.all, expressions.if_expr) +---@type Snippet if expression (multi-line) expressions.if_expr_multiline = s({ trig = 'iff', dscr = 'If expression (multi lines)', @@ -50,6 +46,7 @@ expressions.if_expr_multiline = s({ }) table.insert(expressions.all, expressions.if_expr_multiline) +---@type Snippet if expression (multi-way) expressions.if_expr_multiway = s({ trig = 'ifmw', dscr = 'If expression (multi-way)', @@ -68,6 +65,7 @@ expressions.if_expr_multiway = s({ }) table.insert(expressions.all, expressions.if_expr_multiway) +---@type Snippet case expression (pattern match) expressions.case = s({ trig = 'case', dscr = 'Case expression (pattern match)', @@ -84,6 +82,7 @@ expressions.case = s({ }) table.insert(expressions.all, expressions.case) +---@type Snippet lambda case (pattern match) expressions.lambdacase = s({ trig = '\\case', dscr = 'Lambda (pattern match)', diff --git a/lua/haskell-snippets/functions.lua b/lua/haskell-snippets/functions.lua index eb6e731..78ceab3 100644 --- a/lua/haskell-snippets/functions.lua +++ b/lua/haskell-snippets/functions.lua @@ -5,12 +5,8 @@ ---@brief ]] ---@class FunctionSnippetCollection ----@field fun Snippet Function and type signature ----@field func Snippet Function and type signature (multi-line) ----@field lambda Snippet Lambda - ----@type FunctionSnippetCollection local functions = { + ---@type Snippet[] All function-related snippets all = {}, } @@ -81,6 +77,7 @@ local function build_multi_line_function_snippet(...) return build_function_snippet(true, ...) end +---@type Snippet Function and type signature functions.fn = s({ trig = 'fn', dscr = 'Function and type signature', @@ -91,6 +88,7 @@ functions.fn = s({ }) table.insert(functions.all, functions.fn) +---@type Snippet Function and type signature (multi-line) functions.func = s({ trig = 'func', dscr = 'Function and type signature (multi-line)', @@ -103,6 +101,7 @@ functions.func = s({ }) table.insert(functions.all, functions.func) +---@type Snippet Lambda functions.lambda = s({ trig = '\\', dscr = 'Lambda', diff --git a/lua/haskell-snippets/module.lua b/lua/haskell-snippets/module.lua index 540505d..4326a64 100644 --- a/lua/haskell-snippets/module.lua +++ b/lua/haskell-snippets/module.lua @@ -5,11 +5,8 @@ ---@brief ]] ---@class ModuleSnippetCollection ----@field mod Snippet Module declaration ----@field qual Snippet Qualified import - ----@type ModuleSnippetCollection local module = { + ---@type Snippet[] All module-related snippets all = {}, } @@ -22,19 +19,58 @@ local text = ls.text_node local insert = ls.insert_node local choice = ls.choice_node local dynamic = ls.dynamic_node +local func = ls.function_node local has_treesitter, parsers = pcall(require, 'nvim-treesitter.parsers') local hs_lang = has_treesitter and parsers.ft_to_lang('haskell') or 'haskell' local has_haskell_parser = pcall(vim.treesitter.get_string_parser, '', 'haskell') +--- Parses without injections +local function fast_parse(lang_tree) + if lang_tree._valid then + return lang_tree._trees + end + local parser = lang_tree._parser + local old_trees = lang_tree._trees + return parser:parse(old_trees[1], lang_tree._source) +end + +---@param apply fun(module_name: string):(string|nil) Callback to apply the module name to +---@param content string The content to parse from +---@param query_string? string The tree-sitter query string with a '@mod' capture +---@return string|nil +local function treesitter_module_name(apply, content, query_string) + assert(has_haskell_parser, 'No tree-sitter parser for Haskell found.') + query_string = query_string or '(module) @mod' + local module_query = vim.treesitter.query.parse(hs_lang, query_string) + local lang_tree = vim.treesitter.get_string_parser(content, hs_lang, { injections = { [hs_lang] = '' } }) + local root = fast_parse(lang_tree):root() + ---@diagnostic disable-next-line + for _, match in module_query:iter_matches(root, content) do + for _, node in ipairs(match) do + local txt = vim.treesitter.get_node_text(node, content) + return apply(txt) + end + end +end + +---@return string | nil +local function get_buf_module_name(_) + local buf_content = table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), '\n') + return treesitter_module_name(function(mod) + return vim.print(mod) + end, buf_content, '[(module)(qualified_module)] @mod') +end + local function get_module_name_node() - local module_name = util.lsp_get_module_name() + local module_name = util.lsp_get_module_name() or get_buf_module_name() if module_name then return sn(nil, { text(module_name) }) end return sn(nil, { insert(1) }) end +---@type Snippet Module declaration module.mod = s({ trig = 'mod', dscr = 'Module declaration', @@ -60,16 +96,6 @@ module.mod = s({ }) table.insert(module.all, module.mod) ---- Parses without injections -local function fast_parse(lang_tree) - if lang_tree._valid then - return lang_tree._trees - end - local parser = lang_tree._parser - local old_trees = lang_tree._trees - return parser:parse(old_trees[1], lang_tree._source) -end - local function get_qualified_name_node(args) local node_ref = args[1] local module_name = node_ref and #node_ref > 0 and node_ref[1] @@ -79,23 +105,18 @@ local function get_qualified_name_node(args) local import_stmt = 'import qualified ' .. module_name local choices = { insert(1) } if has_haskell_parser then - local module_query = vim.treesitter.query.parse(hs_lang, '(module) @mod') - local lang_tree = vim.treesitter.get_string_parser(import_stmt, hs_lang, { injections = { [hs_lang] = '' } }) - local root = fast_parse(lang_tree):root() - ---@diagnostic disable-next-line - for _, match in module_query:iter_matches(root, import_stmt) do - for _, node in ipairs(match) do - local txt = vim.treesitter.get_node_text(node, import_stmt) - table.insert(choices, 1, text(txt:sub(1, 1))) - table.insert(choices, 1, text(txt)) - end - end + treesitter_module_name(function(mod) + vim.print(mod) + table.insert(choices, 1, text(mod:sub(1, 1))) + table.insert(choices, 1, text(mod)) + end, import_stmt) end return sn(nil, { choice(1, choices), }) end +---@type Snippet Qualified import module.qual = s({ trig = 'qual', dscr = 'Qualified import', @@ -107,4 +128,43 @@ module.qual = s({ }) table.insert(module.all, module.qual) +if has_haskell_parser then + ---@type Snippet | nil Import (child module) + module.impc = s({ + trig = 'impc', + dscr = 'Import child module', + }, { + text('import '), + func(get_buf_module_name, {}, {}), + text('.'), + choice(1, { + text('Internal'), + text('Types'), + sn(nil, { + insert(1), + }), + }), + }) + table.insert(module.all, module.impc) + + ---@type Snippet | nil Qualified import (child module) + module.qualc = s({ + trig = 'qualc', + dscr = 'Qualified import of child module', + }, { + text('import qualified '), + func(get_buf_module_name, {}, {}), + text('.'), + choice(1, { + text('Internal'), + text('Types'), + sn(nil, { + insert(1), + }), + }), + text(' as '), + dynamic(2, get_qualified_name_node, { 1 }), + }) + table.insert(module.all, module.qualc) +end return module diff --git a/lua/haskell-snippets/pragmas.lua b/lua/haskell-snippets/pragmas.lua index 58890a8..5448fd2 100644 --- a/lua/haskell-snippets/pragmas.lua +++ b/lua/haskell-snippets/pragmas.lua @@ -5,13 +5,8 @@ ---@brief ]] ---@class PragmaSnippetCollection ----@field prag Snippet Compiler pragma ----@field lang Snippet Language pragma ----@field discover Snippet Hspec/Sydtest discover GHC option ----@field nowarn Snippet GHC option - ----@type PragmaSnippetCollection local pragmas = { + ---@type Snippet[] All pragma-related snippets all = {}, } @@ -170,6 +165,7 @@ local function language_extension_interior_snippet() }) end +---@type Snippet Compiler pragma pragmas.prag = s({ trig = 'prag', dscr = 'Compiler pragma', @@ -214,6 +210,7 @@ pragmas.prag = s({ }) table.insert(pragmas.all, pragmas.prag) +---@type Snippet Language pragma pragmas.lang = s({ trig = 'lang', dscr = 'LANGUAGE pragma', @@ -236,6 +233,7 @@ local function get_module_name() }) end +---@type Snippet Hspec/Sydtest discover GHC option pragmas.discover = s({ trig = 'discover', dscr = 'hspec/sydtest discover GHC option', @@ -251,6 +249,7 @@ pragmas.discover = s({ }) table.insert(pragmas.all, pragmas.discover) +---@type Snippet GHC option pragmas.nowarn = s({ trig = 'nowarn', dscr = 'GHC option to disable warnings', diff --git a/lua/haskell-snippets/quasiquotes.lua b/lua/haskell-snippets/quasiquotes.lua index 40f98bb..cc098e6 100644 --- a/lua/haskell-snippets/quasiquotes.lua +++ b/lua/haskell-snippets/quasiquotes.lua @@ -5,11 +5,8 @@ ---@brief ]] ---@class QuasiQuoteSnippetCollection ----@field qq Snippet QuasiQuote ----@field sql Snippet postgres-simple [sql||] QuasiQuote - ----@type QuasiQuoteSnippetCollection local quasiquotes = { + ---@type Snippet[] All quasiquote-related snippets all = {}, } @@ -21,6 +18,7 @@ local dynamic = ls.dynamic_node local util = require('haskell-snippets.util') +---@type Snippet QuasiQuote quasiquotes.qq = s({ trig = 'qq', dscr = 'QuasiQuote', @@ -34,6 +32,7 @@ quasiquotes.qq = s({ }) table.insert(quasiquotes.all, quasiquotes.qq) +---@type Snippet postgres-simple [sql||] QuasiQuote quasiquotes.sql = s({ trig = 'sql', dscr = 'postgres-simple sql QuasiQuote', diff --git a/lua/haskell-snippets/util.lua b/lua/haskell-snippets/util.lua index 3f45108..ba0e921 100644 --- a/lua/haskell-snippets/util.lua +++ b/lua/haskell-snippets/util.lua @@ -66,9 +66,12 @@ function util.indent_newline_insert(txt, extra_indent) end end +---@diagnostic disable-next-line: deprecated +local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients + ---@return string|nil function util.lsp_get_module_name() - if #vim.lsp.get_active_clients { bufnr = 0 } > 0 then + if #get_clients { bufnr = 0 } > 0 then for _, lens in pairs(vim.lsp.codelens.get(0)) do local name = lens.command.title:match('module (.*) where') if name then