nvim: Telescope toggle cwd
Sérgio Araújo

Sérgio Araújo @voyeg3r

About: I am a Free Software enthusiast and a (neo)?vim addicted, I also like shell script, sed, awk, and as you can see I love Regular Expressions.

Location:
Brazil
Joined:
Sep 23, 2017

nvim: Telescope toggle cwd

Publish Date: Jun 23
1 4

Intro

Being abble to change the scope of a srarch, grep or other commands gives us more control.

Finding the root dir

--- Returns project_root
--- @param bufnr number|nil Buffer
--- @return string root dir
M.root = function(bufnr)
  bufnr = bufnr or 0
  local markers = {
    '.git',
    '.hg',
    '.svn',
    'pyproject.toml',
    'setup.py',
    'requirements.txt',
    'package.json',
    'tsconfig.json',
    'Makefile',
    'CMakeLists.txt',
    '.nvim-root',
  }

  return vim.fs.root(bufnr, markers) or vim.fn.getcwd()
end
Enter fullscreen mode Exit fullscreen mode

Custom telescope find_files

This module will be called via keymap

-- File: ~/.config/nvim/lua/core/utils/telescope.lua
-- Last Change: 2025-06-23
-- Author: Sergio Araujo

local M = {}

local actions = require('telescope.actions')
local pickers = require('telescope.pickers')
local finders = require('telescope.finders')
local conf = require('telescope.config').values
local ts_utils = require('telescope.utils')
local builtin = require('telescope.builtin')
local utils = require('telescope.utils')

local get_root = require('core.utils.file_system').root

-- Função para path_display que remove prefixo Termux e mantém estilo tail+path
local function oldfiles_path_display(_, path)
  local termux_prefix = '/data/data/com.termux/files/home/'
  if path:sub(1, #termux_prefix) == termux_prefix then path = path:sub(#termux_prefix + 1) end

  local tail = utils.path_tail(path)
  local display = string.format('%s %s', tail, path)
  local hl_start = #tail + 1
  local hl_end = #display

  return display, { { { hl_start, hl_end }, 'Comment' } }
end

-- Garante um diretório válido mesmo que buffer esteja vazio
local function get_valid_buf_dir()
  local dir = ts_utils.buffer_dir()
  return (dir and dir ~= '') and dir or vim.loop.cwd()
end

-- Find files com toggle de cwd
M.find_files_with_toggle = function()
  local root = get_root()
  if not root or root == '' then
    vim.notify('Root dir não encontrado. Você está fora de um projeto?', vim.log.levels.WARN, {
      title = 'Find Files',
    })
    return
  end

  local buf_dir = get_valid_buf_dir()
  local current_cwd = root

  local function picker()
    builtin.find_files({
      cwd = current_cwd,
      attach_mappings = function(prompt_bufnr, map)
        map('i', '<a-d>', function()
          actions.close(prompt_bufnr)
          local new_cwd = (current_cwd == root) and buf_dir or root
          local title = 'Find Files'

          if new_cwd ~= current_cwd then
            current_cwd = new_cwd
            vim.notify('cwd switched to: ' .. current_cwd, vim.log.levels.INFO, { title = title })
          else
            vim.notify('cwd não foi alterado (já em ' .. current_cwd .. ')', vim.log.levels.WARN, { title = title })
          end

          picker()
        end)
        return true
      end,
    })
  end

  picker()
end

-- Grep customizado com toggle de cwd e preservação do prompt
M.custom_grep_with_toggle = function()
  local root = get_root()
  if not root or root == '' then
    vim.notify('Root dir não encontrado. Você está fora de um projeto?', vim.log.levels.WARN, {
      title = 'Custom Grep',
    })
    return
  end

  local buf_dir = get_valid_buf_dir()
  local current_cwd = root
  local current_prompt = ''

  local function picker()
    pickers
      .new({}, {
        prompt_title = 'Custom Grep',
        finder = finders.new_job(function(prompt)
          if prompt == '' then return nil end
          current_prompt = prompt
          return { 'rg', '--vimgrep', '--no-heading', prompt }
        end, nil, { cwd = current_cwd }),
        previewer = conf.grep_previewer({}),
        sorter = conf.generic_sorter({}),
        default_text = current_prompt,
        attach_mappings = function(prompt_bufnr, map)
          map('i', '<a-d>', function()
            actions.close(prompt_bufnr)
            local new_cwd = (current_cwd == root) and buf_dir or root
            local title = 'Custom Grep'

            if new_cwd ~= current_cwd then
              current_cwd = new_cwd
              vim.notify('cwd switched to: ' .. current_cwd, vim.log.levels.INFO, { title = title })
            else
              vim.notify('cwd não foi alterado (já em ' .. current_cwd .. ')', vim.log.levels.WARN, { title = title })
            end

            picker()
          end)
          return true
        end,
      })
      :find()
  end

  picker()
end

-- Oldfiles com path_display customizado para Termux
M.oldfiles_clean = function()
  builtin.oldfiles({
    path_display = oldfiles_path_display,
    layout_strategy = 'vertical', -- ou "horizontal", conforme preferir
    layout_config = {
      height = 0.5, -- aqui você diminui a altura (0.3 = 30% da tela)
      width = 0.7, -- opcional: ajustar largura também
      prompt_position = 'top',
      preview_cutoff = 1, -- para esconder preview em telas pequenas
    },
  })
end

return M
Enter fullscreen mode Exit fullscreen mode

The keymaps

In your telescope keys table place

-- Adjust the module path to reflect you realitty
{
      '<leader>ff',
      function() require('core.utils.telescope').find_files_with_toggle() end,
      desc = 'Find Files (toggle raiz/buffer com Alt+D)',
},
{
      '<leader>fg',
      function() require('core.utils.telescope').custom_grep_with_toggle() end,
      desc = 'Custom Grep (rg) toggle root/buffer com <A-d>',
},
Enter fullscreen mode Exit fullscreen mode

Comments 4 total

  • gokayburuc.dev
    gokayburuc.devJun 24, 2025

    First of all, thank you for sharing your code and your ideas. Since there are different levels of users on this platform, from basic to senior, if you briefly state in the title of the article who the article is written for and how they will use it, you can help people get an idea about the article before moving on.

    My second suggestion is that if you do not want to write a long article but want to explain your code; After pasting your script via ChatGPT, you can make your code more understandable for those who are not Lua literate with the help of the "Break apart the code and add line by line comments" prompt. In this way, it will be a clearer sharing and will be open to development.

    • gokayburuc.dev
      gokayburuc.devJun 24, 2025

      ChatGPT code break apart sample for your first script:


      Here's a line-by-line breakdown of your Lua function with inline comments and explanations:

      --- Returns project_root
      --- @param bufnr number|nil Buffer
      --- @return string root dir
      
      Enter fullscreen mode Exit fullscreen mode
      • These are annotations (commonly used in LSP or documentation generators).
      • bufnr: Optional buffer number.
      • Return: A string representing the root directory of the project.

      M.root = function(bufnr)
      
      Enter fullscreen mode Exit fullscreen mode
      • Define a function root in table M.
      • It accepts an optional parameter bufnr (buffer number).

        bufnr = bufnr or 0
      
      Enter fullscreen mode Exit fullscreen mode
      • If bufnr is not provided (nil), default to 0, which refers to the current buffer.

        local markers = {
          '.git',
          '.hg',
          '.svn',
          'pyproject.toml',
          'setup.py',
          'requirements.txt',
          'package.json',
          'tsconfig.json',
          'Makefile',
          'CMakeLists.txt',
          '.nvim-root',
        }
      
      Enter fullscreen mode Exit fullscreen mode
      • Define a list of root markers — files or folders that are typically found in a project's root.
      • These include common VCS folders (.git, .hg, .svn) and build/config files for Python, Node.js, C/C++, etc.

        return vim.fs.root(bufnr, markers) or vim.fn.getcwd()
      
      Enter fullscreen mode Exit fullscreen mode
      • Use vim.fs.root(bufnr, markers) to find the nearest ancestor directory of the buffer that contains one of the markers.
      • If no such directory is found, fall back to the current working directory returned by vim.fn.getcwd().

      Summary

      This function:

      • Tries to locate the project root based on common marker files or directories.
      • Falls back to the current working directory if none are found.
      • Useful for plugins, file navigation, and language server configuration in Neovim.

      Related Keywords

      neovim, vim.fs.root, project root detection, lua, vim.fn.getcwd, LSP, buffer, marker files, file system traversal.

      • Sérgio Araújo
        Sérgio AraújoJun 25, 2025

        Fantastic suggestion! I really apreciate!

        • gokayburuc.dev
          gokayburuc.devJun 25, 2025

          Thank you for your understanding. We are here to support each other to improve our common knowledge. I'm using this special technique to analyze Github Repositories (dotfiles, Rust Repos etc.). It helps me to understand the code quickly. See you at your new article Sergio!

          Keep writing, keep sharing, keep learning ;)

Add comment