├── cmp-jira-screenshot.png ├── lua └── cmp_jira │ ├── init.lua │ ├── config.lua │ ├── utils.lua │ └── source.lua ├── LICENSE ├── README.md └── tests └── utils_spec.lua /cmp-jira-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lttr/cmp-jira/HEAD/cmp-jira-screenshot.png -------------------------------------------------------------------------------- /lua/cmp_jira/init.lua: -------------------------------------------------------------------------------- 1 | local source = require("cmp_jira.source") 2 | 3 | local M = {} 4 | 5 | M.setup = function(overrides) 6 | require("cmp").register_source("cmp_jira", source.new(overrides)) 7 | end 8 | 9 | return M 10 | -------------------------------------------------------------------------------- /lua/cmp_jira/config.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | filetypes = { 'gitcommit' }, 3 | jira = { 4 | url = '', 5 | email = '', 6 | jql = 'assignee=%s+and+resolution=unresolved', 7 | }, 8 | } 9 | 10 | return M 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 msvechla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lua/cmp_jira/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.parse_api_response = function(response) 4 | local ok, parsed = pcall(vim.json.decode, response) 5 | if not ok then 6 | return false, {} 7 | end 8 | 9 | if not parsed.issues then 10 | return false, {} 11 | end 12 | 13 | local issues = {} 14 | for _, issue in ipairs(parsed.issues) do 15 | 16 | if not issue.key then 17 | return false, {} 18 | end 19 | 20 | local summary_val = '' 21 | if issue.fields then 22 | if issue.fields.summary then 23 | summary_val = issue.fields.summary 24 | end 25 | end 26 | 27 | table.insert(issues, { 28 | key = issue.key, 29 | summary = summary_val, 30 | }) 31 | end 32 | 33 | return true, issues 34 | end 35 | 36 | M.get_basic_auth = function(config) 37 | local user = M.get_user(config) 38 | local api_key = vim.fn.getenv("JIRA_USER_API_KEY") 39 | return user .. ':' .. api_key 40 | end 41 | 42 | M.get_auth_header = function(config) 43 | local api_key = vim.fn.getenv("JIRA_USER_API_KEY") 44 | return string.format("Bearer %s", api_key) 45 | end 46 | 47 | M.get_request_url = function(config) 48 | local url = M.get_jira_url(config) 49 | local jql = M.get_jql(config) 50 | return string.format('%s/rest/api/2/search?fields=summary&jql=', url) .. jql 51 | end 52 | 53 | M.get_jql = function(config) 54 | local username = M.get_username(config) 55 | return string.format(config.jira.jql, username) 56 | end 57 | 58 | M.get_jira_url = function(config) 59 | local url = config.jira.url 60 | if vim.fn.exists("$JIRA_WORKSPACE_URL") == 1 then 61 | url = vim.fn.getenv("JIRA_WORKSPACE_URL") 62 | end 63 | return url 64 | end 65 | 66 | M.get_user = function(config) 67 | local user = config.jira.email 68 | if vim.fn.exists("$JIRA_USER_EMAIL") == 1 then 69 | user = vim.fn.getenv("JIRA_USER_EMAIL") 70 | end 71 | return user 72 | end 73 | 74 | M.get_username = function(config) 75 | local user = M.get_user(config) 76 | return string.gsub(user, "@", "\\u0040") 77 | end 78 | 79 | return M 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmp-jira 2 | 3 | [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) completion source for Jira. 4 | 5 | ![screenshot](./cmp-jira-screenshot.png) 6 | 7 | ## Getting started 8 | 9 | Use your favorit plugin manager to install the plugin: 10 | 11 | ```lua 12 | use 'https://gitlab.com/msvechla/cmp-jira.git' 13 | ``` 14 | 15 | Then setup the cmp source by following these steps: 16 | 17 | ```lua 18 | require'cmp'.setup { 19 | sources = { 20 | { name = 'cmp_jira' } 21 | } 22 | } 23 | 24 | require("cmp_jira").setup() 25 | ``` 26 | 27 | ## Configuration 28 | 29 | To authenticate to the JIRA API, an API-Key is required. 30 | Setup your API-Key at: 31 | 32 | Then set the `JIRA_USER_API_KEY` environment variable, e.g.: 33 | 34 | ```bash 35 | export JIRA_USER_API_KEY='MyAPIKey' 36 | ``` 37 | 38 | Additionally, the following can be configured via environment variables: 39 | 40 | ```bash 41 | export JIRA_WORKSPACE_URL=https://jira.example.com 42 | export JIRA_USER_EMAIL=test.user@example.com 43 | ``` 44 | 45 | Alternatively the workspace `url` and `email` can be configured during the setup as well: 46 | 47 | ```lua 48 | require("cmp_jira").setup({ 49 | file_types = {"gitcommit"} 50 | jira = { 51 | -- email: optional, alternatively specify via $JIRA_USER_EMAIL 52 | email = "test.user@example.com" 53 | -- url: optional, alternatively specify via $JIRA_WORKSPACE_URL 54 | url = "https://jira.example.com" 55 | -- jql: optional, lua format string, escaped username/email will be passed to string.format() 56 | jql = "assignee=%s+and+resolution=unresolved" 57 | } 58 | }) 59 | ``` 60 | 61 | To filter the issues that are retrieved, you can optionally tweak the [JQL](https://support.atlassian.com/jira-service-management-cloud/docs/use-advanced-search-with-jira-query-language-jql/). 62 | 63 | The `config.jql` is treated as a lua format string, which will get the escaped username/email passed. 64 | Defaults to: `assignee=%s+and+resolution=unresolved` 65 | 66 | 67 | ## Acknowledgements 68 | 69 | - inspired by [coc-jira-complete](https://github.com/jberglinds/coc-jira-complete) 70 | - inspired by [cmp-git](https://github.com/petertriho/cmp-git) 71 | - thanks to [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for curl support & the testing framework 72 | 73 | ## This [repo](https://github.com/lttr/cmp-jira) was forked from https://gitlab.com/msvechla/cmp-jira 74 | -------------------------------------------------------------------------------- /lua/cmp_jira/source.lua: -------------------------------------------------------------------------------- 1 | local cmp = require("cmp") 2 | local curl = require "plenary.curl" 3 | local utils = require("cmp_jira.utils") 4 | 5 | local source = { 6 | config = {}, 7 | filetypes = {}, 8 | cache = {} 9 | } 10 | 11 | source.new = function(overrides) 12 | local self = 13 | setmetatable( 14 | {}, 15 | { 16 | __index = source 17 | } 18 | ) 19 | 20 | self.config = vim.tbl_extend("force", require("cmp_jira.config"), overrides or {}) 21 | for _, item in ipairs(self.config.filetypes) do 22 | self.filetypes[item] = true 23 | end 24 | 25 | -- defaults 26 | if self.config.jira.jql == nil or self.config.jira.jql == "" then 27 | self.config.jira.jql = "(assignee = currentUser() OR reporter = currentUser()) order by updated DESC" 28 | end 29 | 30 | return self 31 | end 32 | 33 | function source:is_available() 34 | return self.filetypes["*"] ~= nil or self.filetypes[vim.bo.filetype] ~= nil 35 | end 36 | 37 | function source:complete(_, callback) 38 | -- try to get the items from cache first before calling the API 39 | local bufnr = vim.api.nvim_get_current_buf() 40 | if self.cache[bufnr] then 41 | callback({ items = self.cache[bufnr] }) 42 | return true 43 | end 44 | 45 | local req_url = utils.get_request_url(self.config) 46 | 47 | -- run curl command 48 | curl.get( 49 | req_url, 50 | { 51 | headers = { 52 | Authorization = utils.get_auth_header() 53 | }, 54 | callback = function(out) 55 | local ok, parsed_issues = utils.parse_api_response(out.body) 56 | if not ok then 57 | return false 58 | end 59 | print(vim.inspect(parsed_issues)) 60 | 61 | local items = {} 62 | for _, issue in ipairs(parsed_issues) do 63 | table.insert( 64 | items, 65 | { 66 | label = string.format("%s: %s", issue.key, issue.summary), 67 | filterText = string.format("%s: %s", issue.key, issue.summary), 68 | insertText = issue.key, 69 | sortText = issue.key 70 | } 71 | ) 72 | end 73 | 74 | -- update the cache 75 | self.cache[bufnr] = items 76 | 77 | callback({ items = items }) 78 | return true 79 | end 80 | } 81 | ) 82 | 83 | return false 84 | end 85 | 86 | function source:get_debug_name() 87 | return "cmp_jira" 88 | end 89 | 90 | return source 91 | -------------------------------------------------------------------------------- /tests/utils_spec.lua: -------------------------------------------------------------------------------- 1 | describe("utils", function() 2 | local utils = require('cmp_jira.utils') 3 | 4 | it("can parse sample correct api response", function() 5 | ok, issues = utils.parse_api_response([[ 6 | { 7 | "expand": "schema,names", 8 | "startAt": 0, 9 | "maxResults": 50, 10 | "total": 2, 11 | "issues": [ 12 | { 13 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 14 | "id": "335410", 15 | "self": "https://testing.com/rest/api/2/issue/1", 16 | "key": "AWESOME-1337", 17 | "fields": { 18 | "summary": "test summary 1" 19 | } 20 | }, 21 | { 22 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 23 | "id": "335408", 24 | "self": "https://testing.com/rest/api/2/issue/2", 25 | "key": "TEST-42", 26 | "fields": { 27 | "summary": "test summary 2" 28 | } 29 | } 30 | ] 31 | } 32 | ]]) 33 | 34 | assert.equals(true, ok) 35 | assert.same({ 36 | { 37 | key = "AWESOME-1337", 38 | summary = "test summary 1", 39 | }, 40 | { 41 | key = "TEST-42", 42 | summary = "test summary 2", 43 | }, 44 | }, issues) 45 | 46 | end) 47 | 48 | it("returns error with incorrect api response", function() 49 | ok, issues = utils.parse_api_response([[ 50 | { 51 | ]]) 52 | 53 | assert.equals(false, ok) 54 | assert.same({}, issues) 55 | 56 | end) 57 | 58 | it("parses with missing summary value", function() 59 | ok, issues = utils.parse_api_response([[ 60 | { 61 | "issues": [ 62 | { 63 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 64 | "id": "335410", 65 | "self": "https://testing.com/rest/api/2/issue/1", 66 | "key": "AWESOME-1337", 67 | "wasd": { 68 | "asd": "test summary 1" 69 | } 70 | }, 71 | { 72 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 73 | "id": "335408", 74 | "self": "https://testing.com/rest/api/2/issue/2", 75 | "key": "TEST-42", 76 | "fields": { 77 | "summary": "test summary 2" 78 | } 79 | } 80 | ] 81 | } 82 | ]]) 83 | 84 | assert.equals(true, ok) 85 | assert.same({ 86 | { 87 | key = "AWESOME-1337", 88 | summary = "", 89 | }, 90 | { 91 | key = "TEST-42", 92 | summary = "test summary 2", 93 | }, 94 | }, issues) 95 | 96 | end) 97 | 98 | it("errors with missing issue key", function() 99 | ok, issues = utils.parse_api_response([[ 100 | { 101 | "issues": [ 102 | { 103 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 104 | "id": "335410", 105 | "self": "https://testing.com/rest/api/2/issue/1", 106 | "wasd": { 107 | "asd": "test summary 1" 108 | } 109 | }, 110 | { 111 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 112 | "id": "335408", 113 | "self": "https://testing.com/rest/api/2/issue/2", 114 | "key": "TEST-42", 115 | "fields": { 116 | "summary": "test summary 2" 117 | } 118 | } 119 | ] 120 | } 121 | ]]) 122 | 123 | assert.equals(false, ok) 124 | assert.same({}, issues) 125 | 126 | end) 127 | 128 | it("can get user", function() 129 | local user = utils.get_user({ 130 | jira = { 131 | email = "test.user@example.com" 132 | }, 133 | }) 134 | 135 | assert.equals("test.user@example.com", user) 136 | end) 137 | 138 | it("can get username", function() 139 | local user = utils.get_username({ 140 | jira = { 141 | email = "test.user@example.com" 142 | }, 143 | }) 144 | 145 | assert.equals("test.user\\u0040example.com", user) 146 | end) 147 | 148 | it("can get jira url", function() 149 | local url = utils.get_jira_url({ 150 | jira = { 151 | url = "https://jira.example.com" 152 | }, 153 | }) 154 | 155 | assert.equals("https://jira.example.com", url) 156 | end) 157 | 158 | end) 159 | --------------------------------------------------------------------------------