Browse Source

initial

master
Lucas 1 month ago
commit
5931cb012f
  1. 6
      .stylua.toml
  2. 21
      LICENSE
  3. 63
      README.md
  4. 23
      lua/django/helpers.lua
  5. 10
      lua/django/init.lua
  6. 399
      lua/django/input.lua
  7. 17
      lua/django/lazy.lua
  8. 94
      lua/django/mappings.lua
  9. 267
      lua/django/utils.lua

6
.stylua.toml

@ -0,0 +1,6 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "None"

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 django-nvim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

63
README.md

@ -0,0 +1,63 @@
# django.nvim
Collection of commands for Django development.
---
## Requirements
- [Neovim (v0.9.0 or latest)](https://github.com/neovim/neovim)
### Required dependencies
- [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim)
- [akinsho/toggleterm.nvim](https://github.com/akinsho/toggleterm.nvim)
- [MunifTanjim/nui.nvim](https://github.com/MunifTanjim/nui.nvim)
---
## Installation
Using [lazy.nvim](https://github.com/folke/lazy.nvim)
```lua
return {
{
'https://git.lucasf.dev/public/django.nvim',
config = function()
local django = require("django").setup()
end
}
}
```
Using [packer.nvim](https://github.com/wbthomason/packer.nvim)
```lua
use {'https://git.lucasf.dev/public/django.nvim'}
```
Using [vim-plug.nvim](https://github.com/junegunn/vim-plug)
```lua
Plug 'https://git.lucasf.dev/public/django.nvim'
```
---
## Usage
### Default Mappings
| Mappings | Action |
| ------------ | ------------------------------------- |
| `<leader>ja` | Create django app |
| `<leader>jc` | Perform manage.py check |
| `<leader>je` | Create `.env` file |
| `<leader>jh` | Perform manage.py showmigrations |
| `<leader>ji` | Perform manage.py migrate |
| `<leader>jm` | Perform manage.py makemigrations |
| `<leader>jp` | Create python package |
| `<leader>jr` | Perform manage.py runserver |
| `<leader>jr` | Perform manage.py shell or shell_plus |
| `<leader>jy` | Run celery |

23
lua/django/helpers.lua

@ -0,0 +1,23 @@
local plen_status_ok, _ = pcall(require, "plenary")
if not plen_status_ok then return end
local M = {}
local Path = require "plenary.path"
function M.get_path(str) return str:match "(.*[/\\])" end
function M.add_trailing_slash(value)
if value:sub(-1) ~= Path.path.sep then return value .. Path.path.sep end
return value
end
function M.split(inputstr, sep)
if sep == nil then sep = Path.path.sep end
local t = {}
for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
table.insert(t, str)
end
return t
end
return M

10
lua/django/init.lua

@ -0,0 +1,10 @@
local mapping = require "django.mappings"
local M = {}
function M.setup()
mapping.setup_global_mappings()
end
return M

399
lua/django/input.lua

@ -0,0 +1,399 @@
local nui,_ = pcall(require, "nui.input")
if not nui then return end
local M = { fn = {} }
local fn = {}
local state = {
query = '',
history = nil,
idx_hist = 0,
hooks = {},
cmdline = {},
user_opts = {},
prompt_length = 0,
prompt_content = ''
}
local defaults = {
cmdline = {
enable_keymaps = true,
smart_history = true,
prompt = ': '
},
popup = {
position = {
row = '10%',
col = '50%',
},
size = {
width = '60%',
},
border = {
style = 'rounded',
},
win_options = {
winhighlight = 'Normal:Normal,FloatBorder:FloatBorder',
},
},
hooks = {
before_mount = function(input)
end,
after_mount = function(input)
end,
set_keymaps = function(imap, feedkeys)
end
}
}
M.inp = nil
fn.check_nvim = function()
if vim.fn.has('nvim-0.7') == 1 then
fn.map = function(lhs, rhs)
vim.keymap.set('i', lhs, rhs, { buffer = M.inp.bufnr, noremap = true })
end
fn.nmap = function(lhs, rhs)
vim.keymap.set('n', lhs, rhs, { buffer = M.inp.bufnr, noremap = true })
end
else
fn.map = function(lhs, rhs)
if type(rhs) == 'string' then
vim.api.nvim_buf_set_keymap(M.inp.bufnr, 'i', lhs, rhs, { noremap = true })
else
M.inp:map('i', lhs, rhs, { noremap = true }, true)
end
end
fn.nmap = function(lhs, rhs)
M.inp:map('n', lhs, rhs, { noremap = true }, true)
end
end
end
M.setup = function(config, input_opts, callback)
config = config or {}
input_opts = input_opts or {}
state.user_opts = config
defaults.cmdline.prompt = input_opts.prompt or ": "
local popup_options = fn.merge(defaults.popup, config.popup)
state.hooks = fn.merge(defaults.hooks, config.hooks)
state.cmdline = fn.merge(defaults.cmdline, config.cmdline)
state.prompt_length = state.cmdline.prompt:len()
state.prompt_content = state.cmdline.prompt
return {
popup = popup_options,
input = {
prompt = state.cmdline.prompt,
default_value = input_opts.default_value,
on_change = fn.on_change(),
on_close = function() fn.reset_history() end,
on_submit = callback
}
}
end
M.open = function(opts, callback)
local ui = M.setup(state.user_opts, opts, callback)
fn.check_nvim()
M.inp = require('nui.input')(ui.popup, ui.input)
state.hooks.before_mount(M.inp)
M.inp:mount()
vim.bo.omnifunc = 'v:lua._fine_cmdline_omnifunc'
if state.cmdline.enable_keymaps then
fn.keymaps()
end
if vim.fn.has('nvim-0.7') == 0 then
fn.map('<BS>', function() fn.prompt_backspace(state.prompt_length) end)
end
state.hooks.set_keymaps(fn.map, fn.feedkeys)
state.hooks.after_mount(M.inp)
end
fn.on_change = function()
local prev_hist_idx = 0
return function(value)
if prev_hist_idx == state.idx_hist then
state.query = value
return
end
if value == '' then
return
end
prev_hist_idx = state.idx_hist
end
end
fn.keymaps = function()
fn.map('<Esc>', M.fn.close)
fn.map('<C-c>', M.fn.close)
fn.nmap('<Esc>', M.fn.close)
fn.nmap('<C-c>', M.fn.close)
fn.map('<Tab>', M.fn.complete_or_next_item)
fn.map('<S-Tab>', M.fn.stop_complete_or_previous_item)
if state.cmdline.smart_history then
fn.map('<Up>', M.fn.up_search_history)
fn.map('<Down>', M.fn.down_search_history)
else
fn.map('<Up>', M.fn.up_history)
fn.map('<Down>', M.fn.down_history)
end
end
M.fn.close = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-e>')
else
fn.feedkeys('<Space>')
vim.defer_fn(function()
local ok = pcall(M.inp.input_props.on_close)
if not ok then
pcall(vim.api.nvim_win_close, M.inp.winid, true)
pcall(vim.api.nvim_buf_delete, M.inp.bufnr, { force = true })
end
end, 3)
end
end
M.fn.up_search_history = function()
if vim.fn.pumvisible() == 1 then return end
local prompt = state.prompt_length
local line = vim.fn.getline('.')
local user_input = line:sub(prompt + 1, vim.fn.col('.'))
if line:len() == prompt then
M.fn.up_history()
return
end
fn.cmd_history()
local idx = state.idx_hist == 0 and 1 or (state.idx_hist + 1)
while (state.history[idx]) do
local cmd = state.history[idx]
if vim.startswith(cmd, state.query) then
state.idx_hist = idx
fn.replace_line(cmd)
return
end
idx = idx + 1
end
state.idx_hist = 1
if user_input ~= state.query then
fn.replace_line(state.query)
end
end
M.fn.down_search_history = function()
if vim.fn.pumvisible() == 1 then return end
local prompt = state.prompt_length
local line = vim.fn.getline('.')
local user_input = line:sub(prompt + 1, vim.fn.col('.'))
if line:len() == prompt then
M.fn.down_history()
return
end
fn.cmd_history()
local idx = state.idx_hist == 0 and #state.history or (state.idx_hist - 1)
while (state.history[idx]) do
local cmd = state.history[idx]
if vim.startswith(cmd, state.query) then
state.idx_hist = idx
fn.replace_line(cmd)
return
end
idx = idx - 1
end
state.idx_hist = #state.history
if user_input ~= state.query then
fn.replace_line(state.query)
end
end
M.fn.up_history = function()
if vim.fn.pumvisible() == 1 then return end
fn.cmd_history()
state.idx_hist = state.idx_hist + 1
local cmd = state.history[state.idx_hist]
if not cmd then
state.idx_hist = 0
return
end
fn.replace_line(cmd)
end
M.fn.down_history = function()
if vim.fn.pumvisible() == 1 then return end
fn.cmd_history()
state.idx_hist = state.idx_hist - 1
local cmd = state.history[state.idx_hist]
if not cmd then
state.idx_hist = 0
return
end
fn.replace_line(cmd)
end
M.fn.complete_or_next_item = function()
state.uses_completion = true
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-n>')
else
fn.feedkeys('<C-x><C-o>')
end
end
M.fn.stop_complete_or_previous_item = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-p>')
else
fn.feedkeys('<C-x><C-z>')
end
end
M.fn.next_item = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-n>')
end
end
M.fn.previous_item = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-p>')
end
end
M.omnifunc = function(start, base)
local prompt_length = state.prompt_length
local line = vim.fn.getline('.')
local input = line:sub(prompt_length + 1)
if start == 1 then
local split = vim.split(input, ' ')
local last_word = split[#split]
local len = #line - #last_word
for i = #split - 1, 1, -1 do
local word = split[i]
if vim.endswith(word, [[\\]]) then
break
elseif vim.endswith(word, [[\]]) then
len = len - #word - 1
else
break
end
end
return len
end
return vim.api.nvim_buf_call(vim.fn.bufnr('#'), function()
return vim.fn.getcompletion(input .. base, 'file')
end)
end
fn.replace_line = function(cmd)
vim.cmd('normal! V"_c')
vim.api.nvim_buf_set_lines(
M.inp.bufnr,
vim.fn.line('.') - 1,
vim.fn.line('.'),
true,
{ state.prompt_content .. cmd }
)
vim.api.nvim_win_set_cursor(
M.inp.winid,
{ vim.fn.line('$'), vim.fn.getline('.'):len() }
)
end
fn.cmd_history = function()
if state.history then return end
local history_string = vim.fn.execute('history cmd')
local history_list = vim.split(history_string, '\n')
local results = {}
for i = #history_list, 3, -1 do
local item = history_list[i]
local _, finish = string.find(item, '%d+ +')
table.insert(results, string.sub(item, finish + 1))
end
state.history = results
end
fn.reset_history = function()
state.idx_hist = 0
state.history = nil
state.query = ''
end
fn.merge = function(defaults, override)
return vim.tbl_deep_extend(
'force',
{},
defaults,
override or {}
)
end
fn.feedkeys = function(keys)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes(keys, true, true, true),
'n',
true
)
end
fn.prompt_backspace = function(prompt)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = cursor[1]
local col = cursor[2]
if col ~= prompt then
local completion = vim.fn.pumvisible() == 1 and state.uses_completion
if completion then fn.feedkeys('<C-x><C-z>') end
vim.api.nvim_buf_set_text(0, line - 1, col - 1, line - 1, col, { '' })
vim.api.nvim_win_set_cursor(0, { line, col - 1 })
if completion then fn.feedkeys('<C-x><C-o>') end
end
end
_fine_cmdline_omnifunc = M.omnifunc
return M

17
lua/django/lazy.lua

@ -0,0 +1,17 @@
local lazy = {}
--- Require on index.
---
--- Will only require the module after the first index of a module.
--- Only works for modules that export a table.
---@param require_path string
---@return table
lazy.require = function(require_path)
return setmetatable({}, {
__index = function(_, key) return require(require_path)[key] end,
__newindex = function(_, key, value) require(require_path)[key] = value end,
})
end
return lazy

94
lua/django/mappings.lua

@ -0,0 +1,94 @@
local django = require "django.utils"
local mappings = {
n = {
["<leader>j"] = { desc = "îśť Django" },
["<leader>ja"] = {
function()
django.create_app()
end,
desc = "create app",
},
["<leader>jp"] = {
function()
django.create_package()
end,
desc = "create python package",
},
["<leader>js"] = {
function()
django.manage_shell_plus(true)
end,
desc = "shell plus",
},
["<leader>je"] = {
function()
django.create_env_file()
end,
desc = "create env file",
},
["<leader>jr"] = {
function()
django.manage_run_server "make run"
end,
desc = "run server",
},
["<leader>jm"] = {
function()
django.manage_make_migrations()
end,
desc = "make migrations",
},
["<leader>ji"] = {
function()
django.manage_migrate()
end,
desc = "migrate",
},
["<leader>jc"] = {
function()
django.check()
end,
desc = "check",
},
["<leader>jh"] = {
function()
django.show_migrations()
end,
desc = "show migrations",
},
["<leader>jy"] = {
function()
django.run_celery()
end,
desc = "run celery",
},
},
}
local M = {}
local function key_map(mod, lhs, rhs, opts)
if type(lhs) == "string" then
vim.keymap.set(mod, lhs, rhs, opts)
elseif type(lhs) == "table" then
for _, key in pairs(lhs) do
vim.keymap.set(mod, key, rhs, opts)
end
end
end
function M.setup_global_mappings()
if mappings then
for k, v in pairs(mappings.n) do
if unpack(v) == nil then
key_map("n", k, "", { desc = v.desc })
else
key_map("n", k, unpack(v), { desc = v.desc })
end
end
end
end
return M

267
lua/django/utils.lua

@ -0,0 +1,267 @@
local plen_status_ok, _ = pcall(require, "plenary")
if not plen_status_ok then
return
end
local term_stat_ok, _ = pcall(require, "toggleterm.terminal")
if not term_stat_ok then
return
end
local inp = require "django.input"
local helpers = require "django.helpers"
local toggleterm = require "toggleterm"
local terms = require "toggleterm.terminal"
local CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
local function generate_secret_key()
local chars = {}
for x in CHARS:gmatch "[a-zA-Z0-9!@#$%^&*(-_=+)]" do
table.insert(chars, x)
end
local key = ""
math.randomseed(os.time())
for _ = 1, 50 do
key = key .. chars[math.random(1, #chars)]
end
return key
end
local M = {}
local Path = require "plenary.path"
local env_opts = {
secret_key = generate_secret_key(),
allowed_hosts = { "localhost", "10.0.2.2", "127.0.0.1" },
local_ip = "",
debug = "True",
database = {
user = "postgres",
password = "postgres",
dbname = "db",
port = "5433",
address = "127.0.0.1",
},
admin = {
username = "admin",
email = "adm@email.com",
password = "",
},
email = {
host = "",
port = "587",
host_user = "",
host_password = "",
use_tls = "True",
default_from_email = "",
},
celery = {
result_backend = "django-db",
broker_url = "redis://localhost:6379",
accept_content = "application/json",
result_serializer = "json",
task_serializer = "json",
timezone = "America/Sao_Paulo",
task_time_limit = "30 * 60",
},
}
M.title = "Django.nvim"
local function execute_command(cmd, args)
local ok, err = pcall(cmd, args)
---@diagnostic disable-next-line: param-type-mismatch
if not ok then
pcall(vim.notify, err, vim.log.levels.ERROR)
end
end
local function setup_opts(opts)
vim.validate { opts = { opts, "table", true } }
opts = vim.tbl_deep_extend("force", env_opts, opts or {})
vim.validate {
secret_key = { opts.secret_key, "string" },
allowed_hosts = { opts.allowed_hosts, "table" },
database = { opts.database, "table" },
admin = { opts.admin, "table" },
email = { opts.email, "table" },
celery = { opts.celery, "table" },
}
return opts
end
local function file_exists(name)
local f = io.open(name, "r")
if f ~= nil then
io.close(f)
return true
else
return false
end
end
local function create_template_static_dirs(name, dir)
execute_command(os.execute, "mkdir -p " .. dir .. "/static/" .. name .. "/css")
execute_command(os.execute, "mkdir -p " .. dir .. "/static/" .. name .. "/img")
execute_command(os.execute, "mkdir -p " .. dir .. "/static/" .. name .. "/js")
execute_command(os.execute, "mkdir -p " .. dir .. "/templates/" .. name)
end
local function get_app_name(path)
local name = helpers.split(helpers.add_trailing_slash(path))
return name[#name]
end
local function check_app(path)
return vim.loop.fs_stat(helpers.add_trailing_slash(path) .. "apps.py") ~= nil
end
local function get_module_name(path)
return path:gsub(Path.path.sep, ".")
end
local function perform_create_app(value)
local appname = get_app_name(value)
if appname then
if vim.loop.fs_stat(value) == nil then
execute_command(os.execute, "mkdir " .. value)
end
if check_app(value) == true then
vim.notify("This app already exists", vim.log.levels.ERROR, { title = M.title })
return
end
execute_command(os.execute, "./manage.py startapp " .. appname .. " " .. value)
if check_app(value) == true then
create_template_static_dirs(value, value)
execute_command(
os.execute,
"echo -e \"from django.urls import path\n\napp_name = '"
.. appname
.. "'\n\nurlpatterns = []\" > "
.. value
.. "/urls.py"
)
execute_command(
os.execute,
'echo -e " internal_app = True" >> ' .. helpers.add_trailing_slash(value) .. "apps.py"
)
local cmd = string.format(
'sed -i "s/"%s"/"%s"/" %s',
appname,
get_module_name(value),
helpers.add_trailing_slash(value) .. "apps.py"
)
os.execute(cmd)
end
vim.notify("App " .. value .. " created!", vim.log.levels.INFO, { title = M.title .. " - startapp" })
end
end
local function perform_create_package(value)
if vim.loop.fs_stat(value) == nil then
execute_command(os.execute, "mkdir -p " .. value)
execute_command(os.execute, "touch " .. helpers.add_trailing_slash(value) .. "__init__.py")
vim.notify("Package " .. value .. " created!", vim.log.levels.INFO, { title = M.title .. " - create package" })
end
end
local function env_ip(ip)
if #ip > 0 then
return ", " .. ip
end
return ip
end
function M.create_app()
inp.open({ prompt = "create app: " }, perform_create_app)
end
function M.create_package()
inp.open({ prompt = "create package: " }, perform_create_package)
end
function M.create_env_file(opts)
local opts = setup_opts(opts)
if file_exists ".env" then
vim.notify("File .env already exists", vim.log.levels.WARN, { title = "Create env file" })
return
end
local env_file = io.open(".env", "w")
local env_text = [[
SECRET_KEY=']] .. opts.secret_key .. "'\n" .. "ALLOWED_HOSTS=localhost, 10.0.2.2, 127.0.0.1" .. env_ip(opts.local_ip) .. "\n" .. "DEBUG=" .. opts.debug .. "\n\n" .. "DATABASE_URL=postgres://" .. opts.database.user .. ":" .. opts.database.password .. "@" .. opts.database.address .. ":" .. opts.database.port .. "/" .. opts.database.dbname .. "\n\n" .. "DB_USER=\n" .. "DB_PASSWORD=\n" .. "DB_HOST=\n" .. "DB_PORT=\n" .. "\n" .. "ADMIN_USERNAME=" .. opts.admin.username .. "\n" .. "ADMIN_EMAIL=" .. opts.admin.email .. "\n" .. "ADMIN_PASSWORD=" .. opts.admin.password .. "\n\n" .. "EMAIL_HOST=" .. opts.email.host .. "\n" .. "EMAIL_PORT=" .. opts.email.port .. "\n" .. "EMAIL_HOST_USER=" .. opts.email.host_user .. "\n" .. "EMAIL_HOST_PASSWORD=" .. opts.email.host_password .. "\n" .. "EMAIL_USE_TLS=" .. opts.email.use_tls .. "\n" .. "DEFAULT_FROM_EMAIL=" .. opts.email.default_from_email .. "\n\n" .. "CELERY_RESULT_BACKEND=" .. opts.celery.result_backend .. "\n" .. "CELERY_BROKER_URL=" .. opts.celery.broker_url .. "\n" .. "CELERY_ACCEPT_CONTENT=" .. opts.celery.accept_content .. "\n" .. "CELERY_RESULT_SERIALIZER=" .. opts.celery.result_serializer .. "\n" .. "CELERY_TASK_SERIALIZER=" .. opts.celery.task_serializer .. "\n" .. "CELERY_TIMEZONE=" .. opts.celery.timezone .. "\n" .. "CELERY_TASK_TIME_LIMIT=" .. opts.celery.task_time_limit .. "\n\n" .. "AWS_ACCESS_KEY_ID=\n" .. "AWS_SECRET_ACCESS_KEY=\n" .. "AWS_STORAGE_BUCKET_NAME=\n" .. "AWS_S3_ENDPOINT_URL=\n" .. "AWS_LOCATION=\n"
if env_file ~= nil then
env_file:write(env_text)
env_file:close()
end
end
function M.manage_run_server(custom_command)
local term_num = 2
local term = terms.get(term_num, true)
if not file_exists "Makefile" then
if not file_exists "makefile" then
custom_command = "./manage.py runserver 0.0.0.0:8000"
end
end
if term then
term:toggle()
else
toggleterm.exec(custom_command, term_num, 100, ".", "float", "runserver")
end
end
function M.manage_shell_plus(is_plus)
local term_num = 10
local term = terms.get(term_num, true)
local cmd = "./manage.py shell"
if is_plus then
cmd = "./manage.py shell_plus"
end
if term then
term:toggle()
else
toggleterm.exec(cmd, term_num, 100, ".", "float", "shell_plus", true)
end
end
function M.manage_make_migrations()
local term_num = 3
toggleterm.exec("./manage.py makemigrations", term_num, 100, ".", "float", "make_migrations", false)
end
function M.manage_migrate()
local term_num = 4
toggleterm.exec("./manage.py migrate", term_num, 100, ".", "float", "migrate", false)
end
function M.check()
local term_num = 5
toggleterm.exec("./manage.py check", term_num, 100, ".", "float", "check", false)
end
function M.show_migrations()
local term_num = 6
toggleterm.exec("./manage.py showmigrations", term_num, 100, ".", "float", "showmigrations", false)
end
function M.run_celery()
local term_num = 7
local term = terms.get(term_num, true)
if term then
term:toggle()
else
toggleterm.exec("make run_celery", term_num, 100, ".", "float", "run_celery", false)
end
end
return M
Loading…
Cancel
Save