diff --git a/nvim/api.go b/nvim/api.go index ab75dee5..33f3b147 100644 --- a/nvim/api.go +++ b/nvim/api.go @@ -921,6 +921,49 @@ func (b *Batch) CreateBuffer(listed bool, scratch bool, buffer *Buffer) { b.call("nvim_create_buf", buffer, listed, scratch) } +// OpenTerm opens a terminal instance in a buffer. +// +// By default (and currently the only option) the terminal will not be +// connected to an external process. Instead, input send on the channel +// will be echoed directly by the terminal. This is useful to disply +// ANSI terminal sequences returned as part of a rpc message, or similar. +// +// Note that to directly initiate the terminal using the right size, display the +// buffer in a configured window before calling this. For instance, for a +// floating display, first create an empty buffer using CreateBuffer, +// then display it using OpenWindow, and then call this function. +// Then "nvim_chan_send" cal be called immediately to process sequences +// in a virtual terminal having the intended size. +// +// The buffer arg is the buffer to use (expected to be empty). +// +// The opts arg is optional parameters. Reserved for future use. +func (v *Nvim) OpenTerm(buffer Buffer, opts map[string]interface{}) (channel int, err error) { + err = v.call("nvim_open_term", &channel, buffer, opts) + return channel, err +} + +// OpenTerm opens a terminal instance in a buffer. +// +// By default (and currently the only option) the terminal will not be +// connected to an external process. Instead, input send on the channel +// will be echoed directly by the terminal. This is useful to disply +// ANSI terminal sequences returned as part of a rpc message, or similar. +// +// Note that to directly initiate the terminal using the right size, display the +// buffer in a configured window before calling this. For instance, for a +// floating display, first create an empty buffer using CreateBuffer, +// then display it using OpenWindow, and then call this function. +// Then "nvim_chan_send" cal be called immediately to process sequences +// in a virtual terminal having the intended size. +// +// The buffer arg is the buffer to use (expected to be empty). +// +// The opts arg is optional parameters. Reserved for future use. +func (b *Batch) OpenTerm(buffer Buffer, opts map[string]interface{}, channel *int) { + b.call("nvim_open_term", channel, buffer, opts) +} + // OpenWindow open a new window. // // Currently this is used to open floating and external windows. @@ -2611,6 +2654,26 @@ func (b *Batch) WindowConfig(window Window, config *WindowConfig) { b.call("nvim_win_get_config", config, window) } +// HideWindow closes the window and hide the buffer it contains (like ":hide" with a +// windowID). +// +// Like ":hide" the buffer becomes hidden unless another window is editing it, +// or "bufhidden" is "unload", "delete" or "wipe" as opposed to ":close" or +// CloseWindow, which will close the buffer. +func (v *Nvim) HideWindow(window Window) error { + return v.call("nvim_win_hide", nil, window) +} + +// HideWindow closes the window and hide the buffer it contains (like ":hide" with a +// windowID). +// +// Like ":hide" the buffer becomes hidden unless another window is editing it, +// or "bufhidden" is "unload", "delete" or "wipe" as opposed to ":close" or +// CloseWindow, which will close the buffer. +func (b *Batch) HideWindow(window Window) { + b.call("nvim_win_hide", nil, window) +} + // CloseWindow close a window. // // This is equivalent to |:close| with count except that it takes a window id. diff --git a/nvim/api_def.go b/nvim/api_def.go index e9073db8..a5791d8b 100644 --- a/nvim/api_def.go +++ b/nvim/api_def.go @@ -422,6 +422,27 @@ func CreateBuffer(listed, scratch bool) (buffer Buffer) { name(nvim_create_buf) } +// OpenTerm opens a terminal instance in a buffer. +// +// By default (and currently the only option) the terminal will not be +// connected to an external process. Instead, input send on the channel +// will be echoed directly by the terminal. This is useful to disply +// ANSI terminal sequences returned as part of a rpc message, or similar. +// +// Note that to directly initiate the terminal using the right size, display the +// buffer in a configured window before calling this. For instance, for a +// floating display, first create an empty buffer using CreateBuffer, +// then display it using OpenWindow, and then call this function. +// Then "nvim_chan_send" cal be called immediately to process sequences +// in a virtual terminal having the intended size. +// +// The buffer arg is the buffer to use (expected to be empty). +// +// The opts arg is optional parameters. Reserved for future use. +func OpenTerm(buffer Buffer, opts map[string]interface{}) (channel int) { + name(nvim_open_term) +} + // OpenWindow open a new window. // // Currently this is used to open floating and external windows. @@ -1252,6 +1273,16 @@ func WindowConfig(window Window) (config WindowConfig) { returnPtr() } +// HideWindow closes the window and hide the buffer it contains (like ":hide" with a +// windowID). +// +// Like ":hide" the buffer becomes hidden unless another window is editing it, +// or "bufhidden" is "unload", "delete" or "wipe" as opposed to ":close" or +// CloseWindow, which will close the buffer. +func HideWindow(window Window) { + name(nvim_win_hide) +} + // CloseWindow close a window. // // This is equivalent to |:close| with count except that it takes a window id. diff --git a/nvim/api_tool.go b/nvim/api_tool.go index 9c9582e8..971ad385 100644 --- a/nvim/api_tool.go +++ b/nvim/api_tool.go @@ -420,6 +420,8 @@ var specialAPIs = map[string]bool{ "nvim_exec_lua": true, "nvim_buf_call": true, "nvim_set_decoration_provider": true, + "nvim_chan_send": true, // FUNC_API_LUA_ONLY + "nvim_notify": true, // implements underling nlua(vim.notify) } func compareFunctions(functions []*Function) error { diff --git a/nvim/nvim.go b/nvim/nvim.go index 1ae066fc..69e8cdfa 100644 --- a/nvim/nvim.go +++ b/nvim/nvim.go @@ -673,6 +673,53 @@ func (b *Batch) ExecuteLua(code string, result interface{}, args ...interface{}) b.call("nvim_execute_lua", result, code, args) } +// Notify the user with a message. +// +// Relays the call to vim.notify. By default forwards your message in the +// echo area but can be overriden to trigger desktop notifications. +// +// The msg arg is message to display to the user. +// +// The logLevel arg is the LogLevel. +// +// The opts arg is reserved for future use. +func (v *Nvim) Notify(msg string, logLevel LogLevel, opts map[string]interface{}) error { + if logLevel == LogErrorLevel { + return v.WritelnErr(msg) + } + + chunks := []TextChunk{ + { + Text: msg, + }, + } + return v.Echo(chunks, true, opts) +} + +// Notify the user with a message. +// +// Relays the call to vim.notify. By default forwards your message in the +// echo area but can be overriden to trigger desktop notifications. +// +// The msg arg is message to display to the user. +// +// The logLevel arg is the LogLevel. +// +// The opts arg is reserved for future use. +func (b *Batch) Notify(msg string, logLevel LogLevel, opts map[string]interface{}) { + if logLevel == LogErrorLevel { + b.WritelnErr(msg) + return + } + + chunks := []TextChunk{ + { + Text: msg, + }, + } + b.Echo(chunks, true, opts) +} + // decodeExt decodes a MsgPack encoded number to go int value. func decodeExt(p []byte) (int, error) { switch { diff --git a/nvim/nvim_test.go b/nvim/nvim_test.go index 39a75a21..554b2926 100644 --- a/nvim/nvim_test.go +++ b/nvim/nvim_test.go @@ -107,6 +107,7 @@ func TestAPI(t *testing.T) { t.Run("RuntimeFiles", testRuntimeFiles(v)) t.Run("AllOptionsInfo", testAllOptionsInfo(v)) t.Run("OptionsInfo", testOptionsInfo(v)) + t.Run("OpenTerm", testTerm(v)) } func testBufAttach(v *Nvim) func(*testing.T) { @@ -506,13 +507,14 @@ func testBuffer(v *Nvim) func(*testing.T) { func testWindow(v *Nvim) func(*testing.T) { return func(t *testing.T) { t.Run("Nvim", func(t *testing.T) { - t.Parallel() - wins, err := v.Windows() if err != nil { t.Fatal(err) } if len(wins) != 1 { + for i := 0; i < len(wins); i++ { + t.Logf("wins[%d]: %v", i, wins[i]) + } t.Fatalf("expected one win, found %d wins", len(wins)) } if wins[0] == 0 { @@ -532,14 +534,40 @@ func testWindow(v *Nvim) func(*testing.T) { t.Fatalf("got %s but want %s", got, want) } - if err := v.SetCurrentWindow(win); err != nil { + win, err = v.CurrentWindow() + if err != nil { + t.Fatal(err) + } + if err := v.Command("split"); err != nil { + t.Fatal(err) + } + win2, err := v.CurrentWindow() + if err != nil { t.Fatal(err) } + + if err := v.HideWindow(win2); err != nil { + t.Fatalf("failed to HideWindow(%v)", win2) + } + wins2, err := v.Windows() + if err != nil { + t.Fatal(err) + } + if len(wins2) != 1 { + for i := 0; i < len(wins2); i++ { + t.Logf("wins[%d]: %v", i, wins2[i]) + } + t.Fatalf("expected one win, found %d wins", len(wins2)) + } + if wins2[0] == 0 { + t.Fatalf("wins[0] == 0") + } + if win != wins2[0] { + t.Fatalf("win2 is not wins2[0]: want: %v, win2: %v ", wins2[0], win) + } }) t.Run("Batch", func(t *testing.T) { - t.Parallel() - b := v.NewBatch() var wins []Window @@ -568,10 +596,36 @@ func testWindow(v *Nvim) func(*testing.T) { t.Fatalf("got %s but want %s", got, want) } - b.SetCurrentWindow(win) + b.CurrentWindow(&win) + if err := b.Execute(); err != nil { + t.Fatal(err) + } + + b.Command("split") + var win2 Window + b.CurrentWindow(&win2) if err := b.Execute(); err != nil { t.Fatal(err) } + + b.HideWindow(win2) + var wins2 []Window + b.Windows(&wins2) + if err := b.Execute(); err != nil { + t.Fatal(err) + } + if len(wins2) != 1 { + for i := 0; i < len(wins2); i++ { + t.Logf("wins[%d]: %v", i, wins2[i]) + } + t.Fatalf("expected one win, found %d wins", len(wins2)) + } + if wins2[0] == 0 { + t.Fatalf("wins[0] == 0") + } + if win != wins2[0] { + t.Fatalf("win2 is not wins2[0]: want: %v, win2: %v ", wins2[0], win) + } }) } } @@ -956,11 +1010,40 @@ func testMessage(v *Nvim) func(*testing.T) { t.Fatalf("WritelnErr(%q) = %q, want: %q", wantWritelnErr, gotWritelnErr, wantWritelnErr) } - // cleanup v:statusmsg - if err := v.SetVVar("statusmsg", ""); err != nil { + // clear messages + if _, err := v.Exec(":messages clear", false); err != nil { t.Fatalf("failed to SetVVar: %v", err) } + const wantNotifyMsg = `hello Notify` + if err := v.Notify(wantNotifyMsg, LogInfoLevel, make(map[string]interface{})); err != nil { + t.Fatalf("failed to Notify: %v", err) + } + gotNotifyMsg, err := v.Exec(":messages", true) + if err != nil { + t.Fatalf("failed to messages command: %v", err) + } + if wantNotifyMsg != gotNotifyMsg { + t.Fatalf("Notify(%[1]q, %[2]q) = %[3]q, want: %[1]q", wantNotifyMsg, LogInfoLevel, gotNotifyMsg) + } + + // clear messages + if _, err := v.Exec(":messages clear", false); err != nil { + t.Fatalf("failed to SetVVar: %v", err) + } + + const wantNotifyErr = `hello Notify Error` + if err := v.Notify(wantNotifyErr, LogErrorLevel, make(map[string]interface{})); err != nil { + t.Fatalf("failed to Notify: %v", err) + } + var gotNotifyErr string + if err := v.VVar("errmsg", &gotNotifyErr); err != nil { + t.Fatalf("could not get v:errmsg nvim variable: %v", err) + } + if wantNotifyErr != gotNotifyErr { + t.Fatalf("Notify(%[1]q, %[2]q) = %[3]q, want: %[1]q", wantNotifyErr, LogErrorLevel, gotNotifyErr) + } + // clear messages if _, err := v.Exec(":messages clear", false); err != nil { t.Fatalf("failed to SetVVar: %v", err) @@ -1023,9 +1106,51 @@ func testMessage(v *Nvim) func(*testing.T) { t.Fatalf("b.WritelnErr(%q) = %q, want: %q", wantWritelnErr, gotWritelnErr, wantWritelnErr) } - // cleanup v:statusmsg - if err := v.SetVVar("statusmsg", ""); err != nil { - t.Fatalf("failed to SetVVar: %v", err) + // clear messages + b.Exec(":messages clear", false, new(string)) + if err := b.Execute(); err != nil { + t.Fatalf("failed to \":messages clear\" command: %v", err) + } + + const wantNotifyMsg = `hello Notify` + b.Notify(wantNotifyMsg, LogInfoLevel, make(map[string]interface{})) + if err := b.Execute(); err != nil { + t.Fatalf("failed to Notify: %v", err) + } + var gotNotifyMsg string + b.Exec(":messages", true, &gotNotifyMsg) + if err := b.Execute(); err != nil { + t.Fatalf("failed to \":messages\" command: %v", err) + } + if wantNotifyMsg != gotNotifyMsg { + t.Fatalf("Notify(%[1]q, %[2]q) = %[3]q, want: %[1]q", wantNotifyMsg, LogInfoLevel, gotNotifyMsg) + } + + // clear messages + b.Exec(":messages clear", false, new(string)) + b.SetVVar("statusmsg", "") + if err := b.Execute(); err != nil { + t.Fatalf("failed to \":messages clear\" command: %v", err) + } + + const wantNotifyErr = `hello Notify Error` + b.Notify(wantNotifyErr, LogErrorLevel, make(map[string]interface{})) + if err := b.Execute(); err != nil { + t.Fatalf("failed to Notify: %v", err) + } + var gotNotifyErr string + b.VVar("errmsg", &gotNotifyErr) + if err := b.Execute(); err != nil { + t.Fatalf("could not get v:errmsg nvim variable: %v", err) + } + if wantNotifyErr != gotNotifyErr { + t.Fatalf("Notify(%[1]q, %[2]q) = %[3]q, want: %[1]q", wantNotifyErr, LogErrorLevel, gotNotifyErr) + } + + // clear messages + b.Exec(":messages clear", false, new(string)) + if err := b.Execute(); err != nil { + t.Fatalf("failed to \":messages clear\" command: %v", err) } }) } @@ -1802,6 +1927,77 @@ func testOptionsInfo(v *Nvim) func(*testing.T) { } } +// TODO(zchee): correct testcase +func testTerm(v *Nvim) func(*testing.T) { + return func(t *testing.T) { + t.Run("Nvim", func(t *testing.T) { + t.Parallel() + + buf, err := v.CreateBuffer(true, true) + if err != nil { + t.Fatal(err) + } + + cfg := &WindowConfig{ + Relative: "editor", + Width: 79, + Height: 31, + Row: 1, + Col: 1, + } + if _, err := v.OpenWindow(buf, false, cfg); err != nil { + t.Fatal(err) + } + + termID, err := v.OpenTerm(buf, make(map[string]interface{})) + if err != nil { + t.Fatal(err) + } + + data := "\x1b[38;2;00;00;255mTRUECOLOR\x1b[0m" + if err := v.Call("chansend", nil, termID, data); err != nil { + t.Fatal(err) + } + }) + + t.Run("Batch", func(t *testing.T) { + t.Parallel() + + b := v.NewBatch() + + var buf Buffer + b.CreateBuffer(true, true, &buf) + if err := b.Execute(); err != nil { + t.Fatal(err) + } + + cfg := &WindowConfig{ + Relative: "editor", + Width: 79, + Height: 31, + Row: 1, + Col: 1, + } + var win Window + b.OpenWindow(buf, false, cfg, &win) + + var termID int + b.OpenTerm(buf, make(map[string]interface{}), &termID) + + if err := b.Execute(); err != nil { + t.Fatal(err) + } + + data := "\x1b[38;2;00;00;255mTRUECOLOR\x1b[0m" + b.Call("chansend", nil, termID, data) + + if err := b.Execute(); err != nil { + t.Fatal(err) + } + }) + } +} + func TestDial(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("not supported dial unix socket on windows GOOS") @@ -1890,3 +2086,54 @@ func clearBuffer(tb testing.TB, v *Nvim, buffer Buffer) { tb.Fatal(err) } } + +func TestLogLevel_String(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + level LogLevel + want string + }{ + { + name: "Trace", + level: LogTraceLevel, + want: "TraceLevel", + }, + { + name: "Debug", + level: LogDebugLevel, + want: "DebugLevel", + }, + { + name: "Info", + level: LogInfoLevel, + want: "InfoLevel", + }, + { + name: "Warn", + level: LogWarnLevel, + want: "WarnLevel", + }, + { + name: "Error", + level: LogErrorLevel, + want: "ErrorLevel", + }, + { + name: "unkonwn", + level: LogLevel(-1), + want: "unkonwn Level", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if got := tt.level.String(); got != tt.want { + t.Errorf("LogLevel.String() = %v, want %v", tt.want, got) + } + }) + } +} diff --git a/nvim/types.go b/nvim/types.go index d5608613..103b71bb 100644 --- a/nvim/types.go +++ b/nvim/types.go @@ -473,7 +473,7 @@ type WindowConfig struct { Win Window `msgpack:"win,omitempty"` // Anchor is the decides which corner of the float to place at row and col. - Anchor string `msgpack:"anchor,omitempty" empty:"NW"` + Anchor string `msgpack:"anchor,omitempty"` // Width is the window width (in character cells). Minimum of 1. Width int `msgpack:"width" empty:"1"` @@ -553,3 +553,35 @@ type OptionInfo struct { // FlagList whether the list of single char flags. FlagList bool `msgpack:"flaglist"` } + +// LogLevel represents a nvim log level. +type LogLevel int + +// list of LogLevels. +// +// Should kept sync neovim LogLevel. +const ( + LogTraceLevel LogLevel = iota + LogDebugLevel + LogInfoLevel + LogWarnLevel + LogErrorLevel +) + +// String returns a string representation of the LogLevel. +func (level LogLevel) String() string { + switch level { + case LogTraceLevel: + return "TraceLevel" + case LogDebugLevel: + return "DebugLevel" + case LogInfoLevel: + return "InfoLevel" + case LogWarnLevel: + return "WarnLevel" + case LogErrorLevel: + return "ErrorLevel" + default: + return "unkonwn Level" + } +}