Saturday, September 6, 2008

Vim pr0n: Creating named marks

A few days ago in #vim, someone asked if it is possible to create "named marks" with vim, i.e. marking a location with a name instead of a letter or number. The answer is no, but it was an interesting idea, so I turned down my pornography and wrote a script to do it.

When writing the code, I used prototype based OO — mainly to provide a more complex example for my previous raving.

Marks in vim


In vim you can mark a location in a file so you can quickly jump back there. A mark consists of a position (line/column) and an identifier (a single letter). Every buffer has its own set of lowercase marks (letters a–z) that exist only as long as the buffer exists. There is also a set of global marks using capital letters. These marks are persistent and there is only one set.

More info can be found at :help mark-motions.

Enter "named marks"


In the IRC chat, the guy wanted the same functionality as the uppercase marks, but with more descriptive names. Which is fair enough IMO since "A" doesn't really tell you anything about the position it jumps to.

So named marks are global, persistent marks with descriptive names.

The code


OMG check this out:

  1 "start the named mark prototype
  2 let s:NamedMark = {}
  3
  4 "the file the marks are stored in
  5 let s:NamedMark.Filename = expand('~/.namedMarks')
  6
  7 "constructor
  8 function! s:NamedMark.New(name, column, line, path)
  9   if a:name =~ ' '
 10     throw "IllegalNamedmarkNameError illegal name:" . a:name
 11   endif
 12
 13   let newNamedMark = copy(self)
 14   let newNamedMark.name = a:name
 15   let newNamedMark.column = a:column
 16   let newNamedMark.line = a:line
 17   let newNamedMark.path = a:path
 18   return newNamedMark
 19 endfunction
 20
 21 "lazy load and cache all named marks
 22 function! s:NamedMark.All()
 23   if !exists("s:NamedMark.AllMarks")
 24     let s:NamedMark.AllMarks = s:NamedMark.Read()
 25   endif
 26   return s:NamedMark.AllMarks
 27 endfunction
 28
 29 "create and add a new mark to the list
 30 function! s:NamedMark.Add(name, column, line, path)
 31
 32   try
 33     "if the mark already exists, just update it
 34     let mark = s:NamedMark.FindFor(a:name)
 35     let mark.column = a:column
 36     let mark.line = a:line
 37     let mark.path = a:path
 38
 39   catch /NamedMarkNotFoundError/
 40     let newMark = s:NamedMark.New(a:name, a:column, a:line, a:path)
 41     call add(s:NamedMark.All(), newMark)
 42
 43   finally
 44     call s:NamedMark.Write()
 45   endtry
 46 endfunction
 47
 48 "find the mark with the given name
 49 function! s:NamedMark.FindFor(name)
 50   for i in s:NamedMark.All()
 51     if i.name == a:name
 52       return i
 53     endif
 54   endfor
 55   throw "NamedMarkNotFoundError no mark found for name: \"".a:name.'"'
 56 endfunction
 57
 58 "get a list of all mark names
 59 function! s:NamedMark.Names()
 60   let names = []
 61   for i in s:NamedMark.All()
 62     call add(names, i.name)
 63   endfor
 64   return names
 65 endfunction
 66
 67 "delete this mark
 68 function! s:NamedMark.delete()
 69   call remove(s:NamedMark.All(), index(s:NamedMark.All(), self))
 70   call s:NamedMark.Write()
 71 endfunction
 72
 73 "go to this mark
 74 function! s:NamedMark.recall()
 75   exec "edit " . self.path
 76   call cursor(self.line, self.column)
 77 endfunction
 78
 79 "read the marks from the filesystem and return the list
 80 function! s:NamedMark.Read()
 81   let marks = []
 82   if filereadable(s:NamedMark.Filename)
 83     let lines = readfile(s:NamedMark.Filename)
 84     for i in lines
 85       let name   = substitute(i, '^\(.\{-}\) \d\{-} \d\{-} .*$', '\1', '')
 86       let column = substitute(i, '^.\{-} \(\d\{-}\) \d\{-} .*$', '\1', '')
 87       let line   = substitute(i, '^.\{-} \d\{-} \(\d\{-}\) .*$', '\1', '')
 88       let path   = substitute(i, '^.\{-} \d\{-} \d\{-} \(.*\)$', '\1', '')
 89
 90       let namedMark = s:NamedMark.New(name, column, line, path)
 91       call add(marks, namedMark)
 92     endfor
 93   endif
 94   return marks
 95 endfunction
 96
 97 "write all named marks to the filesystem
 98 function! s:NamedMark.Write()
 99   let lines = []
100   for i in s:NamedMark.All()
101     call add(lines, i.name .' '. i.column .' '. i.line .' '. i.path)
102   endfor
103   call writefile(lines, s:NamedMark.Filename)
104 endfunction
105
106 "NM command, adds a new named mark
107 command! -nargs=1
108   \ NM call s:NamedMark.Add('<args>', col("."), line("."), expand("%:p"))
109
110 "RM command, recalls a named mark
111 command! -nargs=1 -complete=customlist,s:CompleteNamedMarks
112   \ RM call s:NamedMark.FindFor('<args>').recall()
113
114 "DeleteNamedMark command
115 command! -nargs=1 -complete=customlist,s:CompleteNamedMarks
116   \ DeleteNamedMark call s:NamedMark.FindFor('<args>').delete()
117
118 "used by the above commands for cmd line completion
119 function! s:CompleteNamedMarks(A,L,P)
120   return filter(s:NamedMark.Names(), 'v:val =~ "^' . a:A . '"')
121 endfunction


lol, so... wtf does that do?


From a users perspective, if you were to shove this code in your vimrc, or in a plugin, it would provide three commands:

  • NM: create a new named mark, e.g. :NM main-function would create the main-function mark and bind it to the current file and cursor position. If that mark already existed, it would be overwritten

  • RM: recall a previous mark, e.g. :RM main-function would jump the cursor back to the same file and position where main-function was created

  • DeleteNamedMark: delete a named mark, e.g. :DeleteNamedMark main-function would remove main-function from the mark list.


From a programming perspective, the code defines one prototype object (lines 1–104), the three commands (lines 106–116), and a completion function for two of the commands (lines 118–121).

If I had to jack off to one part of this code, it would be the prototype object. It contains:

  • Seven class methods and two class variables which are used to maintain the list of named marks, including reading/writing to the filesystem.

  • Two instance methods for deleting and recalling marks.

  • Four instance variables specifying the position and name of the mark.


If I was actually going to turn this into a plugin, I would want to flesh it out a bit and add, for example, better error handling and a command similar to the existing :marks command to list named marks.

3 comments:

  1. vim makes my peepee come to attention!

    ReplyDelete
  2. hey !

    I was on #vim sometimes, asking some help about a problem that's look AFAIK impossible to solve with vim too. Maybe you'll have an idea. The problem appear while I try to make switch a emac's friends (nobody's perfect) who use this feature.

    Long story short, it consist in highlighting with var scoping awareness. ( ~ local and "global")

    a brief example (esp. look at var "name")

    1class MyClass {
    2
    3 String name = "Foo";
    4
    5 void printName(){
    6 cout << name << "\n";
    7 }
    8
    9 void printNameBar(){
    A String name = "Bar";
    B cout << name << "\n";
    C }
    D}

    well (sorry for poor numbering). so, the aim is to have name in line 3 and 6 in the same color and in a different color on line A and B. An idea ?


    (I apologize too for my poor english.)

    ReplyDelete