-
Notifications
You must be signed in to change notification settings - Fork 0
/
pel-file.el
674 lines (622 loc) · 29.1 KB
/
pel-file.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
;;; pel-file.el --- File Management utilities -*-lexical-binding: t-*-
;; Copyright (C) 2020, 2021, 2022, 2023, 2024 Pierre Rouleau
;; Author: Pierre Rouleau <prouleau001@gmail.com>
;; This file is part of the PEL package
;; This file is not part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;; -----------------------------------------------------------------------------
;;; Commentary:
;;
;; This file holds the logic to extract a file name or URL from the point
;; location, and then open the file inside a specified Emacs window or the URL
;; inside an external process frame.
;;
;; The point can be located anywhere inside the file or URL in all
;; cases except when the file name includes embedded spaces. To
;; extract a file name with embedded spaces, the file name must be
;; enclosed with double quotes and the point must be located on the
;; opening quote. Line and column numbers are also supported.
;;
;; The file is opened inside a specific window if it is specified by numeric
;; argument. If a window already contains the file and no argument specify
;; where to open the file, just move point to that window. The numeric argument
;; identify a cardinal direction to the target window. If the pointed window is
;; the minibuffer or a dedicated window the command fails.
;;
;; Credits: The pathstop string with Unicode block characters
;; originally borrowed from Xah Lee's xah-open-file-at-cursor
;; function at his web site:
;; `http://ergoemacs.org/emacs/emacs_open_file_path_fast.html'.
;; Implementation call hierarchy
;; -----------------------------
;;
;; * pel-show-filename-at-point (A)
;; - pel-filename-at-point
;; - pel-string-at-point
;;
;;
;; * pel-show-filename-parts-at-point (A)
;; * pel-find-file-at-point-in-window (A)
;; - pel-filename-parts-at-point
;; - pel--complete-filename-for
;; - pel--lib-filename
;; - pel-prompt-for-filename
;; - pel--show-edit-action
;;
;; -----------------------------------------------------------------------------
;;; Dependencies:
(require 'pel--base) ; use: pel-val-or-default,
; pel-goto-position,
; pel-system-is-windows-p
(require 'pel-prompt) ; use: `pel-prompt-select-read'
(require 'pel-read) ; use: pel-string-at-point
(require 'pel-window) ; use pel-window-direction-for
;; ; pel-window-valid-for-editing-p
(require 'pel-filex) ; use: `pel-open-in-os-app'
(eval-when-compile (require 'subr-x)) ; use: inlined: string-trim
;; -----------------------------------------------------------------------------
;;; Code:
;; pel-find-file-at-point-in-window
;; --------------------------------
(defun pel--dir-name-if (path-name directory-only)
"Return PATH-NAME complete unless DIRECTORY-ONLY is non-nil.
In that case return the directory part of PATH-NAME."
(if directory-only
(file-name-directory path-name)
path-name))
(defun pel-filename-parts-at-point (&optional keep-file-url directory-only)
"Extract and return (filename line column) from string at point.
A file URL is stripped from the string unless KEEP-FILE-URL is non-nil.
If DIRECTORY-ONLY is non-nil the directory name is extracted instead of the
complete file name.
.
Return:
- nil if no valid file name found at point.
- a (\\='http . url-string) cons where url-string is the URL.
- a list of (name-type filename line column) if a file name is present.
where:
- name-type := symbol : fname | fname-w-ddrv | http
: \\='fname := normal file name, with/without Unix-style path
: \\='fname-w-ddrv := filename with disk drive letter
- filename : filename string (or its directory if DIRECTORY-ONLY is non-nil).
- line : integer, or nil for missing number. Starts at 1.
- column : integer, or nil for missing number. Starts at 0.
The function accepts Unix and Windows style file names and path.
It accepts ':' and '@' as separators between the elements.
Spaces are accepted within the file name and between the separators
but *only* when the complete string is enclosed in double quotes
*and* when point is located at the first quote."
(let ((str (pel-filename-at-point)))
(unless keep-file-url
(setq str (replace-regexp-in-string "^file:////?/?" "/" str)))
;; first check for web URIs and return them.
(if (string-match-p "\\`https?://" str)
(cons 'http str)
(if (string-match-p "\\`file://" str)
(cons 'http str)
;; - Regexp provides ability to match with and without line and columns
;; - If line is present it's a number between 2 separators, with each
;; each separator being either ':' or '@' with optional spaces.
;; - If column is present it follows the second separator. The column
;; group can hold any alphanumeric and some punctuation characters.
;;
;; Prior to processing the string, the potential file URI prefix
;; is removed:
;; "file:////?/?" optional prefix is removed.
;; The standard allows 3 slashes but some people use
;; 4 or 5, so they are removed too.
;;
;; The overall structure of the regexp identifies the following groups:
;; G1?G2((G5 G6)(G8 G9)?)?
;; where:
;; - G1 := MS-DOS/Windows disk-drive letter and colon. nil if absent.
;; - G2 := filename
;; - G6 := line number string (only digits), if it exists (it can be nil)
;; - G9 := column field. It may be any text, may start with number,
;; maybe nil.
;; The numbers are extracted with string-to-number which return 0
;; if it's text.
;; Note: The regexp is split to fit in 80 columns: the G-g identify
;; the beginning and end of a group in the expression that spans
;; 3 lines.
(if (string-match
;;
;;
(concat
;; G1 g1 G2 g2 G3 G4 G5 g5
;; (-----------) (--------) ( ( (----------)
"^\\`\\([a-zA-Z]:\\)?\\([^:@]+?\\)\\(\\(\\( *[:@] *\\)"
;; G6 g6 g4 G7 G8
;; (---------) ) ( (----------)
"\\([0-9]+?\\)\\)\\(\\( *[:@] *\\)"
;; G9 g7 g3
;; (---------------------) ) )
"\\([[:alnum:] ,:;\\.]+\\)\\)\\)?\\'")
str)
(let* ((ddrv_str (match-string 1 str))
(fpath_str (concat ddrv_str (match-string 2 str)))
;; line to 0 if no line in str.
(match6 (match-string 6 str))
(line_num (if match6 (string-to-number match6)))
;; change line 0 to line 1
(line_num (if (equal line_num 0) 1 line_num))
;; column to nil if no line or column in str.
(match9 (match-string 9 str))
(col_num (when (and match6 match9)
(string-to-number match9))))
(list (if ddrv_str 'fname-w-ddrv 'fname)
(or (pel--dir-name-if fpath_str directory-only)
default-directory)
line_num
col_num))
;; For reasons I don't yet understand, the above regexp does not work
;; if only one separator; with line number follows the file name.
;; So, I try again, with a different regexp, not looking
;; for a column.
(if (string-match
;; G1 G2 G3 G4
;; (-----------) (---------) ( ) (---------)
"^\\`\\([a-zA-Z]:\\)?\\([^:@]+?\\)\\(:\\)\\([0-9]+?\\)\\'"
str)
(let* ((ddrv_str (match-string 1 str))
(fpath_str (concat ddrv_str (match-string 2 str)))
;; line to 0 if no line in str.
(line_num (string-to-number (pel-val-or-default
(match-string 4 str) "")))
;; but change line 0 to line 1
(line_num (if (equal line_num 0) 1 line_num)))
(list (if ddrv_str 'fname-w-ddrv 'fname)
(pel--dir-name-if fpath_str directory-only)
line_num
nil))))))))
(defun pel-prompt-for-filename (default-filename)
"Prompt for a file name, with DEFAULT-FILENAME shown.
The DEFAULT-FILENAME must be a string or nil.
User can either accept the filename or modify it.
If the file does not already exist, a confirmation is requested.
Returns the filename string."
(unless default-filename
(setq default-filename ""))
;; read-file-name is flexible but I find it non-obvious. To get it to show
;; the filename, it seems to have to be placed in the DIR argument.
;; With it a single word (no path) will be interpreted as a file in the local
;; directory (which is what I want).
;; MUSTMATCH and PREDICATE are set to ensure the file exists. If it does not
;; the user must confirm and then the caller can create it.
(expand-file-name (read-file-name
;; PROMPT
"Open? (C-g to quit): "
;; DIR
default-filename
;; DEFAULT_FILENAME
nil
;; MUSTMATCH
'confirm
;; INITIAL
nil
;; PREDICATE
'file-exists-p)))
(defun pel--lib-filename (filename)
"Infer or prompt for library filename using incomplete FILENAME.
Return (filename . action), where:
- filename is the file to create or open, or nil if no file to handle.
- action is: \\='create | \\='edit | reason-for-nil-filename
nil if user gave up, otherwise return the file name to open."
(if (and (require 'pel-prompt nil :no-error)
(fboundp 'pel-y-n-e-or-l-p))
(let ((action
(pel-y-n-e-or-l-p
(format "\
File「%s」not found. Create it, edit name or find Library file? "
filename))))
(cond
((equal action 'yes)
(cons filename 'create))
;;
((equal action 'no)
(cons nil "Cancelled."))
;;
((equal action 'edit)
(let ((filename (pel-prompt-for-filename filename)))
(cons filename (if (file-exists-p filename)
'edit
'create))))
;;
((equal action 'findlib)
(when (and (require 'find-func)
(fboundp 'find-library-name))
(let ((filename (find-library-name (file-name-base filename))))
(cons filename (if (file-exists-p filename)
'edit
'create)))))))
(error "Function pel-prompt not loaded")))
(defvar-local pel-filename-at-point-finders nil
"List of functions to use to find file from name.
Each function in the list must:
- Accept one argument: a string representing the file name.
- Return a list of strings; each one must be the complete
absolute path of a found file.
If nothing is found the function must return nil.
The `pel-generic-find-file' is a good example of such a function.
The `pel-erlang-find-file' is another example, specific to the Erlang
programming language.
When several functions are provided, each function is tried in
turn. The first function that returns a list of string wins: the
search stops. If several files are listed the
caller (`pel--complete-filename-for') prompts the user for the
file to select.")
(defun pel--find-by-finders (filename)
"Find complete path of FILENAME using file finders if any.
File finders functions are identified by `pel-filename-at-point-finders'.
Return a list of path string of file found if any is found, otherwise return
nil."
(when pel-filename-at-point-finders
(let ((found nil)
(finders pel-filename-at-point-finders))
(while (and finders
(not found))
(setq found
(funcall (car finders) filename))
(setq finders (cdr finders)))
found)))
(defun pel--complete-filename-for (filename)
"Identify the complete file name for a potentially incomplete FILENAME.
Prompt the user if necessary. In some case the user may want to create a
new file with a specified filename.
Return: (filename . action)
where: - filename:= string or nil: the filename to act upon if not nil.
- action := \\='edit | \\='create | message-string
where the message string is returned with nil to describe why
we do not edit or create the file."
(if (file-exists-p filename)
(cons filename 'edit)
(let ((found-filenames (pel--find-by-finders filename)))
(if found-filenames
(if (> (length found-filenames) 1)
(cons (pel-prompt-select-read "Select file" found-filenames)
'edit)
(cons (car found-filenames) 'edit))
(let* ((filename.action (pel--lib-filename filename))
(selected-filename (car filename.action))
(selected-action (cdr filename.action)))
(cons (or selected-filename filename)
selected-action))))))
(defun pel--show-edit-action (action filename &optional line column target)
"Display message showing ACTION done on FILENAME at LINE/COLUMN or TARGET.
ACTION := symbol | string
FILENAME := string
LINE := integer | nil
COLUMN := integer | nil
TARGET := string | nil"
(message "%s %s%s%s" action
filename
(if (or line column target) " at " "")
(if target
(format "target: %s" target)
(format " %s%s"
(format "line:%d" (or line 1))
(if column (format "col:%d" column) "")))))
;;-pel-autoload
(defun pel-find-file-at-point-in-window (&optional n filename-filter)
"Open file/URL of name located at/around point in specified window.
.
Optional arguments:
- N identifies target window,
- FILENAME-FILTER, if specified, is a function that takes the extracted
filename and returns a potentially modified filename to use.
*Window selection:*
- Only effective for opening file. Ignored when opening a URL.
- If no argument is provided,
- If file is already open in an existing window, select that window.
- If file is not already opened in a window, select the window
according to the number number of windows in the current frame:
- 2 windows in frame: open in *other* window
- 1 window in frame: split window sensibly and open in new window.
- otherwise, open in current window.
- If a prefix numeric argument N is supplied, it identifies the location
of the target window:
- N < 0 := \\='new
- N = 0 := \\='other
- N = 1,3 or 7:= select the window according to the number number of windows
in the current frame:
- 2 windows in frame: open in *other* window
- 1 window in frame: split window sensibly and open in new
window.
- otherwise, open in current window.
- For N= 2, 4, 5, 6 or 8, select window pointed by what is pointed
by cursor positionned at the layout of numeric keypad:
- 8 := \\='up
- 4 := \\='left 5 := \\='current 6 := \\='right
- 2 := \\='down
- For N=9 := open the file in the system\\='s browser.
- For N>= 20 or N<=-20, open the directory identified by the path
inside the window or browser identified by the (abs N) - 20.
- Explicitly selecting the minibuffer window, a dedicated window
or a non-existing window is not allowed. Instead the command creates
a new window for the file.
.
*File/URL selection at point:*
If the string starts with `http:/' or `https:/' it is
identified as a URL. In that case a browser process is
launched to open the URL. If N is 9, open the file in a browser.
Otherwise the string is used as a file name.
- The file string can have line and column integer numbers
using the following sections: {filename}{sep}{line}{sep}{column}
where:
- {filename} is the filename with or without path and
extension. The filename can, but does not need to,
use a \"file:///\" RFC-3986 file URI prefix.
- {sep} is a separator, one of the \\=':\\=' or \\='@\\=' character
- {line} is an integer identifying a line inside the file.
If none is specified, point is moved at the first line.
If the line number is too large, point is moved at the last
line of the file.
- {column} is an integer identifying a column inside that line.
Its possible to specify a column number without a line number,
the line number is interpreted as being the first line.
- {filename} is the only mandatory component. The other sections
are optional, but they must be used in order.
- Space characters are allowed in the {filename} and {sep}
sections *only* when:
- the entire string is enclosed in double quotes
- point is located just before (at) the opening quote.
- If there are no space in the entire string, point can be
located anywhere.
.
*Opening a file:*
- If the identified filename corresponds to an existing
file-system file or a currently un-committed buffer, the
function opens a, or visit the already existing, buffer for
that file, in the selected window.
- If the file does not exist, the user is prompted to:
- edit (modify) the file name and then open that, or
- use the filename as is, create a new buffer for the
unmodified file name,
- search a library file corresponding to the identified
file name, or
- decline and quit.
If the user responds negatively the function displays a message
stating that nothing was opened. Otherwise it proceeds.
If the selection was to find a library file and nothing is found
the function prints an error message and quits.
.
When an action is taken the action and the URL or (potentially expanded)
filename is displayed in the echo area, showing line and column number if they
were specified."
(interactive "P")
;; - grab file name/URL at point
;; - if point is a URL, launch the system browser for it.
;; - otherwise, check if this filename is already in a buffer in a window
;; - select window:
;; - if N is nil and a buffer holds the file, check if a window is
;; currently displaying the buffer that holds the file. If so, use that
;; window. Otherwise, search for a window the normal way.
(let* ((n-value (prefix-numeric-value n))
(directory-only (cond
((>= n-value 20)
(setq n-value (- n-value 20))
t)
((<= n-value -20)
(setq n-value (+ n-value 20))
t)
(t nil)))
(use-browser (eq 9 n-value))
(fileparts (pel-filename-parts-at-point use-browser directory-only))
(file-kind (car fileparts)))
(cond
((eq file-kind 'http)
(browse-url (cdr fileparts))
(pel--show-edit-action "browse" (cdr fileparts)))
;; nothing found
((not file-kind)
(user-error "No valid filename/URL at point!"))
;; A filename string found at point. It might be incomplete.
;; If incomplete: complete it, prompt user if necessary.
;; Then check if filename is currently opened in a buffer
;; and if that buffer is in a window already.
;; At this point: fileparts := (kind filename line column)
(t
(let ((filename (cadr fileparts))
fn-action
target-regxp)
(when filename-filter
(let ((fname.target (funcall filename-filter filename)))
(setq filename (car fname.target))
(setq target-regxp (cdr fname.target))))
(setq fn-action (pel--complete-filename-for filename))
(setq filename (expand-file-name (car fn-action)))
(if use-browser
;; It's a file, not a URL, but user requested opening the file
;; inside the the default browser or the OS default application
;; for this type of file: use pel-open-in-os-app for that.
(pel-open-in-os-app filename)
(let* ((action (cdr fn-action))
(buffer (or (find-buffer-visiting filename)
(when (fboundp 'dired-buffers-for-dir)
(car (dired-buffers-for-dir filename)))))
(window (when buffer (get-buffer-window buffer)))
(line (caddr fileparts))
(column (cadddr fileparts)))
(if (and window (null n))
;; file is already in a buffer and window and position
;; is not imposed by argument n: use that existing
;; window and move point to where specified if any.
(progn
(select-window window)
(if target-regxp
(progn
(goto-char (point-min))
(re-search-forward target-regxp))
(pel-goto-position line column))
(pel--show-edit-action "show" filename
line column target-regxp))
;; the file is not inside a existing window,
;; but a buffer may hold the file.
;; Since find-file will open that buffer then
;; what is needed now is to determine what window to use
;; and open the file inside that window.
;; The filename might be absolute, relative, incomplete.
(let ((direction (pel-window-direction-for
n-value nil :for-editing)))
(cond
((eq action 'edit)
(progn
(pel-window-select direction)
(find-file filename)
(if target-regxp
(progn
(goto-char (point-min))
(re-search-forward target-regxp))
(pel-goto-position line column))
(pel--show-edit-action action filename
line column
target-regxp)))
((eq action 'create)
(progn
(pel-window-select direction)
(find-file filename)
(pel--show-edit-action action filename)))
((stringp action) (message "%s" action))
(t
(error "Internal error condition detected!"))))))))))))
;; --
;;-pel-autoload
(defun pel-show-filename-parts-at-point (&optional keep-file-url)
"Display file parts extracted from point. Testing utility."
(interactive "P")
(message "%S" (pel-filename-parts-at-point keep-file-url)))
;; -----------------------------------------------------------------------------
;; Show filename at point
;; ----------------------
(defun pel-filename-at-point ()
"Return the file name at point (or marked region).
Spaces inside the filename are accepted *only* when point is
located before a double quote to the left of the filename.
Spaces between the quote and the first character and last
character or the filename are accepted but removed. When
executed from with a buffer in sh-mode, the shell variables found
in the string are expanded and the delimiters include the '=' and
':' characters. This helps extracting file names in shell
scripts.
Variable name expansion:
- In shell and TCL mode buffers, perform $VAR
variable substitution in the file name.
That's useful for environment variables in file names.
Limitation: the file name delimiters currently used are
relatively safe but not sufficient for all cases. These will
probably have to be modified to be a user option in a future version. "
(if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(save-excursion
(let ((delimiters
"\t\n\"`'‘’“”|「」<>〔〕〈〉《》【】〖〗«»‹›❮❯❬❭〘〙·。"))
;; In shell modes, allow delimiting the filenames by path separators
;; and equal sign used in various statements.
(when (eq major-mode 'sh-mode)
(setq delimiters (concat "=:" delimiters)))
(unless (memq major-mode '(rst-mode markdown-mode))
(setq delimiters (concat "()[]{}" delimiters)))
(let ((fname (string-trim (pel-string-at-point delimiters))))
(when (memq major-mode '(sh-mode tcl-mode))
(require 'env nil :noerror)
(setq fname (substitute-env-vars fname)))
(if (string= (substring fname -1) ":")
(substring fname 0 -1)
fname))))))
;;-pel-autoload
(defun pel-show-filename-at-point ()
"Display file name at point in the mini-buffer."
(interactive)
(message "File name :=「%s」" (pel-filename-at-point)))
;;-pel-autoload
(defun pel-load-visited-file (&optional use-elc)
"Load the elisp file visited by current buffer.
Load the file source, not the byte-compiled version unless
the optional USE-ELC argument is specified.
Interactively use any prefix argument."
(interactive "P")
(let ((fn (pel-current-buffer-filename)))
(if (string= (file-name-extension fn) "el")
(let ((fn (file-name-sans-extension fn)))
(if use-elc
(load-file (concat fn ".elc"))
(load-file (concat fn ".el"))))
(user-error "Cannot load %s. It is not an Emacs Lisp file!" fn))))
;;-pel-autoload
(defun pel-open-file-in-other-dir ()
"Open file of same name as current one present in another directory.
First prompt with the name of the directory of currently visited
file using the default completion mechanism (`ido' by
default). Use the prompt to select the name of the other
directory (which must already exist). Use C-f to edit the
directory path without completion. Once the directory name is
selected hit Return to open the same file in the selected other
directory."
(interactive)
(let* ((filename (pel-current-buffer-filename))
(file-basename (file-name-nondirectory filename))
(file-dirname (file-name-directory filename))
(new-dirname
(expand-file-name
(read-file-name
"Other dir? (C-g to quit, C-f to edit: "
file-dirname
nil
'confirm
nil
'file-directory-p))))
(find-file (format "%s/%s" new-dirname file-basename))))
;; ---------------------------------------------------------------------------
(defconst pel-alternate-extension-alist '(("c" . "h")
("h" . "c")
("cc" . "hh")
("hh" . "cc")
("cpp" . "hpp")
("hpp" . "cpp")
("cxx" . "hxx")
("hxx" . "cxx"))
"Alternate file extensions for C and C++.")
(defun pel--alternate-extension-for (ext)
"Return alternate extension for EXT extension, a string.
Return a string if one is found, nil otherwise."
(cdr (assoc ext pel-alternate-extension-alist)))
;;-pel-autoload
(defun pel-open-file-alternate ()
"Open a file with same name but an alternate extension.
The new extension depends on the current file extension.
The list of alternate extensions is currently very limited
and restricted to C and C++.
If the alternate file is not found, save the file basename in the
kill ring and prompt for the file name to open.
This is very limited as it is. It will be improved later."
(interactive)
(let* ((fname (pel-current-buffer-filename))
(ext (pel-current-buffer-file-extension))
(bname (file-name-sans-extension fname))
(alt-ext (pel--alternate-extension-for ext)))
(if alt-ext
(let ((alt-fname (format "%s.%s" bname alt-ext)))
(if (file-exists-p alt-fname)
(find-file alt-fname)
;; On failure remember base name of file (without path) in kill
;; ring and prompt for the file.
(kill-new (file-name-nondirectory bname))
(ido-find-file)
;; remove that entry from kill ring (naive, should probably check
;; for the value and remove that: todo later)
(setq kill-ring (cdr kill-ring))))
(user-error "No alternate extension for %s" ext))))
;; -----------------------------------------------------------------------------
(provide 'pel-file)
;;; pel-file.el ends here