├── LICENSE ├── .gitignore ├── test └── test_pdf_with_images.pdf ├── plugin └── pdfview.vim ├── lua └── pdfview │ ├── utils.lua │ ├── parser.lua │ ├── init.lua │ └── renderer.lua └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | temp 2 | test/* 3 | -------------------------------------------------------------------------------- /test/test_pdf_with_images.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basola21/PDFview/HEAD/test/test_pdf_with_images.pdf -------------------------------------------------------------------------------- /plugin/pdfview.vim: -------------------------------------------------------------------------------- 1 | if has('nvim') 2 | command! PDFview lua require('pdfview').telescope_open() 3 | endif 4 | 5 | -------------------------------------------------------------------------------- /lua/pdfview/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.is_iterm2() 4 | return vim.env.TERM_PROGRAM == "iTerm.app" 5 | end 6 | 7 | function M.is_kitty() 8 | return vim.env.TERM == "xterm-kitty" 9 | end 10 | 11 | return M 12 | -------------------------------------------------------------------------------- /lua/pdfview/parser.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Check if the PDF file exists 4 | local function file_exists(pdf_path) 5 | return vim.fn.filereadable(pdf_path) ~= 0 6 | end 7 | 8 | -- Build the shell command for extracting text 9 | local function build_command(pdf_path, start_page, end_page) 10 | local escaped_pdf_path = vim.fn.shellescape(pdf_path) 11 | local page_args = "" 12 | if start_page and end_page then 13 | page_args = string.format("-f %d -l %d", start_page, end_page) 14 | end 15 | return string.format("pdftotext -layout %s %s -", page_args, escaped_pdf_path) 16 | end 17 | 18 | -- Run the shell command and return the output 19 | local function run_command(cmd) 20 | local result = vim.fn.system(cmd) 21 | if result and #result > 0 then 22 | return result 23 | else 24 | vim.api.nvim_err_writeln("PDFview: Failed to extract text from PDF.") 25 | return nil 26 | end 27 | end 28 | 29 | -- Main function to extract text from the PDF with optional page range 30 | function M.extract_text(pdf_path, start_page, end_page) 31 | if not file_exists(pdf_path) then 32 | vim.api.nvim_err_writeln("PDFview: File does not exist: " .. pdf_path) 33 | return nil 34 | end 35 | 36 | local cmd = build_command(pdf_path, start_page, end_page) 37 | return run_command(cmd) 38 | end 39 | 40 | return M 41 | -------------------------------------------------------------------------------- /lua/pdfview/init.lua: -------------------------------------------------------------------------------- 1 | local parser = require("pdfview.parser") 2 | local renderer = require("pdfview.renderer") 3 | local telescope = require("telescope.builtin") 4 | local previewers = require("telescope.previewers") 5 | local actions = require("telescope.actions") 6 | local action_state = require("telescope.actions.state") 7 | 8 | local M = {} 9 | 10 | -- Function to open the full PDF text (runs when PDF is selected) 11 | function M.open(pdf_path) 12 | local text = parser.extract_text(pdf_path) 13 | if text then 14 | renderer.display_text(text) 15 | end 16 | end 17 | 18 | -- Function to extract and display the first page (used for preview) 19 | function M.preview_first_page(pdf_path) 20 | local first_page_text = parser.extract_text(pdf_path, 1, 1) 21 | if first_page_text then 22 | return first_page_text 23 | else 24 | return "Could not extract text from the first page of the PDF." 25 | end 26 | end 27 | 28 | -- Custom previewer for Telescope to show the first page 29 | local pdf_previewer = previewers.new_buffer_previewer({ 30 | define_preview = function(self, entry) 31 | local pdf_path = entry.path 32 | local preview_text = M.preview_first_page(pdf_path) 33 | vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, vim.split(preview_text, "\n")) 34 | end, 35 | }) 36 | 37 | -- Telescope function with preview and open functionality 38 | function M.telescope_open() 39 | telescope.find_files({ 40 | prompt_title = "Select PDF", 41 | find_command = { "find", ".", "-type", "f", "-name", "*.pdf" }, 42 | previewer = pdf_previewer, 43 | attach_mappings = function(_, map) 44 | map("i", "", function(prompt_bufnr) 45 | local selected_file = action_state.get_selected_entry(prompt_bufnr) 46 | actions.close(prompt_bufnr) 47 | local pdf_path = selected_file.path 48 | M.open(pdf_path) 49 | end) 50 | return true 51 | end, 52 | }) 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /lua/pdfview/renderer.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- State to keep track of pages 4 | local state = { 5 | current_page = 1, 6 | total_pages = 0, 7 | pages = {}, 8 | } 9 | 10 | -- Function to display the current page 11 | function M.display_current_page() 12 | local buf = state.buf 13 | 14 | -- Set buffer to modifiable before making changes 15 | vim.api.nvim_set_option_value("modifiable", true, { buf = buf }) 16 | -- Clear existing buffer content 17 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, {}) 18 | 19 | -- Get the lines for the current page 20 | local page_lines = state.pages[state.current_page] 21 | 22 | -- Set the lines in the buffer 23 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, page_lines) 24 | 25 | -- Set buffer back to non-modifiable 26 | vim.api.nvim_set_option_value("modifiable", false, { buf = buf }) 27 | 28 | -- Update statusline or virtual text with page information 29 | M.update_page_info() 30 | end 31 | 32 | -- Function to update page information 33 | function M.update_page_info() 34 | local buf = state.buf 35 | local page_info = string.format("Page %d/%d", state.current_page, state.total_pages) 36 | 37 | -- Set virtual text at the end of the buffer 38 | vim.api.nvim_buf_clear_namespace(buf, state.ns_id, 0, -1) 39 | vim.api.nvim_buf_set_extmark(buf, state.ns_id, 0, 0, { 40 | virt_text = { { page_info, "Comment" } }, 41 | virt_text_pos = "right_align", 42 | }) 43 | end 44 | 45 | -- Function to split text into pages 46 | function M.paginate_text(text, lines_per_page) 47 | local lines = vim.split(text, "\n") 48 | local pages = {} 49 | for i = 1, #lines, lines_per_page do 50 | local page = vim.list_slice(lines, i, i + lines_per_page - 1) 51 | table.insert(pages, page) 52 | end 53 | return pages 54 | end 55 | 56 | -- Function to initialize the buffer and display the first page 57 | function M.display_text(text) 58 | -- Default lines per page 59 | local lines_per_page = 50 -- You can make this configurable 60 | 61 | -- Split text into pages 62 | state.pages = M.paginate_text(text, lines_per_page) 63 | state.total_pages = #state.pages 64 | state.current_page = 1 65 | 66 | -- Create a new buffer or reuse existing one 67 | if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then 68 | state.buf = vim.api.nvim_create_buf(false, true) 69 | end 70 | local buf = state.buf 71 | 72 | -- Set buffer-local options using nvim_set_option_value 73 | vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf }) 74 | vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) 75 | vim.api.nvim_set_option_value("swapfile", false, { buf = buf }) 76 | vim.api.nvim_set_option_value("modifiable", false, { buf = buf }) 77 | vim.api.nvim_set_option_value("filetype", "pdfview", { buf = buf }) 78 | 79 | state.ns_id = vim.api.nvim_create_namespace("PDFview") 80 | 81 | -- Display the current page 82 | M.display_current_page() 83 | 84 | -- Open a new window with the buffer 85 | vim.api.nvim_set_current_buf(buf) 86 | end 87 | 88 | -- Function to go to the next page 89 | function M.next_page() 90 | if state.current_page < state.total_pages then 91 | state.current_page = state.current_page + 1 92 | M.display_current_page() 93 | else 94 | vim.api.nvim_echo({ { "PDFview: Already at the last page.", "WarningMsg" } }, false, {}) 95 | end 96 | end 97 | 98 | -- Function to go to the previous page 99 | function M.previous_page() 100 | if state.current_page > 1 then 101 | state.current_page = state.current_page - 1 102 | M.display_current_page() 103 | else 104 | vim.api.nvim_echo({ { "PDFview: Already at the first page.", "WarningMsg" } }, false, {}) 105 | end 106 | end 107 | 108 | return M 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # PDFview.nvim - A Neovim Plugin for Viewing PDFs 3 | 4 | **PDFview.nvim** is a Neovim plugin designed for users who want to open, view, and navigate PDF documents directly within Neovim. It is particularly useful for those who want to integrate their workflow with Obsidian or other note-taking systems, allowing you to quickly open PDFs, extract text, and navigate through pages, all from within Neovim. 5 | 6 | --- 7 | 8 | ## Features 9 | 10 | - **Open PDF Files**: Use Telescope to quickly search and open PDF files. 11 | - **Extract Text**: Extract the text from a PDF using `pdftotext` for easy reading or note-taking. 12 | - **Pagination**: Navigate through the document using next/previous page commands. 13 | - **Customizable Pagination**: Set how many lines per page should be displayed. 14 | - **Virtual Text Display**: See page numbers displayed in the buffer. 15 | 16 | ![Preview](https://i.imgur.com/ClDZhnc.gif) 17 | 18 | --- 19 | 20 | ## Installation 21 | 22 | ### Prerequisites 23 | 24 | - **Neovim** version 0.5 or higher 25 | - **Telescope.nvim**: The plugin uses Telescope to search for PDF files. 26 | - **pdftotext**: This plugin relies on the `pdftotext` command-line tool to extract text from PDFs. Install `pdftotext` using the following command: 27 | ```bash 28 | sudo apt install poppler-utils 29 | ``` 30 | 31 | ### Installation with LazyVim 32 | 33 | To install `PDFview.nvim` using **LazyVim**, add the following configuration to your Neovim setup: 34 | 35 | ```lua 36 | { 37 | "basola21/PDFview", 38 | lazy = false, 39 | dependencies = { "nvim-telescope/telescope.nvim" } 40 | } 41 | ``` 42 | 43 | ### Mappings 44 | 45 | You can add the following key mappings to your Neovim configuration for easy navigation through the PDF pages: 46 | 47 | ```lua 48 | -- Navigate to the next page in the PDF 49 | map("n", "jj", ":lua require('pdfview.renderer').next_page()", { desc = "PDFview: Next page" }) 50 | 51 | -- Navigate to the previous page in the PDF 52 | map("n", "kk", ":lua require('pdfview.renderer').previous_page()", { desc = "PDFview: Previous page" }) 53 | ``` 54 | 55 | --- 56 | 57 | ## Usage 58 | 59 | 1. **Opening a PDF File** 60 | Use the following command to open a PDF: 61 | ```lua 62 | require('pdfview').open() 63 | ``` 64 | This will open Telescope's file finder, allowing you to search for PDF files in your project or system. 65 | 66 | 2. **Navigating Pages** 67 | Use the defined key mappings to navigate between pages: 68 | - `jj` for the next page. 69 | - `kk` for the previous page. 70 | 71 | 3. **Extracting Text from a PDF** 72 | When you select a PDF using Telescope, the plugin extracts the text using `pdftotext` and displays it in a buffer, allowing for easy reading or note-taking. 73 | 74 | 4. **Adding Autocmd** 75 | Add these lines in your nvim config to open pdf's with PDFview: 76 | ```lua 77 | vim.api.nvim_create_autocmd("BufReadPost", { 78 | pattern = "*.pdf", 79 | callback = function() 80 | local file_path = vim.api.nvim_buf_get_name(0) 81 | require("pdfview").open(file_path) 82 | end, 83 | }) 84 | ``` 85 | --- 86 | 87 | ## Configuration 88 | 89 | The default lines per page are set to `50`. You can change this value by modifying the `lines_per_page` variable in the `renderer.lua` file. 90 | 91 | --- 92 | 93 | ## Planned Features 94 | 95 | - **Improved Navigation**: Refine the pagination and scrolling behavior for a smoother reading experience. 96 | - **Customization**: Add options for setting default configurations like `lines_per_page` and buffer options. 97 | - **Document Search**: Implement a search feature to find specific text within the PDF. 98 | - **Improved Structure**: Enhance the project structure for better maintainability and scalability. 99 | 100 | --- 101 | 102 | ## Contribution 103 | 104 | Contributions are welcome! Feel free to open issues or submit pull requests to help improve the plugin. 105 | 106 | 1. Fork the repository. 107 | 2. Create a new branch (`git checkout -b feature/my-feature`). 108 | 3. Commit your changes (`git commit -m 'Add some feature'`). 109 | 4. Push to the branch (`git push origin feature/my-feature`). 110 | 5. Open a pull request. 111 | 112 | --- 113 | 114 | ## License 115 | 116 | This project is licensed under the MIT License. 117 | 118 | --- 119 | 120 | ## Support 121 | 122 | If you encounter any issues or have feature requests, please open an issue in the [GitHub repository](https://github.com/basola21/PDFview). 123 | 124 | --- 125 | 126 | Enjoy using **PDFview.nvim** for your Neovim-based PDF viewing and note-taking workflow! 127 | --------------------------------------------------------------------------------