Naviguer par retrait

15

Je souhaite naviguer entre les lignes d'un fichier en fonction de l'indentation. Le fichier est structuré par indentation: une ligne plus indentée que la ligne précédente est un enfant de la ligne précédente, une ligne qui a la même indentation que la ligne précédente est son frère. Je recherche principalement trois commandes:

  • Passez au frère suivant, c'est-à-dire à la ligne suivante avec la même indentation, en sautant les lignes qui sont plus en retrait, mais en ne sautant pas au-delà d'une ligne qui est moins en retrait.
  • Passez au frère précédent, c'est-à-dire la même chose dans l'autre sens.
  • Déplacer vers le parent, c'est-à-dire vers la ligne précédente avec moins de retrait.

La position de la colonne du point ne doit pas changer.

Ceux - ci sont analogues pour des données structurées indentation à forward-sexp, backward-sexpet backward-up-listpour les données structurées sexp. L'indentation correspond à la structure du programme dans des langages tels que Haskell et Python; ces fonctions peuvent être particulièrement utiles dans ce contexte mais je ne recherche rien de spécifique au mode (mon cas d'utilisation principal est des données structurées d'intention dans un autre format de fichier).

La coloration des niveaux d'indentation peut aider à naviguer manuellement avec Up/ Downmais je veux quelque chose d'automatique.

Cette question de super utilisateur est similaire mais avec des exigences plus faibles et n'a actuellement pas de réponses qui répondent à mes exigences.

Gilles 'SO- arrête d'être méchant'
la source
set-selective-displayVous rapproche- t- on de ce dont vous avez besoin?
Kaushal Modi
1
@KaushalModi C'est utile, et je n'en savais rien, alors merci, mais ce n'est pas toujours ce dont j'ai besoin. Tout à l'heure, je voulais me déplacer et voir les enfants des lignes que je traversais.
Gilles 'SO- arrête d'être méchant'
Merci d'avoir posé cette question; J'étais sur le point de poser en gros la même question. La seule chose supplémentaire que j'aimerais, c'est "passer au dernier frère", c'est-à-dire la dernière ligne qui a la même indentation, et non les lignes qui sont moins indentées. (L'équivalent de répéter « passer à la suivante frères et soeurs » jusqu'à ce qu'il n'y en a pas.)
ShreevatsaR
Je viens de remarquer le paquetage indent-toolsdans melpa ( indent-tools ), qui fonctionne probablement à cet effet. Le premier commit a eu lieu le 16 mai 2016, environ 3 mois après que cette question a été posée.
ShreevatsaR

Réponses:

4

En examinant les quatre réponses disponibles actuellement ( deux sur Super User et deux sur cette question), je vois les problèmes suivants:

  • Les uns sur SuperUser par Stefan et Peng Bai (déplacement , ligne par ligne, en regardant indentation courant) ne met pas en oeuvre en conservant la position actuelle de la colonne et le déplacement jusqu'à la mère,
  • La réponse de Dan (en utilisant re-search-forward pour trouver la ligne suivante avec la même indentation) saute les lignes avec moins d'indentation: il ne sait pas quand il n'y a pas de prochain frère et peut donc passer à quelque chose qui n'est pas un frère mais un enfant d'un autre parent… un "cousin" suivant peut-être.
  • La réponse de Gilles (en utilisant le mode contour) ne conserve pas la position de la colonne et ne fonctionne pas avec les lignes à indentation nulle (lignes de "haut niveau"). De plus, en regardant son code outline.el, il va de toute façon ligne par ligne (en utilisant outline-next-visible-heading) dans notre cas, car (presque) toutes les lignes correspondraient à l'expression rationnelle du contour et compteraient comme un "en-tête".

Donc, rassemblant quelques idées de chacun, j'ai ce qui suit: avancer ligne par ligne, sauter les lignes vides et plus en retrait. Si vous êtes à indentation égale, c'est le prochain frère. L'idée de base ressemble à ceci:

(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)))

Convenablement généralisé (avant / arrière / haut / bas), ce que j'utilise ressemble à ce qui suit actuellement:

(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"))

Il y a encore plus de fonctionnalités souhaitables, et regarder outline.elet réimplémenter certaines d'entre elles peut aider, mais je suis satisfait de cela pour l'instant, pour mes besoins.

ShreevatsaR
la source
@Gilles: Merci pour les modifications! On dirait que (current-line)c'est quelque chose misc-fns.elque j'ai en quelque sorte dans mon installation Aquamacs dans le cadre d'une oneonone.elbibliothèque.
ShreevatsaR
6

Cette fonctionnalité existe dans Emacs. Le mode Plan décrit un document comme contenant des lignes de titre avec un niveau et dispose de fonctions pour se déplacer entre les niveaux. Nous pouvons définir chaque ligne comme une ligne de titre avec un niveau qui reflète son indentation: défini outline-regexpsur l'indentation. Plus précisément, l'empreinte ainsi que le premier caractère non-espaces (et le début du fichier est le plus haut niveau): \`\|\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

Dans Emacs 22.1–24.3, vous pouvez simplifier ceci pour:

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

Ensuite, vous pouvez utiliser les commandes de mouvement de contour :

  • C-C @ C-f( outline-forward-same-level) pour passer au frère suivant;
  • C-C @ C-b( outline-backward-same-level) pour passer au frère précédent;
  • C-C @ C-u( outline-up-heading) pour passer au parent.

Un onglet et un espace comptent pour la même quantité d'indentation. Si vous avez un mélange d'onglets et d'espaces, définissez-le tab-widthcorrectement et appelezuntabify .

Si le mode principal actuel a des paramètres de contour, ils peuvent entrer en conflit. Dans ce cas, vous pouvez utiliser l'une des nombreuses solutions à modes majeurs multiples , la plus simple étant de créer un tampon indirect et de le définir sur Outline Major Mode. En mode Plan principal, les raccourcis clavier par défaut sont plus simples à saisir:, C-c C-fetc.

Gilles 'SO- arrête d'être méchant'
la source
Il semble que cela devrait fonctionner, mais ne fonctionne pas pour moi pour une raison quelconque. M-x make-local-variable RET outline-regexp RETn'accepte pas cette variable et dit seulement «[Aucune correspondance]». Je dois encore y regarder plus attentivement.
ShreevatsaR
@ShreevatsaR C'est un changement incompatible dans Emacs 24.4: outline-regexpn'est plus un defcustom et ne peut pas être réglé interactivement si facilement.
Gilles 'SO- arrête d'être méchant'
Très bien merci. Il y a deux problèmes mineurs: (1) Si vous êtes au plus haut niveau (une ligne sans indentation, ce qui signifie, je suppose, aucune correspondance pour le contour-regexp), alors ni en avant ni en arrière ne fonctionne, et pour une raison quelconque, il monte de deux lignes (2) quand il va au frère suivant ou précédent, il va au début de la ligne (colonne 0) mais ce serait bien de conserver la colonne. (Comme vous le spécifiez dans la question.) Je suppose que ces deux peuvent être des limitations du mode contour lui-même.
ShreevatsaR
5

Les trois commandes suivantes, testées au minimum, devraient permettre une navigation de base par des lignes en retrait. Toutes mes excuses pour la répétition du code.

(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
la source
C'est bien (après le correctif - je ne comprends pas ce que vous essayez de faire en soustrayant 1 à (current-column)mais cela empêche le curseur de bouger), mais ne correspond pas exactement à mes spécifications: se déplacer à un niveau d'indentation passe moins - lignes en retrait.
Gilles 'SO- arrête d'être méchant'
Ça ne marche pas. Par exemple, ind-forward-siblingrecherche simplement la ligne suivante avec la même indentation, donc elle saute les lignes avec moins d'indentation (elle avance même lorsqu'il n'y a pas de frère en avant).
ShreevatsaR