Navegar por recuo

15

Eu quero navegar entre as linhas de um arquivo com base no recuo. O arquivo é estruturado por recuo: uma linha mais recuada que a linha anterior é filha da linha anterior, uma linha que tem o mesmo recuo que a linha anterior é seu irmão. Estou procurando principalmente três comandos:

  • Vá para o próximo irmão, ou seja, a próxima linha com o mesmo recuo, pulando as linhas mais recuadas, mas não pulando além de uma linha menos recuada.
  • Vá para o irmão anterior, ou seja, a mesma coisa na outra direção.
  • Mover para o pai, ou seja, para a linha anterior com menos recuo.

A posição da coluna do ponto não deve mudar.

Estes são análogos para dados estruturados-recuo para forward-sexp, backward-sexpe backward-up-listpara dados estruturados-sexp. O recuo corresponde à estrutura do programa em linguagens como Haskell e Python; essas funções podem ser especialmente úteis nesse contexto, mas não estou procurando nada específico do modo (meu caso de uso principal são dados estruturados por intenção dentro de outro formato de arquivo).

Os níveis de recuo para colorir podem ajudar a navegar manualmente com Up/ Downmas eu quero algo automático.

Essa pergunta do superusuário é semelhante, mas com requisitos mais fracos e, atualmente, não possui respostas que atendam aos meus requisitos.

Gilles 'SO- parar de ser mau'
fonte
Você set-selective-displayfica perto do que precisa?
precisa
1
@KaushalModi É útil, e eu não sabia disso, então obrigado, mas nem sempre é o que eu preciso. Agora, eu queria me mudar e ver os filhos das linhas que eu estava passando.
Gilles 'SO- stop being evil
Obrigado por fazer esta pergunta; Eu estava prestes a fazer basicamente a mesma pergunta apenas menos bem. A única coisa adicional que eu gostaria é "passar para o último irmão", ou seja, a última linha que tem o mesmo recuo, não pular linhas que são menos recuadas. (O equivalente de repetição "movimento para o próximo irmão" até que não haja qualquer.)
ShreevatsaR
Acabei de notar o pacote indent-toolsem melpa ( indent-tools ), que provavelmente funciona para esse fim. O primeiro commit foi em 16 de maio de 2016, cerca de 3 meses após a pergunta.
ShreevatsaR

Respostas:

4

Examinando as quatro respostas disponíveis atualmente ( duas no Superusuário e duas nesta questão), vejo os seguintes problemas:

  • Os que estão no SuperUser de Stefan e Peng Bai (movendo linha por linha, observando o recuo atual) não implementam a retenção da posição atual da coluna e a mudança para o pai,
  • A resposta de Dan (usando re-search-forward para encontrar a próxima linha com o mesmo recuo) pula as linhas com menos recuo: não sabe quando não há próximo irmão e, portanto, pode passar para algo que não é um irmão mas um filho de outro pai ... um próximo "primo" talvez.
  • A resposta de Gilles (usando o modo de contorno) não mantém a posição da coluna e não funciona com linhas com recuo zero (linhas de "nível superior"). Além disso, olhando seu código outline.el, ele também está basicamente indo de linha em linha de qualquer maneira (usando outline-next-visible-heading) em nosso caso, pois (quase) todas as linhas corresponderiam ao regexp de contorno e contariam como um "cabeçalho".

Então, reunindo algumas idéias de cada uma, tenho o seguinte: avançar linha por linha, passando por linhas vazias e mais recuadas. Se você estiver em igual recuo, então é o próximo irmão. A ideia básica é assim:

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

Generalizado adequadamente (para frente / para trás / para cima / para baixo), o que estou usando se parece com o seguinte atualmente:

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

Ainda há algumas funcionalidades desejáveis, e observar outline.ele reimplementar algumas delas pode ajudar, mas estou feliz com isso por enquanto, para meus propósitos.

ShreevatsaR
fonte
@Gilles: Obrigado pelas edições! Parece que (current-line)foi algo misc-fns.elque de alguma forma eu tenho na minha instalação do Aquamacs como parte de alguma oneonone.elbiblioteca.
ShreevatsaR
6

Esse recurso existe no Emacs. O modo de estrutura de tópicos descreve um documento como contendo linhas de cabeçalho com um nível e possui facilidades para se mover entre os níveis. Podemos definir cada linha como uma linha de cabeçalho com um nível que reflete seu recuo: definido outline-regexpcomo recuo. Mais precisamente, o recuo mais o primeiro caractere não-espaço em branco (e o início do arquivo é o nível mais 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

No Emacs 22.1 a 24.3, você pode simplificar isso 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

Então você pode usar comandos de movimento de estrutura de tópicos :

  • C-C @ C-f( outline-forward-same-level) para passar para o próximo irmão;
  • C-C @ C-b( outline-backward-same-level) para passar para o irmão anterior;
  • C-C @ C-u( outline-up-heading) para ir para o pai.

Uma guia e um espaço contam para a mesma quantidade de recuo. Se você tiver uma mistura de guias e espaços, defina tab-widthadequadamente e ligueuntabify .

Se o modo principal atual tiver configurações de estrutura de tópicos, elas poderão entrar em conflito. Nesse caso, você pode usar uma das muitas soluções de vários modos principais , sendo a mais simples criar um buffer indireto e configurá-lo para o modo principal de estrutura de tópicos. No modo principal de estrutura de tópicos, os atalhos de teclado padrão são mais simples de digitar C-c C-f:, etc.

Gilles 'SO- parar de ser mau'
fonte
Parece que deve funcionar, mas na verdade não funciona para mim por algum motivo. M-x make-local-variable RET outline-regexp RETnão aceita essa variável e diz apenas `[No match]`. Ainda estou para analisá-lo com mais cuidado.
ShreevatsaR
@ShreevatsaR É uma mudança incompatível no Emacs 24.4: outline-regexpnão é mais um defcustom e não pode ser definido de forma interativa tão facilmente.
Gilles 'SO- stop be evil'
Muito bom obrigado. Existem dois pequenos problemas: (1) Se você está no nível mais alto (uma linha sem recuo, o que eu acho meio páreo para o esboço-regexp) então nem para a frente nem para trás obras, e por algum motivo ele vai até dois linhas (2) quando passa para o irmão seguinte ou anterior, vai para o início da linha (coluna 0), mas seria bom manter a coluna. (Como você especifica na pergunta.) Acho que ambos podem ser limitações do próprio modo de estrutura de tópicos.
ShreevatsaR
5

Os três comandos a seguir, minimamente testados, devem permitir a navegação básica por linhas recuadas. Desculpas pela repetição de 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
fonte
Isso é bom (após a correção - eu não entendo o que você estava tentando fazer com a subtração de 1 para, (current-column)mas faz com que o cursor não se mova), mas não atende exatamente às minhas especificações: mover em um nível de indentação passa menos- linhas recuadas.
Gilles 'SO- stop be evil'
Isso não funciona. Por exemplo, ind-forward-siblingbasta procurar a próxima linha com o mesmo recuo, para que ela pule as linhas com menos recuo (ela avança mesmo quando não há irmãos à frente).
ShreevatsaR