Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vim: Project-wide Occurrences #1767

Merged
merged 7 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ UNRELEASED
the correct name of the current unit in the presence of wrapping (#1776)
+ editor modes
- emacs: add basic support for project-wide occurrences (#1766)
- vim: add basic support for project-wide occurrences (#1767, @Julow)

merlin 5.0
==========
Expand Down
95 changes: 79 additions & 16 deletions vim/merlin/autoload/merlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import os
import sys
import itertools
from sys import platform

enclosing_types = [] # nothing to see here
Expand Down Expand Up @@ -84,6 +85,21 @@ def catch_and_print(f, msg=None):
def concat_map(f, args):
return [item for arg in args for item in f(arg)]

# Format an integer or a string into a form that can be parsed back by Vim.
def vim_value(v):
if isinstance(v, int):
return str(v)
if isinstance(v, str):
return "'%s'" % v.replace("'", "''")
raise Exception("Failed to convert into a vim value: %s" % str(v))

# Format a dictionnary containing integer and string values into a Vim record.
def vim_record(d):
def vim_field(f):
key, val = f
return "'%s':%s" % (key, vim_value(val))
return "{" + ",".join(map(vim_field, d.items())) + "}"

######## PROCESS MANAGEMENT

def current_context():
Expand Down Expand Up @@ -323,9 +339,10 @@ def command_motion(cmd, target, pos):
except MerlinExc as e:
try_print_error(e)

def command_occurrences(pos):
def command_occurrences(pos, project_wide):
try:
lst_or_err = command("occurrences", "-identifier-at", fmtpos(pos))
scope_args = ["-scope", "project"] if project_wide else []
lst_or_err = command("occurrences", "-identifier-at", fmtpos(pos), *scope_args)
if not isinstance(lst_or_err, list):
print(lst_or_err)
else:
Expand Down Expand Up @@ -479,30 +496,75 @@ def vim_document_at_cursor(path):
def vim_document_under_cursor():
vim_document_at_cursor(None)

# Read the lines numbers present in [lines] from file [fname]. End-of-line is
# stripped from each line.
def read_lines_of_file(fname, lines):
lines = iter(sorted(set(lines)))
next_line = next(lines)
n = 1
r = {}
try:
try:
with open(fname) as inp:
for line in inp:
if next_line == n:
r[n] = line.rstrip("\n")
next_line = next(lines)
n += 1
except FileNotFoundError:
pass
# File has been truncated or not found
while True:
r[next_line] = ""
next_line = next(lines)
except StopIteration:
return r

# From the result of [command_occurrences], read the start line for each
# results. Generate the same occurrences with the 'text' field added.
def with_text_previews(occurs):
preview_lines_by_file = {
fname: read_lines_of_file(fname, [ oc['start']['line'] for oc in occurs ])
for fname, occurs
in itertools.groupby(occurs, lambda oc: oc.get('file'))
if fname != None }
for oc in occurs:
lnum = oc['start']['line']
if 'file' not in oc: # Current buffer
text = vim.current.buffer[lnum - 1]
else:
text = preview_lines_by_file[oc['file']][lnum]
yield { "text": text, **oc }

# Occurrences
def vim_occurrences(vimvar):
def vim_occurrences(vimvar, project_wide):
vim.command("let %s = []" % vimvar)
line, col = vim.current.window.cursor
lst = command_occurrences((line, col))
lst = map(lambda x: x['start'], lst)
lst = command_occurrences((line, col), project_wide)
bufnr = vim.current.buffer.number
cur_fname = vim.current.buffer.name
nr = 0
cursorpos = 0
for pos in lst:
lnum = pos['line']
lcol = pos['col']
if (lnum, lcol) <= (line, col): cursorpos = nr
text = vim.current.buffer[lnum - 1]
text = text.replace("'", "''")
vim.command("let l:tmp = {'bufnr':%d,'lnum':%d,'col':%d,'vcol':0,'nr':%d,'pattern':'','text':'%s','type':'I','valid':1}" %
(bufnr, lnum, lcol + 1, nr, text))
nr = nr + 1
for pos in with_text_previews(lst):
lnum = pos['start']['line']
lcol = pos['start']['col']
occur = { "lnum": lnum, "col": lcol + 1, "vcol": 0, "nr": nr,
"pattern": "", "type": "I", "valid": 1 , "text": pos['text'] }
if 'file' not in pos or pos['file'] == cur_fname:
# Occurrence is in the current buffer
if (lnum, lcol) <= (line, col): cursorpos = nr
occur["bufnr"] = bufnr
else:
occur["filename"] = pos['file']
vim.command("let l:tmp = " + vim_record(occur))
vim.command("call add(%s, l:tmp)" % vimvar)
nr = nr + 1
return cursorpos + 1

def vim_occurrences_search():
project_wide = False
line, col = vim.current.window.cursor
lst = command_occurrences((line, col))
lst = command_occurrences((line, col), project_wide)
result = ""
over = ""
start_col = 0
Expand All @@ -521,8 +583,9 @@ def vim_occurrences_search():
return "[%s, '%s', '%s']" % (start_col, over, result)

def vim_occurrences_replace(content):
project_wide = False
cursor = vim.current.window.cursor
lst = command_occurrences(cursor)
lst = command_occurrences(cursor, project_wide)
lst.reverse()
for pos in lst:
if pos['start']['line'] == pos['end']['line']:
Expand Down
26 changes: 23 additions & 3 deletions vim/merlin/autoload/merlin.vim
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ endif
let s:current_dir=expand("<sfile>:p:h")
silent! MerlinPy import sys, vim
MerlinPy if not vim.eval("s:current_dir") in sys.path:
\ sys.path.append(vim.eval("s:current_dir"))
\ sys.path.insert(0, vim.eval("s:current_dir"))

MerlinPy import merlin

Expand Down Expand Up @@ -481,19 +481,37 @@ endfunction
function! merlin#Occurrences()
let l:occurrences = []
let l:pos = 0
MerlinPy vim.command ("let l:pos = %d" % merlin.vim_occurrences("l:occurrences"))
MerlinPy vim.command ("let l:pos = %d" % merlin.vim_occurrences("l:occurrences", False))

if l:occurrences == []
return
endif

call setloclist(0, l:occurrences)
execute ":ll! " . l:pos
if l:pos > 0
execute ":ll! " . l:pos
endif
if g:merlin_display_occurrence_list
lopen
endif
endfunction

function! merlin#OccurrencesProjectWide()
let l:occurrences = []
let l:pos = 0
MerlinPy vim.command ("let l:pos = %d" % merlin.vim_occurrences("l:occurrences", True))
if l:occurrences == []
return
endif
call setqflist(l:occurrences)
if l:pos > 0
execute ":cc! " . l:pos
endif
if g:merlin_display_occurrence_list
copen
endif
endfunction

function! merlin#OccurrencesRename(text)
MerlinPy merlin.vim_occurrences_replace(vim.eval("a:text"))
endfunction
Expand Down Expand Up @@ -749,6 +767,8 @@ function! merlin#Register()
command! -buffer -nargs=0 MerlinOccurrences call merlin#Occurrences()
nmap <silent><buffer> <Plug>(MerlinSearchOccurrencesForward) :call merlin_find#OccurrencesSearch('/')<cr>:let v:searchforward=1<cr>
nmap <silent><buffer> <Plug>(MerlinSearchOccurrencesBackward) :call merlin_find#OccurrencesSearch('?')<cr>:let v:searchforward=0<cr>
" Project-wide occurrences
command! -buffer -nargs=0 MerlinOccurrencesProjectWide call merlin#OccurrencesProjectWide()

" Rename
command! -buffer -nargs=* MerlinRename call merlin#OccurrencesRename(<f-args>)
Expand Down