Navegar por sangría

15

Quiero navegar entre las líneas de un archivo basado en la sangría. El archivo está estructurado por sangría: una línea que está más sangrada que la línea anterior es hija de la línea anterior, una línea que tiene la misma sangría que la línea anterior es su hermano. Estoy buscando principalmente tres comandos:

  • Pasa al siguiente hermano, es decir, a la siguiente línea con la misma sangría, saltando las líneas que están más sangradas, pero no saltando más allá de una línea que está menos sangrada.
  • Moverse al hermano anterior, es decir, lo mismo en la otra dirección.
  • Mover al padre, es decir a la línea anterior con menos sangría

La posición de la columna del punto no debe cambiar.

Estos son los análogos de los datos de sangrado entre estructurado para forward-sexp, backward-sexpy backward-up-listpara los datos SEXP-estructurado. La sangría corresponde a la estructura del programa en lenguajes como Haskell y Python; Estas funciones pueden ser especialmente útiles en este contexto, pero no busco nada específico del modo (mi caso de uso principal son los datos estructurados por la intención dentro de otro formato de archivo).

Colorear los niveles de sangría puede ayudar a navegar manualmente con Up/ Downpero quiero algo automático.

Esta pregunta de súper usuario es similar pero con requisitos más débiles y actualmente no tiene respuestas que cumplan con mis requisitos.

Gilles 'SO- deja de ser malvado'
fuente
¿ set-selective-displayTe acerca a lo que necesitas?
Kaushal Modi
1
@KaushalModi Es útil, y no sabía sobre esto, así que gracias, pero no siempre es lo que necesito. Justo ahora, quería moverme y ver a los niños de las líneas por las que me estaba moviendo.
Gilles 'SO- deja de ser malvado'
Gracias por hacer esta pregunta; Estaba a punto de hacer básicamente la misma pregunta, pero menos bien. La única cosa adicional que me gustaría es "pasar al último hermano", es decir, la última línea que tiene la misma sangría, no saltar las líneas que tienen menos sangría. (El equivalente a repetir "pasar al próximo hermano" hasta que no haya ninguno.)
ShreevatsaR
Acabo de notar el paquete indent-toolsen melpa ( herramientas de sangría ), que probablemente funcione para este propósito. El primer compromiso fue el 16 de mayo de 2016, aproximadamente 3 meses después de que se hiciera esta pregunta.
ShreevatsaR

Respuestas:

4

Al examinar las cuatro respuestas disponibles actualmente ( dos en Superusuario y dos en esta pregunta), veo los siguientes problemas:

  • Los que están en SuperUser de Stefan y Peng Bai (moviéndose línea por línea, mirando la sangría actual) no implementan la retención de la posición actual de la columna y se mueven hacia el padre,
  • La respuesta de Dan (usando re-search-forward para encontrar la siguiente línea con la misma sangría) salta las líneas con menos sangría: no sabe cuándo no hay un hermano próximo y, por lo tanto, puede pasar a algo que no sea un hermano pero un hijo de otro padre ... un próximo "primo" tal vez.
  • La respuesta de Gilles (usando el modo de esquema) no retiene la posición de la columna y no funciona con líneas con sangría cero (líneas de "nivel superior"). Además, al observar su código outline.el, también va básicamente línea por línea de todos modos (usando outline-next-visible-heading) en nuestro caso, ya que (casi) todas las líneas coincidirían con la expresión regular del esquema y contarían como un "encabezado".

Entonces, al reunir algunas ideas de cada una, tengo lo siguiente: avanzar línea por línea, saltando las líneas vacías y más sangradas. Si tiene la misma sangría, entonces es el próximo hermano. La idea básica se ve así:

(defun indentation-get-next-sibling-line ()
  "The line number of the next sibling, or nil if there isn't any."
  (let ((wanted-indentation (current-indentation)))
    (save-excursion
      (while (and (zerop (forward-line))  ; forward-line returns 0 on success
               (or (eolp)  ; Skip past blank lines and more-indented lines
                 (> (current-indentation) wanted-indentation))))
      ;; Now we can't go further. Which case is it?
      (if (and (not (eobp)) (= (current-indentation) wanted-indentation))
        (line-number-at-pos)
        nil))))

(defun indentation-forward-to-next-sibling ()
  (interactive)
  (let ((saved-column (current-column)))
    (forward-line (- (indentation-get-next-sibling-line) (line-number-at-pos)))
    (move-to-column saved-column)))

Adecuadamente generalizado (adelante / atrás / arriba / abajo), lo que estoy usando se parece a lo siguiente actualmente:

(defun indentation-get-next-good-line (direction skip good)
  "Moving in direction `direction', and skipping over blank lines and lines that
satisfy relation `skip' between their indentation and the original indentation,
finds the first line whose indentation satisfies predicate `good'."
  (let ((starting-indentation (current-indentation))
         (lines-moved direction))
    (save-excursion
      (while (and (zerop (forward-line direction))
               (or (eolp)  ; Skip past blank lines and other skip lines
                 (funcall skip (current-indentation) starting-indentation)))
        (setq lines-moved (+ lines-moved direction)))
      ;; Now we can't go further. Which case is it?
      (if (and
            (not (eobp))
            (not (bobp))
            (funcall good (current-indentation) starting-indentation))
        lines-moved
        nil))))

(defun indentation-get-next-sibling-line ()
  "The line number of the next sibling, if any."
  (indentation-get-next-good-line 1 '> '=))

(defun indentation-get-previous-sibling-line ()
  "The line number of the previous sibling, if any"
  (indentation-get-next-good-line -1 '> '=))

(defun indentation-get-parent-line ()
  "The line number of the parent, if any."
  (indentation-get-next-good-line -1 '>= '<))

(defun indentation-get-child-line ()
  "The line number of the first child, if any."
  (indentation-get-next-good-line +1 'ignore '>))


(defun indentation-move-to-line (func preserve-column name)
  "Move the number of lines given by func. If not possible, use `name' to say so."
  (let ((saved-column (current-column))
          (lines-to-move-by (funcall func)))
    (if lines-to-move-by
      (progn
        (forward-line lines-to-move-by)
        (move-to-column (if preserve-column
                          saved-column
                          (current-indentation))))
      (message "No %s to move to." name))))

(defun indentation-forward-to-next-sibling ()
  "Move to the next sibling if any, retaining column position."
  (interactive "@")
  (indentation-move-to-line 'indentation-get-next-sibling-line t "next sibling"))

(defun indentation-backward-to-previous-sibling ()
  "Move to the previous sibling if any, retaining column position."
  (interactive "@")
  (indentation-move-to-line 'indentation-get-previous-sibling-line t "previous sibling"))

(defun indentation-up-to-parent ()
  "Move to the parent line if any."
  (interactive "@")
  (indentation-move-to-line 'indentation-get-parent-line nil "parent"))

(defun indentation-down-to-child ()
  "Move to the first child line if any."
  (interactive "@")
  (indentation-move-to-line 'indentation-get-child-line nil "child"))

Todavía hay algo más de funcionalidad deseable, y mirar outline.ely reimplementar algo de eso puede ayudar, pero estoy contento con esto por ahora, para mis propósitos.

ShreevatsaR
fuente
@Gilles: ¡Gracias por las ediciones! Parece que (current-line)fue algo de lo misc-fns.elque de alguna manera tengo en mi instalación de Aquamacs como parte de alguna oneonone.elbiblioteca.
ShreevatsaR
6

Esta característica existe en Emacs. El modo de esquema describe un documento que contiene líneas de encabezado con un nivel y tiene facilidades para moverse entre niveles. Podemos definir cada línea como una línea de encabezado con un nivel que refleje su sangría: establecer outline-regexpen la sangría. Más precisamente, la muesca más el primero no está en blanco (y el principio del archivo es el nivel más alto): \`\|\s-+\S-.

M-x load-libray outline RET
M-: (make-local-variable 'outline-regexp) RET
M-: (setq outline-regexp "\\`\\|\\s-+\\S-") RET
M-x outline-minor-mode RET

En Emacs 22.1–24.3 puede simplificar esto para:

M-x load-libray outline RET
M-1 M-x set-variable RET outline-regexp RET "\\`\\|\\s-+\\S-" RET
M-x outline-minor-mode RET

Luego puede usar comandos de movimiento de contorno :

  • C-C @ C-f( outline-forward-same-level) para pasar al siguiente hermano;
  • C-C @ C-b( outline-backward-same-level) para pasar al hermano anterior;
  • C-C @ C-u( outline-up-heading) para pasar al padre.

Una pestaña y un espacio cuentan para la misma cantidad de sangría. Si tiene una combinación de pestañas y espacios, configúrelos tab-widthadecuadamente y llameuntabify .

Si el modo principal actual tiene configuraciones de esquema, pueden entrar en conflicto. En este caso, puede usar una de las muchas soluciones de modos principales múltiples , la más simple es crear un búfer indirecto y establecerlo en Modo principal de esquema. En el Modo principal del esquema, los atajos de teclado predeterminados son más simples de escribir: C-c C-fetc.

Gilles 'SO- deja de ser malvado'
fuente
Parece que debería funcionar, pero en realidad no funciona para mí por alguna razón. M-x make-local-variable RET outline-regexp RETno acepta esa variable, y dice solo `[Sin coincidencia]`. Todavía tengo que estudiarlo con más cuidado.
ShreevatsaR
@ShreevatsaR Es un cambio incompatible en Emacs 24.4: outline-regexpya no es una costumbre y no se puede configurar interactivamente tan fácilmente.
Gilles 'SO- deja de ser malvado'
Muy bonito, gracias. Hay dos problemas menores: (1) Si se encuentra en el nivel más alto (una línea sin sangría, que supongo que los medios no puede competir con el contorno-regexp), entonces ni hacia adelante ni hacia atrás las obras, y por alguna razón que va hasta dos líneas (2) cuando va al hermano siguiente o anterior, va al comienzo de la línea (columna 0) pero sería bueno retener la columna. (Como especifica en la pregunta.) Supongo que ambas pueden ser limitaciones del modo de esquema en sí.
ShreevatsaR
5

Los siguientes tres comandos, mínimamente probados, deberían permitir la navegación básica por líneas sangradas. Disculpas por la repetición del código.

(defun ind-forward-sibling ()
  "Move forward to the next sibling line with the same indentation."
  (interactive)
  (save-match-data
    (let ((col (current-column))
          (pad (progn
                 (back-to-indentation)
                 (current-column))))
      (end-of-line 1)
      (re-search-forward (concat "^\\s-\\{"
                                 (number-to-string pad)
                                 "\\}[^ ]") nil t)
      (move-to-column col))))

(defun ind-backward-sibling ()
  "Move backward to the next sibling line with the same indentation."
  (interactive)
  (save-match-data
    (let ((col (current-column))
          (pad (progn
                 (back-to-indentation)
                 (current-column))))
      (beginning-of-line 1)
      (re-search-backward (concat "^\\s-\\{"
                                 (number-to-string pad)
                                 "\\}[^ ]") nil t)
      (move-to-column col))))

(defun ind-up-parent ()
  "Move up to parent line with less indentation."
  (interactive)
  (save-match-data
    (let ((col (current-column))
          (pad (progn
                 (back-to-indentation)
                 (current-column))))
      (when (> pad 0)
        (beginning-of-line 1)
        (re-search-backward (concat "^\\s-\\{0,"
                                    (number-to-string (1- pad))
                                    "\\}[^ ]") nil t))
      (move-to-column col))))
Dan
fuente
Eso es bueno (después de la solución, no entiendo lo que intentabas hacer restando 1 (current-column)pero hace que el cursor no se mueva), pero no cumple exactamente con mi especificación: moverse en un nivel de sangría se mueve menos Líneas sangradas.
Gilles 'SO- deja de ser malvado'
Esto no funciona Por ejemplo, ind-forward-siblingsimplemente busca la siguiente línea con la misma sangría, por lo que salta las líneas con menos sangría (avanza incluso cuando no hay hermanos hacia adelante).
ShreevatsaR