nvi
v0.0.4
Published
Very opinionated Node.JS VI clone
Downloads
2
Readme
Very opinionated Node.JS VI clone
Installation
npm install nvi -g
Usage
nvi
Rambling Specification:
this will become my dream [collaborative] editor.
i'm just taking the best parts of vim and making them better line numbers buffers macros modes block edit highly configurable via modern programming (javascript or coffeescript) plugins (by default does almost nothing) nerdtree ctags object browser
using multi-colored cursors and the shortest possible network language to communicate changes
e.g. the collab feature should have: follow mode multiple cursors multiple colored cursors colored fg/background text matching users who edited support for syntax highlighting
only transmit files when they are needing to be opened, as they need to be opened, and only the parts of the file that need to be rendered
detect changes to files on disk using mtime and auto-sync those to the group
detect an out-of-sync state (what is hash of chars on line RAND characters RANDx - RANDY?) if its not the same then resend the document
later i would add support for:
coffeescript syntax highlighting type inference code completion
there's only one buffer per file a buffer does not have to be a file multiple views can share a buffer a view can have one or more cursor
buffer is closely tied to the view it only fetches enough bytes to fill the view it grabs chunks equal to the current view's line in bytes
buffer is aware of how many views are using it before it seeks the file for more data, it determines which stripes are being used across all views it considers overlapping views and only fetches each line it needs once, in order from first byte to last byte in file and and when it fetches an area of the buffer that is overlapping in the view render it updates both views not one then again for the other
so views have: w, x, buf_offset_y, cursors: pos: x:, y:
cursors have: user_id, color, pos: { x:, y: },
users have name and color
cursors are relative to their views
for my collaborative mode, we'll connect at first over a regular tcp socket there will be one instance in a 'host' mode and zero or more instances in a 'guest' mode the guests will not receive mirror copies of the entire directory the host will share the directory skeleton for the current directory and recursively but only upon a watch request watchers will receive push notification from the host (e.g. file added or removed under watchdir)
any file a client requests to open, the client, and all other clients will be aware of client will request the file from host host will broadcast to other clients that the client has opened the file and when it has closed the file
host will spoon-feed data to the clients like a buffer would so the clients only get the data they need to look at that moment
also, diffs are only broadcasted within the context of the all clients collective views, as well
if a file changes on the host system then we only broadcast that change if it is within the overlapped views of clients
this is very similar to how ssh vim tmux session would go, except we now have multiple cursors
cursor position updates will be notified (client->host) and rebroadcasted (others) with a throttle but diffs will not be based on cursor position and exact keypress matches they will be actual patch diffs in a minimalist format
and what about binary files? these will not attempt to be diffed on modification, the latest (mtime) copy overrides everyone else's (clients will need to have a filesystem mtime watch too) the process for updating a binary file as a client would be: connect right-click > download the file or folder modify the file locally mtime inotify triggers guests to receive the bin file as well any new files created (atime) get auto-shared to the host
client can be asked to resolve merge conflicts
its basically like a rapid git session
except i don't like all the assumptions made above re. bin files i think if the client wants to share a new file, it should be explicit, at least for now and let's see if it becomes a hassle or if a client wants to modify an existing binfile, it should also be explicit because in reality these will change a lot (e.g. during a photoshop editing session) and we really only care about the final result and most likely the group will want to talk it over and approve it before accepting it anyway
it would be cool if it had git support so that team commits could be saved with attribution for the authors involved when the commit was made and so the commits only happened on the host side same with pushes
NO that silly git diff shit is not applicable; it doesn't resolve changes on the same line so i need my own i think instead i will just translate the cursor/block edit operations to binary opcodes
basically whatever you can do with a block, there's an opcode for that so shift selection one character right, shift selection one character left (unlikely use case) duplicate selection up/down delete selection insert before selection insert after selection move selection up/down backspace/delete x characters insert "literal" characters; assume we are working on an ascii file always pretty confident that's a safe assumption; collaborative editing binary files will have their own app and experience move cursor absolute x, y or relative -x, -y
when syncing hydrabuffers / views clients will write changes to disk, without filler as the view scrolls up/down the file will be prepended/appended until the file is complete
if a user changes a file outside of nvim which they don't have a complete dataset for hmm
ok getting too fancy here / prematurely optimizing / overengineering let's K.I.S.S.
we'll transfer the whole ascii file to clients as they request to view it and then we'll work to keep it in sync from there its really not that much data to transfer
except am i solving anything the other way around? a lot of times sync errors happen while users are editing in the areas we aren't currently watching for changes in
i kind of like my ssh-vim-like approach
ok so in that vain, the client does not buffer or store ANY data locally this way it cannot possibly go out of sync if it does, they just close/reopen the file to get back in sync sync will happen on a per-view basis
same as if you opened a file and someone changed it while you were reading it
this is also good because it functionally reinforces the idea of requiring clients to check-in new files or binary file changes by issuing special commands to have them imported to the host
and then with that i can refuse to render binary files altogether just display an error like 'binary files unsupported; utf8 text only'
so if someone wanted to edit a binary file in this flow, they would have to issue a special command to fetch the binfile then edit it locally then issue a special command to upload the binfile thus greatly reducing incidental sync errors and unnecessary data transfer
for a view statusbar just show:
relative path, bold filename dont show line endings like 'unix | mac | win' thats pointless instead highlight whitespace aggressively and with favoritism for unix show percentage of file remaining and cursor x:y pos
show treeview directory structure i'll have to implement this too because it needs remote support
the modes i'll implement will be: NORMAL except i'll call it COMBO INSERT except i'll call it NORMAL REPLACE i'll keep this the same V-LINE except i'll call it LINE-BLOCK V-BLOCK exept i'll call it BLOCK
treat views like a tiling window manager create a Tab object like tmux-style tabs tabs will have user's names in collab mode
so it starts with one tab and one view you can never have fewer than one view open if you do its just resetting the file to an untitled in-memory buffer
when you add another view you can only do so by using a specific key combination / cmd to indicate a vsplit or hsplit or new tab
so when this happens, the default is to divide the tab 50/50 then the user can click and drag to resize from there
a view can never be less than 1 char high and 1 char wide; for every line that isnt wrapped but is cutoff by a short viewport, a > is ending that line
opening more than one tab creates a 1char high tabbar at top of the screen
i may split View into ReadOnlyView extended by EditableView
HydraBuffer doesn't need to know about cursors the view is always the one i keep landing in its the one that colors the cursor and shows it moving around the view it has really nothing to do with the buffer if a cursor moves off of a view, does it matter? (if a tree falls in the forest, does it make a sound?) no.
so there's a difference between a stream and a buffer here the host is going to need to open a buffer this also means that the entire file that is being edited needs to be able to fit within the available ram memory if we are going to save it back to the disk whole again this is all a limitation of not being able to write in the middle of a file hmm i mean technically that's difficult because if anyone could do that without a lock on the file then you'd never know where you last left off in order to insert your changes plus even if a fancy filesystem supported this you'd still have to implement your own layer on top of that like a .vdi or .vmdk file to make it fully compatible with other filesystems e.g., windows, sftp, nfs, etc.
the traditional options i have at my disposal are: writeFile (total replacement) and appendFile (write to end only)
so i can only use writeFile() which means i have to pass the entire file which means the entire file must fit in memory which means buffering only the maximum rendered by the views in memory is pointless
well unless i get really creative with diffs for example, if while using a readStream, i notate the offset location and i assume that the file had a lock on it (could make my own .*.lck files) then i would never need the whole file in memory i could just replay chunks from the original file up to the point of my stream offset location then write my modified contents and write chunks of all this to a temporary file on disk (/tmp/nvi-filename.tmp) and then mv the new file over the top of the old
i could get even more crafty and only store in-memory my changes and the location that they occurred at and just keep those in-memory until a save action is requested then do the above replay-and-merge flushing-chunks-to-tmp-disk strategy
this way we can collaborate on huge text files, if we want, too but on average it will reduce the amount of data needing to be thrown around
so what format would my in-memory buffer take? i could use the diff/patch format and record line-by-line changes except some really long lines could mess with us there
110,10,115:abcdefgh
bah, i'm making it too complicated. i can be a bad-ass later. its getting late.
so i'll just use buffers for now and always assume that the file will fit in memory and that transmitting a file-at-a-time from host to client is fast enough
add funny -- COMBO X10! -- counter to the statusbar as each combo is executed within a 1 sec delay
there's a view statusbar and a window statusbar the window statusbar is always visible really they both are i guess the view one gets added by a plugin for gvim but mine will be there in core
now this is getting fun! i'm going to take some liberties with this to make vim easier for newbs
no matter where you are in vim, you should always see the current mode in a reliable place, because modes are so fundamental to vi operation my modes will be color coded like the vi powerline theme they will always appear in the view statusbar except for COMMAND mode which will appear in the statusbar this will make it very obvious what is going on
additionally, my vim will begin the user in INSERT mode except remember i'm calling it NORMAL mode so that when they start out its most like what they might be famliar with: notepad, and they hvae to hit ESC to get to COMBO mode (what i call the vi NORMAL mode)
make it so all @w and @h are the outer dims and any decorators like tabs, statusbars, etc. must fit inside that and relative to their parent dims who controls them
instead of doing a layered TUI that redraws layers ontop of layers if i need to get more efficient i could make a pixel/char registry which is basically a matrix of function pointers the first cell points to a function that controls its appearance and subsequent cells point back to the id of the first cell of which they also belong then i just loop through the cells to determine which functions need to execute to redraw the screen
its important to differentiate between the Terminal.cursor (there is only one; used to draw the screen) and the ViewCursor (there can be one or more per view; used by users to traverse a view) additionally between a ViewCursor that is the currently possessed (uses Terminal.cursor) and a ViewCursor that is not possessed (unfocused, or someone else's cursor; drawn manually)