Atlas.nvim
August 5, 2026 · View on GitHub
Atlas.nvim
A Neovim plugin for managing GitHub/Bitbucket/GitLab PRs and Jira/GitHub/GitLab issues without leaving your editor.
Caution
Still in early development, will have breaking changes!
Table of Contents
Installation
Using lazy.nvim
{
"emrearmagan/atlas.nvim",
dependencies = {
"nvim-tree/nvim-web-devicons", -- optional but recommended
"MeanderingProgrammer/render-markdown.nvim", -- optional but recommended
"esmuellert/codediff.nvim", -- optional (PullRequest diff)
"sindrets/diffview.nvim", -- optional; or "dlyongemallo/diffview-plus.nvim"
},
-- See Configuration below
opts = {},
}
Using packer.nvim
use {
"emrearmagan/atlas.nvim",
config = function()
-- See Configuration below
require("atlas").setup({})
end
}
Tip
It's a good idea to run :checkhealth atlas to see if everything is set up correctly.
Requirements
- Neovim:
0.10+ gitandcurlon$PATH- Jira: Jira Cloud REST API v3 (
*.atlassian.net) or Jira Server REST API v2 - Bitbucket: Bitbucket Cloud REST API 2.0 (
api.bitbucket.org) - GitHub: GitHub CLI (
gh) authenticated withgh auth login - GitLab: GitLab REST API v4 (
gitlab.comor self-hosted), Personal Access Token withapiscope
Note
I have only tested this with my personal and work accounts. If you encounter any issues, please feel free to open an issue. See: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
I have also not tested with self-hosted GitLab instances, but in theory it should work. If it doesn't, feel free to open an issue. If it does work, please remove this note :)
Configuration
{
-- Too lazy to manage a statusline per split? Same. Make it global.
global_statusline = true,
pulls = {
-- See Pulls Configuration below.
providers = {
---@type AtlasBitbucketConfig
bitbucket = {},
---@type AtlasGitHubConfig
github = {},
---@type AtlasGitLabPullsConfig
gitlab = {},
},
},
issues = {
-- See Issue Configuration below.
providers = {
---@type AtlasJiraIssuesConfig
jira = {},
---@type AtlasGitHubIssuesConfig
github = {},
---@type AtlasGitLabIssuesConfig
gitlab = {},
},
},
}
Set global_statusline = false to leave Neovim's laststatus option unchanged.
Commands
:AtlasIssues [provider]- Open Atlas issues domain:AtlasPulls [provider]- Open Atlas pulls domain:AtlasDiff <base>...<head>or:AtlasDiff <pull-request-url>- Open a local Git range or pull request review:AtlasNotes- Inspect local review notes across pull requests:AtlasCreatePR- Create a pull request from the current branch:AtlasCreateIssue- Create an issue (GitHub / GitLab / Jira):AtlasSearch [provider]- Search configured pull-request and issue providers:AtlasOpen <target>- Open a provider URL, Jira key, repository reference, or PR/issue number:AtlasClearCache- Clear Atlas disk and memory cache:AtlasLogs- Toggle Atlas logs
Pulls
Use :AtlasPulls [provider] to browse and manage pull requests from GitHub, Bitbucket, and GitLab.
Pulls Configuration
pulls = {
diff = {
-- Any command that accepts explicit <base>...<head> Git revisions.
open_cmd = "AtlasDiff", -- default; for example "DiffviewOpen" or "CodeDiff".
-- AtlasDiff options; external viewers use their own configuration.
layout = "inline", -- "inline" or "side-by-side".
compact = true, -- Start with only changed hunks and surrounding context visible.
compact_context_lines = 3, -- Context lines shown around hunks in compact mode.
show_review_panel = false, -- Set true to show comments and notes when AtlasDiff opens.
explorer = {
grouped = true, -- Group changed files by directory.
hidden = false,
show_commits = false, -- Set true to show commits below changed files initially.
width = 40,
initial_focus = "explorer", -- "explorer" or "diff".
ignore = { ".git/**", ".jj/**" },
},
},
repo_config = {
-- Maps `workspace/repo` to local paths. Used for checkout, diffs, and custom actions.
paths = {
["your-workspace/*"] = "~/code/repos/*",
["your-workspace/atlas"] = "~/code/atlas",
},
settings = {
["your-workspace/atlas"] = {
readme = "README.md", -- optional, defaults to README.md
pr_template = ".github/pull_request_template.md", -- optional, defaults to .github/pull_request_template.md
},
},
},
custom_actions = {}, -- See Custom Actions below.
},
GitHub
pulls = {
providers = {
github = {
cache_ttl = 300,
---@type AtlasGitHubViewConfig[]
views = {
{
name = "My PRs",
key = "1",
layout = "plain",
search = "author:@me sort:updated-desc",
},
{
name = "Team",
key = "2",
layout = "compact",
search = "org:your-org sort:updated-desc",
},
{
name = "Repo",
key = "3",
layout = "plain",
search = "repo:your-org/your-repo",
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Drafts"] = "is:pr is:draft author:@me",
["Recently merged"] = "is:pr is:merged author:@me sort:updated-desc",
["Review requested"] = "is:pr is:open review-requested:@me",
},
},
},
},
},
Bitbucket
pulls = {
providers = {
bitbucket = {
user = vim.env.BITBUCKET_USER,
token = vim.env.BITBUCKET_TOKEN,
cache_ttl = 300,
---@type AtlasBitbucketViewConfig[]
views = {
{
name = "Me",
key = "M",
layout = "compact",
repos = {
{ workspace = "your-workspace", repo = "atlas" },
},
---@param pr PullRequest
---@param ctx { user: PullsUser|nil }
filter = function(pr, ctx)
local user = ctx.user
return pr.author and user and pr.author.id == user.id
end,
},
{
name = "Team",
key = "1",
layout = "plain", -- "compact" or "plain"
repos = {
{ workspace = "your-workspace", repo = "atlas" },
{ workspace = "your-workspace", repo = "other-repo" },
},
},
},
},
},
},
GitLab
Auth uses a Personal Access Token with the api scope. Set base_url to https://gitlab.com or your self-hosted instance.
pulls = {
providers = {
gitlab = {
base_url = "https://gitlab.com",
token = vim.env.GITLAB_TOKEN,
cache_ttl = 300,
---@type AtlasGitLabPullsViewConfig[]
views = {
{
name = "Assigned",
key = "1",
scope = "assigned_to_me",
},
{
name = "Reviewing",
key = "3",
scope = "all",
extra_params = { reviewer_id = "Me" },
},
-- Single project
{
name = "GitLab",
key = "G",
project = "gitlab-org/gitlab",
},
-- Whole group, all projects under it
{
name = "GitLab Org",
key = "O",
group = "gitlab-org",
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Reviewing"] = { scope = "all", extra_params = { reviewer_id = "Me" } },
["Merged by me"] = { scope = "all", state = "merged", author_username = "me" },
},
},
},
},
},
Issues
Use :AtlasIssues [provider] to browse and manage Jira, GitHub, and GitLab issues.
Issue Configuration
issues = {
max_results = 100,
with_relationships = true, -- Fetch parent/subissue relationships for plain issue tree views.
custom_actions = {}, -- See Custom Actions below.
}
Jira
Note
If you're only looking for Jira support, check out https://github.com/letieu/jira.nvim. This plugin was the main inspiration for this project. Jira support is included here mainly because I wanted a single tool that works with both Atlassian products.
Important
The markdown editor for issue descriptions and comments is still experimental and may not work perfectly in all cases. You can toggle between markdown and ADF view in the overview tab to see the raw ADF content and how it translates to markdown. If you encounter any issues with the markdown editor, please open an issue with details.
issues = {
providers = {
jira = {
base_url = "https://your-site.atlassian.net",
email = "you@example.com",
--- See: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
token = "your_jira_api_token",
auth_method = "basic", -- "basic" or "bearer", defaults to "basic". If using bearer, set `token` to your API token.
api_type = "cloud", -- either "cloud" or "server", defaults to "cloud". Cloud API is v3, server API is v2
cache_ttl = 300,
project_config = {
-- The Jira custom field ID used for story points. Defaults to "customfield_10016".
story_points_field = "customfield_10016",
issue_types = {
["Maintenance"] = { icon = "", hl_group = "AtlasTextWarning" },
["Infrastructure"] = { icon = "", hl_group = "AtlasLogInfo" },
},
KAN = {
customfield_10003 = {
name = "Approvers",
format = function(value)
if type(value) ~= "table" or #value == 0 then
return nil -- nil hides the field
end
return table.concat(value, ", ")
end,
hl_group = "AtlasChipActive",
display = "chip", -- "chip" or "table"
},
},
},
---@type AtlasJiraViewConfig[]
views = {
{
name = "My Board",
key = "M",
layout = "plain",
jql = "project = KAN AND assignee = currentUser() ORDER BY updated DESC",
},
{
name = "Team Board",
key = "T",
layout = "compact",
jql = "project = KAN ORDER BY updated DESC",
},
},
bookmarks = {
key = "J", -- default
label = "JQL", -- default
items = {
["Backlog"] = "project = KAN AND statusCategory != Done AND (sprint IS EMPTY OR sprint NOT IN openSprints()) ORDER BY Rank ASC",
["Next sprint"] = "project = KAN AND sprint in futureSprints() ORDER BY Rank ASC",
["My open"] = "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC",
},
},
},
},
},
GitHub Issues
issues = {
providers = {
github = {
cache_ttl = 300,
---@type AtlasGitHubIssuesViewConfig[]
views = {
{
name = "Assigned",
key = "1",
layout = "plain",
search = "assignee:@me is:open",
},
{
name = "Created",
key = "2",
layout = "compact",
search = "author:@me is:open",
},
{
name = "Mentions",
key = "3",
layout = "plain",
search = "mentions:@me is:open",
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["Bugs"] = "is:issue is:open label:bug",
["Recently closed"] = "is:issue is:closed author:@me sort:updated-desc",
},
},
},
},
},
GitLab Issues
Auth uses a Personal Access Token with the api scope. Set base_url to https://gitlab.com or your self-hosted instance.
issues = {
providers = {
gitlab = {
base_url = "https://gitlab.com",
token = vim.env.GITLAB_TOKEN,
cache_ttl = 300,
---@type AtlasGitLabIssuesViewConfig[]
views = {
{
name = "Assigned",
key = "1",
scope = "assigned_to_me",
state = "opened",
},
{
name = "Created",
key = "2",
scope = "created_by_me",
state = "opened",
},
{
name = "All open",
key = "3",
scope = "all",
state = "opened",
-- Anything not covered by the explicit fields below can be passed via `extra_params`.
extra_params = { ["not[labels]"] = "wontfix" },
},
},
bookmarks = {
key = "S", -- default
label = "Search", -- default
items = {
["No labels"] = { scope = "all", state = "opened",
extra_params = { ["not[labels]"] = "*" } },
["Closed"] = { scope = "created_by_me", state = "closed" },
},
},
},
},
},
Features
Atlas keeps the pull request and issue workflows you use throughout the day inside Neovim.
Review Pull Requests
Press the configured pulls.open_diff key (gd by default) on a pull request to start a review.
- See pending, resolved, and outdated provider threads at their diff locations.
- Review provider tasks and GitHub checklists alongside the comments they belong to.
- Add, reply to, edit, delete, resolve, or reopen comments when supported.
- Browse provider comments and local notes in AtlasDiff's bottom list; use
zato expand an item. - Submit pending comments with an optional review summary when supported.
Note
Alternative viewers: CodeDiff, Diffview, and Diffview-plus can display Atlas comment, task, and local-note overlays, but their integrations rely on plugin internals and may break after upstream changes.
Local notes
Local notes let you leave something on a diff without posting it to the pull request. Each note is attached to a file and line and can be an ISSUE, SUGGESTION, NOTE, or PRAISE. Diff views mark notes as outdated when their saved line changes.
For scripts, use bin/atlas-notes. Notes added there appear in AtlasDiff, CodeDiff, Diffview, Diffview-plus, and :AtlasNotes:
./bin/atlas-notes add \
--target https://github.com/owner/repository/pull/123 \
--file lua/review_queue.lua --line 19 \
--context "local item = queue[index]" \
--type suggestion --body "Should this be a bool?"
My dotfiles include a Pi extension that wraps this script so review agents can list and add notes.
View Pipelines
View pipelines and their jobs, inspect their status, and read job logs directly in Atlas. Retry failed pipelines or jobs and cancel work that is still running.
Create Pull Requests
:AtlasCreatePR opens the pull request form for the current branch. The newest commit supplies the title. Atlas first reads the configured pr_template, or .github/pull_request_template.md by default.
Without a template, Atlas groups conventional commits into sections, recognizes leading Jira keys such as [JIRA-123], links commit hashes and issue references, collects references under Related, and appends the diffstat. If no commits use a conventional prefix, it uses a linked plain commit list instead.
Edit the title and description, choose the target branch and reviewers, set the draft state, or preview commits and diffstat before submitting.
Create Issues
:AtlasCreateIssue opens the creation flow for the configured issue providers. GitHub and GitLab use the current repository, while Jira uses the configured instance. The forms support Markdown descriptions and provider-specific fields such as labels, assignees, milestones, and Jira issue types.
GitHub, GitLab, and Jira can apply a saved Markdown template or save the current description as a new one. Templates are shared between providers and stored under Neovim's data directory.
Notifications
Open GitHub and GitLab notifications inside Atlas, refresh them, open the related item, and mark notifications as read or done without leaving Neovim.
Keep the work that needs your attention visible.
Bookmarks
Turn frequently used GitHub and GitLab searches or Jira JQL into named shortcuts. Use bookmarks for review queues, recurring project views, and the searches you return to throughout the day.
Bookmarks appear alongside your configured views, keeping important queries one action away.
Custom Actions
Add project-specific actions to pull requests and issues. Custom actions receive the current item and provider context, making it possible to call local scripts, open repositories in tmux, copy branch names, or connect Atlas to your own tooling.
Configuration
pulls = {
repo_config = {
paths = {
["your-workspace/*"] = "~/code/repos/*",
},
settings = {},
},
custom_actions = {
{
id = "open_tmux_window",
label = "Open repo in tmux window",
confirmation = true,
---@param pr PullRequest
---@param ctx AtlasPullsCustomActionContext
---@param done fun(ok: boolean|nil, message: string|nil)
run = function(_, ctx, done)
if not ctx.repo_path then
done(false, "No repo path")
return
end
vim.system({ "tmux", "new-window", "-c", ctx.repo_path }, { text = true }, function(res)
vim.schedule(function()
if res.code ~= 0 then
done(false, "Failed to open tmux window")
return
end
done(true, "Opened tmux window")
end)
end)
end,
},
},
},
issues = {
custom_actions = {
{
id = "copy_branch_name",
label = "Copy branch name",
---@param issue Issue
---@param ctx AtlasIssuesCustomActionContext
---@param done fun(ok: boolean|nil, message: string|nil)
run = function(issue, ctx, done)
local branch = string.format("%s/%s", issue.key, issue.summary:lower():gsub("%s+", "-"))
vim.fn.setreg("+", branch)
done(true, "Copied: " .. branch)
end,
},
},
},
Events
Atlas emits these User events after the corresponding cleanup or setup has completed:
AtlasUIClosedfor the main pulls/issues dashboard.AtlasDiffOpenedandAtlasDiffClosedfor the native AtlasDiff view.AtlasReviewAttachedandAtlasReviewDetachedfor Atlas review overlays in AtlasDiff, CodeDiff, and Diffview.
Keymaps
Set an action to false to disable it, or set it to a list to add aliases.
keymaps = {
ui = {
next_item = "j",
previous_item = "k",
first_item = "gg",
last_item = "G",
help = "g?", -- { "g?", "<leader>?" } would add aliases
close = "q", -- false would disable it
toggle_panel = "p",
toggle_fold = "za",
toggle_all_folds = "zA",
previous_panel_tab = "<S-Tab>",
next_panel_tab = "<Tab>",
open_notifications = "N",
notifications_mark_read = "r",
notifications_mark_done = "d",
notifications_refresh = "R",
toggle_subscription = "gS",
refresh = "r",
refresh_view = "R",
open_actions = "A",
open_in_browser = "gx",
copy_id = "y",
copy_url = "Y",
show_details = "K",
search = "?",
},
issues = {
transition_issue = "gs",
change_assignee = "ga",
change_reporter = "gr",
edit_issue = "ge",
create_issue = "c",
},
pulls = {
open_diff = "gd",
checkout = "gc",
review = {
toggle_approval = "ga",
request_changes = "gr",
submit_review = "gs",
open_file = "<CR>",
toggle_explorer_grouping = "T",
toggle_layout = "t",
toggle_compact = "u",
next_hunk = "]h",
previous_hunk = "[h",
next_file = { "]f", "<Tab>" },
previous_file = { "[f", "<S-Tab>" },
toggle_file_reviewed = "-",
toggle_commits = "gC",
toggle_review_panel = "gR",
next_comment = "]c",
previous_comment = "[c",
next_note = "]n",
previous_note = "[n",
view_thread = "K",
edit_comment = "e",
add_task = "T",
add_comment = "c",
submit_comment = "C",
delete_comment = "dd",
add_note = "n",
toggle_resolved = "x",
},
filter_status_open = "gpo",
filter_status_merged = "gpm",
filter_status_declined = "gpd",
},
},
Contributing
Contributions are welcome! If you'd like to contribute, please open an issue or pull request on GitHub. See CONTRIBUTING.md.
Contributors ✨
Thanks go to these wonderful people (emoji key):
Riza Khan 💻 |
Cydralic 💻 |
franroa 💻 🐛 |
hiddederidder 💻 |
Xamcost 💻 |
Niklas Treml 💻 🐛 |
Noe Trevino 💻 |
Jean-Frederic Mainville 💻 |
License
MIT License - see LICENSE for details.