我也說(shuō)說(shuō)Emacs吧(2) - Emacs其實(shí)就是函數(shù)的組合

Emacs本質(zhì)上是函數(shù)的組合

從幫助上看emacs有何不同

Vim和Sublime Text等編輯器,本質(zhì)上是一個(gè)編輯器。
比如我們看看vim的幫助,是這個(gè)風(fēng)格的,比如我要看i命令的幫助:

<insert>        or                              *i* *insert* *<Insert>*
i                       Insert text before the cursor [count] times.
                        When using CTRL-O in Insert mode |i_CTRL-O| the count
                        is not supported.

再看看emacs的幫助,是這樣的風(fēng)格,比如我們看Ctrl-n鍵的幫助:

C-n runs the command next-line (found in global-map), which is an interactive
compiled Lisp function in ‘simple.el’.

It is bound to C-n.

(next-line &optional ARG TRY-VSCROLL)

This function is for interactive use only;
in Lisp code use ‘forward-line’ instead.

Move cursor vertically down ARG lines.
Interactively, vscroll tall lines if ‘a(chǎn)uto-window-vscroll’ is enabled.
Non-interactively, use TRY-VSCROLL to control whether to vscroll tall
lines: if either ‘a(chǎn)uto-window-vscroll’ or TRY-VSCROLL is nil, this
function will not vscroll.

ARG defaults to 1.

If there is no character in the target line exactly under the current column,
the cursor is positioned after the character in that line which spans this
column, or at the end of the line if it is not long enough.
If there is no line in the buffer after this one, behavior depends on the
value of ‘next-line-add-newlines’.  If non-nil, it inserts a newline character
to create a line, and moves the cursor to that line.  Otherwise it moves the
cursor to the end of the buffer.

If the variable ‘line-move-visual’ is non-nil, this command moves
by display lines.  Otherwise, it moves by buffer lines, without
taking variable-width characters or continued lines into account.
See M-x next-logical-line for a command that always moves by buffer lines.

The command C-x C-n can be used to create
a semipermanent goal column for this command.
Then instead of trying to move exactly vertically (or as close as possible),
this command moves to the specified goal column (or as close as possible).
The goal column is stored in the variable ‘goal-column’, which is nil
when there is no goal column.  Note that setting ‘goal-column’
overrides ‘line-move-visual’ and causes this command to move by buffer
lines rather than by display lines.

更進(jìn)一步,我們可以點(diǎn)擊simple.el進(jìn)去看看,可以看到它的源碼:

(defun next-line (&optional arg try-vscroll)
  "Move cursor vertically down ARG lines.
...就是上面貼過(guò)的幫助,此處略過(guò)
"
  (declare (interactive-only forward-line))
  (interactive "^p\np")
  (or arg (setq arg 1))
  (if (and next-line-add-newlines (= arg 1))
      (if (save-excursion (end-of-line) (eobp))
      ;; When adding a newline, don't expand an abbrev.
      (let ((abbrev-mode nil))
        (end-of-line)
        (insert (if use-hard-newlines hard-newline "\n")))
    (line-move arg nil nil try-vscroll))
    (if (called-interactively-p 'interactive)
    (condition-case err
        (line-move arg nil nil try-vscroll)
      ((beginning-of-buffer end-of-buffer)
       (signal (car err) (cdr err))))
      (line-move arg nil nil try-vscroll)))
  nil)

從上面可以看到,比起vim是個(gè)相對(duì)黑盒,需要文檔描述的編輯器,emacs是個(gè)比較簡(jiǎn)單直接的家伙。簡(jiǎn)單到,基本上就是一堆函數(shù)的組合而己,我們做編輯時(shí),就是直接調(diào)用這些函數(shù)。為了方便,我們把這些函數(shù)綁定到快捷鍵上。

只要知道要調(diào)用哪個(gè)函數(shù),不想記任何快捷鍵的話,只需要記住一個(gè)就可以了,就是Alt+X,然后就可以輸入要執(zhí)行的函數(shù)名,去執(zhí)行這個(gè)命令。
Emacs的功能,都是由lisp或C語(yǔ)言實(shí)現(xiàn)的函數(shù)來(lái)實(shí)現(xiàn)的,所有的源碼都是開(kāi)放的,在新的版本中,直接都可以通過(guò)幫助功能來(lái)查看,非常方便。
在emacs里,Alt鍵的命令也都可以通過(guò)先按Esc再按另一個(gè)鍵的方式來(lái)實(shí)現(xiàn)。

從擴(kuò)展方向上看emacs的不同

vim的擴(kuò)展

Emacs是第一個(gè)著名的以可擴(kuò)展能力而聞名的編輯器,同時(shí)期的主流編輯器在這方面都要向emacs學(xué)習(xí)。到了現(xiàn)在,Sublime Text和Vim等編輯器的擴(kuò)展功能也是非常值得稱(chēng)道而且受歡迎的。
但是,不管是Vim還是Sublime Text,甚至更強(qiáng)大一些的Visual Studio Code和Atom,它們的做法都是把擴(kuò)展的接口開(kāi)放出來(lái),大家按照開(kāi)放出來(lái)的接口來(lái)寫(xiě)擴(kuò)展。

比如我們先看vim,以我在mac OS下的vim 8.0.600為例,它支持下列feature:

Huge version without GUI.  Features included (+) or not (-):
+acl             +clipboard       +dialog_con      +file_in_path    +job             -lua             +mouse_sgr       +path_extra      +rightleft       +tag_old_static  +user_commands   +writebackup
+arabic          +cmdline_compl   +diff            +find_in_path    +jumplist        +menu            -mouse_sysmouse  +perl            +ruby            -tag_any_white   +vertsplit       -X11
+autocmd         +cmdline_hist    +digraphs        +float           +keymap          +mksession       +mouse_urxvt     +persistent_undo +scrollbind      -tcl             +virtualedit     -xfontset
-balloon_eval    +cmdline_info    -dnd             +folding         +lambda          +modify_fname    +mouse_xterm     +postscript      +signs           +termguicolors   +visual          -xim
-browse          +comments        -ebcdic          -footer          +langmap         +mouse           +multi_byte      +printer         +smartindent     +terminfo        +visualextra     -xpm
++builtin_terms  +conceal         +emacs_tags      +fork()          +libcall         -mouseshape      +multi_lang      +profile         +startuptime     +termresponse    +viminfo         -xsmp
+byte_offset     +cryptv          +eval            -gettext         +linebreak       +mouse_dec       -mzscheme        +python          +statusline      +textobjects     +vreplace        -xterm_clipboard
+channel         +cscope          +ex_extra        -hangul_input    +lispindent      -mouse_gpm       +netbeans_intg   -python3         -sun_workshop    +timers          +wildignore      -xterm_save
+cindent         +cursorbind      +extra_search    +iconv           +listcmds        -mouse_jsbterm   +num64           +quickfix        +syntax          +title           +wildmenu
-clientserver    +cursorshape     +farsi           +insert_expand   +localmap        +mouse_netterm   +packages        +reltime         +tag_binary      -toolbar         +windows

帶加號(hào)的是我裝的vim支持的功能,減號(hào)為不支持。從中可以看到,我用的這個(gè)版本的vim支持:python,ruby,perl三種語(yǔ)言,而不支持python3,tcl和lua語(yǔ)言來(lái)寫(xiě)擴(kuò)展。

比如我們看一個(gè)vim擴(kuò)展的官方例子:

 1 " Vim global plugin for correcting typing mistakes
 2 " Last Change: 2000 Oct 15
 3 " Maintainer: Bram Moolenaar <Bram@vim.org>
 4 " License: This file is placed in the public domain.
 5
 6 if exists("g:loaded_typecorr")
 7 finish
 8 endif
 9 let g:loaded_typecorr = 1
 10
 11 let s:save_cpo = &cpo
 12 set cpo&vim
 13
 14 iabbrev teh the
 15 iabbrev otehr other
 16 iabbrev wnat want
 17 iabbrev synchronisation
 18 \ synchronization
 19 let s:count = 4
 20
 21 if !hasmapto('<Plug>TypecorrAdd')
 22 map <unique> <Leader>a <Plug>TypecorrAdd
 23 endif
 24 noremap <unique> <script> <Plug>TypecorrAdd <SID>Add
 25
 26 noremenu <script> Plugin.Add\ Correction <SID>Add
 27
 28 noremap <SID>Add :call <SID>Add(expand("<cword>"), 1)<CR>
 29
 30 function s:Add(from, correct)
 31 let to = input("type the correction for " . a:from . ": ")
 32 exe ":iabbrev " . a:from . " " . to
 33 if a:correct | exe "normal viws\<C?R>\" \b\e" | endif
 34 let s:count = s:count + 1
 35 echo s:count . " corrections now"
 36 endfunction
 37
 38 if !exists(":Correct")
 39 command ?nargs=1 Correct :call s:Add(<q?args>, 0)
 40 endif
 41
 42 let &cpo = s:save_cpo
 43 unlet s:save_cpo

在vimscript中,通過(guò)exe命令可以執(zhí)行vim本身的命令。

反正是調(diào)用API接口么,那么可以支持很多語(yǔ)言了。
比如可以通過(guò):pe perl腳本的方式,直接執(zhí)行perl語(yǔ)句??梢酝ㄟ^(guò):help :perl查看:

:pe[rl] {cmd}           Execute Perl command {cmd}.  The current package
                        is "main".

Perl語(yǔ)言寫(xiě)vim插件例:

        function! WhitePearl()
        perl << EOF     
                VIM::Msg("pearls are nice for necklaces");
                VIM::Msg("rubys for rings");
                VIM::Msg("pythons for bags");
                VIM::Msg("tcls????");
        EOF
        endfunction

常用的perl可調(diào)用接口有:

  :perl VIM::Msg("Text")                # displays a message
  :perl VIM::Msg("Error", "ErrorMsg")   # displays an error message
  :perl VIM::Msg("remark", "Comment")   # displays a highlighted message
  :perl VIM::SetOption("ai")            # sets a vim option
  :perl $nbuf = VIM::Buffers()          # returns the number of buffers
  :perl @buflist = VIM::Buffers()       # returns array of all buffers
  :perl $mybuf = (VIM::Buffers('qq.c'))[0] # returns buffer object for 'qq.c'
  :perl @winlist = VIM::Windows()       # returns array of all windows
  :perl $nwin = VIM::Windows()          # returns the number of windows
  :perl ($success, $v) = VIM::Eval('&path') # $v: option 'path', $success: 1
  :perl ($success, $v) = VIM::Eval('&xyz')  # $v: '' and $success: 0
  :perl $v = VIM::Eval('expand("<cfile>")') # expands <cfile>
  :perl $curwin->SetHeight(10)          # sets the window height
  :perl @pos = $curwin->Cursor()        # returns (row, col) array
  :perl @pos = (10, 10)
  :perl $curwin->Cursor(@pos)           # sets cursor to @pos
  :perl $curwin->Cursor(10,10)          # sets cursor to row 10 col 10
  :perl $mybuf = $curwin->Buffer()      # returns the buffer object for window
  :perl $curbuf->Name()                 # returns buffer name
  :perl $curbuf->Number()               # returns buffer number
  :perl $curbuf->Count()                # returns the number of lines
  :perl $l = $curbuf->Get(10)           # returns line 10
  :perl @l = $curbuf->Get(1 .. 5)       # returns lines 1 through 5
  :perl $curbuf->Delete(10)             # deletes line 10
  :perl $curbuf->Delete(10, 20)         # delete lines 10 through 20
  :perl $curbuf->Append(10, "Line")     # appends a line
  :perl $curbuf->Append(10, "Line1", "Line2", "Line3") # appends 3 lines
  :perl @l = ("L1", "L2", "L3")
  :perl $curbuf->Append(10, @l)         # appends L1, L2 and L3
  :perl $curbuf->Set(10, "Line")        # replaces line 10
  :perl $curbuf->Set(10, "Line1", "Line2")      # replaces lines 10 and 11
  :perl $curbuf->Set(10, @l)            # replaces 3 lines

類(lèi)似的,:py可以調(diào)用python語(yǔ)言,:rub可以使用ruby語(yǔ)言.

使用python的例子:

        :python from vim import *
        :python from string import upper
        :python current.line = upper(current.line)
        :python print "Hello"
        :python str = current.buffer[42]

Python調(diào)用vim命令的例子:

        :py print "Hello"               # displays a message
        :py vim.command(cmd)            # execute an Ex command
        :py w = vim.windows[n]          # gets window "n"
        :py cw = vim.current.window     # gets the current window
        :py b = vim.buffers[n]          # gets buffer "n"
        :py cb = vim.current.buffer     # gets the current buffer
        :py w.height = lines            # sets the window height
        :py w.cursor = (row, col)       # sets the window cursor position
        :py pos = w.cursor              # gets a tuple (row, col)
        :py name = b.name               # gets the buffer file name
        :py line = b[n]                 # gets a line from the buffer
        :py lines = b[n:m]              # gets a list of lines
        :py num = len(b)                # gets the number of lines
        :py b[n] = str                  # sets a line in the buffer
        :py b[n:m] = [str1, str2, str3] # sets a number of lines at once
        :py del b[n]                    # deletes a line
        :py del b[n:m]                  # deletes a number of lines

調(diào)用python文件,可以通過(guò):pyfile或:pyf命令調(diào)用。

Ruby語(yǔ)言寫(xiě)vim插件的例子:

        function! RedGem()
        ruby << EOF
        class Garnet
                def initialize(s)
                        @buffer = VIM::Buffer.current
                        vimputs(s)
                end
                def vimputs(s)
                        @buffer.append(@buffer.count,s)
                end
        end
        gem = Garnet.new("pretty")
        EOF
        endfunction

Ruby調(diào)用vim接口的示例:

        print "Hello"                         # displays a message
        VIM.command(cmd)                      # execute an Ex command
        num = VIM::Window.count               # gets the number of windows
        w = VIM::Window[n]                    # gets window "n"
        cw = VIM::Window.current              # gets the current window
        num = VIM::Buffer.count               # gets the number of buffers
        b = VIM::Buffer[n]                    # gets buffer "n"
        cb = VIM::Buffer.current              # gets the current buffer
        w.height = lines                      # sets the window height
        w.cursor = [row, col]                 # sets the window cursor position
        pos = w.cursor                        # gets an array [row, col]
        name = b.name                         # gets the buffer file name
        line = b[n]                           # gets a line from the buffer
        num = b.count                         # gets the number of lines
        b[n] = str                            # sets a line in the buffer
        b.delete(n)                           # deletes a line
        b.append(n, str)                      # appends a line after n
        line = VIM::Buffer.current.line       # gets the current line
        num = VIM::Buffer.current.line_number # gets the current line number
        VIM::Buffer.current.line = "test"     # sets the current line number

再來(lái)一個(gè)lua語(yǔ)言的:

        function! CurrentLineInfo()
        lua << EOF
        local linenr = vim.window().line
        local curline = vim.buffer()[linenr]
        print(string.format("Current line [%d] has %d chars",
                linenr, #curline))
        EOF
        endfunction

tcl語(yǔ)言的:

        function! DefineDate()
            tcl << EOF
            proc date {} {      
                return [clock format [clock seconds]]
            }
        EOF
        endfunction

Atom的擴(kuò)展

Atom的擴(kuò)展的主力語(yǔ)言是CoffeeScript。

YourNameWordCountView = require './your-name-word-count-view'
{CompositeDisposable} = require 'atom'

module.exports = YourNameWordCount =
  yourNameWordCountView: null
  modalPanel: null
  subscriptions: null

  activate: (state) ->
    @yourNameWordCountView = new YourNameWordCountView(state.yourNameWordCountViewState)
    @modalPanel = atom.workspace.addModalPanel(item: @yourNameWordCountView.getElement(), visible: false)

    # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    @subscriptions = new CompositeDisposable

    # Register command that toggles this view
    @subscriptions.add atom.commands.add 'atom-workspace',
      'your-name-word-count:toggle': => @toggle()

  deactivate: ->
    @modalPanel.destroy()
    @subscriptions.dispose()
    @wordcountView.destroy()

  serialize: ->
    yourNameWordCountViewState: @yourNameWordCountView.serialize()

  toggle: ->
    console.log 'YourNameWordCount was toggled!'

    if @modalPanel.isVisible()
      @modalPanel.hide()
    else
      @modalPanel.show()

Visual Studio Code的擴(kuò)展

Visual Studio Code的插件可以用JavaScript或者TypeScript來(lái)開(kāi)發(fā)。
下面是個(gè)空的TypeScript的例子:

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

    // Use the console to output diagnostic information (console.log) and errors (console.error)
    // This line of code will only be executed once when your extension is activated
    console.log('Congratulations, your extension "my-first-extension" is now active!');

    // The command has been defined in the package.json file
    // Now provide the implementation of the command with  registerCommand
    // The commandId parameter must match the command field in package.json
    var disposable = vscode.commands.registerCommand('extension.sayHello', () => {
        // The code you place here will be executed every time your command is executed

        // Display a message box to the user
        vscode.window.showInformationMessage('Hello World!');
    });

    context.subscriptions.push(disposable);
}

Sublime Text的擴(kuò)展

Sublime Text的擴(kuò)展是用Python寫(xiě)的,舉個(gè)處理回車(chē)換行的例子:

def normalize_line_endings(self, string):
    string = string.replace('\r\n', '\n').replace('\r', '\n')
    line_endings = self.view.settings().get('default_line_ending')
    if line_endings == 'windows':
        string = string.replace('\n', '\r\n')
    elif line_endings == 'mac':
        string = string.replace('\n', '\r')
    return string

emacs的擴(kuò)展

Emacs與以上的編輯器的最大不同的就是,無(wú)所謂擴(kuò)展了,哪塊看不順眼就直接改了就是了。反正大部分都是綁定到快捷鍵上的函數(shù)而己。

比如,學(xué)習(xí)emacs的教程中,最開(kāi)始講的函數(shù)就是find-file,它的作用是打開(kāi)文件,綁定在C-x C-f鍵上。水木社區(qū)官方QQ群進(jìn)群?jiǎn)栴}就是問(wèn)C-x C-f的作用是什么。

Emacs的好處是沒(méi)有秘密,我們直接看源碼,看看它是做啥的:

(defun find-file (filename &optional wildcards)
... ;文檔略
  (interactive
   (find-file-read-args "Find file: "
                        (confirm-nonexistent-file-or-buffer)))
  (let ((value (find-file-noselect filename nil nil wildcards)))
    (if (listp value)
    (mapcar 'switch-to-buffer (nreverse value))
      (switch-to-buffer value))))

這么基礎(chǔ)的命令,在其它編輯器中,一般沒(méi)人改吧。但是在spacemacs中,C-x C-f就默認(rèn)綁定ido-find-file函數(shù)上了。

(defun ido-find-file ()
  (interactive)
  (ido-file-internal ido-default-file-method))

ido是Emacs的一個(gè)插件,意思是『Interactive Do』,在交互式操作方式上對(duì)基礎(chǔ)的Emacs功能上有所增強(qiáng)。
即使在相對(duì)古老的emacs 23.x版本上,ido插件也是官方發(fā)布版本中的一部分。只不過(guò)官方版本上的鍵綁定還是綁到基礎(chǔ)命令上。我們可以選擇綁一個(gè)鍵給它,也可以通過(guò)Alt-x來(lái)運(yùn)行它。

后面學(xué)習(xí)emacs的功能的時(shí)候,我們都會(huì)講,標(biāo)準(zhǔn)emacs是如何做的,spacemancs是如何做的。大家可以看到,有很多標(biāo)準(zhǔn)emacs綁定的快捷鍵,在spacemacs上根本就不靈了,被綁定到別的功能上了。

所以,與其記快捷鍵,不如記函數(shù)名吧。也不用太精確,輸?shù)臅r(shí)候能找到就行。查幫助時(shí)會(huì)提升它綁到哪個(gè)鍵上,或者記不住就自己綁一個(gè)喜歡的也可以。

在Emacs中查找?guī)椭?/h2>

個(gè)人覺(jué)得,Emacs的文檔確實(shí)不如Vim的文檔寫(xiě)得好。但是,Emacs的文檔也仍然是非常豐富的,實(shí)在查不到,咱還可以直接看代碼。

官網(wǎng)上的文檔

官網(wǎng)上的文檔還是相當(dāng)豐富的:https://www.gnu.org/software/emacs/manual/

除了基本功能外,常用的大插件的文檔也不少,比如我用emacs寫(xiě)代碼比較多,經(jīng)常用cc-mode,cc-mode的文檔也很詳細(xì):https://www.gnu.org/software/emacs/manual/html_node/ccmode/index.html

在emacs中查找?guī)椭?/h3>

既然我們已經(jīng)了解到emacs就是一堆函數(shù)的組合,那么提供幫助功能,肯定也是調(diào)用相應(yīng)的函數(shù)了。沒(méi)錯(cuò),正是這樣!

如果想完整地閱讀手冊(cè),可以調(diào)用info函數(shù),默認(rèn)綁定在C-h i組合上。

info頁(yè)面

如果看到手冊(cè)或者書(shū)上有講一個(gè)快捷鍵是做什么的,我們可以查找它所對(duì)應(yīng)的函數(shù)。查找一個(gè)按鍵的定義:C-h k (describe-key):比如我們就可以查查C-h k對(duì)應(yīng)的功能:

C-h k runs the command describe-key, which is an interactive compiled
Lisp function.

It is bound to C-h k, <f1> k, <help> k, <menu-bar> <help-menu>
<describe> <describe-key-1>.

(describe-key &optional KEY UNTRANSLATED UP-EVENT)

Display documentation of the function invoked by KEY.
KEY can be any kind of a key sequence; it can include keyboard events,
mouse events, and/or menu events.  When calling from a program,
pass KEY as a string or a vector.

If non-nil, UNTRANSLATED is a vector of the corresponding untranslated events.
It can also be a number, in which case the untranslated events from
the last key sequence entered are used.
UP-EVENT is the up-event that was discarded by reading KEY, or nil.

If KEY is a menu item or a tool-bar button that is disabled, this command
temporarily enables it to allow getting help on disabled items and buttons.

從幫助中我們可以看到,快捷鍵綁到C-h k,而實(shí)際調(diào)用的函數(shù)是describe-key.

同樣,我們還可以通過(guò)C-h f (describe-function)來(lái)查詢(xún)一個(gè)函數(shù)的功能。

describe-function is an interactive compiled Lisp function.

It is bound to C-h f, <f1> f, <help> f, <menu-bar> <help-menu>
<describe> <describe-function>.

(describe-function FUNCTION)

Display the full documentation of FUNCTION (a symbol).

另外,還有查詢(xún)變量的describe-variable函數(shù),綁定到C-h v鍵上。

小結(jié)

  1. emacs的基本用法,就是調(diào)用一些函數(shù)而己。這些函數(shù)可以通過(guò)Alt-x加上函數(shù)名去調(diào)用。
  2. 常用函數(shù)可以綁定到一些快捷鍵上。很多emacs入門(mén)教程講的就是這些功能的用法
  3. info用于在emacs中查看手冊(cè), describe-funciton查找函數(shù)用法,對(duì)于lisp函數(shù)經(jīng)??梢灾边_(dá)源碼。describe-key查找鍵值綁定。它們默認(rèn)的綁定是C-h i, C-h f和C-h k。在以后的emacs歲月里,您會(huì)經(jīng)常用到它們的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容