-;;; csv-mode.el --- major mode for editing comma-separated value files
+;;; csv-mode.el --- Major mode for editing comma/char separated values -*- lexical-binding: t -*-
-;; Copyright (C) 2003, 2004, 2012 Free Software Foundation, Inc
+;; Copyright (C) 2003, 2004, 2012, 2013 Free Software Foundation, Inc
;; Author: Francis J. Wright <F.J.Wright at qmul.ac.uk>
;; Time-stamp: <23 August 2004>
;; URL: http://centaur.maths.qmul.ac.uk/Emacs/
-;; Version: 1.0
+;; Version: 1.2
;; Keywords: convenience
;; This package is free software; you can redistribute it and/or modify
;;; Commentary:
-;; This package is intended for use with GNU Emacs 21 (only) and
-;; implements the following commands to process records of CSV
-;; (comma-separated value) type: `csv-sort-fields' and
-;; `csv-sort-numeric-fields' sort respectively lexicographically and
-;; numerically on a specified field or column; `csv-reverse-region'
-;; reverses the order. They are based closely on, and use, code in
-;; `sort.el'. `csv-kill-fields' and `csv-yank-fields' respectively
-;; kill and yank fields or columns, although they do not use the
-;; normal kill ring. `csv-kill-fields' can kill more than one field
-;; at once, but multiple killed fields can be yanked only as a fixed
-;; group equivalent to a single field. `csv-align-fields' aligns
-;; fields into columns; `csv-unalign-fields' undoes such alignment;
-;; separators can be hidden within aligned records. `csv-transpose'
-;; interchanges rows and columns. For details, see the documentation
-;; for the individual commands.
-
-;; CSV mode supports a generalised comma-separated values format
-;; (character-separated values) in which the fields can be separated
-;; by any of several single characters, specified by the value of the
-;; customizable user option `csv-separators'. CSV data fields can be
-;; delimited by quote characters (and must if they contain separator
-;; characters). This implementation supports quoted fields, where the
-;; quote characters allowed are specified by the value of the
-;; customizable user option `csv-field-quotes'. By default, the only
-;; separator is a comma and the only field quote is a double quote.
-;; These user options can be changed ONLY by CUSTOMIZING them,
-;; e.g. via the command `customize-variable'.
+;; This package implements CSV mode, a major mode for editing records
+;; in a generalized CSV (character-separated values) format. It binds
+;; finds with prefix ".csv" to `csv-mode' in `auto-mode-alist'.
+
+;; In CSV mode, the following commands are available:
+
+;; - C-c C-s (`csv-sort-fields') and C-c C-n (`csv-sort-numeric-fields')
+;; respectively sort lexicographically and numerically on a
+;; specified field or column.
+
+;; - C-c C-r (`csv-reverse-region') reverses the order. (These
+;; commands are based closely on, and use, code in `sort.el'.)
+
+;; - C-c C-k (`csv-kill-fields') and C-c C-y (`csv-yank-fields') kill
+;; and yank fields or columns, although they do not use the normal
+;; kill ring. C-c C-k can kill more than one field at once, but
+;; multiple killed fields can be yanked only as a fixed group
+;; equivalent to a single field.
+
+;; - C-c C-a (`csv-align-fields') aligns fields into columns
+
+;; - C-c C-u (`csv-unalign-fields') undoes such alignment; separators
+;; can be hidden within aligned records.
+
+;; - C-c C-t (`csv-transpose') interchanges rows and columns. For
+;; details, see the documentation for the individual commands.
+
+;; CSV mode can recognize fields separated by any of several single
+;; characters, specified by the value of the customizable user option
+;; `csv-separators'. CSV data fields can be delimited by quote
+;; characters (and must if they contain separator characters). This
+;; implementation supports quoted fields, where the quote characters
+;; allowed are specified by the value of the customizable user option
+;; `csv-field-quotes'. By default, the only separator is a comma and
+;; the only field quote is a double quote. These user options can be
+;; changed ONLY by customizing them, e.g. via M-x customize-variable.
;; CSV mode commands ignore blank lines and comment lines beginning
;; with the value of the buffer local variable `csv-comment-start',
"Regexp to match a field separator.
Set by customizing `csv-separators' -- do not set directly!")
-(defvar csv-skip-regexp nil
+(defvar csv--skip-regexp nil
"Regexp used by `skip-chars-forward' etc. to skip fields.
Set by customizing `csv-separators' -- do not set directly!")
"Font lock keywords to highlight the field separators in CSV mode.
Set by customizing `csv-separators' -- do not set directly!")
-(defcustom csv-separators '(",")
+(defcustom csv-separators '("," "\t")
"Field separators: a list of *single-character* strings.
For example: (\",\"), the default, or (\",\" \";\" \":\").
Neighbouring fields may be separated by any one of these characters.
The first is used when inserting a field separator into the buffer.
All must be different from the field quote characters, `csv-field-quotes'."
;; Suggested by Eckhard Neber <neber@mwt.e-technik.uni-ulm.de>
- :group 'CSV
:type '(repeat string)
;; FIXME: Character would be better, but in Emacs 21.3 does not display
;; correctly in a customization buffer.
value)
(custom-set-default variable value)
(setq csv-separator-chars (mapcar 'string-to-char value)
- csv-skip-regexp (apply 'concat "^\n" csv-separators)
+ csv--skip-regexp (apply 'concat "^\n" csv-separators)
csv-separator-regexp (apply 'concat `("[" ,@value "]"))
csv-font-lock-keywords
;; NB: csv-separator-face variable evaluates to itself.
- `((,csv-separator-regexp . csv-separator-face)))))
+ `((,csv-separator-regexp (0 'csv-separator-face))))))
(defcustom csv-field-quotes '("\"")
"Field quotes: a list of *single-character* strings.
For example: (\"\\\"\"), the default, or (\"\\\"\" \"'\" \"`\").
A field can be delimited by a pair of any of these characters.
All must be different from the field separators, `csv-separators'."
- :group 'CSV
:type '(repeat string)
;; Character would be better, but in Emacs 21 does not display
;; correctly in a customization buffer.
Such comment lines are ignored by CSV mode commands.
Default value of buffer-local variable `csv-comment-start'.
Changing this variable does not affect any existing CSV mode buffer."
- :group 'CSV
:type '(choice (const :tag "None" nil) string)
:set (lambda (variable value)
(custom-set-default variable value)
"Aligned field style: one of 'left, 'centre, 'right or 'auto.
Alignment style used by `csv-align-fields'.
Auto-alignment means left align text and right align numbers."
- :group 'CSV
:type '(choice (const left) (const centre)
(const right) (const auto)))
(defcustom csv-align-padding 1
"Aligned field spacing: must be a positive integer.
Number of spaces used by `csv-align-fields' after separators."
- :group 'CSV
:type 'integer)
(defcustom csv-header-lines 0
"Header lines to skip when setting region automatically."
- :group 'CSV
:type 'integer)
-(defcustom csv-invisibility-default nil
+(defcustom csv-invisibility-default t
"If non-nil, make separators in aligned records invisible."
- :group 'CSV
:type 'boolean)
(defface csv-separator-face
- '((((class color)) (:foreground "red"))
- (t (:weight bold)))
- "CSV mode face used to highlight separators."
- :group 'CSV)
-
-;; This mechanism seems to keep XEmacs happy:
-(defvar csv-separator-face 'csv-separator-face
- "Face name to use to highlight separators.")
+ '((t :inherit escape-glyph))
+ "CSV mode face used to highlight separators.")
\f
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Mode definition, key bindings and menu
(defconst csv-mode-line-format
;; See bindings.el for details of `mode-line-format' construction.
- (append (butlast (default-value 'mode-line-format) 2)
- (cons `(csv-field-index-string
- ("" csv-field-index-string
- ,(propertize "--" 'help-echo csv-mode-line-help-echo)))
- (last (default-value 'mode-line-format) 2)))
+ (let* ((ml (copy-sequence (default-value 'mode-line-format)))
+ (x (or (memq 'mode-line-position ml) (last 3 ml))))
+ (when x
+ (setcdr x (cons
+ `(csv-field-index-string
+ ("" csv-field-index-string
+ ;; ,(propertize "--" 'help-echo csv-mode-line-help-echo)
+ ))
+ (cdr x))))
+ ml)
"Mode line format string for CSV mode.")
(defvar csv-mode-map
buffer-invisibility-spec csv-invisibility-default
;; Mode line to support `csv-field-index-mode':
mode-line-format csv-mode-line-format)
+ (set (make-local-variable 'truncate-lines) t)
;; Enable or disable `csv-field-index-mode' (could probably do this
;; a bit more efficiently):
(csv-field-index-mode (symbol-value 'csv-field-index-mode)))
;; Must be run interactively to activate mark!
(let* ((arg current-prefix-arg) (default-field 1)
(region
- (if (and transient-mark-mode (not mark-active))
+ (if (not (use-region-p))
;; Set region automatically:
(save-excursion
- (let (startline lbp)
- (if arg
- (beginning-of-line)
- (setq lbp (line-beginning-position))
- (while (re-search-backward csv-separator-regexp lbp 1)
- ;; Move as far as possible, i.e. to beginning of line.
- (setq default-field (1+ default-field))))
- (if (csv-not-looking-at-record)
- (error "Point may not be within CSV records"))
- (setq startline (point))
+ (if arg
+ (beginning-of-line)
+ (let ((lbp (line-beginning-position)))
+ (while (re-search-backward csv-separator-regexp lbp 1)
+ ;; Move as far as possible, i.e. to beginning of line.
+ (setq default-field (1+ default-field)))))
+ (if (csv-not-looking-at-record)
+ (error "Point must be within CSV records"))
+ (let ((startline (point)))
;; Set mark at beginning of region:
(while (not (or (bobp) (csv-not-looking-at-record)))
(forward-line -1))
(barf-if-buffer-read-only)
(csv-sort-fields-1 field beg end
(lambda () (csv-sort-skip-fields field) nil)
- (lambda () (skip-chars-forward csv-skip-regexp))))
+ (lambda () (skip-chars-forward csv--skip-regexp))))
(defun csv-sort-numeric-fields (field beg end)
"Sort lines in region numerically by the ARGth field of each line.
(defsubst csv-end-of-field ()
"Skip forward over one field."
- (skip-syntax-forward " ")
+ (skip-chars-forward " ")
(if (eq (char-syntax (following-char)) ?\")
(goto-char (scan-sexps (point) 1)))
- (skip-chars-forward csv-skip-regexp))
+ (skip-chars-forward csv--skip-regexp))
(defsubst csv-beginning-of-field ()
"Skip backward over one field."
(skip-syntax-backward " ")
(if (eq (char-syntax (preceding-char)) ?\")
(goto-char (scan-sexps (point) -1)))
- (skip-chars-backward csv-skip-regexp))
+ (skip-chars-backward csv--skip-regexp))
(defun csv-forward-field (arg)
"Move forward across one field, cf. `forward-sexp'.
(defcustom csv-field-index-delay 0.125
"Time in seconds to delay before updating field index display."
- :group 'CSV
:type '(number :tag "seconds"))
(defvar csv-field-index-idle-timer nil)
Returns the new status of CSV-Field-Index mode (non-nil means on).
When CSV-Field-Index mode is enabled, the current field index appears in
the mode line after `csv-field-index-delay' seconds of Emacs idle time."
- :group 'CSV
:global t
:init-value t ; for documentation, since default is t
;; This macro generates a function that first sets the mode
(csv-kill-one-column (car fields)))))
(setq csv-killed-fields (nreverse csv-killed-fields)))
-(defmacro csv-kill-one-field (field killed-fields)
+(defun csv-kill-one-field (field)
"Kill field with index FIELD in current line.
-Save killed field by `push'ing onto KILLED-FIELDS.
-Assumes point is at beginning of line.
-Called by `csv-kill-one-column' and `csv-kill-many-columns'."
- `(progn
- ;; Move to start of field to kill:
- (csv-sort-skip-fields ,field)
- ;; Kill to end of field (cf. `kill-region'):
- (push (delete-and-extract-region
- (point)
- (progn (csv-end-of-field) (point)))
- ,killed-fields)
- (if (eolp) (delete-char -1) ; delete trailing separator at eol
- (delete-char 1)))) ; or following separator otherwise
+Return killed text. Assumes point is at beginning of line."
+ ;; Move to start of field to kill:
+ (csv-sort-skip-fields field)
+ ;; Kill to end of field (cf. `kill-region'):
+ (prog1 (delete-and-extract-region
+ (point)
+ (progn (csv-end-of-field) (point)))
+ (if (eolp)
+ (unless (bolp) (delete-char -1)) ; Delete trailing separator at eol
+ (delete-char 1)))) ; or following separator otherwise.
(defun csv-kill-one-column (field)
"Kill field with index FIELD in all lines in (narrowed) buffer.
Ignore blank and comment lines."
(while (not (eobp))
(or (csv-not-looking-at-record)
- (csv-kill-one-field field csv-killed-fields))
+ (push (csv-kill-one-field field) csv-killed-fields))
(forward-line)))
(defun csv-kill-many-columns (fields)
(setq field (car fields)
fields (cdr fields))
(beginning-of-line)
- (csv-kill-one-field field killed-fields))
+ (push (csv-kill-one-field field) killed-fields))
(push (mapconcat 'identity killed-fields (car csv-separators))
csv-killed-fields)))
(forward-line)))
;;; Aligning fields
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defun csv--column-widths ()
+ (let ((widths '()))
+ ;; Construct list of column widths:
+ (while (not (eobp)) ; for each record...
+ (or (csv-not-looking-at-record)
+ (let ((w widths)
+ (beg (point)) ; Beginning of current field.
+ x)
+ (while (not (eolp))
+ (csv-end-of-field)
+ (setq x (- (point) beg)) ; Field width.
+ (if w
+ (if (> x (car w)) (setcar w x))
+ (setq w (list x)
+ widths (nconc widths w)))
+ (or (eolp) (forward-char)) ; Skip separator.
+ (setq w (cdr w)
+ beg (point)))))
+ (forward-line))
+ widths))
+
(defun csv-align-fields (hard beg end)
"Align all the fields in the region to form columns.
The alignment style is specified by `csv-align-style'. The number of
re-aligning. \(Unaligning always makes separators visible.)
When called non-interactively, use hard alignment if HARD is non-nil\;
-BEG and END specify the region to align."
- (interactive (csv-interactive-args))
- (setq end (set-marker (make-marker) end))
- (csv-unalign-fields hard beg end) ; if hard then barfs if buffer read only
+BEG and END specify the region to align.
+If there is no selected region, default to the whole buffer."
+ (interactive (cons current-prefix-arg
+ (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (list (point-min) (point-max)))))
+ (setq end (copy-marker end))
+ (csv-unalign-fields hard beg end) ; If hard then barfs if buffer read only.
(save-excursion
(save-restriction
(narrow-to-region beg end)
(set-marker end nil)
(goto-char (point-min))
- (let (widths)
- ;; Construct list of column widths:
- (while (not (eobp)) ; for each record...
- (or (csv-not-looking-at-record)
- (let ((w widths) x)
- (setq beg (point)) ; Beginning of current field.
- (while (not (eolp))
- (csv-end-of-field)
- (setq x (- (point) beg)) ; Field width.
- (if w
- (if (> x (car w)) (setcar w x))
- (setq w (list x)
- widths (nconc widths w)))
- (or (eolp) (forward-char)) ; Skip separator.
- (setq w (cdr w)
- beg (point)))))
- (forward-line))
+ (let ((widths (csv--column-widths)))
;; Align fields:
(goto-char (point-min))
(while (not (eobp)) ; for each record...
- (or (csv-not-looking-at-record)
- (let ((w widths) (padding 0) x)
- (setq beg (point)) ; beginning of current field
- (while (and w (not (eolp)))
- (let ((left-padding 0) (right-padding 0) overlay)
- (csv-end-of-field)
- (set-marker end (point)) ; end of current field
- ;; FIXME: Don't assume length=string-width!
- (setq x (- (point) beg) ; field width
- x (- (car w) x)) ; required padding
-
- ;; beg = beginning of current field
- ;; end = (point) = end of current field
-
- ;; Compute required padding:
- (cond
- ((eq csv-align-style 'left)
- ;; Left align -- pad on the right:
- (setq left-padding csv-align-padding
- right-padding x))
- ((eq csv-align-style 'right)
- ;; Right align -- pad on the left:
- (setq left-padding (+ csv-align-padding x)))
- ((eq csv-align-style 'auto)
- ;; Auto align -- left align text, right align numbers:
- (if (string-match "\\`[-+.[:digit:]]+\\'"
- (buffer-substring beg (point)))
- ;; Right align -- pad on the left:
- (setq left-padding (+ csv-align-padding x))
- ;; Left align -- pad on the right:
- (setq left-padding csv-align-padding
- right-padding x)))
- ((eq csv-align-style 'centre)
- ;; Centre -- pad on both left and right:
- (let ((y (/ x 2))) ; truncated integer quotient
- (setq left-padding (+ csv-align-padding y)
- right-padding (- x y)))))
-
- (cond
- (hard
- ;; Hard alignment...
- (when (> left-padding 0) ; Pad on the left.
- ;; Insert spaces before field:
- (if (= beg end) ; null field
- (insert (make-string left-padding ?\ ))
- (goto-char beg) ; beginning of current field
+ (unless (csv-not-looking-at-record)
+ (let ((w widths)
+ (column 0)) ;Desired position of left-side of this column.
+ (while (and w (not (eolp)))
+ (let* ((beg (point))
+ (align-padding (if (bolp) 0 csv-align-padding))
+ (left-padding 0) (right-padding 0)
+ (field-width
+ ;; FIXME: Don't assume length=string-width!
+ (progn (csv-end-of-field) (- (point) beg)))
+ (column-width (pop w))
+ (x (- column-width field-width))) ; Required padding.
+ (set-marker end (point)) ; End of current field.
+ ;; beg = beginning of current field
+ ;; end = (point) = end of current field
+
+ ;; Compute required padding:
+ (cond
+ ((eq csv-align-style 'left)
+ ;; Left align -- pad on the right:
+ (setq left-padding align-padding
+ right-padding x))
+ ((eq csv-align-style 'right)
+ ;; Right align -- pad on the left:
+ (setq left-padding (+ align-padding x)))
+ ((eq csv-align-style 'auto)
+ ;; Auto align -- left align text, right align numbers:
+ (if (string-match "\\`[-+.[:digit:]]+\\'"
+ (buffer-substring beg (point)))
+ ;; Right align -- pad on the left:
+ (setq left-padding (+ align-padding x))
+ ;; Left align -- pad on the right:
+ (setq left-padding align-padding
+ right-padding x)))
+ ((eq csv-align-style 'centre)
+ ;; Centre -- pad on both left and right:
+ (let ((y (/ x 2))) ; truncated integer quotient
+ (setq left-padding (+ align-padding y)
+ right-padding (- x y)))))
+
+ (cond
+ (hard ;; Hard alignment...
+ (when (> left-padding 0) ; Pad on the left.
+ ;; Insert spaces before field:
+ (if (= beg end) ; null field
(insert (make-string left-padding ?\ ))
- (goto-char end))) ; end of current field
- (unless (eolp)
- (if (> right-padding 0) ; pad on the right
- ;; Insert spaces after field:
- (insert (make-string right-padding ?\ )))
- ;; Make separator (potentially) invisible;
- ;; in Emacs 21.3, neighbouring overlays
- ;; conflict, so use the following only
- ;; with hard alignment:
- (let ((ol (make-overlay (point) (1+ (point)) nil t)))
- (overlay-put ol 'invisible t)
- (overlay-put ol 'evaporate t))
- (forward-char))) ; skip separator
-
- ;; Soft alignment...
- ;; FIXME: Use (space :align-to ...) display property.
-
- (buffer-invisibility-spec ; csv-hide-separators
-
- ;; Hide separators...
- ;; Merge right-padding from previous field
- ;; with left-padding from this field:
- (setq padding (+ padding left-padding))
- (when (> padding 0)
(goto-char beg) ; beginning of current field
- (if (bolp)
- ;; Display spaces before first field
- ;; by overlaying first character:
- (overlay-put
- (make-overlay (point) (1+ (point)))
- 'before-string
- (make-string padding ?\ ))
- ;; Display separator as spaces:
+ (insert (make-string left-padding ?\ ))
+ (goto-char end))) ; end of current field
+ (unless (eolp)
+ (if (> right-padding 0) ; pad on the right
+ ;; Insert spaces after field:
+ (insert (make-string right-padding ?\ )))
+ ;; Make separator (potentially) invisible;
+ ;; in Emacs 21.3, neighbouring overlays
+ ;; conflict, so use the following only
+ ;; with hard alignment:
+ (let ((ol (make-overlay (point) (1+ (point)) nil t)))
+ (overlay-put ol 'invisible t)
+ (overlay-put ol 'evaporate t))
+ (forward-char))) ; skip separator
+
+ ;; Soft alignment...
+ (buffer-invisibility-spec ; csv-invisibility-default
+
+ ;; Hide separators...
+ ;; Merge right-padding from previous field
+ ;; with left-padding from this field:
+ (if (zerop column)
+ (when (> left-padding 0)
+ ;; Display spaces before first field
+ ;; by overlaying first character:
(overlay-put
- (make-overlay (1- (point)) (point) nil nil t)
- ;; 'face 'secondary-selection)) ; test
- ;; 'display (make-string padding ?\ )))
- ;; Above 'display mangles buffer
- ;; horribly if any string is empty!
- 'display `(space :width ,padding)))
- (goto-char end)) ; end of current field
- (unless (eolp)
- (setq padding right-padding)
- (forward-char))) ; skip separator
-
- (t ;; Do not hide separators...
+ (make-overlay beg (1+ beg))
+ 'before-string
+ (make-string left-padding ?\ )))
+ ;; Display separator as spaces:
+ (with-silent-modifications
+ (put-text-property
+ (1- beg) beg
+ 'display `(space :align-to
+ ,(+ left-padding column)))))
+ (unless (eolp) (forward-char)) ; Skip separator.
+ (setq column (+ column column-width align-padding)))
+
+ (t ;; Do not hide separators...
+ (let ((overlay (make-overlay beg (point) nil nil t)))
(when (> left-padding 0) ; Pad on the left.
;; Display spaces before field:
- (setq overlay (make-overlay beg (point) nil nil t))
(overlay-put overlay 'before-string
(make-string left-padding ?\ )))
(unless (eolp)
(if (> right-padding 0) ; Pad on the right.
;; Display spaces after field:
(overlay-put
- (or overlay
- (make-overlay beg (point) nil nil t))
+ overlay
'after-string (make-string right-padding ?\ )))
- (forward-char))) ; Skip separator.
-
- ))
+ (forward-char)))) ; Skip separator.
- (setq w (cdr w)
- beg (point)))))
+ )))))
(forward-line)))))
(set-marker end nil))
an argument then also remove all spaces and tabs around separators.
Also make all invisible separators visible again.
Ignore blank and comment lines. When called non-interactively, remove
-spaces and tabs if HARD non-nil\; BEG and END specify region to unalign."
- (interactive (csv-interactive-args))
+spaces and tabs if HARD non-nil\; BEG and END specify region to unalign.
+If there is no selected region, default to the whole buffer."
+ (interactive (cons current-prefix-arg
+ (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (list (point-min) (point-max)))))
;; Remove any soft alignment:
(mapc 'delete-overlay (overlays-in beg end))
+ (with-silent-modifications
+ (remove-list-of-text-properties beg end '(display)))
(when hard
(barf-if-buffer-read-only)
;; Remove any white-space padding around separators: