Apply elisp function to any file - emacs

How to apply an elisp function to a specific file, which is not in an open buffer?
Eg (my/apply delete-duplicate-lines "~/tmp")

I think you want to do
(with-current-buffer (find-file-noselect "~/tmp")
(delete-duplicate-lines (point-min) (point-max))
(save-buffer))
If you want more information on how to programmatically operate on buffer contents read this section of the manual

If you want also to cleanup the buffer etc, you could use
(let ((file "~/tmp"))
(with-temp-file file
(insert-file-contents file)
(delete-duplicate-lines (point-min) (point-max))))

You can easily do this to the marked files of Dired, if you use library Dired+ (dired+.el).
# in Dired prompts you for a function (a function name or a lambda expression), and it then applies the function to the marked files.
By default, it applies the function to each marked file name. The function can do whatever you like, but it needs to accept a file name as argument. (The function need not visit the file.)
With a plain prefix arg (C-u), each marked file is visited and the function is invoked (with no arguments) in the visiting buffer.
M-+ # in Dired does the same thing, but it acts on all marked files and all files marked in any marked subdirectories, defined recursively. (M-+ is a prefix key for operations on the marked files, defined recursively over subdirectories.)
The files acted on are those that are marked in the current Dired
buffer, or all files in the directory if none are marked. Marked
subdirectories are handled recursively in the same way.
Just as for #, a plain prefix arg (C-u) means visit each file and invoke the function with no arguments.
Any other prefix arg behaves according to the ARG argument of the Dired+ version of dired-get-marked-files. In particular, C-u C-u operates on all files in the Dired buffer - it ignores markings.
More precisely:
If ARG is an integer, act on the next ARG files (previous -ARG, if < 0).
If ARG is a cons with element 16, 64, or 256, corresponding to
C-u C-u, C-u C-u C-u, or C-u C-u C-u C-u, then act on all files in the Dired buffer, where:
16 includes no directories (including . and ..)
64 includes directories except . and ..
256 includes all directories (including . and ..)
If ARG is otherwise non-nil, act on the current file only.

Related

How to create a new, unnamed file in emacs?

This is a very simple request: I want to create a new, blank file without giving it a name (yet). I can use the scratch buffer but there's only one. I can C-x C-f and open a new file, but then I have to give it a name and path. If I'm just writing notes to myself or sketching out ideas, I don't want to have to give it a name. How do I create a new, empty, unnamed file?
You can create a new buffer with: C-xb and type in the buffer name and it will create a new buffer with a name that you choose.
If you want to save that buffer, just hit C-x w to create the file with its contents to a desired location.
My recommendation is that you give org-mode a try. It will do what want among a million other things.
Give it a name. Just don't save it.
Not what you wanted to hear, but this is the Emacs way.
Use C-x C-f, giving the (to-be-file-visiting) buffer a name. Edit the text. Do not use C-x C-s to save the buffer to the file (i.e., to disk).
Note that you can first put yourself in a directory whose contents you don't care about, so that if you accidentally do save the buffer there then you can easily find, recognize and toss the file. To change directories, you can use M-x cd. Or just do it by editing the directory part when you first use C-x C-f. Or use C-x d to put yourself in a Dired buffer for the directory.
If you don't want to take the chance of accidentally hitting C-x C-s and thus saving your edits, then use C-x b instead of C-x C-f. You are (even here) prompted for the buffer name. Giving it a new name (not the name of an existing buffer) creates a new buffer. In this case, if you use C-x C-s then Emacs prompts you for the file location to save the buffer in.
Why would you want to use C-x C-f instead of C-x b, if you might not want to save the buffer? Providing a file extension in the file name you give automatically puts the buffer in the proper major mode (typically). Otherwise (for C-x b) you need to put the buffer in the mode you want.
Buffer *scratch* is by default in Lisp-Interaction mode, which is similar to Emacs-Lisp mode (but not the same).
In every other text editor or word processor the intuition is to create a “new file” or a “new buffer”, not to switch to idiosyncratic *scratch* buffer. For example, you write quick notes or thoughts in several different buffers to keep trace of them—later you decide if you throw them away or save them. Or you manipulate a snippet of text or code, but you don't want to change the original buffer, so you just copypaste it to a new temporary buffer.
*scratch* is set to Lisp Interaction mode, but if I want to quickly evaluate some Elisp code, I could eval it running eval-expression (Alt+:) or in Elisp interpreter IELM (Alt+x Enter ielm). Also, if you close *scratch* buffer, it doesn't ask you to save it, so you can accidentally lose all your work. Drew's traditional solution seems too sub-optimal. And I don't buy the argument that “this is how you do it in Emacs”. Emacs is a customizable editor, so you can and should create whatever workflow is comfortable for you.
That's how ErgoEmacs solves it, buy creating a new-empty-buffer command. You can implemented like this:
(defun new-empty-buffer ()
"Opens a new empty buffer."
(interactive)
(switch-to-buffer (generate-new-buffer "untitled"))
(funcall initial-major-mode)
(put 'buffer-offer-save 'permanent-local t)
(setq buffer-offer-save t))
The variable buffer-offer-save resets every time you change a major mode, therefore you need to annotate it with permanent-local. It also prompts only when you exit Emacs. I think it is intuitive for it to also ask, when you close a modified untitled buffer, therefore see my solution on how to upgrade kill-buffer to prompt before closing a modified buffer.
ErgoEmacs revamps the default keybindings completely and has new-empty-buffer bound to Ctrl+N, like in almost all software. Change variable initial-major-mode if you want the new buffer to have another mode on start.
See also:
Emacs: Problems of the Scratch Buffer # Xah Lee
Emacs: New Empty Buffer # Xah Lee
You can try the following snippet, just add it to your .emacs
(defun new-file-tmp()
"Create a new empty file."
(interactive)
(let ((buf (generate-new-buffer "untitled")))
(switch-to-buffer buf)
(put 'buffer-offer-save 'permanent-local t)
(setq buffer-offer-save t)))
(defun tool-bar-local-item-pre (icon def key map after_item &rest props)
"Add an item to the tool bar in map MAP.
ICON names the image, DEF is the key definition and KEY is a symbol
for the fake function key in the menu keymap. Remaining arguments
PROPS are additional items to add to the menu item specification. See
Info node ‘(elisp)Tool Bar’. The item is added after AFTER_ITEM.
ICON is the base name of a file containing the image to use. The
function will first try to use low-color/ICON.xpm if ‘display-color-cells’
is less or equal to 256, then ICON.xpm, then ICON.pbm, and finally
ICON.xbm, using ‘find-image’."
(let* ((image-exp (tool-bar--image-expression icon)))
(define-key-after map (vector key)
`(menu-item ,(symbol-name key) ,def :image ,image-exp ,#props) after_item)
(force-mode-line-update)))
(when (boundp 'tool-bar-map)
(tool-bar-local-item-pre "new" 'new-file-tmp 'new-file-tmp tool-bar-map
'new-file :label "" :help "New untitled File")
(define-key tool-bar-map (vector 'new-file) nil)
;; comment the above line if you want to keep the button for the default behavior
)
(global-set-key (kbd "C-n") 'new-file-tmp)
It defines a new command which creates new empty buffers
Then the code binds it to the top left button of the toolbar, if you are in gui mode, and to the Control-n shortcut.
You can check the post on my site about it.
disclaimer: it's my site
A buffer and a file are not the same thing.
terminology
Regarding buffers,
The text you are editing in Emacs resides in an object called a
buffer. Each time you visit a file, a buffer is used to hold the file's text.
Regarding files,
The operating system stores data permanently in named files, so most
of the text you edit with Emacs comes from a file and is ultimately
stored in a file.
Buffers and files are related through visiting,
Visiting a file means reading its contents into an Emacs buffer so you
can edit them. Emacs makes a new buffer for each file that you visit.
answering the question
Unless I'm mistaken, technically speaking, your question, as written, can't be answered. Pedantically speaking, there's no such thing as (or little practical use for) an unnamed file. A file is a handle for something stored on disk. If you have no handle, then why make a file?
The question can then be interpreted as having two possible meanings1.
1. Making a new buffer (without regard to name)
A new buffer must have a name. An unsaved buffer can be saved to file with a given name using write-file (C-x C-w). You will be prompted for a path/name. Once written, the buffer update to be visiting the file you just wrote.
Since the name of the buffer doesn't matter (until you write it to file), here's a function which creates buffers named *scratch1*, *scratch2*, ... .
(defun create-scratch-buffer ()
"Create a new numbered scratch buffer.
Taken from URL `https://stackoverflow.com/a/21058075/5065796' "
(interactive)
(let ((n 0)
bufname)
(while (progn
(setq bufname (concat "*scratch"
(if (= n 0) "" (int-to-string n))
"*"))
(setq n (1+ n))
(get-buffer bufname)))
(switch-to-buffer (get-buffer-create bufname))
(org-mode)
(if (= n 1) initial-major-mode)))
2. Making a new file (without regard to name)
As hinted at in the new buffer solution, new files can be created with write-file.
When called interactively (M-x make-random-file), this function prompts for a directory. It then writes an empty file named something random like tmp-17388716387615.txt.
(defun make-random-file (dir)
"Make a random file in DIR."
(interactive "DDir: ")
(let* ((filename (concat "tmp-" (int-to-string (random t)) ".txt"))
(filepath (concat dir filename)))
(write-region "" nil filepath nil 'silent nil 'excl)))
As always, if the code above doesn't make sense, look at the documentation for functions and variables with C-h f and C-h v, respectively. And/or read the Introduction to Programming in Emacs Lisp.
1 It was the second meaning which I was searching for solutions to when I found this as the top search engine hit. Apologies if this was a bit obtuse. Figured it was better to share my solutions with y'all than not. :)

in Emacs, how to maintain a list of recent directories?

In Emacs, I use recentf extensively. Rather than calling find-files, I usually call a custom function xsteve-ido-choose-from-recentf instead which allows me to choose from my recentf files.
How do I create and maintain a separate list of recent directories, separate from the list of recent files? So that instead of calling dired, I could call something like ido-choose-from-recent-directories?
You ask, in your comment replying to #Stefan's answer: And how do I get from the above to viewing a list of recent directories? -
The answer is that you use the little-known fact that if the DIRNAME argument to dired is a list of (a) the new Dired buffer name followed by (b) file (or directory) names, then Dired is opened for just those files/dirs. IOW:
M-: (dired (cons DIRED-BUFNAME YOUR-LIST-OF-RECENT-DIRECTORIES))
For example:
M-: (dired '("My Dired Buffer" "/a/recent/dir/" "/another/recent1/" "/another/"))
If you use library Dired+ then you can provide such a list interactively, by using a non-positive prefix arg with dired.
But in this case you want to write a command that first gathers the list of recent directories and then opens Dired for them. This should do it:
(defun dired-recent (buffer)
"Open Dired in BUFFER, showing the recently used directories."
(interactive "BDired buffer name: ")
(let ((dirs (delete-dups
(mapcar (lambda (f/d)
(if (file-directory-p f/d)
f/d
(file-name-directory f/d)))
recentf-list))))
(dired (cons (generate-new-buffer-name buffer) dirs))))
That works for me. However, vanilla Emacs does not let you use i to insert the listing for any directory that is not in the same directory tree as the default-directory of the Dired buffer. That means that the above code will work OK, but you will not be able to insert any of the listed directories.
To be able to do that, load library dired+.el. Dired+ also fixes a couple of other inadequacies wrt the vanilla handling of a cons arg to dired.
The above code, together with Dired+ should give you what you want.
UPDATE
I just added this to Dired+. These are the commands added: diredp-dired-recent-dirs and diredp-dired-recent-dirs-other-window.
UPDATE 2
I made it simple to choose which of the recently used directories to include or exclude. Use a prefix arg to initiate such choosing. With no prefix arg you get all recent dirs. I also made it possible to use a prefix arg to be prompted for the ls switches. Here is the doc s tring of diredp-dired-recent-dirs:
Open Dired in BUFFER, showing recently used directories.
You are prompted for BUFFER.
No prefix arg or a plain prefix arg (`C-u', `C-u C-u', etc.) means
list all of the recently used directories.
With a prefix arg:
* If 0, `-', or plain (`C-u') then you are prompted for the `ls'
switches to use.
* If not plain (`C-u') then:
* If >= 0 then the directories to include are read, one by one.
* If < 0 then the directories to exclude are read, one by one.
When entering directories to include or exclude, use `C-g' to end.
Finally, I added bindings for the commands: C-x R (same window) and C-x 4 R (other window), where R is Shift + r.
You don't need to maintain a separate list (which would be a lot of work). Instead, you can extract that list from the recentf list. E.g.
(delete-dups
(mapcar (lambda (file)
(if (file-directory-p file) file (file-name-directory file)))
recentf-list))
Pragmatic Emacs found the solution.
Here is a function to give you a list of recent directories, using ivy
(part of swiper) to narrow it dynamically, and then open the selected
one in dired.
;; open recent directory, requires ivy (part of swiper)
;; borrows from http://stackoverflow.com/questions/23328037/in-emacs-how-to-maintain-a-list-of-recent-directories
(defun bjm/ivy-dired-recent-dirs ()
"Present a list of recently used directories and open the selected one in dired"
(interactive)
(let ((recent-dirs
(delete-dups
(mapcar (lambda (file)
(if (file-directory-p file) file (file-name-directory file)))
recentf-list))))
(let ((dir (ivy-read "Directory: "
recent-dirs
:re-builder #'ivy--regex
:sort nil
:initial-input nil)))
(dired dir))))
(global-set-key (kbd "C-x C-d") 'bjm/ivy-dired-recent-dirs)
Source:
Open a recent directory in dired: revisited | Pragmatic Emacs

How do I find-file recursively in directories

Often, I know the file name but don't remember or find it cumbersome to specify the exactly directory. I'd like to be able to do:
find-file x/y/*/some_file.txt
where * would allow searching in recursive directories instead of just the current one.
As described here:
M-x find-name-dired
After you provide the directory and the pattern the matching files will be displayed in a Dired buffer. You can navigate the file list (C-n, C-p, etc.) and open files as you wish (Ret).
I've long looked for that feature and I'm now satisfied with what I found: I'm using helm-projectile.
Projectile is a project interaction library. Projects are VCS directories or directories containing a .projectile file. It is based on GNU find (but offers a pure emacs lisp implementation too)
https://github.com/bbatsov/projectile
(you can install it with packages.el). You can use it alone: call projectile-find-file (or projectile-mode and C-c p f). It uses ido for the completion by default, but in that case I prefer the interface of helm-projectile.
My colleagues kept telling me to use Sublime Text because of that feature. Fortunately, I found projectile :)
ido-find-file does this: when the filename that you enter doesn't match
anything in the current directory, it uses something like locate to match
what you wrote against all files on your system.
The setup is just one line:
(ido-mode)
After this, C-x C-f will call ido-find-file.
Also, I usually add:
(setq ido-enable-flex-matching t)
because I like it, but with it you will locate more results,
i.e. foo will also match froo.
In case of files in a project directory, I have used Fiplr that uses a fuzzy search library by the same author.
https://github.com/d11wtq/fiplr
This is available from MELPA.
Other solution that I haven't tried is https://github.com/technomancy/find-file-in-project
edit: I found much better with projectile, see my other answer.
There is find-recursive.el : http://www.webweavertech.com/ovidiu/emacs/find-recursive.txt
Download it, put it in your load path and require it in your .emacs: (it isn't available through ELPA or el-get)
(require 'find-recursive)
now when you call M-x find-recursive, it will ask for a file name to search for recursively, a base directory, then you have to select one file among a result list. It isn't integrated into ido unfortunately, but I like it, it is useful sometimes.
Here is a solution that gives you selectivity over the files and subdirectories searched. To use it you need both Dired+ and Icicles.
The command, used only in Dired, is icicle-visit-marked-file-of-content-recursive (or the -other-window version of it). It is bound to M-+ C-F (for other window, use M-+ C-O -- and yes, those are uppercase F and O).
What it does:
It provides, as candidates for completion, all of the files marked in the current Dired directory, pslus all of those in any marked subdirectories that have Dired buffers, and so on recursively.
You can visit any number of such candidates that match your minibuffer input, including all of them. You can visit some that match a minibuffer input pattern, then change the pattern to match and visit others --- all in the same command invocation.
"Matching" your input can mean either or both:
Matching the file name
Matching the file content
That is, your input can be two-part (separated by hitting C-M-j: a file-name part and a file-content part. Narrow the choices by matching file names, and narrow further by matching text in those files. This search is fast -- it succeeds as soon as it finds a single match in the file.
You can omit either part: match only file names (fast) or only file contents (slower, naturally).
You can combine input patterns -- progressive completion. It is much easier to add additional patterns to match, incrementally and interactively, than it is to try to come up with a single regexp that does just what you want.
Each input pattern (each part of a two-part pattern) can be a regexp. Fuzzy matching is also available.
You can alternatively just insert subdirectories in your Dired buffer, instead of having them as separate Dired buffers. Any files and subdirs marked in a Dired buffer are handled the same way.
If you do not want to descend into subdirs that are marked, you can use the non-recursive version of the command, icicle-visit-marked-file-of-content, bound to C-F (C-O for other-window).
If you also use Bookmark+ then you can bookmark a Dired buffer or a set of Dired buffers. The bookmark records which files and subdirectories are marked, which subdirectories are inserted, and which files and subdirectories are omitted. Jumping to such a bookmark restores all of these things. This gives you a way to snapshot a project, which you can then search using M-+ C-F etc.
A lot of options listed, I'm using next command:
find-file-in-current-directory
You can put it in your .emacs like this:
;; Find file in current directory:
(global-set-key (kbd "C-M-,") 'find-file-in-current-directory)
Interface looks very familiar to those who use fsf tools (https://github.com/junegunn/fzf)
If you have helm, you can use this:
(defun my/helm-find-file-recursively ()
"Recursively find files in glob manner, in the specified directory."
(interactive)
(helm-find 'ask-for-dir))
(global-set-key (kbd "C-c o f") 'my/helm-find-file-recursively)
Icicles command icicle-locate (or icicle-locate-file, if you do not have a locate program for your OS) is made for that. Locate a file anywhere under a given directory (including ~HOME or the root directory).
You can match against any parts of the absolute file name (i.e., the path and the file name). You can use several kinds of matching, including regexp, substring, and fuzzy matching.
For a more programmatic approach (using the built-in files library):
(directory-files-recursively "~/assignments/" "[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}.org")
;; ("~/assignments/a/b/2022-05-18.org"
;; "~/assignments/c/d/2022-07-15.org"
;; "~/assignments/e/f/2022-08-08.org")
Here's a thin wrapper around that function which reads a filename from the minibuffer and returns a list of matching directories (starting from the current directory), from which you select an entry which will then be visited:
(defun my/find-directory ()
"Find the directory containing FILE."
(interactive)
(let ((matches (directory-files-recursively default-directory (read-from-minibuffer "Filename: "))))
(if matches
(find-file
(completing-read
"Pick a directory: "
(mapcar 'file-name-directory matches))))
(message "No matching directories.")))

Emacs: Set tab indent for just one file on the fly

I work on an open source project where the creator sets his tab-indents to 2 spaces.
I'd like to just enable it on the fly for the one file I work on and not other files of the same type. There must be something like M-x set-tab-indent. It is a JavaScript file ending in .js.
I know I can use:
(setq-default tab-width int)
inside my .emacs file, but I rather just call an M-x command to set it and forget it during my duration of working on this file. I tried M-x apropos and Google but couldn't find the specific command.
Thanks.
You can make the variable js-indent-level local to the buffer using:
M-x make-variable-buffer-local <RET> js-indent-level <RET>
Then you can set that variable in the buffer using:
M-x set-variable <RET> js-indent-level <RET> 2
The easiest way to do this for a single buffer is to use M-x set-variable.
Type M-x set-variable and press enter
When prompted for the variable to set, set tab-width then press enter
You'll be prompted with the line Set tab-width (buffer-local) to value:.
Put the value you want, then hit enter
The buffer should instantly be updated with the new value.
You could also use file local variables to automate omrib's solution for that one file, by adding this to it:
// Local Variables:
// js-indent-level: 2
// indent-tabs-mode: nil
// End:
Create a file ".dir-locals.el" in the project's directory and fill it like this:
((nil . ((tab-width . 2))))
This will take care of setting tab-width automatically and you don't have to modify the actual file (which is likely version-controlled.)
See the manual for more information about the format. I believe this requires Emacs 23.
As indicated by others, one issue with the File Local Variables approach is that you need to modify the file, and that's not ideal if you need to keep those declarations out of version control.
If you want the variables to apply to all files under a given directory, then Directory Local Variables is obviously the way to go, and you can implement that with either a .dir-locals.el file, or by calling (dir-locals-set-directory-class):
http://www.emacswiki.org/emacs/DirectoryVariables
http://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html
I prefer the directory class approach myself, and I was thinking that it's a shame that there isn't an analogous approach for file local variables, but I found that the directory class code actually works perfectly with files, and the only issue is that dir-locals-set-directory-class calls file-name-as-directory on its argument, which prevents it from being matched, due to the trailing slash.
The following therefore is a way to configure directory local variables for a single file, without modifying the file itself, or affecting other files under the same parent directory.
(defun my-file-locals-set-directory-class (file class &optional mtime)
"Enable 'directory local' classes for individual files,
by allowing non-directories in `dir-locals-directory-cache'.
Adapted from `dir-locals-set-directory-class'."
(setq file (expand-file-name file))
(unless (assq class dir-locals-class-alist)
(error "No such class `%s'" (symbol-name class)))
(push (list file class mtime) dir-locals-directory-cache))
(dir-locals-set-class-variables
'my-javascript-class
'((nil . ((js-indent-level . 2)
(indent-tabs-mode . nil)))))
(my-file-locals-set-directory-class
"path/to/the/file.js" 'my-javascript-class)
I use a snippet of code in my init.el that tries to auto-detect files that use 2-space indents, and switch Emacs's indentation for that file to 2 spaces when it sees such files:
(add-hook 'js-mode-hook
(lambda ()
(when (string-match-p "^ [A-Za-z]" (buffer-string))
(make-variable-buffer-local 'js-indent-level)
(set-variable 'js-indent-level 2))))

How to make emacs stay in the current directory

When I start working on a project in emacs, I use M-x cd to get into the project root directory. But every time I use C-x C-f to open a file in one of the subdirectories (like app/model/Store.rb) emacs changes current directory to that of the file. Is there a way to make emacs stay at the root?
How about this? It replaces the regular find-file command with your own which always starts in some "root" directory (customize the find-file-root-dir variable):
(defvar find-file-root-dir "~/"
"Directory from which to start all find-file's")
(defun find-file-in-root ()
"Make find-file always start at some root directory."
(interactive)
(let ((default-directory find-file-root-dir))
(call-interactively 'find-file)))
(global-set-key (kbd "C-x C-f") 'find-file-in-root)
Assuming that you want the working directory of a file to be set to whatever the working directory was before you executed find-file, you could try the following:
(defmacro disallow-cd-in-function (fun)
"Prevent FUN (or any function that FUN calls) from changing directory."
`(defadvice ,fun (around dissallow-cd activate)
(let ((old-dir default-directory) ; Save old directory
(new-buf ad-do-it)) ; Capture new buffer
;; If FUN returns a buffer, operate in that buffer in addition
;; to current one.
(when (bufferp new-buf)
(set-buffer new-buf)
(setq default-directory old-dir))
;; Set default-directory in the current buffer
(setq default-directory old-dir))))
Armed with this macro, go search for operations that set the variable default-directory: M-x find-library files; M-x occur (setq default-directory. After some investigation, you discover that the desired function is called find-file-noselect-1. Also, it looks like set-visited-file-name is also a candidate. So:
(disallow-cd-in-function find-file-noselect-1)
(disallow-cd-in-function set-visited-file-name)
Note
Note that (disallow-cd-in-function find-file) would work just fine, but then if you switched to ido-mode, you'd be opening files with ido-find-file instead of find-file. Both of these functions ultimately use find-file-noselect-1, so hitting that with the macro is a more univeral solution.
Is there a way to make emacs stay at the root?
No, there isn't. C-x C-f always visits starting from the default directory of the buffer you are already vising. The default directory, by default, is the same directory as the file. You can change these (separately for every buffer) using M-x cd.
But that is not what you want. What you should do is C-x b to *scratch* (whose default directory is the same as where you launched Emacs from -- in your words "root"), and then visit a new file. And if you need to do this frequently, just open up a dired in there and work your way thru.
I appreciate I'm not answering your question directly, but I noticed you were more specific in your requirements in one of your comments: "I don't use compile or recompile, I just tend to close files I am not working on, since it takes fewer keystrokes to open a file again".
Have you got ido turned on for buffer switching? If you exclude the directory thing for a moment, switching files or buffers with ido is an identical number of keystrokes (C-x C-f vs C-x b, followed by a few characters in the file name). If you include the directory thing, switching files is more tricky for the precisely the reasons you mention. Sticking with buffers is much easier.
Going a step further, with the help of 'anything.el' it's quite easy to abstract away whether a given file is in a buffer or in a file using the file cache. For example, if you do the following:
(file-cache-add-directory-recursively "/my/ruby/project") ".*\\.rb$")
and run 'anything-for-files' (I have it bound to C-x f) all your open buffers are listed, along with all of the files you've just added to the file cache; isolating a given file usually only takes one or two more characters.
Any file in your project is thus 4 or 5 key presses away, and the directory they are in or whether or not they are in a buffer becomes irrelevant.
Hope that's helpful...
Sorry I haven't worked out the details, but you might be able to add a function to find-file-hook that resets the default directory to whatever you want.