Merge branch 'master' into update-justfile-syntax

This commit is contained in:
Mikko 2025-03-14 15:02:49 +02:00 committed by GitHub
commit 6474fd03da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
244 changed files with 5754 additions and 3056 deletions

46
.github/workflows/nightly.yaml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Nightly builds
on:
workflow_dispatch: # Allows manual trigger
schedule:
- cron: '0 0 * * *'
jobs:
nightly:
strategy:
matrix:
go-version: [1.23.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Setup
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
fetch-depth: 0
fetch-tags: true
- name: Build
run: tools/cross-compile.sh nightly
- name: Tag
uses: rickstaa/action-create-tag@v1
with:
tag: nightly
force_push_tag: true
message: "Pre-release nightly"
- name: Publish
uses: softprops/action-gh-release@v2
with:
name: nightly
tag_name: nightly
files: binaries/*
prerelease: true
- name: Cleanup
run: rm -rf binaries

36
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Release builds
on:
workflow_dispatch: # Allows manual trigger
# push:
# tags:
# - 'v*.*.*' # automatically react on semantic versioned tags
jobs:
release:
strategy:
matrix:
go-version: [1.23.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Setup
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Build
run: tools/cross-compile.sh
- name: Publish
uses: softprops/action-gh-release@v2
with:
files: binaries/*
- name: Cleanup
run: rm -rf binaries

View File

@ -4,14 +4,19 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x] go-version: [1.19.x, 1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3 cache: false
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Build - name: Build
run: | run: |

View File

@ -430,7 +430,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
github.com/gdamore/tcell/LICENSE github.com/gdamore/tcell/LICENSE
================ ================
github.com/zyedidia/tcell/LICENSE (fork) github.com/micro-editor/tcell/LICENSE (fork)
================ ================
@ -1048,7 +1048,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
github.com/flynn/json5/LICENSE github.com/flynn/json5/LICENSE
================ ================
github.com/zyedidia/json5/LICENSE (fork) github.com/micro-editor/json5/LICENSE (fork)
================ ================
Decoder code based on package encoding/json from the Go language. Decoder code based on package encoding/json from the Go language.
@ -1108,7 +1108,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/james4k/terminal/LICENSE github.com/james4k/terminal/LICENSE
================ ================
github.com/zyedidia/terminal/LICENSE (fork) github.com/micro-editor/terminal/LICENSE (fork)
================ ================
Copyright (C) 2013 James Gray Copyright (C) 2013 James Gray

View File

@ -5,24 +5,31 @@ VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH
HASH = $(shell git rev-parse --short HEAD) HASH = $(shell git rev-parse --short HEAD)
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \ DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
go run tools/build-date.go) go run tools/build-date.go)
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
GOBIN ?= $(shell go env GOPATH)/bin GOBIN ?= $(shell go env GOPATH)/bin
GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)' GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)'
DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON
VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/' VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/'
CGO_ENABLED := $(if $(CGO_ENABLED),$(CGO_ENABLED),0)
ADDITIONAL_GO_LINKER_FLAGS := ""
GOHOSTOS = $(shell go env GOHOSTOS)
ifeq ($(GOHOSTOS), darwin)
# Native darwin resp. macOS builds need external and dynamic linking
ADDITIONAL_GO_LINKER_FLAGS += $(shell GOOS=$(GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
CGO_ENABLED = 1
endif
build: generate build-quick build: generate build-quick
build-quick: build-quick:
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-dbg: build-dbg:
go build -trimpath -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "$(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
build-tags: fetch-tags generate build-tags: fetch-tags build
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-all: build build-all: build
@ -32,7 +39,7 @@ install: generate
install-all: install install-all: install
fetch-tags: fetch-tags:
git fetch --tags git fetch --tags --force
generate: generate:
GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime

View File

@ -18,25 +18,9 @@ Here is a picture of micro editing its source code.
![Screenshot](./assets/micro-solarized.png) ![Screenshot](./assets/micro-solarized.png)
To see more screenshots of micro, showcasing some of the default color schemes, see [here](https://micro-editor.github.io). To see more screenshots of micro, showcasing some of the default color schemes, see [here](https://micro-editor.github.io).
You can also check out the website for Micro at https://micro-editor.github.io. You can also check out the website for Micro at https://micro-editor.github.io.
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Prebuilt binaries](#pre-built-binaries)
- [Package Managers](#package-managers)
- [Building from source](#building-from-source)
- [Fully static binary](#fully-static-binary)
- [macOS terminal](#macos-terminal)
- [Linux clipboard support](#linux-clipboard-support)
- [Colors and syntax highlighting](#colors-and-syntax-highlighting)
- [Cygwin, Mingw, Plan9](#cygwin-mingw-plan9)
- [Usage](#usage)
- [Documentation and Help](#documentation-and-help)
- [Contributing](#contributing)
- - - - - -
## Features ## Features
@ -68,6 +52,7 @@ You can also check out the website for Micro at https://micro-editor.github.io.
- Small and simple. - Small and simple.
- Easily configurable. - Easily configurable.
- Macros. - Macros.
- Smart highlighting of trailing whitespace and tab vs space errors.
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, … - Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
## Installation ## Installation
@ -87,7 +72,7 @@ Pre-built binaries are distributed in [releases](https://github.com/zyedidia/mic
To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`. To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`.
#### Quick-install script #### Third-party quick-install script
```bash ```bash
curl https://getmic.ro | bash curl https://getmic.ro | bash
@ -137,24 +122,33 @@ for other operating systems. These packages are not guaranteed to be up-to-date.
<!-- * `apt install micro` (Ubuntu 20.04 `focal`, and Debian `unstable | testing | buster-backports`). At the moment, this package (2.0.1-1) is outdated and has a known bug where debug mode is enabled. --> <!-- * `apt install micro` (Ubuntu 20.04 `focal`, and Debian `unstable | testing | buster-backports`). At the moment, this package (2.0.1-1) is outdated and has a known bug where debug mode is enabled. -->
* Linux: Available in distro-specific package managers. * Linux:
* `dnf install micro` (Fedora). * distro-specific package managers:
* `apt install micro` (Ubuntu and Debian). * `dnf install micro` (Fedora).
* `pacman -S micro` (Arch Linux). * `apt install micro` (Ubuntu and Debian).
* `emerge app-editors/micro` (Gentoo). * `pacman -S micro` (Arch Linux).
* `zypper install micro-editor` (SUSE) * `emerge app-editors/micro` (Gentoo).
* `eopkg install micro` (Solus). * `zypper install micro-editor` (SUSE)
* `pacstall -I micro` (Pacstall). * `eopkg install micro` (Solus).
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux. * `pacstall -I micro` (Pacstall).
* Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop). * `apt-get install micro` (ALT Linux)
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
* distro-agnostic package managers:
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
* `flox install micro` (with [Flox](https://flox.dev))
* Windows: [Chocolatey](https://chocolatey.org), [Scoop](https://scoop.sh/) and [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/winget/).
* `choco install micro`. * `choco install micro`.
* `scoop install micro`. * `scoop install micro`.
* `winget install zyedidia.micro`
* OpenBSD: Available in the ports tree and also available as a binary package. * OpenBSD: Available in the ports tree and also available as a binary package.
* `pkd_add -v micro`. * `pkg_add -v micro`.
* NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](http://www.pkgsrc.org/)-current: * NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](http://www.pkgsrc.org/)-current:
* `pkg_add micro` * `pkg_add micro`
* macOS with [MacPorts](https://www.macports.org): * macOS: Available in package managers.
* `sudo port install micro` * `sudo port install micro` (with [MacPorts](https://www.macports.org))
* `brew install micro` (with [Homebrew](https://brew.sh/))
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
* `flox install micro` (with [Flox](https://flox.dev))
**Note for Linux desktop environments:** **Note for Linux desktop environments:**
@ -168,7 +162,7 @@ Without these tools installed, micro will use an internal clipboard for copy and
If your operating system does not have a binary release, but does run Go, you can build from source. If your operating system does not have a binary release, but does run Go, you can build from source.
Make sure that you have Go version 1.16 or greater and Go modules are enabled. Make sure that you have Go version 1.19 or greater and Go modules are enabled.
``` ```
git clone https://github.com/zyedidia/micro git clone https://github.com/zyedidia/micro
@ -186,16 +180,20 @@ You can install directly with `go get` (`go get github.com/zyedidia/micro/cmd/mi
recommended because it doesn't build micro with version information (necessary for the plugin manager), recommended because it doesn't build micro with version information (necessary for the plugin manager),
and doesn't disable debug mode. and doesn't disable debug mode.
### Fully static binary ### Fully static or dynamically linked binary
By default, the micro binary will dynamically link with core system libraries (this is generally By default, the micro binary is linked statically to increase the portability of the prebuilt binaries.
recommended for security and portability). However, there is a fully static prebuilt binary that This behavior can simply be overriden by providing `CGO_ENABLED=1` to the build target.
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
``` ```
CGO_ENABLED=0 make build CGO_ENABLED=1 make build
``` ```
Afterwards the micro binary will dynamically link with the present core system libraries.
**Note for Mac:**
Native macOS builds are done with `CGO_ENABLED=1` forced set to support adding the "Information Property List" in the linker step.
### macOS terminal ### macOS terminal
If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Presets...`, and select `Esc+` for `Left Option Key` in the same menu. The newest versions also support true color. If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Presets...`, and select `Esc+` for `Left Option Key` in the same menu. The newest versions also support true color.

View File

@ -12,5 +12,4 @@ Keywords=text;editor;syntax;terminal;
Exec=micro %F Exec=micro %F
StartupNotify=false StartupNotify=false
Terminal=true Terminal=true
NoDisplay=true
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff; MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;

View File

@ -3,8 +3,8 @@ package main
import ( import (
"bufio" "bufio"
"encoding/gob" "encoding/gob"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -12,6 +12,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
) )
func shouldContinue() bool { func shouldContinue() bool {
@ -39,7 +40,16 @@ func CleanConfig() {
} }
fmt.Println("Cleaning default settings") fmt.Println("Cleaning default settings")
config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
settingsFile := filepath.Join(config.ConfigDir, "settings.json")
err := config.WriteSettings(settingsFile)
if err != nil {
if errors.Is(err, util.ErrOverwrite) {
fmt.Println(err.Error())
} else {
fmt.Println("Error writing settings.json file: " + err.Error())
}
}
// detect unused options // detect unused options
var unusedOptions []string var unusedOptions []string
@ -67,16 +77,20 @@ func CleanConfig() {
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s]) fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
} }
fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json")) fmt.Printf("These options will be removed from %s\n", settingsFile)
if shouldContinue() { if shouldContinue() {
for _, s := range unusedOptions { for _, s := range unusedOptions {
delete(config.GlobalSettings, s) delete(config.GlobalSettings, s)
} }
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json")) err := config.OverwriteSettings(settingsFile)
if err != nil { if err != nil {
fmt.Println("Error writing settings.json file: " + err.Error()) if errors.Is(err, util.ErrOverwrite) {
fmt.Println(err.Error())
} else {
fmt.Println("Error overwriting settings.json file: " + err.Error())
}
} }
fmt.Println("Removed unused options") fmt.Println("Removed unused options")
@ -85,12 +99,13 @@ func CleanConfig() {
} }
// detect incorrectly formatted buffer/ files // detect incorrectly formatted buffer/ files
files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers")) buffersPath := filepath.Join(config.ConfigDir, "buffers")
files, err := os.ReadDir(buffersPath)
if err == nil { if err == nil {
var badFiles []string var badFiles []string
var buffer buffer.SerializedBuffer var buffer buffer.SerializedBuffer
for _, f := range files { for _, f := range files {
fname := filepath.Join(config.ConfigDir, "buffers", f.Name()) fname := filepath.Join(buffersPath, f.Name())
file, e := os.Open(fname) file, e := os.Open(fname)
if e == nil { if e == nil {
@ -105,9 +120,9 @@ func CleanConfig() {
} }
if len(badFiles) > 0 { if len(badFiles) > 0 {
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers")) fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), buffersPath)
fmt.Println("These files store cursor and undo history.") fmt.Println("These files store cursor and undo history.")
fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers")) fmt.Printf("Removing badly formatted files in %s\n", buffersPath)
if shouldContinue() { if shouldContinue() {
removed := 0 removed := 0

View File

@ -18,7 +18,7 @@ func (NullWriter) Write(data []byte) (n int, err error) {
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables // InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
func InitLog() { func InitLog() {
if util.Debug == "ON" { if util.Debug == "ON" {
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, util.FileMode)
if err != nil { if err != nil {
log.Fatalf("error opening file: %v", err) log.Fatalf("error opening file: %v", err)
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"log" "log"
"time"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
@ -47,14 +48,18 @@ func luaImportMicro() *lua.LTable {
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar)) ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println)) ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua)) ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() action.Pane { ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() *action.BufPane {
return action.MainTab().CurPane() return action.MainTab().CurPane()
})) }))
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab)) ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab))
ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList { ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList {
return action.Tabs return action.Tabs
})) }))
ulua.L.SetField(pkg, "Lock", luar.New(ulua.L, &ulua.Lock)) ulua.L.SetField(pkg, "After", luar.New(ulua.L, func(t time.Duration, f func()) {
time.AfterFunc(t, func() {
timerChan <- f
})
}))
return pkg return pkg
} }

View File

@ -4,10 +4,10 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
@ -23,17 +23,13 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard" "github.com/zyedidia/micro/v2/internal/clipboard"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
var ( var (
// Event channel
autosave chan bool
// Command line flags // Command line flags
flagVersion = flag.Bool("version", false, "Show the version number and information") flagVersion = flag.Bool("version", false, "Show the version number and information")
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory") flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
@ -44,8 +40,9 @@ var (
flagClean = flag.Bool("clean", false, "Clean configuration directory") flagClean = flag.Bool("clean", false, "Clean configuration directory")
optionFlags map[string]*string optionFlags map[string]*string
sigterm chan os.Signal sighup chan os.Signal
sighup chan os.Signal
timerChan chan func()
) )
func InitFlags() { func InitFlags() {
@ -68,7 +65,7 @@ func InitFlags() {
fmt.Println("-version") fmt.Println("-version")
fmt.Println(" \tShow the version number and information") fmt.Println(" \tShow the version number and information")
fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n") fmt.Print("\nMicro's plugins can be managed at the command line with the following commands.\n")
fmt.Println("-plugin install [PLUGIN]...") fmt.Println("-plugin install [PLUGIN]...")
fmt.Println(" \tInstall plugin(s)") fmt.Println(" \tInstall plugin(s)")
fmt.Println("-plugin remove [PLUGIN]...") fmt.Println("-plugin remove [PLUGIN]...")
@ -102,7 +99,7 @@ func InitFlags() {
fmt.Println("Version:", util.Version) fmt.Println("Version:", util.Version)
fmt.Println("Commit hash:", util.CommitHash) fmt.Println("Commit hash:", util.CommitHash)
fmt.Println("Compiled on", util.CompileDate) fmt.Println("Compiled on", util.CompileDate)
os.Exit(0) exit(0)
} }
if *flagOptions { if *flagOptions {
@ -118,7 +115,7 @@ func InitFlags() {
fmt.Printf("-%s value\n", k) fmt.Printf("-%s value\n", k)
fmt.Printf(" \tDefault value: '%v'\n", v) fmt.Printf(" \tDefault value: '%v'\n", v)
} }
os.Exit(0) exit(0)
} }
if util.Debug == "OFF" && *flagDebug { if util.Debug == "OFF" && *flagDebug {
@ -139,7 +136,7 @@ func DoPluginFlags() {
CleanConfig() CleanConfig()
} }
os.Exit(0) exit(0)
} }
} }
@ -212,7 +209,7 @@ func LoadInput(args []string) []*buffer.Buffer {
// Option 2 // Option 2
// The input is not a terminal, so something is being piped in // The input is not a terminal, so something is being piped in
// and we should read from stdin // and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin) input, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
screen.TermMessage("Error reading from stdin: ", err) screen.TermMessage("Error reading from stdin: ", err)
input = []byte{} input = []byte{}
@ -226,12 +223,55 @@ func LoadInput(args []string) []*buffer.Buffer {
return buffers return buffers
} }
func checkBackup(name string) error {
target := filepath.Join(config.ConfigDir, name)
backup := util.AppendBackupSuffix(target)
if info, err := os.Stat(backup); err == nil {
input, err := os.ReadFile(backup)
if err == nil {
t := info.ModTime()
msg := fmt.Sprintf(buffer.BackupMsg, target, t.Format("Mon Jan _2 at 15:04, 2006"), backup)
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
if choice%3 == 0 {
// recover
err := os.WriteFile(target, input, util.FileMode)
if err != nil {
return err
}
return os.Remove(backup)
} else if choice%3 == 1 {
// delete
return os.Remove(backup)
} else if choice%3 == 2 {
// abort
return errors.New("Aborted")
}
}
}
return nil
}
func exit(rc int) {
for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(rc)
}
func main() { func main() {
defer func() { defer func() {
if util.Stdout.Len() > 0 { if util.Stdout.Len() > 0 {
fmt.Fprint(os.Stdout, util.Stdout.String()) fmt.Fprint(os.Stdout, util.Stdout.String())
} }
os.Exit(0) exit(0)
}() }()
var err error var err error
@ -256,7 +296,15 @@ func main() {
screen.TermMessage(err) screen.TermMessage(err)
} }
config.InitRuntimeFiles() config.InitRuntimeFiles(true)
config.InitPlugins()
err = checkBackup("settings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
err = config.ReadSettings() err = config.ReadSettings()
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
@ -274,7 +322,12 @@ func main() {
screen.TermMessage(err) screen.TermMessage(err)
continue continue
} }
if err = config.OptionIsValid(k, nativeValue); err != nil {
screen.TermMessage(err)
continue
}
config.GlobalSettings[k] = nativeValue config.GlobalSettings[k] = nativeValue
config.VolatileSettings[k] = true
} }
} }
@ -284,7 +337,7 @@ func main() {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
fmt.Println("Fatal: Micro could not initialize a Screen.") fmt.Println("Fatal: Micro could not initialize a Screen.")
os.Exit(1) exit(1)
} }
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string)) m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
clipErr := clipboard.Initialize(m) clipErr := clipboard.Initialize(m)
@ -303,7 +356,7 @@ func main() {
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Backup() b.Backup()
} }
os.Exit(1) exit(1)
} }
}() }()
@ -312,6 +365,12 @@ func main() {
screen.TermMessage(err) screen.TermMessage(err)
} }
err = checkBackup("bindings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
action.InitBindings() action.InitBindings()
action.InitCommands() action.InitCommands()
@ -352,18 +411,20 @@ func main() {
log.Println(clipErr, " or change 'clipboard' option") log.Println(clipErr, " or change 'clipboard' option")
} }
config.StartAutoSave()
if a := config.GetGlobalOption("autosave").(float64); a > 0 { if a := config.GetGlobalOption("autosave").(float64); a > 0 {
config.SetAutoTime(int(a)) config.SetAutoTime(a)
config.StartAutoSave()
} }
screen.Events = make(chan tcell.Event) screen.Events = make(chan tcell.Event)
sigterm = make(chan os.Signal, 1) util.Sigterm = make(chan os.Signal, 1)
sighup = make(chan os.Signal, 1) sighup = make(chan os.Signal, 1)
signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT) signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
signal.Notify(sighup, syscall.SIGHUP) signal.Notify(sighup, syscall.SIGHUP)
timerChan = make(chan func())
// Here is the event loop which runs in a separate thread // Here is the event loop which runs in a separate thread
go func() { go func() {
for { for {
@ -414,39 +475,26 @@ func DoEvent() {
select { select {
case f := <-shell.Jobs: case f := <-shell.Jobs:
// If a new job has finished while running in the background we should execute the callback // If a new job has finished while running in the background we should execute the callback
ulua.Lock.Lock()
f.Function(f.Output, f.Args) f.Function(f.Output, f.Args)
ulua.Lock.Unlock()
case <-config.Autosave: case <-config.Autosave:
ulua.Lock.Lock()
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Save() b.AutoSave()
} }
ulua.Lock.Unlock()
case <-shell.CloseTerms: case <-shell.CloseTerms:
action.Tabs.CloseTerms()
case event = <-screen.Events: case event = <-screen.Events:
case <-screen.DrawChan(): case <-screen.DrawChan():
for len(screen.DrawChan()) > 0 { for len(screen.DrawChan()) > 0 {
<-screen.DrawChan() <-screen.DrawChan()
} }
case f := <-timerChan:
f()
case b := <-buffer.BackupCompleteChan:
b.RequestedBackup = false
case <-sighup: case <-sighup:
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() { case <-util.Sigterm:
b.Fini() exit(0)
}
}
os.Exit(0)
case <-sigterm:
for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
if e, ok := event.(*tcell.EventError); ok { if e, ok := event.(*tcell.EventError); ok {
@ -454,27 +502,25 @@ func DoEvent() {
if e.Err() == io.EOF { if e.Err() == io.EOF {
// shutdown due to terminal closing/becoming inaccessible // shutdown due to terminal closing/becoming inaccessible
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
return return
} }
ulua.Lock.Lock() if event != nil {
// if event != nil { _, resize := event.(*tcell.EventResize)
if action.InfoBar.HasPrompt { if resize {
action.InfoBar.HandleEvent(event) action.InfoBar.HandleEvent(event)
} else { action.Tabs.HandleEvent(event)
action.Tabs.HandleEvent(event) } else if action.InfoBar.HasPrompt {
action.InfoBar.HandleEvent(event)
} else {
action.Tabs.HandleEvent(event)
}
}
err := config.RunPluginFn("onAnyEvent")
if err != nil {
screen.TermMessage(err)
} }
// }
ulua.Lock.Unlock()
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"testing" "testing"
@ -13,7 +12,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
var tempDir string var tempDir string
@ -26,7 +25,7 @@ func init() {
func startup(args []string) (tcell.SimulationScreen, error) { func startup(args []string) (tcell.SimulationScreen, error) {
var err error var err error
tempDir, err = ioutil.TempDir("", "micro_test") tempDir, err = os.MkdirTemp("", "micro_test")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -35,7 +34,9 @@ func startup(args []string) (tcell.SimulationScreen, error) {
return nil, err return nil, err
} }
config.InitRuntimeFiles() config.InitRuntimeFiles(true)
config.InitPlugins()
err = config.ReadSettings() err = config.ReadSettings()
if err != nil { if err != nil {
return nil, err return nil, err
@ -107,7 +108,10 @@ func handleEvent() {
if e != nil { if e != nil {
screen.Events <- e screen.Events <- e
} }
DoEvent()
for len(screen.DrawChan()) > 0 || len(screen.Events) > 0 {
DoEvent()
}
} }
func injectKey(key tcell.Key, r rune, mod tcell.ModMask) { func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
@ -149,20 +153,32 @@ func openFile(file string) {
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
} }
func createTestFile(name string, content string) (string, error) { func findBuffer(file string) *buffer.Buffer {
testf, err := ioutil.TempFile("", name) var buf *buffer.Buffer
for _, b := range buffer.OpenBuffers {
if b.Path == file {
buf = b
}
}
return buf
}
func createTestFile(t *testing.T, content string) string {
f, err := os.CreateTemp(t.TempDir(), "")
if err != nil { if err != nil {
return "", err t.Fatal(err)
}
defer func() {
if err := f.Close(); err != nil {
t.Fatal(err)
}
}()
if _, err := f.WriteString(content); err != nil {
t.Fatal(err)
} }
if _, err := testf.Write([]byte(content)); err != nil { return f.Name()
return "", err
}
if err := testf.Close(); err != nil {
return "", err
}
return testf.Name(), nil
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -179,25 +195,12 @@ func TestMain(m *testing.M) {
} }
func TestSimpleEdit(t *testing.T) { func TestSimpleEdit(t *testing.T) {
file, err := createTestFile("micro_simple_edit_test", "base content") file := createTestFile(t, "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
var buf *buffer.Buffer if findBuffer(file) == nil {
for _, b := range buffer.OpenBuffers { t.Fatalf("Could not find buffer %s", file)
if b.Path == file {
buf = b
}
}
if buf == nil {
t.Errorf("Could not find buffer %s", file)
return
} }
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
@ -215,25 +218,23 @@ func TestSimpleEdit(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, "firstfoobar\nbase content\n", string(data)) assert.Equal(t, "firstfoobar\nbase content\n", string(data))
} }
func TestMouse(t *testing.T) { func TestMouse(t *testing.T) {
file, err := createTestFile("micro_mouse_test", "base content") file := createTestFile(t, "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
if findBuffer(file) == nil {
t.Fatalf("Could not find buffer %s", file)
}
// buffer: // buffer:
// base content // base content
// the selections need to happen at different locations to avoid a double click // the selections need to happen at different locations to avoid a double click
@ -262,10 +263,9 @@ func TestMouse(t *testing.T) {
// base content // base content
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data)) assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
@ -288,25 +288,23 @@ Ernleȝe test_string æðelen
` `
func TestSearchAndReplace(t *testing.T) { func TestSearchAndReplace(t *testing.T) {
file, err := createTestFile("micro_search_replace_test", srTestStart) file := createTestFile(t, srTestStart)
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
if findBuffer(file) == nil {
t.Fatalf("Could not find buffer %s", file)
}
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl) injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string")) injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string"))
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, srTest2, string(data)) assert.Equal(t, srTest2, string(data))
@ -319,10 +317,9 @@ func TestSearchAndReplace(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err = ioutil.ReadFile(file) data, err = os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, srTest3, string(data)) assert.Equal(t, srTest3, string(data))

View File

@ -25,6 +25,9 @@
<category>TextEditor</category> <category>TextEditor</category>
</categories> </categories>
<releases> <releases>
<release version="2.0.14" date="2024-08-27"/>
<release version="2.0.13" date="2023-10-22"/>
<release version="2.0.12" date="2023-09-06"/>
<release version="2.0.11" date="2022-08-01"/> <release version="2.0.11" date="2022-08-01"/>
</releases> </releases>
<provides> <provides>

View File

@ -170,10 +170,19 @@
"default": false "default": false
}, },
"matchbrace": { "matchbrace": {
"description": "Whether to underline matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options", "description": "Whether to show matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "boolean", "type": "boolean",
"default": true "default": true
}, },
"matchbracestyle": {
"description": "Whether to underline or highlight matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "string",
"enum": [
"underline",
"highlight"
],
"default": "underline"
},
"mkparents": { "mkparents": {
"description": "Whether to create missing directories\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options", "description": "Whether to create missing directories\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "boolean", "type": "boolean",
@ -296,8 +305,8 @@
}, },
"statusline": { "statusline": {
"description": "Whether to display a status line\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options", "description": "Whether to display a status line\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "string", "type": "boolean",
"default": "sudo" "default": true
}, },
"sucmd": { "sucmd": {
"description": "A super user command\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options", "description": "A super user command\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
@ -355,4 +364,4 @@
} }
}, },
"additionalProperties": false "additionalProperties": false
} }

33
go.mod
View File

@ -5,26 +5,35 @@ require (
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/go-errors/errors v1.0.1 github.com/go-errors/errors v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.11 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.7 github.com/mattn/go-runewidth v0.0.16
github.com/micro-editor/json5 v1.0.1-micro
github.com/micro-editor/tcell/v2 v2.0.11
github.com/micro-editor/terminal v0.0.0-20250105114944-ffd0fc59e777
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/sergi/go-diff v1.1.0 github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb github.com/yuin/gopher-lua v1.1.1
github.com/zyedidia/clipper v0.1.1 github.com/zyedidia/clipper v0.1.1
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d golang.org/x/text v0.4.0
github.com/zyedidia/tcell/v2 v2.0.10 // indirect
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef
golang.org/x/text v0.3.8
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
layeh.com/gopher-luar v1.0.7 layeh.com/gopher-luar v1.0.11
) )
replace github.com/kballard/go-shellquote => github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655 require (
github.com/creack/pty v1.1.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/zyedidia/poller v1.0.1 // indirect
golang.org/x/sys v0.6.0 // indirect
)
replace github.com/mattn/go-runewidth => github.com/zyedidia/go-runewidth v0.0.12 replace github.com/kballard/go-shellquote => github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.7 replace layeh.com/gopher-luar v1.0.11 => github.com/layeh/gopher-luar v1.0.11
go 1.16 go 1.19

79
go.sum
View File

@ -19,84 +19,57 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/layeh/gopher-luar v1.0.7 h1:wnfZhYiJM748y1A4qYBfcFeMY9HWbdERny+ZL0f/jWc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/layeh/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk= github.com/layeh/gopher-luar v1.0.11 h1:ss6t9OtykOiETBScJylSMPhuYAtOmpH5rSX10/wCcis=
github.com/layeh/gopher-luar v1.0.11/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5 h1:D7BPnsedXiKo/e8RTFX419/52ICNhU8UKPQGZ/0yiLc=
github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5/go.mod h1:zaPgW/fDiW4MUfEwxpC+GB/bhvX44NJaNHmRAC9auHQ=
github.com/micro-editor/json5 v1.0.1-micro h1:5Y4MuzhkmW0sQQNPvrIVevIOKi557qsznwjRr4iq1AI=
github.com/micro-editor/json5 v1.0.1-micro/go.mod h1:cmlPHZ1JKOXNse0/3zwwKj/GUpzAVkzx4lZDkpHl4q0=
github.com/micro-editor/tcell/v2 v2.0.11 h1:USjdpBSmbocx2yPARbY19KcUSj+ZerScrdmBqGjzoX4=
github.com/micro-editor/tcell/v2 v2.0.11/go.mod h1:kVYk6NOwYJrboL/7IA7cCupk4o2NzyF/0UMLjeEJN/s=
github.com/micro-editor/terminal v0.0.0-20250105114944-ffd0fc59e777 h1:ddEDJwVqnxd8Yxtr6ZlUBBH3Kyf90jLHMJAbmU1bjuA=
github.com/micro-editor/terminal v0.0.0-20250105114944-ffd0fc59e777/go.mod h1:soCc8JsxxfCZd3y08pX67F16Bni5L/xtcAnqEswzs44=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8 h1:woqigIZtZUZxws1zZA99nAvuz2mQrxtWsuZSR9c8I/A=
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8/go.mod h1:6Yhx5ZJl5942QrNRWLwITArVT9okUXc5c3brgWJMoDc= github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8/go.mod h1:6Yhx5ZJl5942QrNRWLwITArVT9okUXc5c3brgWJMoDc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw= github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw=
github.com/zyedidia/clipper v0.1.1/go.mod h1:7YApPNiiTZTXdKKZG92G50qj6mnWEX975Sdu65J7YpQ= github.com/zyedidia/clipper v0.1.1/go.mod h1:7YApPNiiTZTXdKKZG92G50qj6mnWEX975Sdu65J7YpQ=
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew= github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c= github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
github.com/zyedidia/go-runewidth v0.0.12 h1:aHWj8qL3aH7caRzoPBJXe1pEaZBXHpKtfTuiBo5p74Q=
github.com/zyedidia/go-runewidth v0.0.12/go.mod h1:vF8djYdLmG8BJaUZ4CznFYCJ3pFR8m4B4VinTvTTarU=
github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655 h1:Z3RhH6hvcSx7eX6Q/pP6YVsgea/1eMDG99vtWwi3nK4=
github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655/go.mod h1:1sTqqO+kcYzZp43M5VsJe1tns9IzlSeC9jB6c2+o/5Y=
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d h1:zmDMkh22zXOB7gz8jFaI4GpI7llsPgzm38/jG0UgxjE=
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d/go.mod h1:NDJSTTYWivnza6zkRapeX2/LwhKPEMQ7bJxqgDVT78I=
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s= github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE= github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
github.com/zyedidia/tcell/v2 v2.0.9 h1:FxXRkE62N0GPHES7EMLtp2rteYqC9r1kVid8vJN1kOE=
github.com/zyedidia/tcell/v2 v2.0.9/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10-0.20221007181625-f562052bccb8 h1:53ULv4mmLyQDnqbjVxanckP57WSreWHwTmlLJrJEutY=
github.com/zyedidia/tcell/v2 v2.0.10-0.20221007181625-f562052bccb8/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230320201625-54f6acdada4a h1:W4TWa++Wk6uRGxZoxr2nPX1TpIEl+Wxv0mTtocG4TYc=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230320201625-54f6acdada4a/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230831153116-061c5b2c7260 h1:SCAmAacT5BxZsmOFdFy5zwwi6nj1MjA60gydjKdTgXo=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230831153116-061c5b2c7260/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10 h1:6fbbYAx/DYc9A//4jU1OeBrxtc9qJxYCZXCtGQbtTWU=
github.com/zyedidia/tcell/v2 v2.0.10/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef h1:LeB4Qs0Tss4r/Qh8pfsTTqagDYHysfKJLYzAH3MVfu0=
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef/go.mod h1:zeb8MJdcCObFKVvur3n2B4BANIPuo2Q8r4iiNs9Enx0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=

File diff suppressed because it is too large Load Diff

View File

@ -4,17 +4,18 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"unicode" "unicode"
"github.com/zyedidia/json5" "github.com/micro-editor/json5"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2" "github.com/zyedidia/micro/v2/internal/util"
"github.com/micro-editor/tcell/v2"
) )
var Binder = map[string]func(e Event, action string){ var Binder = map[string]func(e Event, action string){
@ -23,9 +24,13 @@ var Binder = map[string]func(e Event, action string){
"terminal": TermMapEvent, "terminal": TermMapEvent,
} }
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
func createBindingsIfNotExist(fname string) { func createBindingsIfNotExist(fname string) {
if _, e := os.Stat(fname); os.IsNotExist(e) { if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) {
ioutil.WriteFile(fname, []byte("{}"), 0644) writeFile(fname, []byte("{}"))
} }
} }
@ -37,7 +42,7 @@ func InitBindings() {
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
screen.TermMessage("Error reading bindings.json file: " + err.Error()) screen.TermMessage("Error reading bindings.json file: " + err.Error())
return return
@ -88,6 +93,10 @@ func BindKey(k, v string, bind func(e Event, a string)) {
return return
} }
if strings.HasPrefix(k, "\x1b") {
screen.RegisterRawSeq(k)
}
bind(event, v) bind(event, v)
// switch e := event.(type) { // switch e := event.(type) {
@ -153,7 +162,6 @@ modSearch:
k = k[5:] k = k[5:]
modifiers |= tcell.ModShift modifiers |= tcell.ModShift
case strings.HasPrefix(k, "\x1b"): case strings.HasPrefix(k, "\x1b"):
screen.Screen.RegisterRawSeq(k)
return RawEvent{ return RawEvent{
esc: k, esc: k,
}, true }, true
@ -173,39 +181,35 @@ modSearch:
// see if the key is in bindingKeys with the Ctrl prefix. // see if the key is in bindingKeys with the Ctrl prefix.
k = string(unicode.ToUpper(rune(k[0]))) + k[1:] k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
if code, ok := keyEvents["Ctrl"+k]; ok { if code, ok := keyEvents["Ctrl"+k]; ok {
var r tcell.Key
// Special case for escape, for some reason tcell doesn't send it with the esc character
if code < 256 && code != 27 {
r = code
}
// It is, we're done.
return KeyEvent{ return KeyEvent{
code: code, code: code,
mod: modifiers, mod: modifiers,
r: rune(r),
}, true }, true
} }
} }
// See if we can find the key in bindingKeys // See if we can find the key in bindingKeys
if code, ok := keyEvents[k]; ok { if code, ok := keyEvents[k]; ok {
var r tcell.Key
// Special case for escape, for some reason tcell doesn't send it with the esc character
if code < 256 && code != 27 {
r = code
}
return KeyEvent{ return KeyEvent{
code: code, code: code,
mod: modifiers, mod: modifiers,
r: rune(r),
}, true }, true
} }
var mstate MouseState = MousePress
if strings.HasSuffix(k, "Drag") {
k = k[:len(k)-4]
mstate = MouseDrag
} else if strings.HasSuffix(k, "Release") {
k = k[:len(k)-7]
mstate = MouseRelease
}
// See if we can find the key in bindingMouse // See if we can find the key in bindingMouse
if code, ok := mouseEvents[k]; ok { if code, ok := mouseEvents[k]; ok {
return MouseEvent{ return MouseEvent{
btn: code, btn: code,
mod: modifiers, mod: modifiers,
state: mstate,
}, true }, true
} }
@ -239,6 +243,24 @@ func findEvent(k string) (Event, error) {
return event, nil return event, nil
} }
func eventsEqual(e1 Event, e2 Event) bool {
seq1, ok1 := e1.(KeySequenceEvent)
seq2, ok2 := e2.(KeySequenceEvent)
if ok1 && ok2 {
if len(seq1.keys) != len(seq2.keys) {
return false
}
for i := 0; i < len(seq1.keys); i++ {
if seq1.keys[i] != seq2.keys[i] {
return false
}
}
return true
}
return e1 == e2
}
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json // TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
// Returns true if the keybinding already existed and a possible error // Returns true if the keybinding already existed and a possible error
func TryBindKey(k, v string, overwrite bool) (bool, error) { func TryBindKey(k, v string, overwrite bool) (bool, error) {
@ -248,7 +270,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
filename := filepath.Join(config.ConfigDir, "bindings.json") filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil { if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
return false, errors.New("Error reading bindings.json file: " + err.Error()) return false, errors.New("Error reading bindings.json file: " + err.Error())
} }
@ -264,28 +286,31 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
} }
found := false found := false
for ev := range parsed { var ev string
for ev = range parsed {
if e, err := findEvent(ev); err == nil { if e, err := findEvent(ev); err == nil {
if e == key { if eventsEqual(e, key) {
if overwrite {
parsed[ev] = v
}
found = true found = true
break break
} }
} }
} }
if found && !overwrite { if found {
return true, nil if overwrite {
} else if !found { parsed[ev] = v
} else {
return true, nil
}
} else {
parsed[k] = v parsed[k] = v
} }
BindKey(k, v, Binder["buffer"]) BindKey(k, v, Binder["buffer"])
txt, _ := json.MarshalIndent(parsed, "", " ") txt, _ := json.MarshalIndent(parsed, "", " ")
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
return true, writeFile(filename, txt)
} }
return false, e return false, e
} }
@ -298,7 +323,7 @@ func UnbindKey(k string) error {
filename := filepath.Join(config.ConfigDir, "bindings.json") filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil { if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
return errors.New("Error reading bindings.json file: " + err.Error()) return errors.New("Error reading bindings.json file: " + err.Error())
} }
@ -315,13 +340,17 @@ func UnbindKey(k string) error {
for ev := range parsed { for ev := range parsed {
if e, err := findEvent(ev); err == nil { if e, err := findEvent(ev); err == nil {
if e == key { if eventsEqual(e, key) {
delete(parsed, ev) delete(parsed, ev)
break break
} }
} }
} }
if strings.HasPrefix(k, "\x1b") {
screen.UnregisterRawSeq(k)
}
defaults := DefaultBindings("buffer") defaults := DefaultBindings("buffer")
if a, ok := defaults[k]; ok { if a, ok := defaults[k]; ok {
BindKey(k, a, Binder["buffer"]) BindKey(k, a, Binder["buffer"])
@ -331,7 +360,8 @@ func UnbindKey(k string) error {
} }
txt, _ := json.MarshalIndent(parsed, "", " ") txt, _ := json.MarshalIndent(parsed, "", " ")
return ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
return writeFile(filename, txt)
} }
return e return e
} }

View File

@ -8,15 +8,16 @@ import (
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type BufAction interface{}
// BufKeyAction represents an action bound to a key. // BufKeyAction represents an action bound to a key.
type BufKeyAction func(*BufPane) bool type BufKeyAction func(*BufPane) bool
@ -44,8 +45,9 @@ func init() {
BufBindings = NewKeyTree() BufBindings = NewKeyTree()
} }
// LuaAction makes a BufKeyAction from a lua function. // LuaAction makes an action from a lua function. It returns either a BufKeyAction
func LuaAction(fn string) func(*BufPane) bool { // or a BufMouseAction depending on the event type.
func LuaAction(fn string, k Event) BufAction {
luaFn := strings.Split(fn, ".") luaFn := strings.Split(fn, ".")
if len(luaFn) <= 1 { if len(luaFn) <= 1 {
return nil return nil
@ -55,33 +57,42 @@ func LuaAction(fn string) func(*BufPane) bool {
if pl == nil { if pl == nil {
return nil return nil
} }
return func(h *BufPane) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h)) var action BufAction
if err != nil { switch k.(type) {
screen.TermMessage(err) case KeyEvent, KeySequenceEvent, RawEvent:
} action = BufKeyAction(func(h *BufPane) bool {
if v, ok := val.(lua.LBool); !ok { val, err := pl.Call(plFn, luar.New(ulua.L, h))
return false if err != nil {
} else { screen.TermMessage(err)
return bool(v) }
} if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
})
case MouseEvent:
action = BufMouseAction(func(h *BufPane, te *tcell.EventMouse) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h), luar.New(ulua.L, te))
if err != nil {
screen.TermMessage(err)
}
if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
})
} }
return action
} }
// BufMapKey maps an event to an action // BufMapEvent maps an event to an action
func BufMapEvent(k Event, action string) { func BufMapEvent(k Event, action string) {
config.Bindings["buffer"][k.Name()] = action config.Bindings["buffer"][k.Name()] = action
switch e := k.(type) { var actionfns []BufAction
case KeyEvent, KeySequenceEvent, RawEvent:
bufMapKey(e, action)
case MouseEvent:
bufMapMouse(e, action)
}
}
func bufMapKey(k Event, action string) {
var actionfns []func(*BufPane) bool
var names []string var names []string
var types []byte var types []byte
for i := 0; ; i++ { for i := 0; ; i++ {
@ -89,9 +100,7 @@ func bufMapKey(k Event, action string) {
break break
} }
// TODO: fix problem when complex bindings have these idx := util.IndexAnyUnquoted(action, "&|,")
// characters (escape them?)
idx := strings.IndexAny(action, "&|,")
a := action a := action
if idx >= 0 { if idx >= 0 {
a = action[:idx] a = action[:idx]
@ -102,7 +111,7 @@ func bufMapKey(k Event, action string) {
action = "" action = ""
} }
var afn func(*BufPane) bool var afn BufAction
if strings.HasPrefix(a, "command:") { if strings.HasPrefix(a, "command:") {
a = strings.SplitN(a, ":", 2)[1] a = strings.SplitN(a, ":", 2)[1]
afn = CommandAction(a) afn = CommandAction(a)
@ -113,7 +122,7 @@ func bufMapKey(k Event, action string) {
names = append(names, "") names = append(names, "")
} else if strings.HasPrefix(a, "lua:") { } else if strings.HasPrefix(a, "lua:") {
a = strings.SplitN(a, ":", 2)[1] a = strings.SplitN(a, ":", 2)[1]
afn = LuaAction(a) afn = LuaAction(a, k)
if afn == nil { if afn == nil {
screen.TermMessage("Lua Error:", a, "does not exist") screen.TermMessage("Lua Error:", a, "does not exist")
continue continue
@ -129,47 +138,52 @@ func bufMapKey(k Event, action string) {
} else if f, ok := BufKeyActions[a]; ok { } else if f, ok := BufKeyActions[a]; ok {
afn = f afn = f
names = append(names, a) names = append(names, a)
} else if f, ok := BufMouseActions[a]; ok {
afn = f
names = append(names, a)
} else { } else {
screen.TermMessage("Error in bindings: action", a, "does not exist") screen.TermMessage("Error in bindings: action", a, "does not exist")
continue continue
} }
actionfns = append(actionfns, afn) actionfns = append(actionfns, afn)
} }
bufAction := func(h *BufPane) bool { bufAction := func(h *BufPane, te *tcell.EventMouse) bool {
cursors := h.Buf.GetCursors()
success := true
for i, a := range actionfns { for i, a := range actionfns {
innerSuccess := true var success bool
for j, c := range cursors { if _, ok := MultiActions[names[i]]; ok {
if c == nil { success = true
continue for _, c := range h.Buf.GetCursors() {
} h.Buf.SetCurCursor(c.Num)
h.Buf.SetCurCursor(c.Num) h.Cursor = c
h.Cursor = c success = success && h.execAction(a, names[i], te)
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
innerSuccess = innerSuccess && h.execAction(a, names[i], j)
} else {
break
} }
} else {
h.Buf.SetCurCursor(0)
h.Cursor = h.Buf.GetActiveCursor()
success = h.execAction(a, names[i], te)
} }
// if the action changed the current pane, update the reference // if the action changed the current pane, update the reference
h = MainTab().CurPane() h = MainTab().CurPane()
success = innerSuccess if h == nil {
// stop, in case the current pane is not a BufPane
break
}
if (!success && types[i] == '&') || (success && types[i] == '|') {
break
}
} }
return true return true
} }
BufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(bufAction)) switch e := k.(type) {
} case KeyEvent, KeySequenceEvent, RawEvent:
BufBindings.RegisterKeyBinding(e, BufKeyActionGeneral(func(h *BufPane) bool {
// BufMapMouse maps a mouse event to an action return bufAction(h, nil)
func bufMapMouse(k MouseEvent, action string) { }))
if f, ok := BufMouseActions[action]; ok { case MouseEvent:
BufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f)) BufBindings.RegisterMouseBinding(e, BufMouseActionGeneral(bufAction))
} else {
// TODO
// delete(BufMouseBindings, k)
bufMapKey(k, action)
} }
} }
@ -200,24 +214,23 @@ type BufPane struct {
// Cursor is the currently active buffer cursor // Cursor is the currently active buffer cursor
Cursor *buffer.Cursor Cursor *buffer.Cursor
// Since tcell doesn't differentiate between a mouse release event // Since tcell doesn't differentiate between a mouse press event
// and a mouse move event with no keys pressed, we need to keep // and a mouse move event with button pressed (nor between a mouse
// track of whether or not the mouse was pressed (or not released) last event to determine // release event and a mouse move event with no buttons pressed),
// mouse release events // we need to keep track of whether or not the mouse was previously
mouseReleased bool // pressed, to determine mouse release and mouse drag events.
// Moreover, since in case of a release event tcell doesn't tell us
// which button was released, we need to keep track of which
// (possibly multiple) buttons were pressed previously.
mousePressed map[MouseEvent]bool
// We need to keep track of insert key press toggle
isOverwriteMode bool
// This stores when the last click was // This stores when the last click was
// This is useful for detecting double and triple clicks // This is useful for detecting double and triple clicks
lastClickTime time.Time lastClickTime time.Time
lastLoc buffer.Loc lastLoc buffer.Loc
// lastCutTime stores when the last ctrl+k was issued. // freshClip returns true if one or more lines have been cut to the clipboard
// It is used for clearing the clipboard to replace it with fresh cut lines. // and have never been pasted yet.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
freshClip bool freshClip bool
// Was the last mouse event actually a double click? // Was the last mouse event actually a double click?
@ -250,7 +263,7 @@ func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
h.tab = tab h.tab = tab
h.Cursor = h.Buf.GetActiveCursor() h.Cursor = h.Buf.GetActiveCursor()
h.mouseReleased = true h.mousePressed = make(map[MouseEvent]bool)
return h return h
} }
@ -276,7 +289,11 @@ func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
func (h *BufPane) finishInitialize() { func (h *BufPane) finishInitialize() {
h.initialRelocate() h.initialRelocate()
h.initialized = true h.initialized = true
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
err := config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
} }
// Resize resizes the pane // Resize resizes the pane
@ -304,7 +321,7 @@ func (h *BufPane) ResizePane(size int) {
} }
// PluginCB calls all plugin callbacks with a certain name and displays an // PluginCB calls all plugin callbacks with a certain name and displays an
// error if there is one and returns the aggregrate boolean response // error if there is one and returns the aggregate boolean response
func (h *BufPane) PluginCB(cb string) bool { func (h *BufPane) PluginCB(cb string) bool {
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h)) b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
if err != nil { if err != nil {
@ -322,6 +339,12 @@ func (h *BufPane) PluginCBRune(cb string, r rune) bool {
return b return b
} }
func (h *BufPane) resetMouse() {
for me := range h.mousePressed {
delete(h.mousePressed, me)
}
}
// OpenBuffer opens the given buffer in this pane. // OpenBuffer opens the given buffer in this pane.
func (h *BufPane) OpenBuffer(b *buffer.Buffer) { func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.Buf.Close() h.Buf.Close()
@ -332,10 +355,7 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.initialRelocate() h.initialRelocate()
// Set mouseReleased to true because we assume the mouse is not being // Set mouseReleased to true because we assume the mouse is not being
// pressed when the editor is opened // pressed when the editor is opened
h.mouseReleased = true h.resetMouse()
// Set isOverwriteMode to false, because we assume we are in the default
// mode when editor is opened
h.isOverwriteMode = false
h.lastClickTime = time.Time{} h.lastClickTime = time.Time{}
} }
@ -395,6 +415,12 @@ func (h *BufPane) Name() string {
return n return n
} }
// ReOpen reloads the file opened in the bufpane from disk
func (h *BufPane) ReOpen() {
h.Buf.ReOpen()
h.Relocate()
}
func (h *BufPane) getReloadSetting() string { func (h *BufPane) getReloadSetting() string {
reloadSetting := h.Buf.Settings["reload"] reloadSetting := h.Buf.Settings["reload"]
return reloadSetting.(string) return reloadSetting.(string)
@ -413,11 +439,11 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
if !yes || canceled { if !yes || canceled {
h.Buf.UpdateModTime() h.Buf.UpdateModTime()
} else { } else {
h.Buf.ReOpen() h.ReOpen()
} }
}) })
} else if reload == "auto" { } else if reload == "auto" {
h.Buf.ReOpen() h.ReOpen()
} else if reload == "disabled" { } else if reload == "disabled" {
h.Buf.DisableReload() h.Buf.DisableReload()
} else { } else {
@ -435,61 +461,44 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
h.paste(e.Text()) h.paste(e.Text())
h.Relocate() h.Relocate()
case *tcell.EventKey: case *tcell.EventKey:
ke := KeyEvent{ ke := keyEvent(e)
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}
done := h.DoKeyEvent(ke) done := h.DoKeyEvent(ke)
if !done && e.Key() == tcell.KeyRune { if !done && e.Key() == tcell.KeyRune {
h.DoRuneInsert(e.Rune()) h.DoRuneInsert(e.Rune())
} }
case *tcell.EventMouse: case *tcell.EventMouse:
cancel := false if e.Buttons() != tcell.ButtonNone {
switch e.Buttons() {
case tcell.Button1:
_, my := e.Position()
if h.Buf.Type.Kind != buffer.BTInfo.Kind && h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
cancel = true
}
case tcell.ButtonNone:
// Mouse event with no click
if !h.mouseReleased {
// Mouse was just released
// mx, my := e.Position()
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
// we could finish the selection based on the release location as described
// below but when the mouse click is within the scroll margin this will
// cause a scroll and selection even for a simple mouse click which is
// not good
// for terminals that don't support mouse motion events, selection via
// the mouse won't work but this is ok
// Relocating here isn't really necessary because the cursor will
// be in the right place from the last mouse event
// However, if we are running in a terminal that doesn't support mouse motion
// events, this still allows the user to make selections, except only after they
// release the mouse
// if !h.doubleClick && !h.tripleClick {
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
// }
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.PrimaryReg)
}
h.mouseReleased = true
}
}
if !cancel {
me := MouseEvent{ me := MouseEvent{
btn: e.Buttons(), btn: e.Buttons(),
mod: metaToAlt(e.Modifiers()), mod: metaToAlt(e.Modifiers()),
state: MousePress,
}
isDrag := len(h.mousePressed) > 0
if e.Buttons() & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone {
h.mousePressed[me] = true
}
if isDrag {
me.state = MouseDrag
} }
h.DoMouseEvent(me, e) h.DoMouseEvent(me, e)
} else {
// Mouse event with no click - mouse was just released.
// If there were multiple mouse buttons pressed, we don't know which one
// was actually released, so we assume they all were released.
pressed := len(h.mousePressed) > 0
for me := range h.mousePressed {
delete(h.mousePressed, me)
me.state = MouseRelease
h.DoMouseEvent(me, e)
}
if !pressed {
// Propagate the mouse release in case the press wasn't for this BufPane
Tabs.ResetMouse()
}
} }
} }
h.Buf.MergeCursors() h.Buf.MergeCursors()
@ -509,6 +518,14 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
InfoBar.ClearGutter() InfoBar.ClearGutter()
} }
} }
cursors := h.Buf.GetCursors()
for _, c := range cursors {
if c.NewTrailingWsY != c.Y && (!c.HasSelection() ||
(c.NewTrailingWsY != c.CurSelection[0].Y && c.NewTrailingWsY != c.CurSelection[1].Y)) {
c.NewTrailingWsY = -1
}
}
} }
// Bindings returns the current bindings tree for this buffer. // Bindings returns the current bindings tree for this buffer.
@ -520,7 +537,10 @@ func (h *BufPane) Bindings() *KeyTree {
} }
// DoKeyEvent executes a key event by finding the action it is bound // DoKeyEvent executes a key event by finding the action it is bound
// to and executing it (possibly multiple times for multiple cursors) // to and executing it (possibly multiple times for multiple cursors).
// Returns true if the action was executed OR if there are more keys
// remaining to process before executing an action (if this is a key
// sequence event). Returns false if no action found.
func (h *BufPane) DoKeyEvent(e Event) bool { func (h *BufPane) DoKeyEvent(e Event) bool {
binds := h.Bindings() binds := h.Bindings()
action, more := binds.NextEvent(e, nil) action, more := binds.NextEvent(e, nil)
@ -534,30 +554,33 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
return more return more
} }
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool { func (h *BufPane) execAction(action BufAction, name string, te *tcell.EventMouse) bool {
if name != "Autocomplete" && name != "CycleAutocompleteBack" { if name != "Autocomplete" && name != "CycleAutocompleteBack" {
h.Buf.HasSuggestions = false h.Buf.HasSuggestions = false
} }
_, isMulti := MultiActions[name] if !h.PluginCB("pre" + name) {
if (!isMulti && cursor == 0) || isMulti { return false
if h.PluginCB("pre" + name) { }
success := action(h)
success = success && h.PluginCB("on"+name)
if isMulti { var success bool
if recordingMacro { switch a := action.(type) {
if name != "ToggleMacro" && name != "PlayMacro" { case BufKeyAction:
curmacro = append(curmacro, action) success = a(h)
} case BufMouseAction:
} success = a(h, te)
}
success = success && h.PluginCB("on"+name)
if _, ok := MultiActions[name]; ok {
if recordingMacro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
} }
return success
} }
} }
return false return success
} }
func (h *BufPane) completeAction(action string) { func (h *BufPane) completeAction(action string) {
@ -611,7 +634,7 @@ func (h *BufPane) DoRuneInsert(r rune) {
c.ResetSelection() c.ResetSelection()
} }
if h.isOverwriteMode { if h.Buf.OverwriteMode {
next := c.Loc next := c.Loc
next.X++ next.X++
h.Buf.Replace(c.Loc, next, string(r)) h.Buf.Replace(c.Loc, next, string(r))
@ -630,9 +653,13 @@ func (h *BufPane) DoRuneInsert(r rune) {
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane { func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab) e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).VSplit(right) e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
MainTab().Panes = append(MainTab().Panes, e) currentPaneIdx := MainTab().GetPane(h.splitID)
if right {
currentPaneIdx++
}
MainTab().AddPane(e, currentPaneIdx)
MainTab().Resize() MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1) MainTab().SetActive(currentPaneIdx)
return e return e
} }
@ -640,9 +667,13 @@ func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane { func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab) e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom) e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
MainTab().Panes = append(MainTab().Panes, e) currentPaneIdx := MainTab().GetPane(h.splitID)
if bottom {
currentPaneIdx++
}
MainTab().AddPane(e, currentPaneIdx)
MainTab().Resize() MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1) MainTab().SetActive(currentPaneIdx)
return e return e
} }
@ -663,6 +694,10 @@ func (h *BufPane) Close() {
// SetActive marks this pane as active. // SetActive marks this pane as active.
func (h *BufPane) SetActive(b bool) { func (h *BufPane) SetActive(b bool) {
if h.IsActive() == b {
return
}
h.BWindow.SetActive(b) h.BWindow.SetActive(b)
if b { if b {
// Display any gutter messages for this line // Display any gutter messages for this line
@ -678,8 +713,12 @@ func (h *BufPane) SetActive(b bool) {
if none && InfoBar.HasGutter { if none && InfoBar.HasGutter {
InfoBar.ClearGutter() InfoBar.ClearGutter()
} }
}
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
}
} }
// BufKeyActions contains the list of all possible key actions the bufhandler could execute // BufKeyActions contains the list of all possible key actions the bufhandler could execute
@ -692,6 +731,9 @@ var BufKeyActions = map[string]BufKeyAction{
"CursorRight": (*BufPane).CursorRight, "CursorRight": (*BufPane).CursorRight,
"CursorStart": (*BufPane).CursorStart, "CursorStart": (*BufPane).CursorStart,
"CursorEnd": (*BufPane).CursorEnd, "CursorEnd": (*BufPane).CursorEnd,
"CursorToViewTop": (*BufPane).CursorToViewTop,
"CursorToViewCenter": (*BufPane).CursorToViewCenter,
"CursorToViewBottom": (*BufPane).CursorToViewBottom,
"SelectToStart": (*BufPane).SelectToStart, "SelectToStart": (*BufPane).SelectToStart,
"SelectToEnd": (*BufPane).SelectToEnd, "SelectToEnd": (*BufPane).SelectToEnd,
"SelectUp": (*BufPane).SelectUp, "SelectUp": (*BufPane).SelectUp,
@ -700,10 +742,16 @@ var BufKeyActions = map[string]BufKeyAction{
"SelectRight": (*BufPane).SelectRight, "SelectRight": (*BufPane).SelectRight,
"WordRight": (*BufPane).WordRight, "WordRight": (*BufPane).WordRight,
"WordLeft": (*BufPane).WordLeft, "WordLeft": (*BufPane).WordLeft,
"SubWordRight": (*BufPane).SubWordRight,
"SubWordLeft": (*BufPane).SubWordLeft,
"SelectWordRight": (*BufPane).SelectWordRight, "SelectWordRight": (*BufPane).SelectWordRight,
"SelectWordLeft": (*BufPane).SelectWordLeft, "SelectWordLeft": (*BufPane).SelectWordLeft,
"SelectSubWordRight": (*BufPane).SelectSubWordRight,
"SelectSubWordLeft": (*BufPane).SelectSubWordLeft,
"DeleteWordRight": (*BufPane).DeleteWordRight, "DeleteWordRight": (*BufPane).DeleteWordRight,
"DeleteWordLeft": (*BufPane).DeleteWordLeft, "DeleteWordLeft": (*BufPane).DeleteWordLeft,
"DeleteSubWordRight": (*BufPane).DeleteSubWordRight,
"DeleteSubWordLeft": (*BufPane).DeleteSubWordLeft,
"SelectLine": (*BufPane).SelectLine, "SelectLine": (*BufPane).SelectLine,
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine, "SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
"SelectToStartOfText": (*BufPane).SelectToStartOfText, "SelectToStartOfText": (*BufPane).SelectToStartOfText,
@ -711,6 +759,8 @@ var BufKeyActions = map[string]BufKeyAction{
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine, "SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
"ParagraphPrevious": (*BufPane).ParagraphPrevious, "ParagraphPrevious": (*BufPane).ParagraphPrevious,
"ParagraphNext": (*BufPane).ParagraphNext, "ParagraphNext": (*BufPane).ParagraphNext,
"SelectToParagraphPrevious": (*BufPane).SelectToParagraphPrevious,
"SelectToParagraphNext": (*BufPane).SelectToParagraphNext,
"InsertNewline": (*BufPane).InsertNewline, "InsertNewline": (*BufPane).InsertNewline,
"Backspace": (*BufPane).Backspace, "Backspace": (*BufPane).Backspace,
"Delete": (*BufPane).Delete, "Delete": (*BufPane).Delete,
@ -731,6 +781,7 @@ var BufKeyActions = map[string]BufKeyAction{
"CopyLine": (*BufPane).CopyLine, "CopyLine": (*BufPane).CopyLine,
"Cut": (*BufPane).Cut, "Cut": (*BufPane).Cut,
"CutLine": (*BufPane).CutLine, "CutLine": (*BufPane).CutLine,
"Duplicate": (*BufPane).Duplicate,
"DuplicateLine": (*BufPane).DuplicateLine, "DuplicateLine": (*BufPane).DuplicateLine,
"DeleteLine": (*BufPane).DeleteLine, "DeleteLine": (*BufPane).DeleteLine,
"MoveLinesUp": (*BufPane).MoveLinesUp, "MoveLinesUp": (*BufPane).MoveLinesUp,
@ -763,6 +814,7 @@ var BufKeyActions = map[string]BufKeyAction{
"ToggleRuler": (*BufPane).ToggleRuler, "ToggleRuler": (*BufPane).ToggleRuler,
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch, "ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
"UnhighlightSearch": (*BufPane).UnhighlightSearch, "UnhighlightSearch": (*BufPane).UnhighlightSearch,
"ResetSearch": (*BufPane).ResetSearch,
"ClearStatus": (*BufPane).ClearStatus, "ClearStatus": (*BufPane).ClearStatus,
"ShellMode": (*BufPane).ShellMode, "ShellMode": (*BufPane).ShellMode,
"CommandMode": (*BufPane).CommandMode, "CommandMode": (*BufPane).CommandMode,
@ -774,8 +826,12 @@ var BufKeyActions = map[string]BufKeyAction{
"AddTab": (*BufPane).AddTab, "AddTab": (*BufPane).AddTab,
"PreviousTab": (*BufPane).PreviousTab, "PreviousTab": (*BufPane).PreviousTab,
"NextTab": (*BufPane).NextTab, "NextTab": (*BufPane).NextTab,
"FirstTab": (*BufPane).FirstTab,
"LastTab": (*BufPane).LastTab,
"NextSplit": (*BufPane).NextSplit, "NextSplit": (*BufPane).NextSplit,
"PreviousSplit": (*BufPane).PreviousSplit, "PreviousSplit": (*BufPane).PreviousSplit,
"FirstSplit": (*BufPane).FirstSplit,
"LastSplit": (*BufPane).LastSplit,
"Unsplit": (*BufPane).Unsplit, "Unsplit": (*BufPane).Unsplit,
"VSplit": (*BufPane).VSplitAction, "VSplit": (*BufPane).VSplitAction,
"HSplit": (*BufPane).HSplitAction, "HSplit": (*BufPane).HSplitAction,
@ -791,6 +847,7 @@ var BufKeyActions = map[string]BufKeyAction{
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor, "RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors, "RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
"SkipMultiCursor": (*BufPane).SkipMultiCursor, "SkipMultiCursor": (*BufPane).SkipMultiCursor,
"SkipMultiCursorBack": (*BufPane).SkipMultiCursorBack,
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace, "JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
"JumpLine": (*BufPane).JumpLine, "JumpLine": (*BufPane).JumpLine,
"Deselect": (*BufPane).Deselect, "Deselect": (*BufPane).Deselect,
@ -804,6 +861,8 @@ var BufKeyActions = map[string]BufKeyAction{
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute // BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
var BufMouseActions = map[string]BufMouseAction{ var BufMouseActions = map[string]BufMouseAction{
"MousePress": (*BufPane).MousePress, "MousePress": (*BufPane).MousePress,
"MouseDrag": (*BufPane).MouseDrag,
"MouseRelease": (*BufPane).MouseRelease,
"MouseMultiCursor": (*BufPane).MouseMultiCursor, "MouseMultiCursor": (*BufPane).MouseMultiCursor,
} }
@ -828,10 +887,16 @@ var MultiActions = map[string]bool{
"SelectRight": true, "SelectRight": true,
"WordRight": true, "WordRight": true,
"WordLeft": true, "WordLeft": true,
"SubWordRight": true,
"SubWordLeft": true,
"SelectWordRight": true, "SelectWordRight": true,
"SelectWordLeft": true, "SelectWordLeft": true,
"SelectSubWordRight": true,
"SelectSubWordLeft": true,
"DeleteWordRight": true, "DeleteWordRight": true,
"DeleteWordLeft": true, "DeleteWordLeft": true,
"DeleteSubWordRight": true,
"DeleteSubWordLeft": true,
"SelectLine": true, "SelectLine": true,
"SelectToStartOfLine": true, "SelectToStartOfLine": true,
"SelectToStartOfText": true, "SelectToStartOfText": true,
@ -849,6 +914,7 @@ var MultiActions = map[string]bool{
"Copy": true, "Copy": true,
"Cut": true, "Cut": true,
"CutLine": true, "CutLine": true,
"Duplicate": true,
"DuplicateLine": true, "DuplicateLine": true,
"DeleteLine": true, "DeleteLine": true,
"MoveLinesUp": true, "MoveLinesUp": true,

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -41,6 +42,7 @@ func InitCommands() {
"unbind": {(*BufPane).UnbindCmd, nil}, "unbind": {(*BufPane).UnbindCmd, nil},
"quit": {(*BufPane).QuitCmd, nil}, "quit": {(*BufPane).QuitCmd, nil},
"goto": {(*BufPane).GotoCmd, nil}, "goto": {(*BufPane).GotoCmd, nil},
"jump": {(*BufPane).JumpCmd, nil},
"save": {(*BufPane).SaveCmd, nil}, "save": {(*BufPane).SaveCmd, nil},
"replace": {(*BufPane).ReplaceCmd, nil}, "replace": {(*BufPane).ReplaceCmd, nil},
"replaceall": {(*BufPane).ReplaceAllCmd, nil}, "replaceall": {(*BufPane).ReplaceAllCmd, nil},
@ -137,23 +139,25 @@ func (h *BufPane) TextFilterCmd(args []string) {
InfoBar.Error("usage: textfilter arguments") InfoBar.Error("usage: textfilter arguments")
return return
} }
sel := h.Cursor.GetSelection() for _, c := range h.Buf.GetCursors() {
if len(sel) == 0 { sel := c.GetSelection()
h.Cursor.SelectWord() if len(sel) == 0 {
sel = h.Cursor.GetSelection() c.SelectWord()
sel = c.GetSelection()
}
var bout, berr bytes.Buffer
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(string(sel))
cmd.Stderr = &berr
cmd.Stdout = &bout
err := cmd.Run()
if err != nil {
InfoBar.Error(err.Error() + " " + berr.String())
return
}
c.DeleteSelection()
h.Buf.Insert(c.Loc, bout.String())
} }
var bout, berr bytes.Buffer
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(string(sel))
cmd.Stderr = &berr
cmd.Stdout = &bout
err := cmd.Run()
if err != nil {
InfoBar.Error(err.Error() + " " + berr.String())
return
}
h.Cursor.DeleteSelection()
h.Buf.Insert(h.Cursor.Loc, bout.String())
} }
// TabMoveCmd moves the current tab to a given index (starts at 1). The // TabMoveCmd moves the current tab to a given index (starts at 1). The
@ -197,10 +201,11 @@ func (h *BufPane) TabMoveCmd(args []string) {
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1) idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
activeTab := Tabs.List[idxFrom] activeTab := Tabs.List[idxFrom]
Tabs.RemoveTab(activeTab.ID()) Tabs.RemoveTab(activeTab.Panes[0].ID())
Tabs.List = append(Tabs.List, nil) Tabs.List = append(Tabs.List, nil)
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:]) copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
Tabs.List[idxTo] = activeTab Tabs.List[idxTo] = activeTab
Tabs.Resize()
Tabs.UpdateNames() Tabs.UpdateNames()
Tabs.SetActive(idxTo) Tabs.SetActive(idxTo)
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1)) // InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
@ -329,31 +334,84 @@ func (h *BufPane) ToggleLogCmd(args []string) {
} }
} }
// ReloadCmd reloads all files (syntax files, colorschemes...) // ReloadCmd reloads all files (syntax files, colorschemes, plugins...)
func (h *BufPane) ReloadCmd(args []string) { func (h *BufPane) ReloadCmd(args []string) {
ReloadConfig() reloadRuntime(true)
} }
// ReloadConfig reloads only the configuration
func ReloadConfig() { func ReloadConfig() {
config.InitRuntimeFiles() reloadRuntime(false)
}
func reloadRuntime(reloadPlugins bool) {
if reloadPlugins {
err := config.RunPluginFn("deinit")
if err != nil {
screen.TermMessage(err)
}
}
config.InitRuntimeFiles(true)
if reloadPlugins {
config.InitPlugins()
}
err := config.ReadSettings() err := config.ReadSettings()
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
} else {
parsedSettings := config.ParsedSettings()
defaultSettings := config.DefaultAllSettings()
for k := range defaultSettings {
if _, ok := config.VolatileSettings[k]; ok {
// reload should not override volatile settings
continue
}
if _, ok := parsedSettings[k]; ok {
err = doSetGlobalOptionNative(k, parsedSettings[k])
} else {
err = doSetGlobalOptionNative(k, defaultSettings[k])
}
if err != nil {
screen.TermMessage(err)
}
}
} }
err = config.InitGlobalSettings()
if err != nil { if reloadPlugins {
screen.TermMessage(err) err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
}
} }
InitBindings() InitBindings()
InitCommands() InitCommands()
if reloadPlugins {
err = config.RunPluginFn("preinit")
if err != nil {
screen.TermMessage(err)
}
err = config.RunPluginFn("init")
if err != nil {
screen.TermMessage(err)
}
err = config.RunPluginFn("postinit")
if err != nil {
screen.TermMessage(err)
}
}
err = config.InitColorscheme() err = config.InitColorscheme()
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
} }
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.UpdateRules() b.ReloadSettings(true)
} }
} }
@ -363,50 +421,93 @@ func (h *BufPane) ReopenCmd(args []string) {
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) { InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
if !canceled && yes { if !canceled && yes {
h.Save() h.Save()
h.Buf.ReOpen() h.ReOpen()
} else if !canceled { } else if !canceled {
h.Buf.ReOpen() h.ReOpen()
} }
}) })
} else { } else {
h.Buf.ReOpen() h.ReOpen()
} }
} }
func (h *BufPane) openHelp(page string) error { func (h *BufPane) openHelp(page string, hsplit bool, forceSplit bool) error {
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil { if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err)) return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err))
} else { } else {
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp) helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
helpBuffer.SetName("Help " + page) helpBuffer.SetName("Help " + page)
helpBuffer.SetOptionNative("hltaberrors", false)
helpBuffer.SetOptionNative("hltrailingws", false)
if h.Buf.Type == buffer.BTHelp { if h.Buf.Type == buffer.BTHelp && !forceSplit {
h.OpenBuffer(helpBuffer) h.OpenBuffer(helpBuffer)
} else { } else if hsplit {
h.HSplitBuf(helpBuffer) h.HSplitBuf(helpBuffer)
} else {
h.VSplitBuf(helpBuffer)
} }
} }
return nil return nil
} }
// HelpCmd tries to open the given help page in a horizontal split // HelpCmd tries to open the given help page according to the split type
// configured with the "helpsplit" option. It can be overriden by the optional
// arguments "-vpslit" or "-hsplit". In case more than one help page is given
// as argument then it opens all of them with the defined split type.
func (h *BufPane) HelpCmd(args []string) { func (h *BufPane) HelpCmd(args []string) {
hsplit := config.GlobalSettings["helpsplit"] == "hsplit"
if len(args) < 1 { if len(args) < 1 {
// Open the default help if the user just typed "> help" // Open the default help if the user just typed "> help"
h.openHelp("help") h.openHelp("help", hsplit, false)
} else { } else {
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil { var topics []string
err := h.openHelp(args[0]) forceSplit := false
if err != nil { const errSplit = "hsplit and vsplit are not allowed at the same time"
InfoBar.Error(err) for _, arg := range args {
switch arg {
case "-vsplit":
if forceSplit {
InfoBar.Error(errSplit)
return
}
hsplit = false
forceSplit = true
case "-hsplit":
if forceSplit {
InfoBar.Error(errSplit)
return
}
hsplit = true
forceSplit = true
default:
topics = append(topics, arg)
}
}
if len(topics) < 1 {
// Do the same as without arg
h.openHelp("help", hsplit, forceSplit)
return
}
if len(topics) > 1 {
forceSplit = true
}
for _, topic := range topics {
if config.FindRuntimeFile(config.RTHelp, topic) != nil {
err := h.openHelp(topic, hsplit, forceSplit)
if err != nil {
InfoBar.Error(err)
}
} else {
InfoBar.Error("Sorry, no help for ", topic)
} }
} else {
InfoBar.Error("Sorry, no help for ", args[0])
} }
} }
} }
// VSplitCmd opens a vertical split with file given in the first argument // VSplitCmd opens one or more vertical splits with the files given as arguments
// If no file is given, it opens an empty buffer in a new split // If no file is given, it opens an empty buffer in a new split
func (h *BufPane) VSplitCmd(args []string) { func (h *BufPane) VSplitCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
@ -415,16 +516,18 @@ func (h *BufPane) VSplitCmd(args []string) {
return return
} }
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) for _, a := range args {
if err != nil { buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
InfoBar.Error(err) if err != nil {
return InfoBar.Error(err)
} return
}
h.VSplitBuf(buf) h.VSplitBuf(buf)
}
} }
// HSplitCmd opens a horizontal split with file given in the first argument // HSplitCmd opens one or more horizontal splits with the files given as arguments
// If no file is given, it opens an empty buffer in a new split // If no file is given, it opens an empty buffer in a new split
func (h *BufPane) HSplitCmd(args []string) { func (h *BufPane) HSplitCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
@ -433,13 +536,15 @@ func (h *BufPane) HSplitCmd(args []string) {
return return
} }
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) for _, a := range args {
if err != nil { buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
InfoBar.Error(err) if err != nil {
return InfoBar.Error(err)
} return
}
h.HSplitBuf(buf) h.HSplitBuf(buf)
}
} }
// EvalCmd evaluates a lua expression // EvalCmd evaluates a lua expression
@ -447,7 +552,8 @@ func (h *BufPane) EvalCmd(args []string) {
InfoBar.Error("Eval unsupported") InfoBar.Error("Eval unsupported")
} }
// NewTabCmd opens the given file in a new tab // NewTabCmd opens one or more tabs with the files given as arguments
// If no file is given, it opens an empty buffer in a new tab
func (h *BufPane) NewTabCmd(args []string) { func (h *BufPane) NewTabCmd(args []string) {
width, height := screen.Screen.Size() width, height := screen.Screen.Size()
iOffset := config.GetInfoBarOffset() iOffset := config.GetInfoBarOffset()
@ -470,73 +576,98 @@ func (h *BufPane) NewTabCmd(args []string) {
} }
} }
func SetGlobalOptionNative(option string, nativeValue interface{}) error { func doSetGlobalOptionNative(option string, nativeValue interface{}) error {
local := false if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) {
for _, s := range config.LocalSettings { return nil
if s == option {
local = true
break
}
} }
if !local { config.GlobalSettings[option] = nativeValue
config.GlobalSettings[option] = nativeValue config.ModifiedSettings[option] = true
config.ModifiedSettings[option] = true delete(config.VolatileSettings, option)
if option == "colorscheme" { if option == "colorscheme" {
// LoadSyntaxFiles() // LoadSyntaxFiles()
config.InitColorscheme() config.InitColorscheme()
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.UpdateRules() b.UpdateRules()
} }
} else if option == "infobar" || option == "keymenu" { } else if option == "infobar" || option == "keymenu" {
Tabs.Resize() Tabs.Resize()
} else if option == "mouse" { } else if option == "mouse" {
if !nativeValue.(bool) { if !nativeValue.(bool) {
screen.Screen.DisableMouse() screen.Screen.DisableMouse()
} else {
screen.Screen.EnableMouse()
}
} else if option == "autosave" {
if nativeValue.(float64) > 0 {
config.SetAutoTime(int(nativeValue.(float64)))
config.StartAutoSave()
} else {
config.SetAutoTime(0)
}
} else if option == "paste" {
screen.Screen.SetPaste(nativeValue.(bool))
} else if option == "clipboard" {
m := clipboard.SetMethod(nativeValue.(string))
err := clipboard.Initialize(m)
if err != nil {
return err
}
} else { } else {
for _, pl := range config.Plugins { screen.Screen.EnableMouse()
if option == pl.Name { }
if nativeValue.(bool) && !pl.Loaded { } else if option == "autosave" {
pl.Load() if nativeValue.(float64) > 0 {
_, err := pl.Call("init") config.SetAutoTime(nativeValue.(float64))
if err != nil && err != config.ErrNoSuchFunction { } else {
screen.TermMessage(err) config.SetAutoTime(0)
} }
} else if !nativeValue.(bool) && pl.Loaded { } else if option == "paste" {
_, err := pl.Call("deinit") screen.Screen.SetPaste(nativeValue.(bool))
if err != nil && err != config.ErrNoSuchFunction { } else if option == "clipboard" {
screen.TermMessage(err) m := clipboard.SetMethod(nativeValue.(string))
} err := clipboard.Initialize(m)
if err != nil {
return err
}
} else {
for _, pl := range config.Plugins {
if option == pl.Name {
if nativeValue.(bool) && !pl.Loaded {
pl.Load()
_, err := pl.Call("init")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
} else if !nativeValue.(bool) && pl.Loaded {
_, err := pl.Call("deinit")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
} }
} }
} }
} }
} }
for _, b := range buffer.OpenBuffers { return nil
b.SetOptionNative(option, nativeValue) }
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
if err := config.OptionIsValid(option, nativeValue); err != nil {
return err
} }
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) // check for local option first...
for _, s := range config.LocalSettings {
if s == option {
return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue)
}
}
// ...if it's not local continue with the globals...
if err := doSetGlobalOptionNative(option, nativeValue); err != nil {
return err
}
// ...at last check the buffer locals
for _, b := range buffer.OpenBuffers {
b.DoSetOptionNative(option, nativeValue)
delete(b.LocalSettings, option)
}
err := config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
if err != nil {
if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
err = errors.Unwrap(err)
}
return err
}
return nil
} }
func SetGlobalOption(option, value string) error { func SetGlobalOption(option, value string) error {
@ -560,16 +691,10 @@ func (h *BufPane) ResetCmd(args []string) {
} }
option := args[0] option := args[0]
defaults := config.DefaultAllSettings()
defaultGlobals := config.DefaultGlobalSettings() if _, ok := defaults[option]; ok {
defaultLocals := config.DefaultCommonSettings() SetGlobalOptionNative(option, defaults[option])
if _, ok := defaultGlobals[option]; ok {
SetGlobalOptionNative(option, defaultGlobals[option])
return
}
if _, ok := defaultLocals[option]; ok {
h.Buf.SetOptionNative(option, defaultLocals[option])
return return
} }
InfoBar.Error(config.ErrInvalidOption) InfoBar.Error(config.ErrInvalidOption)
@ -634,6 +759,11 @@ func (h *BufPane) ShowCmd(args []string) {
InfoBar.Message(option) InfoBar.Message(option)
} }
func parseKeyArg(arg string) string {
// If this is a raw escape sequence, convert it to its raw byte form
return strings.ReplaceAll(arg, "\\x1b", "\x1b")
}
// ShowKeyCmd displays the action that a key is bound to // ShowKeyCmd displays the action that a key is bound to
func (h *BufPane) ShowKeyCmd(args []string) { func (h *BufPane) ShowKeyCmd(args []string) {
if len(args) < 1 { if len(args) < 1 {
@ -641,7 +771,7 @@ func (h *BufPane) ShowKeyCmd(args []string) {
return return
} }
event, err := findEvent(args[0]) event, err := findEvent(parseKeyArg(args[0]))
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
return return
@ -660,9 +790,13 @@ func (h *BufPane) BindCmd(args []string) {
return return
} }
_, err := TryBindKey(args[0], args[1], true) _, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
if err != nil { if err != nil {
InfoBar.Error(err) if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -673,9 +807,13 @@ func (h *BufPane) UnbindCmd(args []string) {
return return
} }
err := UnbindKey(args[0]) err := UnbindKey(parseKeyArg(args[0]))
if err != nil { if err != nil {
InfoBar.Error(err) if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -701,41 +839,67 @@ func (h *BufPane) QuitCmd(args []string) {
// position in the buffer // position in the buffer
// For example: `goto line`, or `goto line:col` // For example: `goto line`, or `goto line:col`
func (h *BufPane) GotoCmd(args []string) { func (h *BufPane) GotoCmd(args []string) {
line, col, err := h.parseLineCol(args)
if err != nil {
InfoBar.Error(err)
return
}
if line < 0 {
line = h.Buf.LinesNum() + 1 + line
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.RemoveAllMultiCursors()
h.Cursor.Deselect(true)
h.GotoLoc(buffer.Loc{col, line})
}
// JumpCmd is a command that will send the cursor to a certain relative
// position in the buffer
// For example: `jump line`, `jump -line`, or `jump -line:col`
func (h *BufPane) JumpCmd(args []string) {
line, col, err := h.parseLineCol(args)
if err != nil {
InfoBar.Error(err)
return
}
line = h.Buf.GetActiveCursor().Y + 1 + line
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.RemoveAllMultiCursors()
h.Cursor.Deselect(true)
h.GotoLoc(buffer.Loc{col, line})
}
// parseLineCol is a helper to parse the input of GotoCmd and JumpCmd
func (h *BufPane) parseLineCol(args []string) (line int, col int, err error) {
if len(args) <= 0 { if len(args) <= 0 {
InfoBar.Error("Not enough arguments") return 0, 0, errors.New("Not enough arguments")
}
line, col = 0, 0
if strings.Contains(args[0], ":") {
parts := strings.SplitN(args[0], ":", 2)
line, err = strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
}
col, err = strconv.Atoi(parts[1])
if err != nil {
return 0, 0, err
}
} else { } else {
h.RemoveAllMultiCursors() line, err = strconv.Atoi(args[0])
if strings.Contains(args[0], ":") { if err != nil {
parts := strings.SplitN(args[0], ":", 2) return 0, 0, err
line, err := strconv.Atoi(parts[0])
if err != nil {
InfoBar.Error(err)
return
}
col, err := strconv.Atoi(parts[1])
if err != nil {
InfoBar.Error(err)
return
}
if line < 0 {
line = h.Buf.LinesNum() + 1 + line
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.GotoLoc(buffer.Loc{col, line})
} else {
line, err := strconv.Atoi(args[0])
if err != nil {
InfoBar.Error(err)
return
}
if line < 0 {
line = h.Buf.LinesNum() + 1 + line
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
h.GotoLoc(buffer.Loc{0, line})
} }
} }
return line, col, nil
} }
// SaveCmd saves the buffer optionally with an argument file name // SaveCmd saves the buffer optionally with an argument file name
@ -743,7 +907,7 @@ func (h *BufPane) SaveCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
h.Save() h.Save()
} else { } else {
h.Buf.SaveAs(args[0]) h.saveBufToFile(args[0], "SaveAs", nil)
} }
} }
@ -804,19 +968,21 @@ func (h *BufPane) ReplaceCmd(args []string) {
nreplaced := 0 nreplaced := 0
start := h.Buf.Start() start := h.Buf.Start()
end := h.Buf.End() end := h.Buf.End()
searchLoc := h.Cursor.Loc
selection := h.Cursor.HasSelection() selection := h.Cursor.HasSelection()
if selection { if selection {
start = h.Cursor.CurSelection[0] start = h.Cursor.CurSelection[0]
end = h.Cursor.CurSelection[1] end = h.Cursor.CurSelection[1]
searchLoc = start // otherwise me might start at the end
} }
if all { if all {
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace) nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex)
} else { } else {
inRange := func(l buffer.Loc) bool { inRange := func(l buffer.Loc) bool {
return l.GreaterEqual(start) && l.LessEqual(end) return l.GreaterEqual(start) && l.LessEqual(end)
} }
searchLoc := h.Cursor.Loc lastMatchEnd := buffer.Loc{-1, -1}
var doReplacement func() var doReplacement func()
doReplacement = func() { doReplacement = func() {
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true) locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
@ -831,6 +997,18 @@ func (h *BufPane) ReplaceCmd(args []string) {
return return
} }
if lastMatchEnd == locs[1] {
// skip empty match right after previous match
if searchLoc == end {
searchLoc = start
lastMatchEnd = buffer.Loc{-1, -1}
} else {
searchLoc = searchLoc.Move(1, h.Buf)
}
doReplacement()
return
}
h.Cursor.SetSelectionStart(locs[0]) h.Cursor.SetSelectionStart(locs[0])
h.Cursor.SetSelectionEnd(locs[1]) h.Cursor.SetSelectionEnd(locs[1])
h.GotoLoc(locs[0]) h.GotoLoc(locs[0])
@ -840,7 +1018,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) { InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
if !canceled && yes { if !canceled && yes {
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace) _, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace, !noRegex)
searchLoc = locs[0] searchLoc = locs[0]
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf) searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
@ -856,6 +1034,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
h.Buf.RelocateCursors() h.Buf.RelocateCursors()
return return
} }
lastMatchEnd = searchLoc
doReplacement() doReplacement()
}) })
} }

View File

@ -3,7 +3,7 @@ package action
var termdefaults = map[string]string{ var termdefaults = map[string]string{
"<Ctrl-q><Ctrl-q>": "Exit", "<Ctrl-q><Ctrl-q>": "Exit",
"<Ctrl-e><Ctrl-e>": "CommandMode", "<Ctrl-e><Ctrl-e>": "CommandMode",
"<Ctrl-w><Ctrl-w>": "NextSplit", "<Ctrl-w><Ctrl-w>": "NextSplit|FirstSplit",
} }
// DefaultBindings returns a map containing micro's default keybindings // DefaultBindings returns a map containing micro's default keybindings

View File

@ -45,23 +45,25 @@ var bufdefaults = map[string]string{
"Alt-]": "DiffNext|CursorEnd", "Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine", "Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab", "Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab", "Alt-.": "NextTab|FirstTab",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
"End": "EndOfLine", "End": "EndOfLine",
"CtrlHome": "CursorStart", "CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd", "CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp", "PageUp": "CursorPageUp",
"PageDown": "CursorPageDown", "PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab", "CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab", "CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
"Alt-g": "ToggleKeyMenu", "Alt-g": "ToggleKeyMenu",
"Ctrl-r": "ToggleRuler", "Ctrl-r": "ToggleRuler",
@ -70,7 +72,7 @@ var bufdefaults = map[string]string{
"Ctrl-b": "ShellMode", "Ctrl-b": "ShellMode",
"Ctrl-q": "Quit", "Ctrl-q": "Quit",
"Ctrl-e": "CommandMode", "Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit", "Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -92,11 +94,13 @@ var bufdefaults = map[string]string{
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch", "Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "ScrollUp", "MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown", "MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"Ctrl-MouseLeft": "MouseMultiCursor", "MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
"AltShiftUp": "SpawnMultiCursorUp", "AltShiftUp": "SpawnMultiCursorUp",
@ -142,8 +146,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
@ -175,8 +179,10 @@ var infodefaults = map[string]string{
"Esc": "AbortCommand", "Esc": "AbortCommand",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "HistoryUp", "MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown", "MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
} }

View File

@ -48,23 +48,25 @@ var bufdefaults = map[string]string{
"Alt-]": "DiffNext|CursorEnd", "Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine", "Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab", "Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab", "Alt-.": "NextTab|FirstTab",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
"End": "EndOfLine", "End": "EndOfLine",
"CtrlHome": "CursorStart", "CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd", "CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp", "PageUp": "CursorPageUp",
"PageDown": "CursorPageDown", "PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab", "CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab", "CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
"Alt-g": "ToggleKeyMenu", "Alt-g": "ToggleKeyMenu",
"Ctrl-r": "ToggleRuler", "Ctrl-r": "ToggleRuler",
@ -73,7 +75,7 @@ var bufdefaults = map[string]string{
"Ctrl-b": "ShellMode", "Ctrl-b": "ShellMode",
"Ctrl-q": "Quit", "Ctrl-q": "Quit",
"Ctrl-e": "CommandMode", "Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit", "Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -95,11 +97,13 @@ var bufdefaults = map[string]string{
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch", "Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "ScrollUp", "MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown", "MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"Ctrl-MouseLeft": "MouseMultiCursor", "MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
"Alt-m": "SpawnMultiCursorSelect", "Alt-m": "SpawnMultiCursorSelect",
@ -145,8 +149,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
@ -178,8 +182,10 @@ var infodefaults = map[string]string{
"Esc": "AbortCommand", "Esc": "AbortCommand",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "HistoryUp", "MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown", "MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
} }

View File

@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type Event interface { type Event interface {
@ -44,6 +44,17 @@ func metaToAlt(mod tcell.ModMask) tcell.ModMask {
return mod return mod
} }
func keyEvent(e *tcell.EventKey) KeyEvent {
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
}
if e.Key() == tcell.KeyRune {
ke.r = e.Rune()
}
return ke
}
func (k KeyEvent) Name() string { func (k KeyEvent) Name() string {
if k.any { if k.any {
return "<any>" return "<any>"
@ -68,7 +79,7 @@ func (k KeyEvent) Name() string {
if k.code == tcell.KeyRune { if k.code == tcell.KeyRune {
s = string(k.r) s = string(k.r)
} else { } else {
s = fmt.Sprintf("Key[%d,%d]", k.code, int(k.r)) s = fmt.Sprintf("Key[%d]", k.code)
} }
} }
if len(m) != 0 { if len(m) != 0 {
@ -100,11 +111,20 @@ func (k KeySequenceEvent) Name() string {
return buf.String() return buf.String()
} }
type MouseState int
const (
MousePress = iota
MouseDrag
MouseRelease
)
// MouseEvent is a mouse event with a mouse button and // MouseEvent is a mouse event with a mouse button and
// any possible key modifiers // any possible key modifiers
type MouseEvent struct { type MouseEvent struct {
btn tcell.ButtonMask btn tcell.ButtonMask
mod tcell.ModMask mod tcell.ModMask
state MouseState
} }
func (m MouseEvent) Name() string { func (m MouseEvent) Name() string {
@ -122,9 +142,17 @@ func (m MouseEvent) Name() string {
mod = "Ctrl-" mod = "Ctrl-"
} }
state := ""
switch m.state {
case MouseDrag:
state = "Drag"
case MouseRelease:
state = "Release"
}
for k, v := range mouseEvents { for k, v := range mouseEvents {
if v == m.btn { if v == m.btn {
return fmt.Sprintf("%s%s", mod, k) return fmt.Sprintf("%s%s%s", mod, k, state)
} }
} }
return "" return ""
@ -138,11 +166,7 @@ func (m MouseEvent) Name() string {
func ConstructEvent(event tcell.Event) (Event, error) { func ConstructEvent(event tcell.Event) (Event, error) {
switch e := event.(type) { switch e := event.(type) {
case *tcell.EventKey: case *tcell.EventKey:
return KeyEvent{ return keyEvent(e), nil
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}, nil
case *tcell.EventRaw: case *tcell.EventRaw:
return RawEvent{ return RawEvent{
esc: e.EscSeq(), esc: e.EscSeq(),

View File

@ -8,6 +8,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight"
) )
// This file is meant (for now) for autocompletion in command mode, not // This file is meant (for now) for autocompletion in command mode, not
@ -17,7 +18,7 @@ import (
// CommandComplete autocompletes commands // CommandComplete autocompletes commands
func CommandComplete(b *buffer.Buffer) ([]string, []string) { func CommandComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b) input, argstart := b.GetArg()
var suggestions []string var suggestions []string
for cmd := range commands { for cmd := range commands {
@ -38,7 +39,7 @@ func CommandComplete(b *buffer.Buffer) ([]string, []string) {
// HelpComplete autocompletes help topics // HelpComplete autocompletes help topics
func HelpComplete(b *buffer.Buffer) ([]string, []string) { func HelpComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b) input, argstart := b.GetArg()
var suggestions []string var suggestions []string
@ -77,19 +78,67 @@ func colorschemeComplete(input string) (string, []string) {
return chosen, suggestions return chosen, suggestions
} }
func contains(s []string, e string) bool { // filetypeComplete autocompletes filetype
for _, a := range s { func filetypeComplete(input string) (string, []string) {
if a == e { var suggestions []string
return true
// We cannot match filetypes just by names of syntax files,
// since those names may be different from the actual filetype values
// specified inside syntax files (e.g. "c++" filetype in cpp.yaml).
// So we need to parse filetype values out of those files.
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
continue
}
header, err := highlight.MakeHeaderYaml(data)
if err != nil {
continue
}
// Prevent duplicated defaults
if header.FileType == "off" || header.FileType == "unknown" {
continue
}
if strings.HasPrefix(header.FileType, input) {
suggestions = append(suggestions, header.FileType)
} }
} }
return false headerLoop:
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
data, err := f.Data()
if err != nil {
continue
}
header, err := highlight.MakeHeader(data)
if err != nil {
continue
}
for _, v := range suggestions {
if v == header.FileType {
continue headerLoop
}
}
if strings.HasPrefix(header.FileType, input) {
suggestions = append(suggestions, header.FileType)
}
}
if strings.HasPrefix("off", input) {
suggestions = append(suggestions, "off")
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
} }
// OptionComplete autocompletes options // OptionComplete autocompletes options
func OptionComplete(b *buffer.Buffer) ([]string, []string) { func OptionComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b) input, argstart := b.GetArg()
var suggestions []string var suggestions []string
for option := range config.GlobalSettings { for option := range config.GlobalSettings {
@ -116,7 +165,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
l := b.LineBytes(c.Y) l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X) l = util.SliceStart(l, c.X)
input, argstart := buffer.GetArg(b) input, argstart := b.GetArg()
completeValue := false completeValue := false
args := bytes.Split(l, []byte{' '}) args := bytes.Split(l, []byte{' '})
@ -172,13 +221,8 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
switch inputOpt { switch inputOpt {
case "colorscheme": case "colorscheme":
_, suggestions = colorschemeComplete(input) _, suggestions = colorschemeComplete(input)
case "fileformat": case "filetype":
if strings.HasPrefix("unix", input) { _, suggestions = filetypeComplete(input)
suggestions = append(suggestions, "unix")
}
if strings.HasPrefix("dos", input) {
suggestions = append(suggestions, "dos")
}
case "sucmd": case "sucmd":
if strings.HasPrefix("sudo", input) { if strings.HasPrefix("sudo", input) {
suggestions = append(suggestions, "sudo") suggestions = append(suggestions, "sudo")
@ -186,15 +230,13 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
if strings.HasPrefix("doas", input) { if strings.HasPrefix("doas", input) {
suggestions = append(suggestions, "doas") suggestions = append(suggestions, "doas")
} }
case "clipboard": default:
if strings.HasPrefix("external", input) { if choices, ok := config.OptionChoices[inputOpt]; ok {
suggestions = append(suggestions, "external") for _, choice := range choices {
} if strings.HasPrefix(choice, input) {
if strings.HasPrefix("internal", input) { suggestions = append(suggestions, choice)
suggestions = append(suggestions, "internal") }
} }
if strings.HasPrefix("terminal", input) {
suggestions = append(suggestions, "terminal")
} }
} }
} }
@ -210,7 +252,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
// PluginCmdComplete autocompletes the plugin command // PluginCmdComplete autocompletes the plugin command
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) { func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b) input, argstart := b.GetArg()
var suggestions []string var suggestions []string
for _, cmd := range PluginCmds { for _, cmd := range PluginCmds {
@ -232,7 +274,7 @@ func PluginComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
l := b.LineBytes(c.Y) l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X) l = util.SliceStart(l, c.X)
input, argstart := buffer.GetArg(b) input, argstart := b.GetArg()
completeValue := false completeValue := false
args := bytes.Split(l, []byte{' '}) args := bytes.Split(l, []byte{' '})

View File

@ -8,7 +8,7 @@ import (
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/info" "github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type InfoKeyAction func(*InfoPane) type InfoKeyAction func(*InfoPane)
@ -83,22 +83,22 @@ func (h *InfoPane) Close() {
func (h *InfoPane) HandleEvent(event tcell.Event) { func (h *InfoPane) HandleEvent(event tcell.Event) {
switch e := event.(type) { switch e := event.(type) {
case *tcell.EventResize:
// TODO
case *tcell.EventKey: case *tcell.EventKey:
ke := KeyEvent{ ke := keyEvent(e)
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}
done := h.DoKeyEvent(ke) done := h.DoKeyEvent(ke)
hasYN := h.HasYN hasYN := h.HasYN
if e.Key() == tcell.KeyRune && hasYN { if e.Key() == tcell.KeyRune && hasYN {
if (e.Rune() == 'y' || e.Rune() == 'Y') && hasYN { y := e.Rune() == 'y' || e.Rune() == 'Y'
h.YNResp = true n := e.Rune() == 'n' || e.Rune() == 'N'
h.DonePrompt(false) if y || n {
} else if (e.Rune() == 'n' || e.Rune() == 'N') && hasYN { h.YNResp = y
h.YNResp = false
h.DonePrompt(false) h.DonePrompt(false)
InfoBindings.ResetEvents()
InfoBufBindings.ResetEvents()
} }
} }
if e.Key() == tcell.KeyRune && !done && !hasYN { if e.Key() == tcell.KeyRune && !done && !hasYN {
@ -122,7 +122,10 @@ func (h *InfoPane) HandleEvent(event tcell.Event) {
} }
} }
// DoKeyEvent executes a key event for the command bar, doing any overridden actions // DoKeyEvent executes a key event for the command bar, doing any overridden actions.
// Returns true if the action was executed OR if there are more keys remaining
// to process before executing an action (if this is a key sequence event).
// Returns false if no action found.
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool { func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
action, more := InfoBindings.NextEvent(e, nil) action, more := InfoBindings.NextEvent(e, nil)
if action != nil && !more { if action != nil && !more {
@ -136,11 +139,25 @@ func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
} }
if !more { if !more {
// If no infopane action found, try to find a bufpane action.
//
// TODO: this is buggy. For example, if the command bar has the following
// two bindings:
//
// "<Ctrl-x><Ctrl-p>": "HistoryUp",
// "<Ctrl-x><Ctrl-v>": "Paste",
//
// the 2nd binding (with a bufpane action) doesn't work, since <Ctrl-x>
// has been already consumed by the 1st binding (with an infopane action).
//
// We should either iterate both InfoBindings and InfoBufBindings keytrees
// together, or just use the same keytree for both infopane and bufpane
// bindings.
action, more = InfoBufBindings.NextEvent(e, nil) action, more = InfoBufBindings.NextEvent(e, nil)
if action != nil && !more { if action != nil && !more {
done := action(h.BufPane) action(h.BufPane)
InfoBufBindings.ResetEvents() InfoBufBindings.ResetEvents()
return done return true
} else if action == nil && !more { } else if action == nil && !more {
InfoBufBindings.ResetEvents() InfoBufBindings.ResetEvents()
} }

View File

@ -3,7 +3,7 @@ package action
import ( import (
"bytes" "bytes"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type PaneKeyAction func(Pane) bool type PaneKeyAction func(Pane) bool

View File

@ -6,7 +6,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type RawPane struct { type RawPane struct {

View File

@ -9,7 +9,7 @@ import (
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/views" "github.com/zyedidia/micro/v2/internal/views"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
// The TabList is a list of tabs and a window to display the tab bar // The TabList is a list of tabs and a window to display the tab bar
@ -107,30 +107,32 @@ func (t *TabList) HandleEvent(event tcell.Event) {
mx, my := e.Position() mx, my := e.Position()
switch e.Buttons() { switch e.Buttons() {
case tcell.Button1: case tcell.Button1:
if my == t.Y && mx == 0 { if my == t.Y && len(t.List) > 1 {
t.Scroll(-4) if mx == 0 {
return t.Scroll(-4)
} else if my == t.Y && mx == t.Width-1 { } else if mx == t.Width-1 {
t.Scroll(4) t.Scroll(4)
} else {
ind := t.LocFromVisual(buffer.Loc{mx, my})
if ind != -1 {
t.SetActive(ind)
}
}
return return
} }
if len(t.List) > 1 { case tcell.ButtonNone:
ind := t.LocFromVisual(buffer.Loc{mx, my}) if t.List[t.Active()].release {
if ind != -1 { // Mouse release received, while already released
t.SetActive(ind) t.ResetMouse()
return return
}
if my == 0 {
return
}
} }
case tcell.WheelUp: case tcell.WheelUp:
if my == t.Y { if my == t.Y && len(t.List) > 1 {
t.Scroll(4) t.Scroll(4)
return return
} }
case tcell.WheelDown: case tcell.WheelDown:
if my == t.Y { if my == t.Y && len(t.List) > 1 {
t.Scroll(-4) t.Scroll(-4)
return return
} }
@ -147,6 +149,56 @@ func (t *TabList) Display() {
} }
} }
func (t *TabList) SetActive(a int) {
t.TabWindow.SetActive(a)
for i, p := range t.List {
if i == a {
if !p.isActive {
p.isActive = true
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, p.CurPane()))
if err != nil {
screen.TermMessage(err)
}
}
} else {
p.isActive = false
}
}
}
// ResetMouse resets the mouse release state after the screen was stopped
// or the pane changed.
// This prevents situations in which mouse releases are received at the wrong place
// and the mouse state is still pressed.
func (t *TabList) ResetMouse() {
for _, tab := range t.List {
if !tab.release && tab.resizing != nil {
tab.resizing = nil
}
tab.release = true
for _, p := range tab.Panes {
if bp, ok := p.(*BufPane); ok {
bp.resetMouse()
}
}
}
}
// CloseTerms notifies term panes that a terminal job has finished.
func (t *TabList) CloseTerms() {
for _, tab := range t.List {
for _, p := range tab.Panes {
if tp, ok := p.(*TermPane); ok {
tp.HandleTermClose()
}
}
}
}
// Tabs is the global tab list // Tabs is the global tab list
var Tabs *TabList var Tabs *TabList
@ -164,6 +216,8 @@ func InitTabs(bufs []*buffer.Buffer) {
} }
} }
} }
screen.RestartCallback = Tabs.ResetMouse
} }
func MainTab() *Tab { func MainTab() *Tab {
@ -177,6 +231,9 @@ func MainTab() *Tab {
type Tab struct { type Tab struct {
*views.Node *views.Node
*display.UIWindow *display.UIWindow
isActive bool
Panes []Pane Panes []Pane
active int active int
@ -214,34 +271,40 @@ func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
// HandleEvent takes a tcell event and usually dispatches it to the current // HandleEvent takes a tcell event and usually dispatches it to the current
// active pane. However if the event is a resize or a mouse event where the user // active pane. However if the event is a resize or a mouse event where the user
// is interacting with the UI (resizing splits) then the event is consumed here // is interacting with the UI (resizing splits) then the event is consumed here
// If the event is a mouse event in a pane, that pane will become active and get // If the event is a mouse press event in a pane, that pane will become active
// the event // and get the event
func (t *Tab) HandleEvent(event tcell.Event) { func (t *Tab) HandleEvent(event tcell.Event) {
switch e := event.(type) { switch e := event.(type) {
case *tcell.EventMouse: case *tcell.EventMouse:
mx, my := e.Position() mx, my := e.Position()
switch e.Buttons() { btn := e.Buttons()
case tcell.Button1: switch {
case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone:
// button press or drag
wasReleased := t.release wasReleased := t.release
t.release = false t.release = false
if t.resizing != nil {
var size int if btn == tcell.Button1 {
if t.resizing.Kind == views.STVert { if t.resizing != nil {
size = mx - t.resizing.X var size int
} else { if t.resizing.Kind == views.STVert {
size = my - t.resizing.Y + 1 size = mx - t.resizing.X
} else {
size = my - t.resizing.Y + 1
}
t.resizing.ResizeSplit(size)
t.Resize()
return
}
if wasReleased {
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
if t.resizing != nil {
return
}
} }
t.resizing.ResizeSplit(size)
t.Resize()
return
} }
if wasReleased { if wasReleased {
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
if t.resizing != nil {
return
}
for i, p := range t.Panes { for i, p := range t.Panes {
v := p.GetView() v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
@ -251,10 +314,15 @@ func (t *Tab) HandleEvent(event tcell.Event) {
} }
} }
} }
case tcell.ButtonNone: case btn == tcell.ButtonNone:
t.resizing = nil // button release
t.release = true t.release = true
if t.resizing != nil {
t.resizing = nil
return
}
default: default:
// wheel move
for _, p := range t.Panes { for _, p := range t.Panes {
v := p.GetView() v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
@ -279,11 +347,16 @@ func (t *Tab) SetActive(i int) {
p.SetActive(false) p.SetActive(false)
} }
} }
}
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, MainTab().CurPane())) // AddPane adds a pane at a given index
if err != nil { func (t *Tab) AddPane(pane Pane, i int) {
screen.TermMessage(err) if len(t.Panes) == i {
t.Panes = append(t.Panes, pane)
return
} }
t.Panes = append(t.Panes[:i+1], t.Panes[i:]...)
t.Panes[i] = pane
} }
// GetPane returns the pane with the given split index // GetPane returns the pane with the given split index

View File

@ -9,8 +9,8 @@ import (
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
"github.com/zyedidia/terminal" "github.com/micro-editor/terminal"
) )
type TermKeyAction func(*TermPane) type TermKeyAction func(*TermPane)
@ -81,6 +81,10 @@ func (t *TermPane) SetID(i uint64) {
t.id = i t.id = i
} }
func (t *TermPane) Name() string {
return t.Terminal.Name()
}
func (t *TermPane) SetTab(tab *Tab) { func (t *TermPane) SetTab(tab *Tab) {
t.tab = tab t.tab = tab
} }
@ -121,11 +125,7 @@ func (t *TermPane) Unsplit() {
// copy-paste // copy-paste
func (t *TermPane) HandleEvent(event tcell.Event) { func (t *TermPane) HandleEvent(event tcell.Event) {
if e, ok := event.(*tcell.EventKey); ok { if e, ok := event.(*tcell.EventKey); ok {
ke := KeyEvent{ ke := keyEvent(e)
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}
action, more := TermBindings.NextEvent(ke, nil) action, more := TermBindings.NextEvent(ke, nil)
if !more { if !more {
@ -159,9 +159,9 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
if t.Status != shell.TTDone { if t.Status != shell.TTDone {
t.WriteString(event.EscSeq()) t.WriteString(event.EscSeq())
} }
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) { } else if e, ok := event.(*tcell.EventMouse); !ok || t.State.Mode(terminal.ModeMouseMask) {
// t.WriteString(event.EscSeq()) // t.WriteString(event.EscSeq())
} else if e != nil { } else {
x, y := e.Position() x, y := e.Position()
v := t.GetView() v := t.GetView()
x -= v.X x -= v.X
@ -188,7 +188,12 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
t.mouseReleased = true t.mouseReleased = true
} }
} }
}
// HandleTermClose is called when a terminal has finished its job
// and should be closed. If that terminal is this termpane's terminal,
// HandleTermClose will close the terminal and the termpane itself.
func (t *TermPane) HandleTermClose() {
if t.Status == shell.TTClose { if t.Status == shell.TTClose {
t.Quit() t.Quit()
} }

View File

@ -2,7 +2,7 @@ package buffer
import ( import (
"bytes" "bytes"
"io/ioutil" "io/fs"
"os" "os"
"sort" "sort"
"strings" "strings"
@ -64,7 +64,7 @@ func (b *Buffer) CycleAutocomplete(forward bool) {
// GetWord gets the most recent word separated by any separator // GetWord gets the most recent word separated by any separator
// (whitespace, punctuation, any non alphanumeric character) // (whitespace, punctuation, any non alphanumeric character)
func GetWord(b *Buffer) ([]byte, int) { func (b *Buffer) GetWord() ([]byte, int) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
l := b.LineBytes(c.Y) l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X) l = util.SliceStart(l, c.X)
@ -73,17 +73,17 @@ func GetWord(b *Buffer) ([]byte, int) {
return []byte{}, -1 return []byte{}, -1
} }
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc.Move(-1, b))) { if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) {
return []byte{}, c.X return []byte{}, c.X
} }
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) args := bytes.FieldsFunc(l, util.IsNonWordChar)
input := args[len(args)-1] input := args[len(args)-1]
return input, c.X - util.CharacterCount(input) return input, c.X - util.CharacterCount(input)
} }
// GetArg gets the most recent word (separated by ' ' only) // GetArg gets the most recent word (separated by ' ' only)
func GetArg(b *Buffer) (string, int) { func (b *Buffer) GetArg() (string, int) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
l := b.LineBytes(c.Y) l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X) l = util.SliceStart(l, c.X)
@ -104,20 +104,20 @@ func GetArg(b *Buffer) (string, int) {
// FileComplete autocompletes filenames // FileComplete autocompletes filenames
func FileComplete(b *Buffer) ([]string, []string) { func FileComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
input, argstart := GetArg(b) input, argstart := b.GetArg()
sep := string(os.PathSeparator) sep := string(os.PathSeparator)
dirs := strings.Split(input, sep) dirs := strings.Split(input, sep)
var files []os.FileInfo var files []fs.DirEntry
var err error var err error
if len(dirs) > 1 { if len(dirs) > 1 {
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
directories, _ = util.ReplaceHome(directories) directories, _ = util.ReplaceHome(directories)
files, err = ioutil.ReadDir(directories) files, err = os.ReadDir(directories)
} else { } else {
files, err = ioutil.ReadDir(".") files, err = os.ReadDir(".")
} }
if err != nil { if err != nil {
@ -153,7 +153,7 @@ func FileComplete(b *Buffer) ([]string, []string) {
// BufferComplete autocompletes based on previous words in the buffer // BufferComplete autocompletes based on previous words in the buffer
func BufferComplete(b *Buffer) ([]string, []string) { func BufferComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()
input, argstart := GetWord(b) input, argstart := b.GetWord()
if argstart == -1 { if argstart == -1 {
return []string{}, []string{} return []string{}, []string{}
@ -166,7 +166,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
var suggestions []string var suggestions []string
for i := c.Y; i >= 0; i-- { for i := c.Y; i >= 0; i-- {
l := b.LineBytes(i) l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) words := bytes.FieldsFunc(l, util.IsNonWordChar)
for _, w := range words { for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen { if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w) strw := string(w)
@ -179,7 +179,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
} }
for i := c.Y + 1; i < b.LinesNum(); i++ { for i := c.Y + 1; i < b.LinesNum(); i++ {
l := b.LineBytes(i) l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) words := bytes.FieldsFunc(l, util.IsNonWordChar)
for _, w := range words { for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen { if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w) strw := string(w)

View File

@ -1,24 +1,26 @@
package buffer package buffer
import ( import (
"errors"
"fmt" "fmt"
"io" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sync/atomic"
"time"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
) )
const backupMsg = `A backup was detected for this file. This likely means that micro const BackupMsg = `A backup was detected for:
crashed while editing this file, or another instance of micro is currently
editing this file.
The backup was created on %s, and the file is %s
This likely means that micro crashed while editing this file,
or another instance of micro is currently editing this file,
or an error occurred while saving this file so it may be corrupted.
The backup was created on %s and its path is:
%s %s
@ -30,90 +32,80 @@ The backup was created on %s, and the file is
Options: [r]ecover, [i]gnore, [a]bort: ` Options: [r]ecover, [i]gnore, [a]bort: `
var backupRequestChan chan *Buffer const backupSeconds = 8
func backupThread() { var BackupCompleteChan chan *Buffer
for {
time.Sleep(time.Second * 8)
for len(backupRequestChan) > 0 {
b := <-backupRequestChan
bfini := atomic.LoadInt32(&(b.fini)) != 0
if !bfini {
b.Backup()
}
}
}
}
func init() { func init() {
backupRequestChan = make(chan *Buffer, 10) BackupCompleteChan = make(chan *Buffer, 10)
go backupThread()
} }
func (b *Buffer) RequestBackup() { func (b *Buffer) RequestBackup() {
if !b.requestedBackup { if !b.RequestedBackup {
select { select {
case backupRequestChan <- b: case backupRequestChan <- b:
default: default:
// channel is full // channel is full
} }
b.requestedBackup = true b.RequestedBackup = true
} }
} }
// Backup saves the current buffer to ConfigDir/backups func (b *Buffer) backupDir() string {
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
if backupdir == "" || err != nil {
backupdir = filepath.Join(config.ConfigDir, "backups")
}
return backupdir
}
func (b *Buffer) keepBackup() bool {
return b.forceKeepBackup || b.Settings["permbackup"].(bool)
}
// Backup saves the current buffer to the backups directory
func (b *Buffer) Backup() error { func (b *Buffer) Backup() error {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return nil return nil
} }
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) backupdir := b.backupDir()
if backupdir == "" || err != nil { if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) {
backupdir = filepath.Join(config.ConfigDir, "backups")
}
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
os.Mkdir(backupdir, os.ModePerm) os.Mkdir(backupdir, os.ModePerm)
} }
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) name := util.DetermineEscapePath(backupdir, b.AbsPath)
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { _, err = b.overwriteFile(name)
if len(b.lines) == 0 { if err == nil {
return BackupCompleteChan <- b
} }
return err
}
// end of line tmp := util.AppendBackupSuffix(name)
eol := []byte{'\n'} _, err := b.overwriteFile(tmp)
if err != nil {
os.Remove(tmp)
return err
}
err = os.Rename(tmp, name)
if err != nil {
os.Remove(tmp)
return err
}
// write lines BackupCompleteChan <- b
if _, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
}
return
}, false)
b.requestedBackup = false
return err return err
} }
// RemoveBackup removes any backup file associated with this buffer // RemoveBackup removes any backup file associated with this buffer
func (b *Buffer) RemoveBackup() { func (b *Buffer) RemoveBackup() {
if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault { if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault {
return return
} }
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) f := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
os.Remove(f) os.Remove(f)
} }
@ -121,13 +113,13 @@ func (b *Buffer) RemoveBackup() {
// Returns true if a backup was applied // Returns true if a backup was applied
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
if info, err := os.Stat(backupfile); err == nil { if info, err := os.Stat(backupfile); err == nil {
backup, err := os.Open(backupfile) backup, err := os.Open(backupfile)
if err == nil { if err == nil {
defer backup.Close() defer backup.Close()
t := info.ModTime() t := info.ModTime()
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath)) msg := fmt.Sprintf(BackupMsg, b.Path, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile)
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
if choice%3 == 0 { if choice%3 == 0 {

View File

@ -7,7 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/fs"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -25,13 +25,12 @@ import (
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight" "github.com/zyedidia/micro/v2/pkg/highlight"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex" "golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
const backupTime = 8000
var ( var (
// OpenBuffers is a list of the currently open buffers // OpenBuffers is a list of the currently open buffers
OpenBuffers []*Buffer OpenBuffers []*Buffer
@ -57,17 +56,13 @@ var (
BTLog = BufType{2, true, true, false} BTLog = BufType{2, true, true, false}
// BTScratch is a buffer that cannot be saved (for scratch work) // BTScratch is a buffer that cannot be saved (for scratch work)
BTScratch = BufType{3, false, true, false} BTScratch = BufType{3, false, true, false}
// BTRaw is is a buffer that shows raw terminal events // BTRaw is a buffer that shows raw terminal events
BTRaw = BufType{4, false, true, false} BTRaw = BufType{4, false, true, false}
// BTInfo is a buffer for inputting information // BTInfo is a buffer for inputting information
BTInfo = BufType{5, false, true, false} BTInfo = BufType{5, false, true, false}
// BTStdout is a buffer that only writes to stdout // BTStdout is a buffer that only writes to stdout
// when closed // when closed
BTStdout = BufType{6, false, true, true} BTStdout = BufType{6, false, true, true}
// ErrFileTooLarge is returned when the file is too large to hash
// (fastdirty is automatically enabled)
ErrFileTooLarge = errors.New("File is too large to hash")
) )
// SharedBuffer is a struct containing info that is shared among buffers // SharedBuffer is a struct containing info that is shared among buffers
@ -90,6 +85,10 @@ type SharedBuffer struct {
// Settings customized by the user // Settings customized by the user
Settings map[string]interface{} Settings map[string]interface{}
// LocalSettings customized by the user for this buffer only
LocalSettings map[string]bool
encoding encoding.Encoding
Suggestions []string Suggestions []string
Completions []string Completions []string
@ -103,7 +102,8 @@ type SharedBuffer struct {
diffLock sync.RWMutex diffLock sync.RWMutex
diff map[int]DiffStatus diff map[int]DiffStatus
requestedBackup bool RequestedBackup bool
forceKeepBackup bool
// ReloadDisabled allows the user to disable reloads if they // ReloadDisabled allows the user to disable reloads if they
// are viewing a file that is constantly changing // are viewing a file that is constantly changing
@ -211,6 +211,11 @@ type Buffer struct {
LastSearchRegex bool LastSearchRegex bool
// HighlightSearch enables highlighting all instances of the last successful search // HighlightSearch enables highlighting all instances of the last successful search
HighlightSearch bool HighlightSearch bool
// OverwriteMode indicates that we are in overwrite mode (toggled by
// Insert key by default) i.e. that typing a character shall replace the
// character under the cursor instead of inserting a character before it.
OverwriteMode bool
} }
// NewBufferFromFileAtLoc opens a new buffer with a given cursor location // NewBufferFromFileAtLoc opens a new buffer with a given cursor location
@ -234,17 +239,20 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
return nil, err return nil, err
} }
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
readonly := os.IsPermission(err)
f.Close()
fileInfo, serr := os.Stat(filename) fileInfo, serr := os.Stat(filename)
if serr != nil && !os.IsNotExist(serr) { if serr != nil && !errors.Is(serr, fs.ErrNotExist) {
return nil, serr return nil, serr
} }
if serr == nil && fileInfo.IsDir() { if serr == nil && fileInfo.IsDir() {
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened") return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
} }
if serr == nil && !fileInfo.Mode().IsRegular() {
return nil, errors.New("Error: " + filename + " is not a regular file and cannot be opened")
}
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
readonly := errors.Is(err, fs.ErrPermission)
f.Close()
file, err := os.Open(filename) file, err := os.Open(filename)
if err == nil { if err == nil {
@ -252,7 +260,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
} }
var buf *Buffer var buf *Buffer
if os.IsNotExist(err) { if errors.Is(err, fs.ErrNotExist) {
// File does not exist -- create an empty buffer with that name // File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename, btype) buf = NewBufferFromString("", filename, btype)
} else if err != nil { } else if err != nil {
@ -322,29 +330,19 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
b.AbsPath = absPath b.AbsPath = absPath
b.Path = path b.Path = path
// this is a little messy since we need to know some settings to read
// the file properly, but some settings depend on the filetype, which
// we don't know until reading the file. We first read the settings
// into a local variable and then use that to determine the encoding,
// readonly, and fileformat necessary for reading the file and
// assigning the filetype.
settings := config.DefaultCommonSettings()
b.Settings = config.DefaultCommonSettings() b.Settings = config.DefaultCommonSettings()
b.LocalSettings = make(map[string]bool)
for k, v := range config.GlobalSettings { for k, v := range config.GlobalSettings {
if _, ok := config.DefaultGlobalOnlySettings[k]; !ok { if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
// make sure setting is not global-only // make sure setting is not global-only
settings[k] = v
b.Settings[k] = v b.Settings[k] = v
} }
} }
config.InitLocalSettings(settings, absPath) config.UpdatePathGlobLocals(b.Settings, absPath)
b.Settings["readonly"] = settings["readonly"]
b.Settings["filetype"] = settings["filetype"]
b.Settings["syntax"] = settings["syntax"]
enc, err := htmlindex.Get(settings["encoding"].(string)) b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string))
if err != nil { if err != nil {
enc = unicode.UTF8 b.encoding = unicode.UTF8
b.Settings["encoding"] = "utf-8" b.Settings["encoding"] = "utf-8"
} }
@ -355,19 +353,22 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
return NewBufferFromString("", "", btype) return NewBufferFromString("", "", btype)
} }
if !hasBackup { if !hasBackup {
reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder()))
var ff FileFormat = FFAuto var ff FileFormat = FFAuto
if size == 0 { if size == 0 {
// for empty files, use the fileformat setting instead of // for empty files, use the fileformat setting instead of
// autodetection // autodetection
switch settings["fileformat"] { switch b.Settings["fileformat"] {
case "unix": case "unix":
ff = FFUnix ff = FFUnix
case "dos": case "dos":
ff = FFDos ff = FFDos
} }
} else {
// in case of autodetection treat as locally set
b.LocalSettings["fileformat"] = true
} }
b.LineArray = NewLineArray(uint64(size), ff, reader) b.LineArray = NewLineArray(uint64(size), ff, reader)
@ -390,10 +391,10 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
} }
b.UpdateRules() b.UpdateRules()
// init local settings again now that we know the filetype // we know the filetype now, so update per-filetype settings
config.InitLocalSettings(b.Settings, b.Path) config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string))
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); errors.Is(err, fs.ErrNotExist) {
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm) os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
} }
@ -544,7 +545,7 @@ func (b *Buffer) ReOpen() error {
} }
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
data, err := ioutil.ReadAll(reader) data, err := io.ReadAll(reader)
txt := string(data) txt := string(data)
if err != nil { if err != nil {
@ -554,7 +555,11 @@ func (b *Buffer) ReOpen() error {
err = b.UpdateModTime() err = b.UpdateModTime()
if !b.Settings["fastdirty"].(bool) { if !b.Settings["fastdirty"].(bool) {
calcHash(b, &b.origHash) if len(data) > LargeFileThreshold {
b.Settings["fastdirty"] = true
} else {
calcHash(b, &b.origHash)
}
} }
b.isModified = false b.isModified = false
b.RelocateCursors() b.RelocateCursors()
@ -568,6 +573,13 @@ func (b *Buffer) RelocateCursors() {
} }
} }
// DeselectCursors removes selection from all cursors
func (b *Buffer) DeselectCursors() {
for _, c := range b.cursors {
c.Deselect(true)
}
}
// RuneAt returns the rune at a given location in the buffer // RuneAt returns the rune at a given location in the buffer
func (b *Buffer) RuneAt(loc Loc) rune { func (b *Buffer) RuneAt(loc Loc) rune {
line := b.LineBytes(loc.Y) line := b.LineBytes(loc.Y)
@ -642,39 +654,122 @@ func (b *Buffer) Size() int {
} }
// calcHash calculates md5 hash of all lines in the buffer // calcHash calculates md5 hash of all lines in the buffer
func calcHash(b *Buffer, out *[md5.Size]byte) error { func calcHash(b *Buffer, out *[md5.Size]byte) {
h := md5.New() h := md5.New()
size := 0
if len(b.lines) > 0 { if len(b.lines) > 0 {
n, e := h.Write(b.lines[0].data) h.Write(b.lines[0].data)
if e != nil {
return e
}
size += n
for _, l := range b.lines[1:] { for _, l := range b.lines[1:] {
n, e = h.Write([]byte{'\n'}) if b.Endings == FFDos {
if e != nil { h.Write([]byte{'\r', '\n'})
return e } else {
h.Write([]byte{'\n'})
} }
size += n h.Write(l.data)
n, e = h.Write(l.data)
if e != nil {
return e
}
size += n
} }
} }
if size > LargeFileThreshold {
return ErrFileTooLarge
}
h.Sum((*out)[:0]) h.Sum((*out)[:0])
}
func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
return nil
}
if header == nil {
header, err = highlight.MakeHeaderYaml(data)
if err != nil {
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
return nil
}
}
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
return nil
}
syndef, err := highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
return nil
}
return syndef
}
// findRealRuntimeSyntaxDef finds a specific syntax definition
// in the user's custom syntax files
func findRealRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
if f.Name() == name {
syndef := parseDefFromFile(f, header)
if syndef != nil {
return syndef
}
}
}
return nil return nil
} }
// findRuntimeSyntaxDef finds a specific syntax definition
// in the built-in syntax files
func findRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
if f.Name() == name {
syndef := parseDefFromFile(f, header)
if syndef != nil {
return syndef
}
}
}
return nil
}
func resolveIncludes(syndef *highlight.Def) {
includes := highlight.GetIncludes(syndef)
if len(includes) == 0 {
return
}
var files []*highlight.File
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
header, err := highlight.MakeHeaderYaml(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
for _, i := range includes {
if header.FileType == i {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
files = append(files, file)
break
}
}
if len(files) >= len(includes) {
break
}
}
highlight.ResolveIncludes(syndef, files)
}
// UpdateRules updates the syntax rules and filetype for this buffer // UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes // This is called when the colorscheme changes
func (b *Buffer) UpdateRules() { func (b *Buffer) UpdateRules() {
@ -683,13 +778,32 @@ func (b *Buffer) UpdateRules() {
} }
ft := b.Settings["filetype"].(string) ft := b.Settings["filetype"].(string)
if ft == "off" { if ft == "off" {
b.ClearMatches()
b.SyntaxDef = nil
return return
} }
b.SyntaxDef = nil
// syntaxFileInfo is an internal helper structure
// to store properties of one single syntax file
type syntaxFileInfo struct {
header *highlight.Header
fileName string
syntaxDef *highlight.Def
}
fnameMatches := []syntaxFileInfo{}
headerMatches := []syntaxFileInfo{}
syntaxFile := "" syntaxFile := ""
foundDef := false foundDef := false
var header *highlight.Header var header *highlight.Header
// search for the syntax file in the user's custom syntax files // search for the syntax file in the user's custom syntax files
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) { for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
if f.Name() == "default" {
continue
}
data, err := f.Data() data, err := f.Data()
if err != nil { if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
@ -701,118 +815,145 @@ func (b *Buffer) UpdateRules() {
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error()) screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
continue continue
} }
file, err := highlight.ParseFile(data)
if err != nil { matchedFileType := false
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) matchedFileName := false
continue matchedFileHeader := false
if ft == "unknown" || ft == "" {
if header.MatchFileName(b.Path) {
matchedFileName = true
}
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
matchedFileHeader = true
}
} else if header.FileType == ft {
matchedFileType = true
} }
if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft { if matchedFileType || matchedFileName || matchedFileHeader {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
syndef, err := highlight.ParseDef(file, header) syndef, err := highlight.ParseDef(file, header)
if err != nil { if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue continue
} }
b.SyntaxDef = syndef
syntaxFile = f.Name() if matchedFileType {
foundDef = true b.SyntaxDef = syndef
break syntaxFile = f.Name()
foundDef = true
break
}
if matchedFileName {
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), syndef})
} else if matchedFileHeader {
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), syndef})
}
} }
} }
// search in the default syntax files if !foundDef {
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) { // search for the syntax file in the built-in syntax files
data, err := f.Data() for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
if err != nil { data, err := f.Data()
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error()) if err != nil {
continue screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
} continue
}
header, err = highlight.MakeHeader(data) header, err = highlight.MakeHeader(data)
if err != nil { if err != nil {
screen.TermMessage("Error reading syntax header file", f.Name(), err) screen.TermMessage("Error reading syntax header file", f.Name(), err)
continue continue
} }
if ft == "unknown" || ft == "" { if ft == "unknown" || ft == "" {
if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) { if header.MatchFileName(b.Path) {
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), nil})
}
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), nil})
}
} else if header.FileType == ft {
syntaxFile = f.Name() syntaxFile = f.Name()
break break
} }
} else if header.FileType == ft { }
syntaxFile = f.Name() }
break
if syntaxFile == "" {
matches := fnameMatches
if len(matches) == 0 {
matches = headerMatches
}
length := len(matches)
if length > 0 {
signatureMatch := false
if length > 1 {
// multiple matching syntax files found, try to resolve the ambiguity
// using signatures
detectlimit := util.IntOpt(b.Settings["detectlimit"])
lineCount := len(b.lines)
limit := lineCount
if detectlimit > 0 && lineCount > detectlimit {
limit = detectlimit
}
matchLoop:
for _, m := range matches {
if m.header.HasFileSignature() {
for i := 0; i < limit; i++ {
if m.header.MatchFileSignature(b.lines[i].data) {
syntaxFile = m.fileName
if m.syntaxDef != nil {
b.SyntaxDef = m.syntaxDef
foundDef = true
}
header = m.header
signatureMatch = true
break matchLoop
}
}
}
}
}
if length == 1 || !signatureMatch {
syntaxFile = matches[0].fileName
if matches[0].syntaxDef != nil {
b.SyntaxDef = matches[0].syntaxDef
foundDef = true
}
header = matches[0].header
}
} }
} }
if syntaxFile != "" && !foundDef { if syntaxFile != "" && !foundDef {
// we found a syntax file using a syntax header file // we found a syntax file using a syntax header file
for _, f := range config.ListRuntimeFiles(config.RTSyntax) { b.SyntaxDef = findRuntimeSyntaxDef(syntaxFile, header)
if f.Name() == syntaxFile {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
syndef, err := highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
b.SyntaxDef = syndef
break
}
}
} }
if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) { if b.SyntaxDef != nil {
includes := highlight.GetIncludes(b.SyntaxDef) b.Settings["filetype"] = b.SyntaxDef.FileType
var files []*highlight.File
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
header, err := highlight.MakeHeaderYaml(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
for _, i := range includes {
if header.FileType == i {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
files = append(files, file)
break
}
}
if len(files) >= len(includes) {
break
}
}
highlight.ResolveIncludes(b.SyntaxDef, files)
}
if b.Highlighter == nil || syntaxFile != "" {
if b.SyntaxDef != nil {
b.Settings["filetype"] = b.SyntaxDef.FileType
}
} else { } else {
b.SyntaxDef = &highlight.EmptyDef // search for the default file in the user's custom syntax files
b.SyntaxDef = findRealRuntimeSyntaxDef("default", nil)
if b.SyntaxDef == nil {
// search for the default file in the built-in syntax files
b.SyntaxDef = findRuntimeSyntaxDef("default", nil)
}
}
if b.SyntaxDef != nil {
resolveIncludes(b.SyntaxDef)
} }
if b.SyntaxDef != nil { if b.SyntaxDef != nil {
@ -914,7 +1055,7 @@ func (b *Buffer) MergeCursors() {
b.EventHandler.active = b.curCursor b.EventHandler.active = b.curCursor
} }
// UpdateCursors updates all the cursors indicies // UpdateCursors updates all the cursors indices
func (b *Buffer) UpdateCursors() { func (b *Buffer) UpdateCursors() {
b.EventHandler.cursors = b.cursors b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor b.EventHandler.active = b.curCursor
@ -939,7 +1080,7 @@ func (b *Buffer) ClearCursors() {
b.cursors = b.cursors[:1] b.cursors = b.cursors[:1]
b.UpdateCursors() b.UpdateCursors()
b.curCursor = 0 b.curCursor = 0
b.GetActiveCursor().ResetSelection() b.GetActiveCursor().Deselect(true)
} }
// MoveLinesUp moves the range of lines up one row // MoveLinesUp moves the range of lines up one row
@ -990,34 +1131,14 @@ var BracePairs = [][2]rune{
{'[', ']'}, {'[', ']'},
} }
// FindMatchingBrace returns the location in the buffer of the matching bracket func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc, bool) {
// It is given a brace type containing the open and closing character, (for example
// '{' and '}') as well as the location to match from
// TODO: maybe can be more efficient with utf8 package
// returns the location of the matching brace
// if the boolean returned is true then the original matching brace is one character left
// of the starting location
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
curLine := []rune(string(b.LineBytes(start.Y)))
startChar := ' '
if start.X >= 0 && start.X < len(curLine) {
startChar = curLine[start.X]
}
leftChar := ' '
if start.X-1 >= 0 && start.X-1 < len(curLine) {
leftChar = curLine[start.X-1]
}
var i int var i int
if startChar == braceType[0] || leftChar == braceType[0] { if char == braceType[0] {
for y := start.Y; y < b.LinesNum(); y++ { for y := start.Y; y < b.LinesNum(); y++ {
l := []rune(string(b.LineBytes(y))) l := []rune(string(b.LineBytes(y)))
xInit := 0 xInit := 0
if y == start.Y { if y == start.Y {
if startChar == braceType[0] { xInit = start.X
xInit = start.X
} else {
xInit = start.X - 1
}
} }
for x := xInit; x < len(l); x++ { for x := xInit; x < len(l); x++ {
r := l[x] r := l[x]
@ -1026,42 +1147,76 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo
} else if r == braceType[1] { } else if r == braceType[1] {
i-- i--
if i == 0 { if i == 0 {
if startChar == braceType[0] { return Loc{x, y}, true
return Loc{x, y}, false, true
}
return Loc{x, y}, true, true
} }
} }
} }
} }
} else if startChar == braceType[1] || leftChar == braceType[1] { } else if char == braceType[1] {
for y := start.Y; y >= 0; y-- { for y := start.Y; y >= 0; y-- {
l := []rune(string(b.lines[y].data)) l := []rune(string(b.lines[y].data))
xInit := len(l) - 1 xInit := len(l) - 1
if y == start.Y { if y == start.Y {
if leftChar == braceType[1] { xInit = start.X
xInit = start.X - 1
} else {
xInit = start.X
}
} }
for x := xInit; x >= 0; x-- { for x := xInit; x >= 0; x-- {
r := l[x] r := l[x]
if r == braceType[0] { if r == braceType[1] {
i++
} else if r == braceType[0] {
i-- i--
if i == 0 { if i == 0 {
if leftChar == braceType[1] { return Loc{x, y}, true
return Loc{x, y}, true, true
}
return Loc{x, y}, false, true
} }
} else if r == braceType[1] {
i++
} }
} }
} }
} }
return start, true, false return start, false
}
// If there is a brace character (for example '{' or ']') at the given start location,
// FindMatchingBrace returns the location of the matching brace for it (for example '}'
// or '['). The second returned value is true if there was no matching brace found
// for given starting location but it was found for the location one character left
// of it. The third returned value is true if the matching brace was found at all.
func (b *Buffer) FindMatchingBrace(start Loc) (Loc, bool, bool) {
// TODO: maybe can be more efficient with utf8 package
curLine := []rune(string(b.LineBytes(start.Y)))
// first try to find matching brace for the given location (it has higher priority)
if start.X >= 0 && start.X < len(curLine) {
startChar := curLine[start.X]
for _, bp := range BracePairs {
if startChar == bp[0] || startChar == bp[1] {
mb, found := b.findMatchingBrace(bp, start, startChar)
if found {
return mb, false, true
}
}
}
}
if b.Settings["matchbraceleft"].(bool) {
// failed to find matching brace for the given location, so try to find matching
// brace for the location one character left of it
if start.X-1 >= 0 && start.X-1 < len(curLine) {
leftChar := curLine[start.X-1]
left := Loc{start.X - 1, start.Y}
for _, bp := range BracePairs {
if leftChar == bp[0] || leftChar == bp[1] {
mb, found := b.findMatchingBrace(bp, left, leftChar)
if found {
return mb, true, true
}
}
}
}
}
return start, false, false
} }
// Retab changes all tabs to spaces or vice versa // Retab changes all tabs to spaces or vice versa
@ -1083,7 +1238,11 @@ func (b *Buffer) Retab() {
} }
l = bytes.TrimLeft(l, " \t") l = bytes.TrimLeft(l, " \t")
b.Lock()
b.lines[i].data = append(ws, l...) b.lines[i].data = append(ws, l...)
b.Unlock()
b.MarkModified(i, i) b.MarkModified(i, i)
dirty = true dirty = true
} }
@ -1126,7 +1285,7 @@ func (b *Buffer) Write(bytes []byte) (n int, err error) {
return len(bytes), nil return len(bytes), nil
} }
func (b *Buffer) updateDiffSync() { func (b *Buffer) updateDiff(synchronous bool) {
b.diffLock.Lock() b.diffLock.Lock()
defer b.diffLock.Unlock() defer b.diffLock.Unlock()
@ -1137,7 +1296,16 @@ func (b *Buffer) updateDiffSync() {
} }
differ := dmp.New() differ := dmp.New()
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
if !synchronous {
b.Lock()
}
bytes := b.Bytes()
if !synchronous {
b.Unlock()
}
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(bytes))
diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false) diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
lineN := 0 lineN := 0
@ -1166,13 +1334,9 @@ func (b *Buffer) updateDiffSync() {
// UpdateDiff computes the diff between the diff base and the buffer content. // UpdateDiff computes the diff between the diff base and the buffer content.
// The update may be performed synchronously or asynchronously. // The update may be performed synchronously or asynchronously.
// UpdateDiff calls the supplied callback when the update is complete.
// The argument passed to the callback is set to true if and only if
// the update was performed synchronously.
// If an asynchronous update is already pending when UpdateDiff is called, // If an asynchronous update is already pending when UpdateDiff is called,
// UpdateDiff does not schedule another update, in which case the callback // UpdateDiff does not schedule another update.
// is not called. func (b *Buffer) UpdateDiff() {
func (b *Buffer) UpdateDiff(callback func(bool)) {
if b.updateDiffTimer != nil { if b.updateDiffTimer != nil {
return return
} }
@ -1183,20 +1347,18 @@ func (b *Buffer) UpdateDiff(callback func(bool)) {
} }
if lineCount < 1000 { if lineCount < 1000 {
b.updateDiffSync() b.updateDiff(true)
callback(true)
} else if lineCount < 30000 { } else if lineCount < 30000 {
b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() { b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
b.updateDiffTimer = nil b.updateDiffTimer = nil
b.updateDiffSync() b.updateDiff(false)
callback(false) screen.Redraw()
}) })
} else { } else {
// Don't compute diffs for very large files // Don't compute diffs for very large files
b.diffLock.Lock() b.diffLock.Lock()
b.diff = make(map[int]DiffStatus) b.diff = make(map[int]DiffStatus)
b.diffLock.Unlock() b.diffLock.Unlock()
callback(true)
} }
} }
@ -1208,9 +1370,7 @@ func (b *Buffer) SetDiffBase(diffBase []byte) {
} else { } else {
b.diffBaseLineCount = strings.Count(string(diffBase), "\n") b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
} }
b.UpdateDiff(func(synchronous bool) { b.UpdateDiff()
screen.Redraw()
})
} }
// DiffStatus returns the diff status for a line in the buffer // DiffStatus returns the diff status for a line in the buffer

View File

@ -20,6 +20,7 @@ type operation struct {
func init() { func init() {
ulua.L = lua.NewState() ulua.L = lua.NewState()
config.InitRuntimeFiles(false)
config.InitGlobalSettings() config.InitGlobalSettings()
config.GlobalSettings["backup"] = false config.GlobalSettings["backup"] = false
config.GlobalSettings["fastdirty"] = true config.GlobalSettings["fastdirty"] = true

View File

@ -20,8 +20,14 @@ type Cursor struct {
buf *Buffer buf *Buffer
Loc Loc
// Last cursor x position // Last visual x position of the cursor. Used in cursor up/down movements
// for remembering the original x position when moving to a line that is
// shorter than current x position.
LastVisualX int LastVisualX int
// Similar to LastVisualX but takes softwrapping into account, i.e. last
// visual x position in a visual (wrapped) line on the screen, which may be
// different from the line in the buffer.
LastWrappedVisualX int
// The current selection as a range of character numbers (inclusive) // The current selection as a range of character numbers (inclusive)
CurSelection [2]Loc CurSelection [2]Loc
@ -30,6 +36,11 @@ type Cursor struct {
// to know what the original selection was // to know what the original selection was
OrigSelection [2]Loc OrigSelection [2]Loc
// The line number where a new trailing whitespace has been added
// or -1 if there is no new trailing whitespace at this cursor.
// This is used for checking if a trailing whitespace should be highlighted
NewTrailingWsY int
// Which cursor index is this (for multiple cursors) // Which cursor index is this (for multiple cursors)
Num int Num int
} }
@ -38,6 +49,8 @@ func NewCursor(b *Buffer, l Loc) *Cursor {
c := &Cursor{ c := &Cursor{
buf: b, buf: b,
Loc: l, Loc: l,
NewTrailingWsY: -1,
} }
c.StoreVisualX() c.StoreVisualX()
return c return c
@ -54,8 +67,9 @@ func (c *Cursor) Buf() *Buffer {
// Goto puts the cursor at the given cursor's location and gives // Goto puts the cursor at the given cursor's location and gives
// the current cursor its selection too // the current cursor its selection too
func (c *Cursor) Goto(b Cursor) { func (c *Cursor) Goto(b Cursor) {
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX c.X, c.Y = b.X, b.Y
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
c.StoreVisualX()
} }
// GotoLoc puts the cursor at the given cursor's location and gives // GotoLoc puts the cursor at the given cursor's location and gives
@ -66,8 +80,8 @@ func (c *Cursor) GotoLoc(l Loc) {
} }
// GetVisualX returns the x value of the cursor in visual spaces // GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int { func (c *Cursor) GetVisualX(wrap bool) int {
if c.buf.GetVisualX != nil { if wrap && c.buf.GetVisualX != nil {
return c.buf.GetVisualX(c.Loc) return c.buf.GetVisualX(c.Loc)
} }
@ -93,7 +107,7 @@ func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
// Start moves the cursor to the start of the line it is on // Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() { func (c *Cursor) Start() {
c.X = 0 c.X = 0
c.LastVisualX = c.GetVisualX() c.StoreVisualX()
} }
// StartOfText moves the cursor to the first non-whitespace rune of // StartOfText moves the cursor to the first non-whitespace rune of
@ -124,7 +138,7 @@ func (c *Cursor) IsStartOfText() bool {
// End moves the cursor to the end of the line it is on // End moves the cursor to the end of the line it is on
func (c *Cursor) End() { func (c *Cursor) End() {
c.X = util.CharacterCount(c.buf.LineBytes(c.Y)) c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
c.LastVisualX = c.GetVisualX() c.StoreVisualX()
} }
// CopySelection copies the user's selection to either "primary" // CopySelection copies the user's selection to either "primary"
@ -179,7 +193,7 @@ func (c *Cursor) Deselect(start bool) {
if start { if start {
c.Loc = c.CurSelection[0] c.Loc = c.CurSelection[0]
} else { } else {
c.Loc = c.CurSelection[1].Move(-1, c.buf) c.Loc = c.CurSelection[1]
} }
c.ResetSelection() c.ResetSelection()
c.StoreVisualX() c.StoreVisualX()
@ -396,13 +410,26 @@ func (c *Cursor) SelectTo(loc Loc) {
// WordRight moves the cursor one word to the right // WordRight moves the cursor one word to the right
func (c *Cursor) WordRight() { func (c *Cursor) WordRight() {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return
}
for util.IsWhitespace(c.RuneUnder(c.X)) { for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) { if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return return
} }
c.Right() c.Right()
} }
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
util.IsNonWordChar(c.RuneUnder(c.X+1)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
return
}
c.Right() c.Right()
for util.IsWordChar(c.RuneUnder(c.X)) { for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) { if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
@ -414,6 +441,10 @@ func (c *Cursor) WordRight() {
// WordLeft moves the cursor one word to the left // WordLeft moves the cursor one word to the left
func (c *Cursor) WordLeft() { func (c *Cursor) WordLeft() {
if c.X == 0 {
c.Left()
return
}
c.Left() c.Left()
for util.IsWhitespace(c.RuneUnder(c.X)) { for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 { if c.X == 0 {
@ -421,6 +452,17 @@ func (c *Cursor) WordLeft() {
} }
c.Left() c.Left()
} }
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
util.IsNonWordChar(c.RuneUnder(c.X-1)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
return
}
c.Left() c.Left()
for util.IsWordChar(c.RuneUnder(c.X)) { for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == 0 { if c.X == 0 {
@ -431,6 +473,132 @@ func (c *Cursor) WordLeft() {
c.Right() c.Right()
} }
// SubWordRight moves the cursor one sub-word to the right
func (c *Cursor) SubWordRight() {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return
}
if util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
return
}
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
return
}
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
if util.IsWhitespace(c.RuneUnder(c.X)) {
return
}
}
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
util.IsUpperLetter(c.RuneUnder(c.X+1)) {
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
if util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
c.Left()
}
} else {
c.Right()
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
}
}
// SubWordLeft moves the cursor one sub-word to the left
func (c *Cursor) SubWordLeft() {
if c.X == 0 {
c.Left()
return
}
c.Left()
if util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
return
}
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
return
}
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
if util.IsWhitespace(c.RuneUnder(c.X)) {
c.Right()
return
}
}
if c.X == 0 {
return
}
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
util.IsUpperLetter(c.RuneUnder(c.X-1)) {
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
if !util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
c.Right()
}
} else {
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
if !util.IsAlphanumeric(c.RuneUnder(c.X)) {
c.Right()
}
}
}
// RuneUnder returns the rune under the given x position // RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune { func (c *Cursor) RuneUnder(x int) rune {
line := c.buf.LineBytes(c.Y) line := c.buf.LineBytes(c.Y)
@ -454,5 +622,6 @@ func (c *Cursor) RuneUnder(x int) rune {
} }
func (c *Cursor) StoreVisualX() { func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX() c.LastVisualX = c.GetVisualX(false)
c.LastWrappedVisualX = c.GetVisualX(true)
} }

View File

@ -104,7 +104,11 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
c.OrigSelection[0] = move(c.OrigSelection[0]) c.OrigSelection[0] = move(c.OrigSelection[0])
c.OrigSelection[1] = move(c.OrigSelection[1]) c.OrigSelection[1] = move(c.OrigSelection[1])
c.Relocate() c.Relocate()
c.LastVisualX = c.GetVisualX() c.StoreVisualX()
}
if useUndo {
eh.updateTrailingWs(t)
} }
} }
@ -249,11 +253,11 @@ func (eh *EventHandler) Execute(t *TextEvent) {
ExecuteTextEvent(t, eh.buf) ExecuteTextEvent(t, eh.buf)
} }
// Undo the first event in the undo stack // Undo the first event in the undo stack. Returns false if the stack is empty.
func (eh *EventHandler) Undo() { func (eh *EventHandler) Undo() bool {
t := eh.UndoStack.Peek() t := eh.UndoStack.Peek()
if t == nil { if t == nil {
return return false
} }
startTime := t.Time.UnixNano() / int64(time.Millisecond) startTime := t.Time.UnixNano() / int64(time.Millisecond)
@ -262,15 +266,16 @@ func (eh *EventHandler) Undo() {
for { for {
t = eh.UndoStack.Peek() t = eh.UndoStack.Peek()
if t == nil { if t == nil {
return break
} }
if t.Time.UnixNano()/int64(time.Millisecond) < endTime { if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
return break
} }
eh.UndoOneEvent() eh.UndoOneEvent()
} }
return true
} }
// UndoOneEvent undoes one event // UndoOneEvent undoes one event
@ -286,23 +291,20 @@ func (eh *EventHandler) UndoOneEvent() {
eh.UndoTextEvent(t) eh.UndoTextEvent(t)
// Set the cursor in the right place // Set the cursor in the right place
teCursor := t.C if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) { eh.cursors[t.C.Num].Goto(t.C)
t.C = *eh.cursors[teCursor.Num] eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
eh.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
} }
// Push it to the redo stack // Push it to the redo stack
eh.RedoStack.Push(t) eh.RedoStack.Push(t)
} }
// Redo the first event in the redo stack // Redo the first event in the redo stack. Returns false if the stack is empty.
func (eh *EventHandler) Redo() { func (eh *EventHandler) Redo() bool {
t := eh.RedoStack.Peek() t := eh.RedoStack.Peek()
if t == nil { if t == nil {
return return false
} }
startTime := t.Time.UnixNano() / int64(time.Millisecond) startTime := t.Time.UnixNano() / int64(time.Millisecond)
@ -311,15 +313,16 @@ func (eh *EventHandler) Redo() {
for { for {
t = eh.RedoStack.Peek() t = eh.RedoStack.Peek()
if t == nil { if t == nil {
return break
} }
if t.Time.UnixNano()/int64(time.Millisecond) > endTime { if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
return break
} }
eh.RedoOneEvent() eh.RedoOneEvent()
} }
return true
} }
// RedoOneEvent redoes one event // RedoOneEvent redoes one event
@ -329,12 +332,9 @@ func (eh *EventHandler) RedoOneEvent() {
return return
} }
teCursor := t.C if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) { eh.cursors[t.C.Num].Goto(t.C)
t.C = *eh.cursors[teCursor.Num] eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
eh.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
} }
// Modifies the text event // Modifies the text event
@ -342,3 +342,58 @@ func (eh *EventHandler) RedoOneEvent() {
eh.UndoStack.Push(t) eh.UndoStack.Push(t)
} }
// updateTrailingWs updates the cursor's trailing whitespace status after a text event
func (eh *EventHandler) updateTrailingWs(t *TextEvent) {
if len(t.Deltas) != 1 {
return
}
text := t.Deltas[0].Text
start := t.Deltas[0].Start
end := t.Deltas[0].End
c := eh.cursors[eh.active]
isEol := func(loc Loc) bool {
return loc.X == util.CharacterCount(eh.buf.LineBytes(loc.Y))
}
if t.EventType == TextEventInsert && c.Loc == end && isEol(end) {
var addedTrailingWs bool
addedAfterWs := false
addedWsOnly := false
if start.Y == end.Y {
addedTrailingWs = util.HasTrailingWhitespace(text)
addedWsOnly = util.IsBytesWhitespace(text)
addedAfterWs = start.X > 0 && util.IsWhitespace(c.buf.RuneAt(Loc{start.X - 1, start.Y}))
} else {
lastnl := bytes.LastIndex(text, []byte{'\n'})
addedTrailingWs = util.HasTrailingWhitespace(text[lastnl+1:])
}
if addedTrailingWs && !(addedAfterWs && addedWsOnly) {
c.NewTrailingWsY = c.Y
} else if !addedTrailingWs {
c.NewTrailingWsY = -1
}
} else if t.EventType == TextEventRemove && c.Loc == start && isEol(start) {
removedAfterWs := util.HasTrailingWhitespace(eh.buf.LineBytes(start.Y))
var removedWsOnly bool
if start.Y == end.Y {
removedWsOnly = util.IsBytesWhitespace(text)
} else {
firstnl := bytes.Index(text, []byte{'\n'})
removedWsOnly = util.IsBytesWhitespace(text[:firstnl])
}
if removedAfterWs && !removedWsOnly {
c.NewTrailingWsY = c.Y
} else if !removedAfterWs {
c.NewTrailingWsY = -1
}
} else if c.NewTrailingWsY != -1 && start.Y != end.Y && c.Loc.GreaterThan(start) &&
((t.EventType == TextEventInsert && c.Y == c.NewTrailingWsY+(end.Y-start.Y)) ||
(t.EventType == TextEventRemove && c.Y == c.NewTrailingWsY-(end.Y-start.Y))) {
// The cursor still has its new trailingws
// but its line number was shifted by insert or remove of lines above
c.NewTrailingWsY = c.Y
}
}

View File

@ -46,10 +46,9 @@ type searchState struct {
type Line struct { type Line struct {
data []byte data []byte
state highlight.State state highlight.State
match highlight.LineMatch match highlight.LineMatch
rehighlight bool lock sync.Mutex
lock sync.Mutex
// The search states for the line, used for highlighting of search matches, // The search states for the line, used for highlighting of search matches,
// separately from the syntax highlighting. // separately from the syntax highlighting.
@ -75,6 +74,7 @@ type LineArray struct {
lines []Line lines []Line
Endings FileFormat Endings FileFormat
initsize uint64 initsize uint64
lock sync.Mutex
} }
// Append efficiently appends lines together // Append efficiently appends lines together
@ -147,20 +147,18 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
la.lines = Append(la.lines, Line{ la.lines = Append(la.lines, Line{
data: data, data: data,
state: nil, state: nil,
match: nil, match: nil,
rehighlight: false,
}) })
} }
// Last line was read // Last line was read
break break
} else { } else {
la.lines = Append(la.lines, Line{ la.lines = Append(la.lines, Line{
data: data[:dlen-1], data: data[:dlen-1],
state: nil, state: nil,
match: nil, match: nil,
rehighlight: false,
}) })
} }
n++ n++
@ -190,22 +188,23 @@ func (la *LineArray) Bytes() []byte {
// newlineBelow adds a newline below the given line number // newlineBelow adds a newline below the given line number
func (la *LineArray) newlineBelow(y int) { func (la *LineArray) newlineBelow(y int) {
la.lines = append(la.lines, Line{ la.lines = append(la.lines, Line{
data: []byte{' '}, data: []byte{' '},
state: nil, state: nil,
match: nil, match: nil,
rehighlight: false,
}) })
copy(la.lines[y+2:], la.lines[y+1:]) copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = Line{ la.lines[y+1] = Line{
data: []byte{}, data: []byte{},
state: la.lines[y].state, state: la.lines[y].state,
match: nil, match: nil,
rehighlight: false,
} }
} }
// Inserts a byte array at a given location // Inserts a byte array at a given location
func (la *LineArray) insert(pos Loc, value []byte) { func (la *LineArray) insert(pos Loc, value []byte) {
la.lock.Lock()
defer la.lock.Unlock()
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
for i := 0; i < len(value); i++ { for i := 0; i < len(value); i++ {
if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') { if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') {
@ -233,24 +232,26 @@ func (la *LineArray) insertByte(pos Loc, value byte) {
// joinLines joins the two lines a and b // joinLines joins the two lines a and b
func (la *LineArray) joinLines(a, b int) { func (la *LineArray) joinLines(a, b int) {
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data) la.lines[a].data = append(la.lines[a].data, la.lines[b].data...)
la.deleteLine(b) la.deleteLine(b)
} }
// split splits a line at a given position // split splits a line at a given position
func (la *LineArray) split(pos Loc) { func (la *LineArray) split(pos Loc) {
la.newlineBelow(pos.Y) la.newlineBelow(pos.Y)
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:]) la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...)
la.lines[pos.Y+1].state = la.lines[pos.Y].state la.lines[pos.Y+1].state = la.lines[pos.Y].state
la.lines[pos.Y].state = nil la.lines[pos.Y].state = nil
la.lines[pos.Y].match = nil la.lines[pos.Y].match = nil
la.lines[pos.Y+1].match = nil la.lines[pos.Y+1].match = nil
la.lines[pos.Y].rehighlight = true
la.deleteToEnd(Loc{pos.X, pos.Y}) la.deleteToEnd(Loc{pos.X, pos.Y})
} }
// removes from start to end // removes from start to end
func (la *LineArray) remove(start, end Loc) []byte { func (la *LineArray) remove(start, end Loc) []byte {
la.lock.Lock()
defer la.lock.Unlock()
sub := la.Substr(start, end) sub := la.Substr(start, end)
startX := runeToByteIndex(start.X, la.lines[start.Y].data) startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data) endX := runeToByteIndex(end.X, la.lines[end.Y].data)
@ -284,11 +285,6 @@ func (la *LineArray) deleteLines(y1, y2 int) {
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])] la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
} }
// DeleteByte deletes the byte at a position
func (la *LineArray) deleteByte(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
}
// Substr returns the string representation between two locations // Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) []byte { func (la *LineArray) Substr(start, end Loc) []byte {
startX := runeToByteIndex(start.X, la.lines[start.Y].data) startX := runeToByteIndex(start.X, la.lines[start.Y].data)
@ -327,11 +323,11 @@ func (la *LineArray) End() Loc {
} }
// LineBytes returns line n as an array of bytes // LineBytes returns line n as an array of bytes
func (la *LineArray) LineBytes(n int) []byte { func (la *LineArray) LineBytes(lineN int) []byte {
if n >= len(la.lines) || n < 0 { if lineN >= len(la.lines) || lineN < 0 {
return []byte{} return []byte{}
} }
return la.lines[n].data return la.lines[lineN].data
} }
// State gets the highlight state for the given line number // State gets the highlight state for the given line number
@ -362,16 +358,14 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch {
return la.lines[lineN].match return la.lines[lineN].match
} }
func (la *LineArray) Rehighlight(lineN int) bool { // Locks the whole LineArray
la.lines[lineN].lock.Lock() func (la *LineArray) Lock() {
defer la.lines[lineN].lock.Unlock() la.lock.Lock()
return la.lines[lineN].rehighlight
} }
func (la *LineArray) SetRehighlight(lineN int, on bool) { // Unlocks the whole LineArray
la.lines[lineN].lock.Lock() func (la *LineArray) Unlock() {
defer la.lines[lineN].lock.Unlock() la.lock.Unlock()
la.lines[lineN].rehighlight = on
} }
// SearchMatch returns true if the location `pos` is within a match // SearchMatch returns true if the location `pos` is within a match

View File

@ -47,6 +47,16 @@ func (l Loc) LessEqual(b Loc) bool {
return l == b return l == b
} }
// Clamp clamps a loc between start and end
func (l Loc) Clamp(start, end Loc) Loc {
if l.GreaterEqual(end) {
return end
} else if l.LessThan(start) {
return start
}
return l
}
// The following functions require a buffer to know where newlines are // The following functions require a buffer to know where newlines are
// Diff returns the distance between two locations // Diff returns the distance between two locations
@ -139,10 +149,5 @@ func ByteOffset(pos Loc, buf *Buffer) int {
// clamps a loc within a buffer // clamps a loc within a buffer
func clamp(pos Loc, la *LineArray) Loc { func clamp(pos Loc, la *LineArray) Loc {
if pos.GreaterEqual(la.End()) { return pos.Clamp(la.Start(), la.End())
return la.End()
} else if pos.LessThan(la.Start()) {
return la.Start()
}
return pos
} }

View File

@ -2,7 +2,7 @@ package buffer
import ( import (
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type MsgType int type MsgType int

View File

@ -5,18 +5,19 @@ import (
"bytes" "bytes"
"errors" "errors"
"io" "io"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync/atomic"
"time"
"unicode" "unicode"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
@ -24,68 +25,178 @@ import (
// because hashing is too slow // because hashing is too slow
const LargeFileThreshold = 50000 const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls type wrappedFile struct {
// the supplied function with the file as io.Writer object, also making sure the file is writeCloser io.WriteCloser
// closed afterwards. withSudo bool
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) { screenb bool
cmd *exec.Cmd
sigChan chan os.Signal
}
type saveResponse struct {
size int
err error
}
type saveRequest struct {
buf *Buffer
path string
withSudo bool
newFile bool
saveResponseChan chan saveResponse
}
var saveRequestChan chan saveRequest
var backupRequestChan chan *Buffer
func init() {
saveRequestChan = make(chan saveRequest, 10)
backupRequestChan = make(chan *Buffer, 10)
go func() {
duration := backupSeconds * float64(time.Second)
backupTicker := time.NewTicker(time.Duration(duration))
for {
select {
case sr := <-saveRequestChan:
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
sr.saveResponseChan <- saveResponse{size, err}
case <-backupTicker.C:
for len(backupRequestChan) > 0 {
b := <-backupRequestChan
bfini := atomic.LoadInt32(&(b.fini)) != 0
if !bfini {
b.Backup()
}
}
}
}
}()
}
func openFile(name string, withSudo bool) (wrappedFile, error) {
var err error
var writeCloser io.WriteCloser var writeCloser io.WriteCloser
var screenb bool var screenb bool
var cmd *exec.Cmd var cmd *exec.Cmd
var sigChan chan os.Signal
if withSudo { if withSudo {
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name) cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
writeCloser, err = cmd.StdinPipe()
if writeCloser, err = cmd.StdinPipe(); err != nil { if err != nil {
return return wrappedFile{}, err
} }
c := make(chan os.Signal, 1) sigChan = make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Reset(os.Interrupt)
go func() { signal.Notify(sigChan, os.Interrupt)
<-c
cmd.Process.Kill()
}()
screenb = screen.TempFini() screenb = screen.TempFini()
// need to start the process now, otherwise when we flush the file // need to start the process now, otherwise when we flush the file
// contents to its stdin it might hang because the kernel's pipe size // contents to its stdin it might hang because the kernel's pipe size
// is too small to handle the full file contents all at once // is too small to handle the full file contents all at once
if e := cmd.Start(); e != nil && err == nil { err = cmd.Start()
if err != nil {
screen.TempStart(screenb) screen.TempStart(screenb)
return err
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(sigChan)
return wrappedFile{}, err
} }
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { } else {
return writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
} if err != nil {
return wrappedFile{}, err
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
err = fn(w)
if err2 := w.Flush(); err2 != nil && err == nil {
err = err2
}
// Call Sync() on the file to make sure the content is safely on disk.
// Does not work with sudo as we don't have direct access to the file.
if !withSudo {
f := writeCloser.(*os.File)
if err2 := f.Sync(); err2 != nil && err == nil {
err = err2
} }
} }
if err2 := writeCloser.Close(); err2 != nil && err == nil {
err = err2 return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
}
func (wf wrappedFile) Write(b *Buffer) (int, error) {
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
b.Lock()
defer b.Unlock()
if len(b.lines) == 0 {
return 0, nil
} }
if withSudo { // end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
eol = []byte{'\n'}
}
if !wf.withSudo {
f := wf.writeCloser.(*os.File)
err := f.Truncate(0)
if err != nil {
return 0, err
}
}
// write lines
size, err := file.Write(b.lines[0].data)
if err != nil {
return 0, err
}
for _, l := range b.lines[1:] {
if _, err = file.Write(eol); err != nil {
return 0, err
}
if _, err = file.Write(l.data); err != nil {
return 0, err
}
size += len(eol) + len(l.data)
}
err = file.Flush()
if err == nil && !wf.withSudo {
// Call Sync() on the file to make sure the content is safely on disk.
f := wf.writeCloser.(*os.File)
err = f.Sync()
}
return size, err
}
func (wf wrappedFile) Close() error {
err := wf.writeCloser.Close()
if wf.withSudo {
// wait for dd to finish and restart the screen if we used sudo // wait for dd to finish and restart the screen if we used sudo
err := cmd.Wait() err := wf.cmd.Wait()
screen.TempStart(screenb) screen.TempStart(wf.screenb)
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(wf.sigChan)
if err != nil { if err != nil {
return err return err
} }
} }
return err
}
return func (b *Buffer) overwriteFile(name string) (int, error) {
file, err := openFile(name, false)
if err != nil {
return 0, err
}
size, err := file.Write(b)
err2 := file.Close()
if err2 != nil && err == nil {
err = err2
}
return size, err
} }
// Save saves the buffer to its default path // Save saves the buffer to its default path
@ -93,9 +204,19 @@ func (b *Buffer) Save() error {
return b.SaveAs(b.Path) return b.SaveAs(b.Path)
} }
// AutoSave saves the buffer to its default path
func (b *Buffer) AutoSave() error {
// Doing full b.Modified() check every time would be costly, due to the hash
// calculation. So use just isModified even if fastdirty is not set.
if !b.isModified {
return nil
}
return b.saveToFile(b.Path, false, true)
}
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
func (b *Buffer) SaveAs(filename string) error { func (b *Buffer) SaveAs(filename string) error {
return b.saveToFile(filename, false) return b.saveToFile(filename, false, false)
} }
func (b *Buffer) SaveWithSudo() error { func (b *Buffer) SaveWithSudo() error {
@ -103,10 +224,10 @@ func (b *Buffer) SaveWithSudo() error {
} }
func (b *Buffer) SaveAsWithSudo(filename string) error { func (b *Buffer) SaveAsWithSudo(filename string) error {
return b.saveToFile(filename, true) return b.saveToFile(filename, true, false)
} }
func (b *Buffer) saveToFile(filename string, withSudo bool) error { func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error {
var err error var err error
if b.Type.Readonly { if b.Type.Readonly {
return errors.New("Cannot save readonly buffer") return errors.New("Cannot save readonly buffer")
@ -118,7 +239,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
return errors.New("Save with sudo not supported on Windows") return errors.New("Save with sudo not supported on Windows")
} }
if b.Settings["rmtrailingws"].(bool) { if !autoSave && b.Settings["rmtrailingws"].(bool) {
for i, l := range b.lines { for i, l := range b.lines {
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace)) leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
@ -136,19 +257,35 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
} }
} }
// Update the last time this file was updated after saving filename, err = util.ReplaceHome(filename)
defer func() { if err != nil {
b.ModTime, _ = util.GetModTime(filename) return err
err = b.Serialize() }
}()
// Removes any tilde and replaces with the absolute path to home newFile := false
absFilename, _ := util.ReplaceHome(filename) fileInfo, err := os.Stat(filename)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
newFile = true
}
if err == nil && fileInfo.IsDir() {
return errors.New("Error: " + filename + " is a directory and cannot be saved")
}
if err == nil && !fileInfo.Mode().IsRegular() {
return errors.New("Error: " + filename + " is not a regular file and cannot be saved")
}
absFilename, err := filepath.Abs(filename)
if err != nil {
return err
}
// Get the leading path to the file | "." is returned if there's no leading path provided // Get the leading path to the file | "." is returned if there's no leading path provided
if dirname := filepath.Dir(absFilename); dirname != "." { if dirname := filepath.Dir(absFilename); dirname != "." {
// Check if the parent dirs don't exist // Check if the parent dirs don't exist
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) { if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) {
// Prompt to make sure they want to create the dirs that are missing // Prompt to make sure they want to create the dirs that are missing
if b.Settings["mkparents"].(bool) { if b.Settings["mkparents"].(bool) {
// Create all leading dir(s) since they don't exist // Create all leading dir(s) since they don't exist
@ -162,49 +299,22 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
} }
} }
var fileSize int saveResponseChan := make(chan saveResponse)
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
enc, err := htmlindex.Get(b.Settings["encoding"].(string)) result := <-saveResponseChan
err = result.err
if err != nil { if err != nil {
return err if errors.Is(err, util.ErrOverwrite) {
} screen.TermMessage(err)
err = errors.Unwrap(err)
fwriter := func(file io.Writer) (e error) { b.UpdateModTime()
if len(b.lines) == 0 {
return
} }
// end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
eol = []byte{'\n'}
}
// write lines
if fileSize, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
fileSize += len(eol) + len(l.data)
}
return
}
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
return err return err
} }
if !b.Settings["fastdirty"].(bool) { if !b.Settings["fastdirty"].(bool) {
if fileSize > LargeFileThreshold { if result.size > LargeFileThreshold {
// For large files 'fastdirty' needs to be on // For large files 'fastdirty' needs to be on
b.Settings["fastdirty"] = true b.Settings["fastdirty"] = true
} else { } else {
@ -212,10 +322,70 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
} }
} }
newPath := b.Path != filename
b.Path = filename b.Path = filename
absPath, _ := filepath.Abs(filename) b.AbsPath = absFilename
b.AbsPath = absPath
b.isModified = false b.isModified = false
b.UpdateRules() b.UpdateModTime()
if newPath {
// need to update glob-based and filetype-based settings
b.ReloadSettings(true)
}
err = b.Serialize()
return err return err
} }
// safeWrite writes the buffer to a file in a "safe" way, preventing loss of the
// contents of the file if it fails to write the new contents.
// This means that the file is not overwritten directly but by writing to the
// backup file first.
func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) {
file, err := openFile(path, withSudo)
if err != nil {
return 0, err
}
defer func() {
if newFile && err != nil {
os.Remove(path)
}
}()
backupDir := b.backupDir()
if _, err := os.Stat(backupDir); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return 0, err
}
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
return 0, err
}
}
backupName := util.DetermineEscapePath(backupDir, path)
_, err = b.overwriteFile(backupName)
if err != nil {
os.Remove(backupName)
return 0, err
}
b.forceKeepBackup = true
size, err := file.Write(b)
if err != nil {
err = util.OverwriteError{err, backupName}
return size, err
}
b.forceKeepBackup = false
if !b.keepBackup() {
os.Remove(backupName)
}
err2 := file.Close()
if err2 != nil && err == nil {
err = err2
}
return size, err
}

View File

@ -2,10 +2,56 @@ package buffer
import ( import (
"regexp" "regexp"
"unicode/utf8"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
// We want "^" and "$" to match only the beginning/end of a line, not the
// beginning/end of the search region if it is in the middle of a line.
// In that case we use padded regexps to require a rune before or after
// the match. (This also affects other empty-string patters like "\\b".)
// The following two flags indicate the padding used.
const (
padStart = 1 << iota
padEnd
)
func findLineParams(b *Buffer, start, end Loc, i int, r *regexp.Regexp) ([]byte, int, int, *regexp.Regexp) {
l := b.LineBytes(i)
charpos := 0
padMode := 0
if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
if end.X < nchars {
l = util.SliceStart(l, end.X+1)
padMode |= padEnd
}
}
if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
if start.X > 0 {
charpos = start.X - 1
l = util.SliceEnd(l, charpos)
padMode |= padStart
}
}
if padMode == padStart {
r = regexp.MustCompile(".(?:" + r.String() + ")")
} else if padMode == padEnd {
r = regexp.MustCompile("(?:" + r.String() + ").")
} else if padMode == padStart|padEnd {
r = regexp.MustCompile(".(?:" + r.String() + ").")
}
return l, charpos, padMode, r
}
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1)) lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
if start.Y > b.LinesNum()-1 { if start.Y > b.LinesNum()-1 {
@ -22,30 +68,19 @@ func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
} }
for i := start.Y; i <= end.Y; i++ { for i := start.Y; i <= end.Y; i++ {
l := b.LineBytes(i) l, charpos, padMode, rPadded := findLineParams(b, start, end, i, r)
charpos := 0
if i == start.Y && start.Y == end.Y { match := rPadded.FindIndex(l)
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
}
match := r.FindIndex(l)
if match != nil { if match != nil {
if padMode&padStart != 0 {
_, size := utf8.DecodeRune(l[match[0]:])
match[0] += size
}
if padMode&padEnd != 0 {
_, size := utf8.DecodeLastRune(l[:match[1]])
match[1] -= size
}
start := Loc{charpos + util.RunePos(l, match[0]), i} start := Loc{charpos + util.RunePos(l, match[0]), i}
end := Loc{charpos + util.RunePos(l, match[1]), i} end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true return [2]Loc{start, end}, true
@ -70,39 +105,39 @@ func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
} }
for i := end.Y; i >= start.Y; i-- { for i := end.Y; i >= start.Y; i-- {
l := b.LineBytes(i) charCount := util.CharacterCount(b.LineBytes(i))
charpos := 0 from := Loc{0, i}.Clamp(start, end)
to := Loc{charCount, i}.Clamp(start, end)
if i == start.Y && start.Y == end.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
}
allMatches := r.FindAllIndex(l, -1)
allMatches := b.findAll(r, from, to)
if allMatches != nil { if allMatches != nil {
match := allMatches[len(allMatches)-1] match := allMatches[len(allMatches)-1]
start := Loc{charpos + util.RunePos(l, match[0]), i} return [2]Loc{match[0], match[1]}, true
end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true
} }
} }
return [2]Loc{}, false return [2]Loc{}, false
} }
func (b *Buffer) findAll(r *regexp.Regexp, start, end Loc) [][2]Loc {
var matches [][2]Loc
loc := start
for {
match, found := b.findDown(r, loc, end)
if !found {
break
}
matches = append(matches, match)
if match[0] != match[1] {
loc = match[1]
} else if match[1] != end {
loc = match[1].Move(1, b)
} else {
break
}
}
return matches
}
// FindNext finds the next occurrence of a given string in the buffer // FindNext finds the next occurrence of a given string in the buffer
// It returns the start and end location of the match (if found) and // It returns the start and end location of the match (if found) and
// a boolean indicating if it was found // a boolean indicating if it was found
@ -146,49 +181,58 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo
} }
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area // ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
// and returns the number of replacements made and the number of runes // and returns the number of replacements made and the number of characters
// added or removed on the last line of the range // added or removed on the last line of the range
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) { func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) {
if start.GreaterThan(end) { if start.GreaterThan(end) {
start, end = end, start start, end = end, start
} }
netrunes := 0 charsEnd := util.CharacterCount(b.LineBytes(end.Y))
found := 0 found := 0
var deltas []Delta var deltas []Delta
for i := start.Y; i <= end.Y; i++ { for i := start.Y; i <= end.Y; i++ {
l := b.lines[i].data l := b.LineBytes(i)
charpos := 0 charCount := util.CharacterCount(l)
if (i == start.Y && start.X > 0) || (i == end.Y && end.X < charCount) {
// This replacement code works in general, but it creates a separate
// modification for each match. We only use it for the first and last
// lines, which may use padded regexps
if start.Y == end.Y && i == start.Y { from := Loc{0, i}.Clamp(start, end)
l = util.SliceStart(l, end.X) to := Loc{charCount, i}.Clamp(start, end)
l = util.SliceEnd(l, start.X) matches := b.findAll(search, from, to)
charpos = start.X found += len(matches)
} else if i == start.Y {
l = util.SliceEnd(l, start.X) for j := len(matches) - 1; j >= 0; j-- {
charpos = start.X // if we counted upwards, the different deltas would interfere
} else if i == end.Y { match := matches[j]
l = util.SliceStart(l, end.X) var newText []byte
if captureGroups {
newText = search.ReplaceAll(b.Substr(match[0], match[1]), replace)
} else {
newText = replace
}
deltas = append(deltas, Delta{newText, match[0], match[1]})
}
} else {
newLine := search.ReplaceAllFunc(l, func(in []byte) []byte {
found++
var result []byte
if captureGroups {
match := search.FindSubmatchIndex(in)
result = search.Expand(result, replace, in, match)
} else {
result = replace
}
return result
})
deltas = append(deltas, Delta{newLine, Loc{0, i}, Loc{charCount, i}})
} }
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
result := []byte{}
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
result = search.Expand(result, replace, in, submatches)
}
found++
if i == end.Y {
netrunes += util.CharacterCount(result) - util.CharacterCount(in)
}
return result
})
from := Loc{charpos, i}
to := Loc{charpos + util.CharacterCount(l), i}
deltas = append(deltas, Delta{newText, from, to})
} }
b.MultipleReplace(deltas) b.MultipleReplace(deltas)
return found, netrunes return found, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd
} }

View File

@ -1,15 +1,13 @@
package buffer package buffer
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors" "errors"
"io"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"golang.org/x/text/encoding"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -31,16 +29,18 @@ func (b *Buffer) Serialize() error {
return nil return nil
} }
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)) var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
return overwriteFile(name, encoding.Nop, func(file io.Writer) error { b.EventHandler,
err := gob.NewEncoder(file).Encode(SerializedBuffer{ b.GetActiveCursor().Loc,
b.EventHandler, b.ModTime,
b.GetActiveCursor().Loc, })
b.ModTime, if err != nil {
})
return err return err
}, false) }
name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)
return util.SafeWrite(name, buf.Bytes(), true)
} }
// Unserialize loads the buffer info from config.ConfigDir/buffers // Unserialize loads the buffer info from config.ConfigDir/buffers
@ -50,7 +50,7 @@ func (b *Buffer) Unserialize() error {
if b.Path == "" { if b.Path == "" {
return nil return nil
} }
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))) file, err := os.Open(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath))
if err == nil { if err == nil {
defer file.Close() defer file.Close()
var buffer SerializedBuffer var buffer SerializedBuffer

View File

@ -1,36 +1,89 @@
package buffer package buffer
import ( import (
"crypto/md5"
"reflect"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
luar "layeh.com/gopher-luar"
) )
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { func (b *Buffer) ReloadSettings(reloadFiletype bool) {
settings := config.ParsedSettings()
config.UpdatePathGlobLocals(settings, b.AbsPath)
oldFiletype := b.Settings["filetype"].(string)
_, local := b.LocalSettings["filetype"]
_, volatile := config.VolatileSettings["filetype"]
if reloadFiletype && !local && !volatile {
// need to update filetype before updating other settings based on it
b.Settings["filetype"] = "unknown"
if v, ok := settings["filetype"]; ok {
b.Settings["filetype"] = v
}
}
// update syntax rules, which will also update filetype if needed
b.UpdateRules()
curFiletype := b.Settings["filetype"].(string)
if oldFiletype != curFiletype {
b.doCallbacks("filetype", oldFiletype, curFiletype)
}
config.UpdateFileTypeLocals(settings, curFiletype)
for k, v := range config.DefaultCommonSettings() {
if k == "filetype" {
// prevent recursion
continue
}
if _, ok := config.VolatileSettings[k]; ok {
// reload should not override volatile settings
continue
}
if _, ok := b.LocalSettings[k]; ok {
// reload should not override local settings
continue
}
if _, ok := settings[k]; ok {
b.DoSetOptionNative(k, settings[k])
} else {
b.DoSetOptionNative(k, v)
}
}
}
func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
oldValue := b.Settings[option]
if reflect.DeepEqual(oldValue, nativeValue) {
return
}
b.Settings[option] = nativeValue b.Settings[option] = nativeValue
if option == "fastdirty" { if option == "fastdirty" {
if !nativeValue.(bool) { if !nativeValue.(bool) {
if !b.Modified() { if b.Size() > LargeFileThreshold {
e := calcHash(b, &b.origHash) b.Settings["fastdirty"] = true
if e == ErrFileTooLarge { } else {
b.Settings["fastdirty"] = false if !b.isModified {
calcHash(b, &b.origHash)
} else {
// prevent using an old stale origHash value
b.origHash = [md5.Size]byte{}
} }
} }
} }
} else if option == "statusline" { } else if option == "statusline" {
screen.Redraw() screen.Redraw()
} else if option == "filetype" { } else if option == "filetype" {
config.InitRuntimeFiles() b.ReloadSettings(false)
err := config.ReadSettings()
if err != nil {
screen.TermMessage(err)
}
err = config.InitGlobalSettings()
if err != nil {
screen.TermMessage(err)
}
config.InitLocalSettings(b.Settings, b.Path)
b.UpdateRules()
} else if option == "fileformat" { } else if option == "fileformat" {
switch b.Settings["fileformat"].(string) { switch b.Settings["fileformat"].(string) {
case "unix": case "unix":
@ -46,6 +99,12 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
b.UpdateRules() b.UpdateRules()
} }
} else if option == "encoding" { } else if option == "encoding" {
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
enc = unicode.UTF8
b.Settings["encoding"] = "utf-8"
}
b.encoding = enc
b.isModified = true b.isModified = true
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind { } else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool) b.Type.Readonly = nativeValue.(bool)
@ -74,12 +133,19 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
} }
} }
} }
}
if b.OptionCallback != nil {
b.OptionCallback(option, nativeValue)
} }
b.doCallbacks(option, oldValue, nativeValue)
}
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
if err := config.OptionIsValid(option, nativeValue); err != nil {
return err
}
b.DoSetOptionNative(option, nativeValue)
b.LocalSettings[option] = true
return nil return nil
} }
@ -96,3 +162,15 @@ func (b *Buffer) SetOption(option, value string) error {
return b.SetOptionNative(option, nativeValue) return b.SetOptionNative(option, nativeValue)
} }
func (b *Buffer) doCallbacks(option string, oldValue interface{}, newValue interface{}) {
if b.OptionCallback != nil {
b.OptionCallback(option, newValue)
}
if err := config.RunPluginFn("onBufferOptionChanged",
luar.New(ulua.L, b), luar.New(ulua.L, option),
luar.New(ulua.L, oldValue), luar.New(ulua.L, newValue)); err != nil {
screen.TermMessage(err)
}
}

View File

@ -5,7 +5,7 @@ import (
"time" "time"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type terminalClipboard struct{} type terminalClipboard struct{}

View File

@ -1,44 +1,49 @@
package config package config
import ( import (
"sync"
"time" "time"
) )
var Autosave chan bool var Autosave chan bool
var autotime int var autotime chan float64
// lock for autosave
var autolock sync.Mutex
func init() { func init() {
Autosave = make(chan bool) Autosave = make(chan bool)
autotime = make(chan float64)
} }
func SetAutoTime(a int) { func SetAutoTime(a float64) {
autolock.Lock() autotime <- a
autotime = a
autolock.Unlock()
}
func GetAutoTime() int {
autolock.Lock()
a := autotime
autolock.Unlock()
return a
} }
func StartAutoSave() { func StartAutoSave() {
go func() { go func() {
var a float64
var t *time.Timer
var elapsed <-chan time.Time
for { for {
autolock.Lock() select {
a := autotime case a = <-autotime:
autolock.Unlock() if t != nil {
if a < 1 { t.Stop()
break for len(elapsed) > 0 {
<-elapsed
}
}
if a > 0 {
if t != nil {
t.Reset(time.Duration(a * float64(time.Second)))
} else {
t = time.NewTimer(time.Duration(a * float64(time.Second)))
elapsed = t.C
}
}
case <-elapsed:
if a > 0 {
t.Reset(time.Duration(a * float64(time.Second)))
Autosave <- true
}
} }
time.Sleep(time.Duration(a) * time.Second)
Autosave <- true
} }
}() }()
} }

View File

@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
// DefStyle is Micro's default style // DefStyle is Micro's default style
@ -52,43 +52,55 @@ func InitColorscheme() error {
Colorscheme = make(map[string]tcell.Style) Colorscheme = make(map[string]tcell.Style)
DefStyle = tcell.StyleDefault DefStyle = tcell.StyleDefault
return LoadDefaultColorscheme() c, err := LoadDefaultColorscheme()
if err == nil {
Colorscheme = c
}
return err
} }
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes // LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
func LoadDefaultColorscheme() error { func LoadDefaultColorscheme() (map[string]tcell.Style, error) {
return LoadColorscheme(GlobalSettings["colorscheme"].(string)) var parsedColorschemes []string
return LoadColorscheme(GlobalSettings["colorscheme"].(string), &parsedColorschemes)
} }
// LoadColorscheme loads the given colorscheme from a directory // LoadColorscheme loads the given colorscheme from a directory
func LoadColorscheme(colorschemeName string) error { func LoadColorscheme(colorschemeName string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
c := make(map[string]tcell.Style)
file := FindRuntimeFile(RTColorscheme, colorschemeName) file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil { if file == nil {
return errors.New(colorschemeName + " is not a valid colorscheme") return c, errors.New(colorschemeName + " is not a valid colorscheme")
} }
if data, err := file.Data(); err != nil { if data, err := file.Data(); err != nil {
return errors.New("Error loading colorscheme: " + err.Error()) return c, errors.New("Error loading colorscheme: " + err.Error())
} else { } else {
Colorscheme, err = ParseColorscheme(string(data)) var err error
c, err = ParseColorscheme(file.Name(), string(data), parsedColorschemes)
if err != nil { if err != nil {
return err return c, err
} }
} }
return nil return c, nil
} }
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object // ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
// Colorschemes are made up of color-link statements linking a color group to a list of colors // Colorschemes are made up of color-link statements linking a color group to a list of colors
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and // For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
// red background // red background
func ParseColorscheme(text string) (map[string]tcell.Style, error) { func ParseColorscheme(name string, text string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
var err error var err error
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`) colorParser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
includeParser := regexp.MustCompile(`include\s+"(.*)"`)
lines := strings.Split(text, "\n") lines := strings.Split(text, "\n")
c := make(map[string]tcell.Style) c := make(map[string]tcell.Style)
if parsedColorschemes != nil {
*parsedColorschemes = append(*parsedColorschemes, name)
}
lineLoop:
for _, line := range lines { for _, line := range lines {
if strings.TrimSpace(line) == "" || if strings.TrimSpace(line) == "" ||
strings.TrimSpace(line)[0] == '#' { strings.TrimSpace(line)[0] == '#' {
@ -96,7 +108,30 @@ func ParseColorscheme(text string) (map[string]tcell.Style, error) {
continue continue
} }
matches := parser.FindSubmatch([]byte(line)) matches := includeParser.FindSubmatch([]byte(line))
if len(matches) == 2 {
// support includes only in case parsedColorschemes are given
if parsedColorschemes != nil {
include := string(matches[1])
for _, name := range *parsedColorschemes {
// check for circular includes...
if name == include {
// ...and prevent them
continue lineLoop
}
}
includeScheme, err := LoadColorscheme(include, parsedColorschemes)
if err != nil {
return c, err
}
for k, v := range includeScheme {
c[k] = v
}
}
continue
}
matches = colorParser.FindSubmatch([]byte(line))
if len(matches) == 3 { if len(matches) == 3 {
link := string(matches[1]) link := string(matches[1])
colors := string(matches[2]) colors := string(matches[2])

View File

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
func TestSimpleStringToStyle(t *testing.T) { func TestSimpleStringToStyle(t *testing.T) {
@ -65,7 +65,7 @@ color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828" color-link constant.string "#E6DB74,#282828"
color-link constant.string.char "#BDE6AD,#282828"` color-link constant.string.char "#BDE6AD,#282828"`
c, err := ParseColorscheme(testColorscheme) c, err := ParseColorscheme("testColorscheme", testColorscheme, nil)
assert.Nil(t, err) assert.Nil(t, err)
fg, bg, _ := c["comment"].Decompose() fg, bg, _ := c["comment"].Decompose()

View File

@ -143,15 +143,3 @@ func FindPlugin(name string) *Plugin {
} }
return pl return pl
} }
// FindAnyPlugin does not require the plugin to be enabled
func FindAnyPlugin(name string) *Plugin {
var pl *Plugin
for _, p := range Plugins {
if p.Name == name {
pl = p
break
}
}
return pl
}

View File

@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -15,7 +14,7 @@ import (
"github.com/blang/semver" "github.com/blang/semver"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/json5" "github.com/micro-editor/json5"
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,7 +5,7 @@ import (
"github.com/blang/semver" "github.com/blang/semver"
"github.com/zyedidia/json5" "github.com/micro-editor/json5"
) )
func TestDependencyResolving(t *testing.T) { func TestDependencyResolving(t *testing.T) {

View File

@ -2,7 +2,6 @@ package config
import ( import (
"errors" "errors"
"io/ioutil"
"log" "log"
"os" "os"
"path" "path"
@ -40,6 +39,10 @@ var allFiles [][]RuntimeFile
var realFiles [][]RuntimeFile var realFiles [][]RuntimeFile
func init() { func init() {
initRuntimeVars()
}
func initRuntimeVars() {
allFiles = make([][]RuntimeFile, NumTypes) allFiles = make([][]RuntimeFile, NumTypes)
realFiles = make([][]RuntimeFile, NumTypes) realFiles = make([][]RuntimeFile, NumTypes)
} }
@ -58,12 +61,6 @@ type realFile string
// some asset file // some asset file
type assetFile string type assetFile string
// some file on filesystem but with a different name
type namedFile struct {
realFile
name string
}
// a file with the data stored in memory // a file with the data stored in memory
type memoryFile struct { type memoryFile struct {
name string name string
@ -83,7 +80,7 @@ func (rf realFile) Name() string {
} }
func (rf realFile) Data() ([]byte, error) { func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf)) return os.ReadFile(string(rf))
} }
func (af assetFile) Name() string { func (af assetFile) Name() string {
@ -95,10 +92,6 @@ func (af assetFile) Data() ([]byte, error) {
return rt.Asset(string(af)) return rt.Asset(string(af))
} }
func (nf namedFile) Name() string {
return nf.name
}
// AddRuntimeFile registers a file for the given filetype // AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) { func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file) allFiles[fileType] = append(allFiles[fileType], file)
@ -113,7 +106,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
// AddRuntimeFilesFromDirectory registers each file from the given directory for // AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern // the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) { func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
files, _ := ioutil.ReadDir(directory) files, _ := os.ReadDir(directory)
for _, f := range files { for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok { if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name()) fullPath := filepath.Join(directory, f.Name())
@ -129,9 +122,17 @@ func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
if err != nil { if err != nil {
return return
} }
assetLoop:
for _, f := range files { for _, f := range files {
if ok, _ := path.Match(pattern, f); ok { if ok, _ := path.Match(pattern, f); ok {
AddRuntimeFile(fileType, assetFile(path.Join(directory, f))) af := assetFile(path.Join(directory, f))
for _, rf := range realFiles[fileType] {
if af.Name() == rf.Name() {
continue assetLoop
}
}
AddRuntimeFile(fileType, af)
} }
} }
} }
@ -158,19 +159,30 @@ func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return realFiles[fileType] return realFiles[fileType]
} }
// InitRuntimeFiles initializes all assets file and the config directory // InitRuntimeFiles initializes all assets files and the config directory.
func InitRuntimeFiles() { // If `user` is false, InitRuntimeFiles ignores the config directory and
// initializes asset files only.
func InitRuntimeFiles(user bool) {
add := func(fileType RTFiletype, dir, pattern string) { add := func(fileType RTFiletype, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern) if user {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
}
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern) AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
} }
initRuntimeVars()
add(RTColorscheme, "colorschemes", "*.micro") add(RTColorscheme, "colorschemes", "*.micro")
add(RTSyntax, "syntax", "*.yaml") add(RTSyntax, "syntax", "*.yaml")
add(RTSyntaxHeader, "syntax", "*.hdr") add(RTSyntaxHeader, "syntax", "*.hdr")
add(RTHelp, "help", "*.md") add(RTHelp, "help", "*.md")
}
// InitPlugins initializes the plugins
func InitPlugins() {
Plugins = Plugins[:0]
initlua := filepath.Join(ConfigDir, "init.lua") initlua := filepath.Join(ConfigDir, "init.lua")
if _, err := os.Stat(initlua); !os.IsNotExist(err) { if _, err := os.Stat(initlua); !os.IsNotExist(err) {
p := new(Plugin) p := new(Plugin)
p.Name = "initlua" p.Name = "initlua"
@ -181,14 +193,14 @@ func InitRuntimeFiles() {
// Search ConfigDir for plugin-scripts // Search ConfigDir for plugin-scripts
plugdir := filepath.Join(ConfigDir, "plug") plugdir := filepath.Join(ConfigDir, "plug")
files, _ := ioutil.ReadDir(plugdir) files, _ := os.ReadDir(plugdir)
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
for _, d := range files { for _, d := range files {
plugpath := filepath.Join(plugdir, d.Name()) plugpath := filepath.Join(plugdir, d.Name())
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() { if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
srcs, _ := ioutil.ReadDir(plugpath) srcs, _ := os.ReadDir(plugpath)
p := new(Plugin) p := new(Plugin)
p.Name = d.Name() p.Name = d.Name()
p.DirName = d.Name() p.DirName = d.Name()
@ -196,7 +208,7 @@ func InitRuntimeFiles() {
if strings.HasSuffix(f.Name(), ".lua") { if strings.HasSuffix(f.Name(), ".lua") {
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name()))) p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
} else if strings.HasSuffix(f.Name(), ".json") { } else if strings.HasSuffix(f.Name(), ".json") {
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name())) data, err := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
if err != nil { if err != nil {
continue continue
} }
@ -218,7 +230,15 @@ func InitRuntimeFiles() {
plugdir = filepath.Join("runtime", "plugins") plugdir = filepath.Join("runtime", "plugins")
if files, err := rt.AssetDir(plugdir); err == nil { if files, err := rt.AssetDir(plugdir); err == nil {
outer:
for _, d := range files { for _, d := range files {
for _, p := range Plugins {
if p.Name == d {
log.Println(p.Name, "built-in plugin overridden by user-defined one")
continue outer
}
}
if srcs, err := rt.AssetDir(filepath.Join(plugdir, d)); err == nil { if srcs, err := rt.AssetDir(filepath.Join(plugdir, d)); err == nil {
p := new(Plugin) p := new(Plugin)
p.Name = d p.Name = d

View File

@ -7,7 +7,7 @@ import (
) )
func init() { func init() {
InitRuntimeFiles() InitRuntimeFiles(false)
} }
func TestAddFile(t *testing.T) { func TestAddFile(t *testing.T) {

View File

@ -4,340 +4,103 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"strconv" "strconv"
"strings" "strings"
"github.com/zyedidia/glob" "github.com/zyedidia/glob"
"github.com/zyedidia/json5" "github.com/micro-editor/json5"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding/htmlindex" "golang.org/x/text/encoding/htmlindex"
) )
type optionValidator func(string, interface{}) error type optionValidator func(string, interface{}) error
var ( // a list of settings that need option validators
ErrInvalidOption = errors.New("Invalid option")
ErrInvalidValue = errors.New("Invalid value")
// The options that the user can set
GlobalSettings map[string]interface{}
// This is the raw parsed json
parsedSettings map[string]interface{}
settingsParseError bool
// ModifiedSettings is a map of settings which should be written to disk
// because they have been modified by the user in this session
ModifiedSettings map[string]bool
)
func init() {
ModifiedSettings = make(map[string]bool)
parsedSettings = make(map[string]interface{})
}
// Options with validators
var optionValidators = map[string]optionValidator{ var optionValidators = map[string]optionValidator{
"autosave": validateNonNegativeValue, "autosave": validateNonNegativeValue,
"clipboard": validateClipboard, "clipboard": validateChoice,
"tabsize": validatePositiveValue, "colorcolumn": validateNonNegativeValue,
"scrollmargin": validateNonNegativeValue, "colorscheme": validateColorscheme,
"scrollspeed": validateNonNegativeValue, "detectlimit": validateNonNegativeValue,
"colorscheme": validateColorscheme, "encoding": validateEncoding,
"colorcolumn": validateNonNegativeValue, "fileformat": validateChoice,
"fileformat": validateLineEnding, "helpsplit": validateChoice,
"encoding": validateEncoding, "matchbracestyle": validateChoice,
"multiopen": validateMultiOpen, "multiopen": validateChoice,
"reload": validateReload, "pageoverlap": validateNonNegativeValue,
"reload": validateChoice,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"tabsize": validatePositiveValue,
} }
func ReadSettings() error { // a list of settings with pre-defined choices
filename := filepath.Join(ConfigDir, "settings.json") var OptionChoices = map[string][]string{
if _, e := os.Stat(filename); e == nil { "clipboard": {"internal", "external", "terminal"},
input, err := ioutil.ReadFile(filename) "fileformat": {"unix", "dos"},
if err != nil { "helpsplit": {"hsplit", "vsplit"},
settingsParseError = true "matchbracestyle": {"underline", "highlight"},
return errors.New("Error reading settings.json file: " + err.Error()) "multiopen": {"tab", "hsplit", "vsplit"},
} "reload": {"prompt", "auto", "disabled"},
if !strings.HasPrefix(string(input), "null") {
// Unmarshal the input into the parsed map
err = json5.Unmarshal(input, &parsedSettings)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json: " + err.Error())
}
// check if autosave is a boolean and convert it to float if so
if v, ok := parsedSettings["autosave"]; ok {
s, ok := v.(bool)
if ok {
if s {
parsedSettings["autosave"] = 8.0
} else {
parsedSettings["autosave"] = 0.0
}
}
}
}
}
return nil
}
func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
var interfaceArr []interface{}
switch option {
case "pluginrepos", "pluginchannels":
return value.AssignableTo(reflect.TypeOf(interfaceArr))
default:
return def.AssignableTo(value)
}
}
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
func InitGlobalSettings() error {
var err error
GlobalSettings = DefaultGlobalSettings()
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
err = fmt.Errorf("Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v), GlobalSettings[k], reflect.TypeOf(GlobalSettings[k]))
continue
}
GlobalSettings[k] = v
}
}
return err
}
// InitLocalSettings scans the json in settings.json and sets the options locally based
// on whether the filetype or path matches ft or glob local settings
// Must be called after ReadSettings
func InitLocalSettings(settings map[string]interface{}, path string) error {
var parseError error
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if strings.HasPrefix(k, "ft:") {
if settings["filetype"].(string) == k[3:] {
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
continue
}
settings[k1] = v1
}
}
} else {
g, err := glob.Compile(k)
if err != nil {
parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
continue
}
if g.MatchString(path) {
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
continue
}
settings[k1] = v1
}
}
}
}
}
return parseError
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
if settingsParseError {
// Don't write settings if there was a parse error
// because this will delete the settings.json if it
// is invalid. Instead we should allow the user to fix
// it manually.
return nil
}
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultGlobalSettings()
// remove any options froms parsedSettings that have since been marked as default
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
cur, okcur := GlobalSettings[k]
if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
delete(parsedSettings, k)
}
}
}
// add any options to parsedSettings that have since been marked as non-default
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
parsedSettings[k] = v
}
}
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
// OverwriteSettings writes the current settings to settings.json and
// resets any user configuration of local settings present in settings.json
func OverwriteSettings(filename string) error {
settings := make(map[string]interface{})
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultGlobalSettings()
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
settings[k] = v
}
}
}
txt, _ := json.MarshalIndent(settings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
name = pl + "." + name
if _, ok := GlobalSettings[name]; !ok {
defaultCommonSettings[name] = defaultvalue
GlobalSettings[name] = defaultvalue
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
if err != nil {
return errors.New("Error writing settings.json file: " + err.Error())
}
} else {
defaultCommonSettings[name] = defaultvalue
}
return nil
}
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterGlobalOption(pl+"."+name, defaultvalue)
}
// RegisterCommonOption creates a new option
func RegisterCommonOption(name string, defaultvalue interface{}) error {
if v, ok := GlobalSettings[name]; !ok {
defaultCommonSettings[name] = defaultvalue
GlobalSettings[name] = defaultvalue
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
if err != nil {
return errors.New("Error writing settings.json file: " + err.Error())
}
} else {
defaultCommonSettings[name] = v
}
return nil
}
// RegisterGlobalOption creates a new global-only option
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
if v, ok := GlobalSettings[name]; !ok {
DefaultGlobalOnlySettings[name] = defaultvalue
GlobalSettings[name] = defaultvalue
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
if err != nil {
return errors.New("Error writing settings.json file: " + err.Error())
}
} else {
DefaultGlobalOnlySettings[name] = v
}
return nil
}
// GetGlobalOption returns the global value of the given option
func GetGlobalOption(name string) interface{} {
return GlobalSettings[name]
} }
// a list of settings that can be globally and locally modified and their
// default values
var defaultCommonSettings = map[string]interface{}{ var defaultCommonSettings = map[string]interface{}{
"autoindent": true, "autoindent": true,
"autosu": false, "autosu": false,
"backup": true, "backup": true,
"backupdir": "", "backupdir": "",
"basename": false, "basename": false,
"colorcolumn": float64(0), "colorcolumn": float64(0),
"cursorline": true, "cursorline": true,
"diffgutter": false, "detectlimit": float64(100),
"encoding": "utf-8", "diffgutter": false,
"eofnewline": true, "encoding": "utf-8",
"fastdirty": false, "eofnewline": true,
"fileformat": "unix", "fastdirty": false,
"filetype": "unknown", "fileformat": defaultFileFormat(),
"hlsearch": false, "filetype": "unknown",
"incsearch": true, "hlsearch": false,
"ignorecase": true, "hltaberrors": false,
"indentchar": " ", "hltrailingws": false,
"keepautoindent": false, "ignorecase": true,
"matchbrace": true, "incsearch": true,
"mkparents": false, "indentchar": " ",
"permbackup": false, "keepautoindent": false,
"readonly": false, "matchbrace": true,
"reload": "prompt", "matchbraceleft": true,
"rmtrailingws": false, "matchbracestyle": "underline",
"ruler": true, "mkparents": false,
"relativeruler": false, "pageoverlap": float64(2),
"savecursor": false, "permbackup": false,
"saveundo": false, "readonly": false,
"scrollbar": false, "relativeruler": false,
"scrollmargin": float64(3), "reload": "prompt",
"scrollspeed": float64(2), "rmtrailingws": false,
"smartpaste": true, "ruler": true,
"softwrap": false, "savecursor": false,
"splitbottom": true, "saveundo": false,
"splitright": true, "scrollbar": false,
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)", "scrollmargin": float64(3),
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help", "scrollspeed": float64(2),
"statusline": true, "smartpaste": true,
"syntax": true, "softwrap": false,
"tabmovement": false, "splitbottom": true,
"tabsize": float64(4), "splitright": true,
"tabstospaces": false, "statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
"useprimary": true, "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
"wordwrap": false, "statusline": true,
} "syntax": true,
"tabmovement": false,
func GetInfoBarOffset() int { "tabsize": float64(4),
offset := 0 "tabstospaces": false,
if GetGlobalOption("infobar").(bool) { "useprimary": true,
offset++ "wordwrap": false,
}
if GetGlobalOption("keymenu").(bool) {
offset += 2
}
return offset
}
// DefaultCommonSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultCommonSettings() map[string]interface{} {
commonsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
commonsettings[k] = v
}
return commonsettings
} }
// a list of settings that should only be globally modified and their // a list of settings that should only be globally modified and their
@ -349,6 +112,7 @@ var DefaultGlobalOnlySettings = map[string]interface{}{
"divchars": "|-", "divchars": "|-",
"divreverse": true, "divreverse": true,
"fakecursor": false, "fakecursor": false,
"helpsplit": "hsplit",
"infobar": true, "infobar": true,
"keymenu": false, "keymenu": false,
"mouse": true, "mouse": true,
@ -371,21 +135,322 @@ var LocalSettings = []string{
"readonly", "readonly",
} }
// DefaultGlobalSettings returns the default global settings for micro var (
// Note that colorscheme is a global only option ErrInvalidOption = errors.New("Invalid option")
func DefaultGlobalSettings() map[string]interface{} { ErrInvalidValue = errors.New("Invalid value")
globalsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings { // The options that the user can set
globalsettings[k] = v GlobalSettings map[string]interface{}
}
for k, v := range DefaultGlobalOnlySettings { // This is the raw parsed json
globalsettings[k] = v parsedSettings map[string]interface{}
} settingsParseError bool
return globalsettings
// ModifiedSettings is a map of settings which should be written to disk
// because they have been modified by the user in this session
ModifiedSettings map[string]bool
// VolatileSettings is a map of settings which should not be written to disk
// because they have been temporarily set for this session only
VolatileSettings map[string]bool
)
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
} }
// DefaultAllSettings returns a map of all settings and their func init() {
// default values (both common and global settings) ModifiedSettings = make(map[string]bool)
VolatileSettings = make(map[string]bool)
}
func validateParsedSettings() error {
var err error
defaults := DefaultAllSettings()
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if strings.HasPrefix(k, "ft:") {
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := defaults[k1]; ok {
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
err = e
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
continue
}
}
}
} else {
if _, e := glob.Compile(k); e != nil {
err = errors.New("Error with glob setting " + k + ": " + e.Error())
delete(parsedSettings, k)
continue
}
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := defaults[k1]; ok {
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
err = e
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
continue
}
}
}
}
continue
}
if k == "autosave" {
// if autosave is a boolean convert it to float
s, ok := v.(bool)
if ok {
if s {
parsedSettings["autosave"] = 8.0
} else {
parsedSettings["autosave"] = 0.0
}
}
continue
}
if _, ok := defaults[k]; ok {
if e := verifySetting(k, v, defaults[k]); e != nil {
err = e
parsedSettings[k] = defaults[k]
continue
}
}
}
return err
}
func ReadSettings() error {
parsedSettings = make(map[string]interface{})
filename := filepath.Join(ConfigDir, "settings.json")
if _, e := os.Stat(filename); e == nil {
input, err := os.ReadFile(filename)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json file: " + err.Error())
}
if !strings.HasPrefix(string(input), "null") {
// Unmarshal the input into the parsed map
err = json5.Unmarshal(input, &parsedSettings)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json: " + err.Error())
}
err = validateParsedSettings()
if err != nil {
return err
}
}
}
return nil
}
func ParsedSettings() map[string]interface{} {
s := make(map[string]interface{})
for k, v := range parsedSettings {
s[k] = v
}
return s
}
func verifySetting(option string, value interface{}, def interface{}) error {
var interfaceArr []interface{}
valType := reflect.TypeOf(value)
defType := reflect.TypeOf(def)
assignable := false
switch option {
case "pluginrepos", "pluginchannels":
assignable = valType.AssignableTo(reflect.TypeOf(interfaceArr))
default:
assignable = defType.AssignableTo(valType)
}
if !assignable {
return fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", option, valType, def, defType)
}
if err := OptionIsValid(option, value); err != nil {
return err
}
return nil
}
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
func InitGlobalSettings() error {
var err error
GlobalSettings = DefaultAllSettings()
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
GlobalSettings[k] = v
}
}
return err
}
// UpdatePathGlobLocals scans the already parsed settings and sets the options locally
// based on whether the path matches a glob
// Must be called after ReadSettings
func UpdatePathGlobLocals(settings map[string]interface{}, path string) {
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && !strings.HasPrefix(k, "ft:") {
g, _ := glob.Compile(k)
if g.MatchString(path) {
for k1, v1 := range v.(map[string]interface{}) {
settings[k1] = v1
}
}
}
}
}
// UpdateFileTypeLocals scans the already parsed settings and sets the options locally
// based on whether the filetype matches to "ft:"
// Must be called after ReadSettings
func UpdateFileTypeLocals(settings map[string]interface{}, filetype string) {
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && strings.HasPrefix(k, "ft:") {
if filetype == k[3:] {
for k1, v1 := range v.(map[string]interface{}) {
if k1 != "filetype" {
settings[k1] = v1
}
}
}
}
}
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
if settingsParseError {
// Don't write settings if there was a parse error
// because this will delete the settings.json if it
// is invalid. Instead we should allow the user to fix
// it manually.
return nil
}
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultAllSettings()
// remove any options froms parsedSettings that have since been marked as default
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
cur, okcur := GlobalSettings[k]
_, vol := VolatileSettings[k]
if def, ok := defaults[k]; ok && okcur && !vol && reflect.DeepEqual(cur, def) {
delete(parsedSettings, k)
}
}
}
// add any options to parsedSettings that have since been marked as non-default
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
parsedSettings[k] = v
}
}
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
txt = append(txt, '\n')
err = writeFile(filename, txt)
}
return err
}
// OverwriteSettings writes the current settings to settings.json and
// resets any user configuration of local settings present in settings.json
func OverwriteSettings(filename string) error {
settings := make(map[string]interface{})
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultAllSettings()
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
settings[k] = v
}
}
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
txt = append(txt, '\n')
err = writeFile(filename, txt)
}
return err
}
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterCommonOption(pl+"."+name, defaultvalue)
}
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterGlobalOption(pl+"."+name, defaultvalue)
}
// RegisterCommonOption creates a new option
func RegisterCommonOption(name string, defaultvalue interface{}) error {
if _, ok := GlobalSettings[name]; !ok {
GlobalSettings[name] = defaultvalue
}
defaultCommonSettings[name] = defaultvalue
return nil
}
// RegisterGlobalOption creates a new global-only option
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
if _, ok := GlobalSettings[name]; !ok {
GlobalSettings[name] = defaultvalue
}
DefaultGlobalOnlySettings[name] = defaultvalue
return nil
}
// GetGlobalOption returns the global value of the given option
func GetGlobalOption(name string) interface{} {
return GlobalSettings[name]
}
func defaultFileFormat() string {
if runtime.GOOS == "windows" {
return "dos"
}
return "unix"
}
func GetInfoBarOffset() int {
offset := 0
if GetGlobalOption("infobar").(bool) {
offset++
}
if GetGlobalOption("keymenu").(bool) {
offset += 2
}
return offset
}
// DefaultCommonSettings returns a map of all common buffer settings
// and their default values
func DefaultCommonSettings() map[string]interface{} {
commonsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
commonsettings[k] = v
}
return commonsettings
}
// DefaultAllSettings returns a map of all common buffer & global-only settings
// and their default values
func DefaultAllSettings() map[string]interface{} { func DefaultAllSettings() map[string]interface{} {
allsettings := make(map[string]interface{}) allsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings { for k, v := range defaultCommonSettings {
@ -410,18 +475,15 @@ func GetNativeValue(option string, realValue interface{}, value string) (interfa
} else if kind == reflect.String { } else if kind == reflect.String {
native = value native = value
} else if kind == reflect.Float64 { } else if kind == reflect.Float64 {
i, err := strconv.Atoi(value) f, err := strconv.ParseFloat(value, 64)
if err != nil { if err != nil {
return nil, ErrInvalidValue return nil, ErrInvalidValue
} }
native = float64(i) native = f
} else { } else {
return nil, ErrInvalidValue return nil, ErrInvalidValue
} }
if err := OptionIsValid(option, native); err != nil {
return nil, err
}
return native, nil return native, nil
} }
@ -437,13 +499,13 @@ func OptionIsValid(option string, value interface{}) error {
// Option validators // Option validators
func validatePositiveValue(option string, value interface{}) error { func validatePositiveValue(option string, value interface{}) error {
tabsize, ok := value.(float64) nativeValue, ok := value.(float64)
if !ok { if !ok {
return errors.New("Expected numeric type for " + option) return errors.New("Expected numeric type for " + option)
} }
if tabsize < 1 { if nativeValue < 1 {
return errors.New(option + " must be greater than 0") return errors.New(option + " must be greater than 0")
} }
@ -464,6 +526,26 @@ func validateNonNegativeValue(option string, value interface{}) error {
return nil return nil
} }
func validateChoice(option string, value interface{}) error {
if choices, ok := OptionChoices[option]; ok {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for " + option)
}
for _, v := range choices {
if val == v {
return nil
}
}
choicesStr := strings.Join(choices, ", ")
return errors.New(option + " must be one of: " + choicesStr)
}
return errors.New("Option has no pre-defined choices")
}
func validateColorscheme(option string, value interface{}) error { func validateColorscheme(option string, value interface{}) error {
colorscheme, ok := value.(string) colorscheme, ok := value.(string)
@ -478,69 +560,7 @@ func validateColorscheme(option string, value interface{}) error {
return nil return nil
} }
func validateClipboard(option string, value interface{}) error {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for clipboard")
}
switch val {
case "internal", "external", "terminal":
default:
return errors.New(option + " must be 'internal', 'external', or 'terminal'")
}
return nil
}
func validateLineEnding(option string, value interface{}) error {
endingType, ok := value.(string)
if !ok {
return errors.New("Expected string type for file format")
}
if endingType != "unix" && endingType != "dos" {
return errors.New("File format must be either 'unix' or 'dos'")
}
return nil
}
func validateEncoding(option string, value interface{}) error { func validateEncoding(option string, value interface{}) error {
_, err := htmlindex.Get(value.(string)) _, err := htmlindex.Get(value.(string))
return err return err
} }
func validateMultiOpen(option string, value interface{}) error {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for multiopen")
}
switch val {
case "tab", "hsplit", "vsplit":
default:
return errors.New(option + " must be 'tab', 'hsplit', or 'vsplit'")
}
return nil
}
func validateReload(option string, value interface{}) error {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for reload")
}
switch val {
case "prompt", "auto", "disabled":
default:
return errors.New(option + " must be 'prompt', 'auto' or 'disabled'")
}
return nil
}

View File

@ -8,7 +8,7 @@ import (
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
// The BufWindow provides a way of displaying a certain section of a buffer. // The BufWindow provides a way of displaying a certain section of a buffer.
@ -58,7 +58,7 @@ func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
if option == "softwrap" || option == "wordwrap" { if option == "softwrap" || option == "wordwrap" {
w.Relocate() w.Relocate()
for _, c := range w.Buf.GetCursors() { for _, c := range w.Buf.GetCursors() {
c.LastVisualX = c.GetVisualX() c.LastWrappedVisualX = c.GetVisualX(true)
} }
} }
} }
@ -129,6 +129,11 @@ func (w *BufWindow) updateDisplayInfo() {
w.bufHeight-- w.bufHeight--
} }
scrollbarWidth := 0
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height && w.Width > 0 {
scrollbarWidth = 1
}
w.hasMessage = len(b.Messages) > 0 w.hasMessage = len(b.Messages) > 0
// We need to know the string length of the largest line number // We need to know the string length of the largest line number
@ -146,16 +151,16 @@ func (w *BufWindow) updateDisplayInfo() {
w.gutterOffset += w.maxLineNumLength + 1 w.gutterOffset += w.maxLineNumLength + 1
} }
prevBufWidth := w.bufWidth if w.gutterOffset > w.Width-scrollbarWidth {
w.gutterOffset = w.Width - scrollbarWidth
w.bufWidth = w.Width - w.gutterOffset
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
w.bufWidth--
} }
prevBufWidth := w.bufWidth
w.bufWidth = w.Width - w.gutterOffset - scrollbarWidth
if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) { if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) {
for _, c := range w.Buf.GetCursors() { for _, c := range w.Buf.GetCursors() {
c.LastVisualX = c.GetVisualX() c.LastWrappedVisualX = c.GetVisualX(true)
} }
} }
} }
@ -233,7 +238,7 @@ func (w *BufWindow) Relocate() bool {
// horizontal relocation (scrolling) // horizontal relocation (scrolling)
if !b.Settings["softwrap"].(bool) { if !b.Settings["softwrap"].(bool) {
cx := activeC.GetVisualX() cx := activeC.GetVisualX(false)
rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X)) rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
if rw == 0 { if rw == 0 {
rw = 1 // tab or newline rw = 1 // tab or newline
@ -277,13 +282,17 @@ func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
break break
} }
} }
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s) for i := 0; i < 2 && vloc.X < w.gutterOffset; i++ {
vloc.X++ screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s) vloc.X++
vloc.X++ }
} }
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) { func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
if vloc.X >= w.gutterOffset {
return
}
symbol := ' ' symbol := ' '
styleName := "" styleName := ""
@ -319,26 +328,28 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc
} else { } else {
lineInt = bloc.Y - cursorLine lineInt = bloc.Y - cursorLine
} }
lineNum := strconv.Itoa(util.Abs(lineInt)) lineNum := []rune(strconv.Itoa(util.Abs(lineInt)))
// Write the spaces before the line number if necessary // Write the spaces before the line number if necessary
for i := 0; i < w.maxLineNumLength-len(lineNum); i++ { for i := 0; i < w.maxLineNumLength-len(lineNum) && vloc.X < w.gutterOffset; i++ {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++ vloc.X++
} }
// Write the actual line number // Write the actual line number
for _, ch := range lineNum { for i := 0; i < len(lineNum) && vloc.X < w.gutterOffset; i++ {
if softwrapped { if softwrapped || (w.bufWidth == 0 && w.Buf.Settings["softwrap"] == true) {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
} else { } else {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle) screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, lineNum[i], nil, lineNumStyle)
} }
vloc.X++ vloc.X++
} }
// Write the extra space // Write the extra space
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle) if vloc.X < w.gutterOffset {
vloc.X++ screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
} }
// getStyle returns the highlight style for the given character position // getStyle returns the highlight style for the given character position
@ -373,18 +384,7 @@ func (w *BufWindow) displayBuffer() {
if b.ModifiedThisFrame { if b.ModifiedThisFrame {
if b.Settings["diffgutter"].(bool) { if b.Settings["diffgutter"].(bool) {
b.UpdateDiff(func(synchronous bool) { b.UpdateDiff()
// If the diff was updated asynchronously, the outer call to
// displayBuffer might already be completed and we need to
// schedule a redraw in order to display the new diff.
// Note that this cannot lead to an infinite recursion
// because the modifications were cleared above so there won't
// be another call to UpdateDiff when displayBuffer is called
// during the redraw.
if !synchronous {
screen.Redraw()
}
})
} }
b.ModifiedThisFrame = false b.ModifiedThisFrame = false
} }
@ -392,26 +392,20 @@ func (w *BufWindow) displayBuffer() {
var matchingBraces []buffer.Loc var matchingBraces []buffer.Loc
// bracePairs is defined in buffer.go // bracePairs is defined in buffer.go
if b.Settings["matchbrace"].(bool) { if b.Settings["matchbrace"].(bool) {
for _, bp := range buffer.BracePairs { for _, c := range b.GetCursors() {
for _, c := range b.GetCursors() { if c.HasSelection() {
if c.HasSelection() { continue
continue }
}
curX := c.X
curLoc := c.Loc
r := c.RuneUnder(curX) mb, left, found := b.FindMatchingBrace(c.Loc)
rl := c.RuneUnder(curX - 1) if found {
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] { matchingBraces = append(matchingBraces, mb)
mb, left, found := b.FindMatchingBrace(bp, curLoc) if !left {
if found { if b.Settings["matchbracestyle"].(string) != "highlight" {
matchingBraces = append(matchingBraces, mb) matchingBraces = append(matchingBraces, c.Loc)
if !left {
matchingBraces = append(matchingBraces, curLoc)
} else {
matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
}
} }
} else {
matchingBraces = append(matchingBraces, c.Loc.Move(-1, b))
} }
} }
} }
@ -455,7 +449,7 @@ func (w *BufWindow) displayBuffer() {
currentLine := false currentLine := false
for _, c := range cursors { for _, c := range cursors {
if bloc.Y == c.Y && w.active { if !c.HasSelection() && bloc.Y == c.Y && w.active {
currentLine = true currentLine = true
break break
} }
@ -482,6 +476,12 @@ func (w *BufWindow) displayBuffer() {
vloc.X = w.gutterOffset vloc.X = w.gutterOffset
} }
bline := b.LineBytes(bloc.Y)
blineLen := util.CharacterCount(bline)
leadingwsEnd := len(util.GetLeadingWhitespace(bline))
trailingwsStart := blineLen - util.CharacterCount(util.GetTrailingWhitespace(bline))
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y) line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
if startStyle != nil { if startStyle != nil {
curStyle = *startStyle curStyle = *startStyle
@ -505,6 +505,37 @@ func (w *BufWindow) displayBuffer() {
// over cursor-line and color-column // over cursor-line and color-column
dontOverrideBackground := origBg != defBg dontOverrideBackground := origBg != defBg
if b.Settings["hltaberrors"].(bool) {
if s, ok := config.Colorscheme["tab-error"]; ok {
isTab := (r == '\t') || (r == ' ' && !showcursor)
if (b.Settings["tabstospaces"].(bool) && isTab) ||
(!b.Settings["tabstospaces"].(bool) && bloc.X < leadingwsEnd && r == ' ' && !isTab) {
fg, _, _ := s.Decompose()
style = style.Background(fg)
dontOverrideBackground = true
}
}
}
if b.Settings["hltrailingws"].(bool) {
if s, ok := config.Colorscheme["trailingws"]; ok {
if bloc.X >= trailingwsStart && bloc.X < blineLen {
hl := true
for _, c := range cursors {
if c.NewTrailingWsY == bloc.Y {
hl = false
break
}
}
if hl {
fg, _, _ := s.Decompose()
style = style.Background(fg)
dontOverrideBackground = true
}
}
}
}
for _, c := range cursors { for _, c := range cursors {
if c.HasSelection() && if c.HasSelection() &&
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) || (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
@ -557,7 +588,15 @@ func (w *BufWindow) displayBuffer() {
for _, mb := range matchingBraces { for _, mb := range matchingBraces {
if mb.X == bloc.X && mb.Y == bloc.Y { if mb.X == bloc.X && mb.Y == bloc.Y {
style = style.Underline(true) if b.Settings["matchbracestyle"].(string) == "highlight" {
if s, ok := config.Colorscheme["match-brace"]; ok {
style = s
} else {
style = style.Reverse(true)
}
} else {
style = style.Underline(true)
}
} }
} }
} }
@ -609,7 +648,7 @@ func (w *BufWindow) displayBuffer() {
wordwidth := 0 wordwidth := 0
totalwidth := w.StartCol - nColsBeforeStart totalwidth := w.StartCol - nColsBeforeStart
for len(line) > 0 { for len(line) > 0 && vloc.X < maxWidth {
r, combc, size := util.DecodeCharacter(line) r, combc, size := util.DecodeCharacter(line)
line = line[size:] line = line[size:]

View File

@ -7,7 +7,7 @@ import (
"github.com/zyedidia/micro/v2/internal/info" "github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type InfoWindow struct { type InfoWindow struct {

View File

@ -291,13 +291,7 @@ func (w *BufWindow) diff(s1, s2 SLoc) int {
// within the buffer boundaries. // within the buffer boundaries.
func (w *BufWindow) Scroll(s SLoc, n int) SLoc { func (w *BufWindow) Scroll(s SLoc, n int) SLoc {
if !w.Buf.Settings["softwrap"].(bool) { if !w.Buf.Settings["softwrap"].(bool) {
s.Line += n s.Line = util.Clamp(s.Line + n, 0, w.Buf.LinesNum()-1)
if s.Line < 0 {
s.Line = 0
}
if s.Line > w.Buf.LinesNum()-1 {
s.Line = w.Buf.LinesNum() - 1
}
return s return s
} }
return w.scroll(s, n) return w.scroll(s, n)

View File

@ -47,6 +47,12 @@ var statusInfo = map[string]func(*buffer.Buffer) string{
} }
return "" return ""
}, },
"overwrite": func(b *buffer.Buffer) string {
if b.OverwriteMode && !b.Type.Readonly {
return "[ovwr] "
}
return ""
},
"lines": func(b *buffer.Buffer) string { "lines": func(b *buffer.Buffer) string {
return strconv.Itoa(b.LinesNum()) return strconv.Itoa(b.LinesNum())
}, },

View File

@ -2,7 +2,7 @@ package display
import ( import (
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"

View File

@ -6,8 +6,8 @@ import (
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
"github.com/zyedidia/terminal" "github.com/micro-editor/terminal"
) )
type TermWindow struct { type TermWindow struct {
@ -110,6 +110,8 @@ func (w *TermWindow) Display() {
} }
if w.State.CursorVisible() && w.active { if w.State.CursorVisible() && w.active {
curx, cury := w.State.Cursor() curx, cury := w.State.Cursor()
screen.ShowCursor(curx+w.X, cury+w.Y) if curx < w.Width && cury < w.Height {
screen.ShowCursor(curx+w.X, cury+w.Y)
}
} }
} }

View File

@ -1,12 +1,16 @@
package info package info
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -17,24 +21,23 @@ func (i *InfoBuf) LoadHistory() {
if config.GetGlobalOption("savehistory").(bool) { if config.GetGlobalOption("savehistory").(bool) {
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history")) file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
var decodedMap map[string][]string var decodedMap map[string][]string
if err == nil { if err != nil {
defer file.Close() if !errors.Is(err, fs.ErrNotExist) {
decoder := gob.NewDecoder(file) i.Error("Error loading history: ", err)
err = decoder.Decode(&decodedMap)
if err != nil {
i.Error("Error loading history:", err)
return
} }
return
}
defer file.Close()
err = gob.NewDecoder(file).Decode(&decodedMap)
if err != nil {
i.Error("Error decoding history: ", err)
return
} }
if decodedMap != nil { if decodedMap != nil {
i.History = decodedMap i.History = decodedMap
} else {
i.History = make(map[string][]string)
} }
} else {
i.History = make(map[string][]string)
} }
} }
@ -49,16 +52,18 @@ func (i *InfoBuf) SaveHistory() {
} }
} }
file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history")) var buf bytes.Buffer
if err == nil { err := gob.NewEncoder(&buf).Encode(i.History)
defer file.Close() if err != nil {
encoder := gob.NewEncoder(file) screen.TermMessage("Error encoding history: ", err)
return
}
err = encoder.Encode(i.History) filename := filepath.Join(config.ConfigDir, "buffers", "history")
if err != nil { err = util.SafeWrite(filename, buf.Bytes(), true)
i.Error("Error saving history:", err) if err != nil {
return screen.TermMessage("Error saving history: ", err)
} return
} }
} }
} }

View File

@ -7,7 +7,7 @@ import (
) )
// The InfoBuf displays messages and other info at the bottom of the screen. // The InfoBuf displays messages and other info at the bottom of the screen.
// It is respresented as a buffer and a message with a style. // It is represented as a buffer and a message with a style.
type InfoBuf struct { type InfoBuf struct {
*buffer.Buffer *buffer.Buffer
@ -143,13 +143,12 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
if i.PromptCallback != nil { if i.PromptCallback != nil {
if canceled { if canceled {
i.Replace(i.Start(), i.End(), "") i.Replace(i.Start(), i.End(), "")
i.PromptCallback("", true)
h := i.History[i.PromptType] h := i.History[i.PromptType]
i.History[i.PromptType] = h[:len(h)-1] i.History[i.PromptType] = h[:len(h)-1]
i.PromptCallback("", true)
} else { } else {
resp := string(i.LineBytes(0)) resp := string(i.LineBytes(0))
i.Replace(i.Start(), i.End(), "") i.Replace(i.Start(), i.End(), "")
i.PromptCallback(resp, false)
h := i.History[i.PromptType] h := i.History[i.PromptType]
h[len(h)-1] = resp h[len(h)-1] = resp
@ -160,6 +159,8 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
break break
} }
} }
i.PromptCallback(resp, false)
} }
// i.PromptCallback = nil // i.PromptCallback = nil
} }

View File

@ -17,7 +17,6 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
@ -27,7 +26,6 @@ import (
) )
var L *lua.LState var L *lua.LState
var Lock sync.Mutex
// LoadFile loads a lua file // LoadFile loads a lua file
func LoadFile(module string, file string, data []byte) error { func LoadFile(module string, file string, data []byte) error {
@ -127,6 +125,7 @@ func importIo() *lua.LTable {
L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter)) L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter))
L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader)) L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader))
L.SetField(pkg, "Pipe", luar.New(L, io.Pipe)) L.SetField(pkg, "Pipe", luar.New(L, io.Pipe))
L.SetField(pkg, "ReadAll", luar.New(L, io.ReadAll))
L.SetField(pkg, "ReadAtLeast", luar.New(L, io.ReadAtLeast)) L.SetField(pkg, "ReadAtLeast", luar.New(L, io.ReadAtLeast))
L.SetField(pkg, "ReadFull", luar.New(L, io.ReadFull)) L.SetField(pkg, "ReadFull", luar.New(L, io.ReadFull))
L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader)) L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader))
@ -372,6 +371,8 @@ func importOs() *lua.LTable {
L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator)) L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator))
L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator)) L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator))
L.SetField(pkg, "Pipe", luar.New(L, os.Pipe)) L.SetField(pkg, "Pipe", luar.New(L, os.Pipe))
L.SetField(pkg, "ReadDir", luar.New(L, os.ReadDir))
L.SetField(pkg, "ReadFile", luar.New(L, os.ReadFile))
L.SetField(pkg, "Readlink", luar.New(L, os.Readlink)) L.SetField(pkg, "Readlink", luar.New(L, os.Readlink))
L.SetField(pkg, "Remove", luar.New(L, os.Remove)) L.SetField(pkg, "Remove", luar.New(L, os.Remove))
L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll)) L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll))
@ -390,6 +391,7 @@ func importOs() *lua.LTable {
L.SetField(pkg, "TempDir", luar.New(L, os.TempDir)) L.SetField(pkg, "TempDir", luar.New(L, os.TempDir))
L.SetField(pkg, "Truncate", luar.New(L, os.Truncate)) L.SetField(pkg, "Truncate", luar.New(L, os.Truncate))
L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir)) L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir))
L.SetField(pkg, "WriteFile", luar.New(L, os.WriteFile))
return pkg return pkg
} }
@ -523,21 +525,16 @@ func importErrors() *lua.LTable {
func importTime() *lua.LTable { func importTime() *lua.LTable {
pkg := L.NewTable() pkg := L.NewTable()
L.SetField(pkg, "After", luar.New(L, time.After))
L.SetField(pkg, "Sleep", luar.New(L, time.Sleep)) L.SetField(pkg, "Sleep", luar.New(L, time.Sleep))
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
L.SetField(pkg, "Since", luar.New(L, time.Since)) L.SetField(pkg, "Since", luar.New(L, time.Since))
L.SetField(pkg, "FixedZone", luar.New(L, time.FixedZone)) L.SetField(pkg, "FixedZone", luar.New(L, time.FixedZone))
L.SetField(pkg, "LoadLocation", luar.New(L, time.LoadLocation)) L.SetField(pkg, "LoadLocation", luar.New(L, time.LoadLocation))
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
L.SetField(pkg, "Date", luar.New(L, time.Date)) L.SetField(pkg, "Date", luar.New(L, time.Date))
L.SetField(pkg, "Now", luar.New(L, time.Now)) L.SetField(pkg, "Now", luar.New(L, time.Now))
L.SetField(pkg, "Parse", luar.New(L, time.Parse)) L.SetField(pkg, "Parse", luar.New(L, time.Parse))
L.SetField(pkg, "ParseDuration", luar.New(L, time.ParseDuration)) L.SetField(pkg, "ParseDuration", luar.New(L, time.ParseDuration))
L.SetField(pkg, "ParseInLocation", luar.New(L, time.ParseInLocation)) L.SetField(pkg, "ParseInLocation", luar.New(L, time.ParseInLocation))
L.SetField(pkg, "Unix", luar.New(L, time.Unix)) L.SetField(pkg, "Unix", luar.New(L, time.Unix))
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
L.SetField(pkg, "Nanosecond", luar.New(L, time.Nanosecond)) L.SetField(pkg, "Nanosecond", luar.New(L, time.Nanosecond))
L.SetField(pkg, "Microsecond", luar.New(L, time.Microsecond)) L.SetField(pkg, "Microsecond", luar.New(L, time.Microsecond))
L.SetField(pkg, "Millisecond", luar.New(L, time.Millisecond)) L.SetField(pkg, "Millisecond", luar.New(L, time.Millisecond))
@ -545,6 +542,15 @@ func importTime() *lua.LTable {
L.SetField(pkg, "Minute", luar.New(L, time.Minute)) L.SetField(pkg, "Minute", luar.New(L, time.Minute))
L.SetField(pkg, "Hour", luar.New(L, time.Hour)) L.SetField(pkg, "Hour", luar.New(L, time.Hour))
// TODO: these raw Go timer APIs don't provide any synchronization
// with micro. Stop exposing them to lua once plugins switch to using
// the safer micro.After() interface instead. See issue #3209
L.SetField(pkg, "After", luar.New(L, time.After))
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
return pkg return pkg
} }

View File

@ -8,7 +8,7 @@ import (
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
// Screen is the tcell screen we use to draw to the terminal // Screen is the tcell screen we use to draw to the terminal
@ -22,6 +22,10 @@ var Screen tcell.Screen
// Events is the channel of tcell events // Events is the channel of tcell events
var Events chan (tcell.Event) var Events chan (tcell.Event)
// RestartCallback is called when the screen is restarted after it was
// temporarily shut down
var RestartCallback func()
// The lock is necessary since the screen is polled on a separate thread // The lock is necessary since the screen is polled on a separate thread
var lock sync.Mutex var lock sync.Mutex
@ -29,6 +33,12 @@ var lock sync.Mutex
// written to even if no event user event has occurred // written to even if no event user event has occurred
var drawChan chan bool var drawChan chan bool
// rawSeq is the list of raw escape sequences that are bound to some actions
// via keybindings and thus should be parsed by tcell. We need to register
// them in tcell every time we reinitialize the screen, so we need to remember
// them in a list
var rawSeq = make([]string, 0)
// Lock locks the screen lock // Lock locks the screen lock
func Lock() { func Lock() {
lock.Lock() lock.Lock()
@ -117,6 +127,34 @@ func SetContent(x, y int, mainc rune, combc []rune, style tcell.Style) {
} }
} }
// RegisterRawSeq registers a raw escape sequence that should be parsed by tcell
func RegisterRawSeq(r string) {
for _, seq := range rawSeq {
if seq == r {
return
}
}
rawSeq = append(rawSeq, r)
if Screen != nil {
Screen.RegisterRawSeq(r)
}
}
// UnregisterRawSeq unregisters a raw escape sequence that should be parsed by tcell
func UnregisterRawSeq(r string) {
for i, seq := range rawSeq {
if seq == r {
rawSeq[i] = rawSeq[len(rawSeq)-1]
rawSeq = rawSeq[:len(rawSeq)-1]
}
}
if Screen != nil {
Screen.UnregisterRawSeq(r)
}
}
// TempFini shuts the screen down temporarily // TempFini shuts the screen down temporarily
func TempFini() bool { func TempFini() bool {
screenWasNil := Screen == nil screenWasNil := Screen == nil
@ -134,6 +172,10 @@ func TempStart(screenWasNil bool) {
if !screenWasNil { if !screenWasNil {
Init() Init()
Unlock() Unlock()
if RestartCallback != nil {
RestartCallback()
}
} }
} }
@ -187,6 +229,10 @@ func Init() error {
Screen.EnableMouse() Screen.EnableMouse()
} }
for _, r := range rawSeq {
Screen.RegisterRawSeq(r)
}
return nil return nil
} }

View File

@ -78,8 +78,10 @@ func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit func(
go func() { go func() {
// Run the process in the background and create the onExit callback // Run the process in the background and create the onExit callback
proc.Run() proc.Run()
jobFunc := JobFunction{onExit, outbuf.String(), userargs} if onExit != nil {
Jobs <- jobFunc jobFunc := JobFunction{onExit, outbuf.String(), userargs}
Jobs <- jobFunc
}
}() }()
return &Job{proc, stdin} return &Job{proc, stdin}

View File

@ -11,6 +11,7 @@ import (
shellquote "github.com/kballard/go-shellquote" shellquote "github.com/kballard/go-shellquote"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
) )
// ExecCommand executes a command using exec // ExecCommand executes a command using exec
@ -95,28 +96,30 @@ func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
// This is a trap for Ctrl-C so that it doesn't kill micro // This is a trap for Ctrl-C so that it doesn't kill micro
// Instead we trap Ctrl-C to kill the program we're running // micro is killed if the signal is ignored only on Windows, so it is
// received
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Reset(os.Interrupt)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
go func() { err = cmd.Start()
for range c { if err == nil {
cmd.Process.Kill() err = cmd.Wait()
if wait {
// This is just so we don't return right away and let the user press enter to return
screen.TermMessage("")
} }
}() } else {
screen.TermMessage(err)
cmd.Start() }
err = cmd.Wait()
output := outputBytes.String() output := outputBytes.String()
if wait {
// This is just so we don't return right away and let the user press enter to return
screen.TermMessage("")
}
// Start the screen back up // Start the screen back up
screen.TempStart(screenb) screen.TempStart(screenb)
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(c)
return output, err return output, err
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/terminal" "github.com/micro-editor/terminal"
) )
type TermType int type TermType int

View File

@ -6,7 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/url"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
@ -16,6 +18,7 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"unicode/utf8"
"github.com/blang/semver" "github.com/blang/semver"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
@ -40,8 +43,46 @@ var (
// Stdout is a buffer that is written to stdout when micro closes // Stdout is a buffer that is written to stdout when micro closes
Stdout *bytes.Buffer Stdout *bytes.Buffer
// Sigterm is a channel where micro exits when written
Sigterm chan os.Signal
// To be used for fails on (over-)write with safe writes
ErrOverwrite = OverwriteError{}
) )
// To be used for file writes before umask is applied
const FileMode os.FileMode = 0666
const OverwriteFailMsg = `An error occurred while writing to the file:
%s
The file may be corrupted now. The good news is that it has been
successfully backed up. Next time you open this file with Micro,
Micro will ask if you want to recover it from the backup.
The backup path is:
%s`
// OverwriteError is a custom error to add additional information
type OverwriteError struct {
What error
BackupName string
}
func (e OverwriteError) Error() string {
return fmt.Sprintf(OverwriteFailMsg, e.What, e.BackupName)
}
func (e OverwriteError) Is(target error) bool {
return target == ErrOverwrite
}
func (e OverwriteError) Unwrap() error {
return e.What
}
func init() { func init() {
var err error var err error
SemVersion, err = semver.Make(Version) SemVersion, err = semver.Make(Version)
@ -217,10 +258,56 @@ func FSize(f *os.File) int64 {
return fi.Size() return fi.Size()
} }
// IsWordChar returns whether or not the string is a 'word character' // IsWordChar returns whether or not a rune is a 'word character'
// Word characters are defined as numbers, letters, or '_' // Word characters are defined as numbers, letters or sub-word delimiters
func IsWordChar(r rune) bool { func IsWordChar(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' return IsAlphanumeric(r) || IsSubwordDelimiter(r)
}
// IsNonWordChar returns whether or not a rune is not a 'word character'
// Non word characters are defined as all characters not being numbers, letters or sub-word delimiters
// See IsWordChar()
func IsNonWordChar(r rune) bool {
return !IsWordChar(r)
}
// IsSubwordDelimiter returns whether or not a rune is a 'sub-word delimiter character'
// i.e. is considered a part of the word and is used as a delimiter between sub-words of the word.
// For now the only sub-word delimiter character is '_'.
func IsSubwordDelimiter(r rune) bool {
return r == '_'
}
// IsAlphanumeric returns whether or not a rune is an 'alphanumeric character'
// Alphanumeric characters are defined as numbers or letters
func IsAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r)
}
// IsUpperAlphanumeric returns whether or not a rune is an 'upper alphanumeric character'
// Upper alphanumeric characters are defined as numbers or upper-case letters
func IsUpperAlphanumeric(r rune) bool {
return IsUpperLetter(r) || unicode.IsNumber(r)
}
// IsLowerAlphanumeric returns whether or not a rune is a 'lower alphanumeric character'
// Lower alphanumeric characters are defined as numbers or lower-case letters
func IsLowerAlphanumeric(r rune) bool {
return IsLowerLetter(r) || unicode.IsNumber(r)
}
// IsUpperLetter returns whether or not a rune is an 'upper letter character'
// Upper letter characters are defined as upper-case letters
func IsUpperLetter(r rune) bool {
// unicode.IsUpper() returns true for letters only
return unicode.IsUpper(r)
}
// IsLowerLetter returns whether or not a rune is a 'lower letter character'
// Lower letter characters are defined as lower-case letters
func IsLowerLetter(r rune) bool {
// unicode.IsLower() returns true for letters only
return unicode.IsLower(r)
} }
// Spaces returns a string with n spaces // Spaces returns a string with n spaces
@ -271,6 +358,28 @@ func RunePos(b []byte, i int) int {
return CharacterCount(b[:i]) return CharacterCount(b[:i])
} }
// IndexAnyUnquoted returns the first position in s of a character from chars.
// Escaped (with backslash) and quoted (with single or double quotes) characters
// are ignored. Returns -1 if not successful
func IndexAnyUnquoted(s, chars string) int {
var e bool
var q rune
for i, r := range s {
if e {
e = false
} else if (q == 0 || q == '"') && r == '\\' {
e = true
} else if r == q {
q = 0
} else if q == 0 && (r == '\'' || r == '"') {
q = r
} else if q == 0 && strings.IndexRune(chars, r) >= 0 {
return i
}
}
return -1
}
// MakeRelative will attempt to make a relative path between path and base // MakeRelative will attempt to make a relative path between path and base
func MakeRelative(path, base string) (string, error) { func MakeRelative(path, base string) (string, error) {
if len(path) > 0 { if len(path) > 0 {
@ -315,7 +424,7 @@ func ReplaceHome(path string) (string, error) {
// This is used for opening files like util.go:10:5 to specify a line and column // This is used for opening files like util.go:10:5 to specify a line and column
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly. // Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
func GetPathAndCursorPosition(path string) (string, []string) { func GetPathAndCursorPosition(path string) (string, []string) {
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`) re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?$`)
match := re.FindStringSubmatch(path) match := re.FindStringSubmatch(path)
// no lines/columns were specified in the path, return just the path with no cursor location // no lines/columns were specified in the path, return just the path with no cursor location
if len(match) == 0 { if len(match) == 0 {
@ -337,8 +446,17 @@ func GetModTime(path string) (time.Time, error) {
return info.ModTime(), nil return info.ModTime(), nil
} }
// EscapePath replaces every path separator in a given path with a % func AppendBackupSuffix(path string) string {
func EscapePath(path string) string { return path + ".micro-backup"
}
// EscapePathUrl encodes the path in URL query form
func EscapePathUrl(path string) string {
return url.QueryEscape(filepath.ToSlash(path))
}
// EscapePathLegacy replaces every path separator in a given path with a %
func EscapePathLegacy(path string) string {
path = filepath.ToSlash(path) path = filepath.ToSlash(path)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// ':' is not valid in a path name on Windows but is ok on Unix // ':' is not valid in a path name on Windows but is ok on Unix
@ -347,6 +465,24 @@ func EscapePath(path string) string {
return strings.ReplaceAll(path, "/", "%") return strings.ReplaceAll(path, "/", "%")
} }
// DetermineEscapePath escapes a path, determining whether it should be escaped
// using URL encoding (preferred, since it encodes unambiguously) or
// legacy encoding with '%' (for backward compatibility, if the legacy-escaped
// path exists in the given directory).
func DetermineEscapePath(dir string, path string) string {
url := filepath.Join(dir, EscapePathUrl(path))
if _, err := os.Stat(url); err == nil {
return url
}
legacy := filepath.Join(dir, EscapePathLegacy(path))
if _, err := os.Stat(legacy); err == nil {
return legacy
}
return url
}
// GetLeadingWhitespace returns the leading whitespace of the given byte array // GetLeadingWhitespace returns the leading whitespace of the given byte array
func GetLeadingWhitespace(b []byte) []byte { func GetLeadingWhitespace(b []byte) []byte {
ws := []byte{} ws := []byte{}
@ -363,6 +499,28 @@ func GetLeadingWhitespace(b []byte) []byte {
return ws return ws
} }
// GetTrailingWhitespace returns the trailing whitespace of the given byte array
func GetTrailingWhitespace(b []byte) []byte {
ws := []byte{}
for len(b) > 0 {
r, size := utf8.DecodeLastRune(b)
if IsWhitespace(r) {
ws = append([]byte(string(r)), ws...)
} else {
break
}
b = b[:len(b)-size]
}
return ws
}
// HasTrailingWhitespace returns true if the given byte array ends with a whitespace
func HasTrailingWhitespace(b []byte) bool {
r, _ := utf8.DecodeLastRune(b)
return IsWhitespace(r)
}
// IntOpt turns a float64 setting to an int // IntOpt turns a float64 setting to an int
func IntOpt(opt interface{}) int { func IntOpt(opt interface{}) int {
return int(opt.(float64)) return int(opt.(float64))
@ -422,19 +580,9 @@ func Clamp(val, min, max int) int {
return val return val
} }
// IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
func IsNonAlphaNumeric(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
}
// IsAutocomplete returns whether a character should begin an autocompletion. // IsAutocomplete returns whether a character should begin an autocompletion.
func IsAutocomplete(c rune) bool { func IsAutocomplete(c rune) bool {
return c == '.' || !IsNonAlphaNumeric(c) return c == '.' || IsWordChar(c)
}
// ParseSpecial replaces escaped ts with '\t'.
func ParseSpecial(s string) string {
return strings.ReplaceAll(s, "\\t", "\t")
} }
// String converts a byte array to a string (for lua plugins) // String converts a byte array to a string (for lua plugins)
@ -507,3 +655,77 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon
} }
return client.Do(req) return client.Do(req)
} }
// SafeWrite writes bytes to a file in a "safe" way, preventing loss of the
// contents of the file if it fails to write the new contents.
// This means that the file is not overwritten directly but by writing to a
// temporary file first.
//
// If rename is true, write is performed atomically, by renaming the temporary
// file to the target file after the data is successfully written to the
// temporary file. This guarantees that the file will not remain in a corrupted
// state, but it also has limitations, e.g. the file should not be a symlink
// (otherwise SafeWrite silently replaces this symlink with a regular file),
// the file creation date in Linux is not preserved (since the file inode
// changes) etc. Use SafeWrite with rename=true for files that are only created
// and used by micro for its own needs and are not supposed to be used directly
// by the user.
//
// If rename is false, write is performed by overwriting the target file after
// the data is successfully written to the temporary file.
// This means that the target file may remain corrupted if overwrite fails,
// but in such case the temporary file is preserved as a backup so the file
// can be recovered later. So it is less convenient than atomic write but more
// universal. Use SafeWrite with rename=false for files that may be managed
// directly by the user, like settings.json and bindings.json.
func SafeWrite(path string, bytes []byte, rename bool) error {
var err error
if _, err = os.Stat(path); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
// Force rename for new files!
rename = true
}
var file *os.File
if !rename {
file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, FileMode)
if err != nil {
return err
}
defer file.Close()
}
tmp := AppendBackupSuffix(path)
err = os.WriteFile(tmp, bytes, FileMode)
if err != nil {
os.Remove(tmp)
return err
}
if rename {
err = os.Rename(tmp, path)
} else {
err = file.Truncate(0)
if err == nil {
_, err = file.Write(bytes)
}
if err == nil {
err = file.Sync()
}
}
if err != nil {
if rename {
os.Remove(tmp)
} else {
err = OverwriteError{err, tmp}
}
return err
}
if !rename {
os.Remove(tmp)
}
return nil
}

View File

@ -185,6 +185,10 @@ func (n *Node) hResizeSplit(i int, size int) bool {
// ResizeSplit resizes a certain split to a given size // ResizeSplit resizes a certain split to a given size
func (n *Node) ResizeSplit(size int) bool { func (n *Node) ResizeSplit(size int) bool {
// TODO: `size < 0` does not work for some reason
if size <= 0 {
return false
}
if len(n.parent.children) <= 1 { if len(n.parent.children) <= 1 {
// cannot resize a lone node // cannot resize a lone node
return false return false
@ -201,7 +205,7 @@ func (n *Node) ResizeSplit(size int) bool {
return n.parent.hResizeSplit(ind, size) return n.parent.hResizeSplit(ind, size)
} }
// Resize sets this node's size and resizes all children accordlingly // Resize sets this node's size and resizes all children accordingly
func (n *Node) Resize(w, h int) { func (n *Node) Resize(w, h int) {
n.W, n.H = w, h n.W, n.H = w, h

View File

@ -1,18 +0,0 @@
package highlight
import "regexp"
// MatchFiletype will use the list of syntax definitions provided and the filename and first line of the file
// to determine the filetype of the file
// It will return the corresponding syntax definition for the filetype
func MatchFiletype(ftdetect [2]*regexp.Regexp, filename string, firstLine []byte) bool {
if ftdetect[0] != nil && ftdetect[0].MatchString(filename) {
return true
}
if ftdetect[1] != nil {
return ftdetect[1].Match(firstLine)
}
return false
}

View File

@ -51,25 +51,9 @@ func runePos(p int, str []byte) int {
return CharacterCount(str[:p]) return CharacterCount(str[:p])
} }
func combineLineMatch(src, dst LineMatch) LineMatch {
for k, v := range src {
if g, ok := dst[k]; ok {
if g == 0 {
dst[k] = v
}
} else {
dst[k] = v
}
}
return dst
}
// A State represents the region at the end of a line // A State represents the region at the end of a line
type State *region type State *region
// EmptyDef is an empty definition.
var EmptyDef = Def{nil, &rules{}}
// LineStates is an interface for a buffer-like object which can also store the states and matches for every line // LineStates is an interface for a buffer-like object which can also store the states and matches for every line
type LineStates interface { type LineStates interface {
LineBytes(n int) []byte LineBytes(n int) []byte
@ -77,6 +61,8 @@ type LineStates interface {
State(lineN int) State State(lineN int) State
SetState(lineN int, s State) SetState(lineN int, s State)
SetMatch(lineN int, m LineMatch) SetMatch(lineN int, m LineMatch)
Lock()
Unlock()
} }
// A Highlighter contains the information needed to highlight a string // A Highlighter contains the information needed to highlight a string
@ -303,7 +289,13 @@ func (h *Highlighter) HighlightString(input string) []LineMatch {
// HighlightStates correctly sets all states for the buffer // HighlightStates correctly sets all states for the buffer
func (h *Highlighter) HighlightStates(input LineStates) { func (h *Highlighter) HighlightStates(input LineStates) {
for i := 0; i < input.LinesNum(); i++ { for i := 0; ; i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
line := input.LineBytes(i) line := input.LineBytes(i)
// highlights := make(LineMatch) // highlights := make(LineMatch)
@ -316,6 +308,7 @@ func (h *Highlighter) HighlightStates(input LineStates) {
curState := h.lastRegion curState := h.lastRegion
input.SetState(i, curState) input.SetState(i, curState)
input.Unlock()
} }
} }
@ -324,7 +317,9 @@ func (h *Highlighter) HighlightStates(input LineStates) {
// This assumes that all the states are set correctly // This assumes that all the states are set correctly
func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) { func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
for i := startline; i <= endline; i++ { for i := startline; i <= endline; i++ {
input.Lock()
if i >= input.LinesNum() { if i >= input.LinesNum() {
input.Unlock()
break break
} }
@ -339,6 +334,7 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int)
} }
input.SetMatch(i, match) input.SetMatch(i, match)
input.Unlock()
} }
} }
@ -350,9 +346,19 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
h.lastRegion = nil h.lastRegion = nil
if startline > 0 { if startline > 0 {
h.lastRegion = input.State(startline - 1) input.Lock()
if startline-1 < input.LinesNum() {
h.lastRegion = input.State(startline - 1)
}
input.Unlock()
} }
for i := startline; i < input.LinesNum(); i++ { for i := startline; ; i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
line := input.LineBytes(i) line := input.LineBytes(i)
// highlights := make(LineMatch) // highlights := make(LineMatch)
@ -366,6 +372,7 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
lastState := input.State(i) lastState := input.State(i)
input.SetState(i, curState) input.SetState(i, curState)
input.Unlock()
if curState == lastState { if curState == lastState {
return i return i
@ -377,6 +384,9 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
// ReHighlightLine will rehighlight the state and match for a single line // ReHighlightLine will rehighlight the state and match for a single line
func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) { func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
input.Lock()
defer input.Unlock()
line := input.LineBytes(lineN) line := input.LineBytes(lineN)
highlights := make(LineMatch) highlights := make(LineMatch)

View File

@ -33,27 +33,28 @@ func (g Group) String() string {
// Then it has the rules which define how to highlight the file // Then it has the rules which define how to highlight the file
type Def struct { type Def struct {
*Header *Header
rules *rules rules *rules
} }
type Header struct { type Header struct {
FileType string FileType string
FtDetect [2]*regexp.Regexp FileNameRegex *regexp.Regexp
HeaderRegex *regexp.Regexp
SignatureRegex *regexp.Regexp
} }
type HeaderYaml struct { type HeaderYaml struct {
FileType string `yaml:"filetype"` FileType string `yaml:"filetype"`
Detect struct { Detect struct {
FNameRgx string `yaml:"filename"` FNameRegexStr string `yaml:"filename"`
HeaderRgx string `yaml:"header"` HeaderRegexStr string `yaml:"header"`
SignatureRegexStr string `yaml:"signature"`
} `yaml:"detect"` } `yaml:"detect"`
} }
type File struct { type File struct {
FileType string FileType string
yamlSrc map[interface{}]interface{}
yamlSrc map[interface{}]interface{}
} }
// A Pattern is one simple syntax rule // A Pattern is one simple syntax rule
@ -97,20 +98,24 @@ func init() {
// A yaml file might take ~400us to parse while a header file only takes ~20us // A yaml file might take ~400us to parse while a header file only takes ~20us
func MakeHeader(data []byte) (*Header, error) { func MakeHeader(data []byte) (*Header, error) {
lines := bytes.Split(data, []byte{'\n'}) lines := bytes.Split(data, []byte{'\n'})
if len(lines) < 3 { if len(lines) < 4 {
return nil, errors.New("Header file has incorrect format") return nil, errors.New("Header file has incorrect format")
} }
header := new(Header) header := new(Header)
var err error var err error
header.FileType = string(lines[0]) header.FileType = string(lines[0])
fnameRgx := string(lines[1]) fnameRegexStr := string(lines[1])
headerRgx := string(lines[2]) headerRegexStr := string(lines[2])
signatureRegexStr := string(lines[3])
if fnameRgx != "" { if fnameRegexStr != "" {
header.FtDetect[0], err = regexp.Compile(fnameRgx) header.FileNameRegex, err = regexp.Compile(fnameRegexStr)
} }
if err == nil && headerRgx != "" { if err == nil && headerRegexStr != "" {
header.FtDetect[1], err = regexp.Compile(headerRgx) header.HeaderRegex, err = regexp.Compile(headerRegexStr)
}
if err == nil && signatureRegexStr != "" {
header.SignatureRegex, err = regexp.Compile(signatureRegexStr)
} }
if err != nil { if err != nil {
@ -132,11 +137,14 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
header := new(Header) header := new(Header)
header.FileType = hdrYaml.FileType header.FileType = hdrYaml.FileType
if hdrYaml.Detect.FNameRgx != "" { if hdrYaml.Detect.FNameRegexStr != "" {
header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx) header.FileNameRegex, err = regexp.Compile(hdrYaml.Detect.FNameRegexStr)
} }
if err == nil && hdrYaml.Detect.HeaderRgx != "" { if err == nil && hdrYaml.Detect.HeaderRegexStr != "" {
header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx) header.HeaderRegex, err = regexp.Compile(hdrYaml.Detect.HeaderRegexStr)
}
if err == nil && hdrYaml.Detect.SignatureRegexStr != "" {
header.SignatureRegex, err = regexp.Compile(hdrYaml.Detect.SignatureRegexStr)
} }
if err != nil { if err != nil {
@ -146,6 +154,37 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
return header, nil return header, nil
} }
// MatchFileName will check the given file name with the stored regex
func (header *Header) MatchFileName(filename string) bool {
if header.FileNameRegex != nil {
return header.FileNameRegex.MatchString(filename)
}
return false
}
func (header *Header) MatchFileHeader(firstLine []byte) bool {
if header.HeaderRegex != nil {
return header.HeaderRegex.Match(firstLine)
}
return false
}
// HasFileSignature checks the presence of a stored signature
func (header *Header) HasFileSignature() bool {
return header.SignatureRegex != nil
}
// MatchFileSignature will check the given line with the stored regex
func (header *Header) MatchFileSignature(line []byte) bool {
if header.SignatureRegex != nil {
return header.SignatureRegex.Match(line)
}
return false
}
func ParseFile(input []byte) (f *File, err error) { func ParseFile(input []byte) (f *File, err error) {
// This is just so if we have an error, we can exit cleanly and return the parse error to the user // This is just so if we have an error, we can exit cleanly and return the parse error to the user
defer func() { defer func() {
@ -170,11 +209,19 @@ func ParseFile(input []byte) (f *File, err error) {
if k == "filetype" { if k == "filetype" {
filetype := v.(string) filetype := v.(string)
if filetype == "" {
return nil, errors.New("empty filetype")
}
f.FileType = filetype f.FileType = filetype
break break
} }
} }
if f.FileType == "" {
return nil, errors.New("missing filetype")
}
return f, err return f, err
} }
@ -191,12 +238,12 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
} }
}() }()
rules := f.yamlSrc src := f.yamlSrc
s = new(Def) s = new(Def)
s.Header = header s.Header = header
for k, v := range rules { for k, v := range src {
if k == "rules" { if k == "rules" {
inputRules := v.([]interface{}) inputRules := v.([]interface{})
@ -209,6 +256,11 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
} }
} }
if s.rules == nil {
// allow empty rules
s.rules = &rules{}
}
return s, err return s, err
} }
@ -303,6 +355,10 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
switch object := val.(type) { switch object := val.(type) {
case string: case string:
if object == "" {
return nil, fmt.Errorf("Empty rule %s", k)
}
if k == "include" { if k == "include" {
ru.includes = append(ru.includes, object) ru.includes = append(ru.includes, object)
} else { } else {
@ -356,30 +412,56 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
r.group = groupNum r.group = groupNum
r.parent = prevRegion r.parent = prevRegion
r.start, err = regexp.Compile(regionInfo["start"].(string)) // start is mandatory
if start, ok := regionInfo["start"]; ok {
start := start.(string)
if start == "" {
return nil, fmt.Errorf("Empty start in %s", group)
}
if err != nil { r.start, err = regexp.Compile(start)
return nil, err if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("Missing start in %s", group)
} }
r.end, err = regexp.Compile(regionInfo["end"].(string)) // end is mandatory
if end, ok := regionInfo["end"]; ok {
end := end.(string)
if end == "" {
return nil, fmt.Errorf("Empty end in %s", group)
}
if err != nil { r.end, err = regexp.Compile(end)
return nil, err if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("Missing end in %s", group)
} }
// skip is optional // skip is optional
if _, ok := regionInfo["skip"]; ok { if skip, ok := regionInfo["skip"]; ok {
r.skip, err = regexp.Compile(regionInfo["skip"].(string)) skip := skip.(string)
if skip == "" {
return nil, fmt.Errorf("Empty skip in %s", group)
}
r.skip, err = regexp.Compile(skip)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
// limit-color is optional // limit-color is optional
if _, ok := regionInfo["limit-group"]; ok { if groupStr, ok := regionInfo["limit-group"]; ok {
groupStr := regionInfo["limit-group"].(string) groupStr := groupStr.(string)
if groupStr == "" {
return nil, fmt.Errorf("Empty limit-group in %s", group)
}
if _, ok := Groups[groupStr]; !ok { if _, ok := Groups[groupStr]; !ok {
numGroups++ numGroups++
Groups[groupStr] = numGroups Groups[groupStr] = numGroups
@ -394,10 +476,17 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
r.limitGroup = r.group r.limitGroup = r.group
} }
r.rules, err = parseRules(regionInfo["rules"].([]interface{}), r) // rules are optional
if rules, ok := regionInfo["rules"]; ok {
r.rules, err = parseRules(rules.([]interface{}), r)
if err != nil {
return nil, err
}
}
if err != nil { if r.rules == nil {
return nil, err // allow empty rules
r.rules = &rules{}
} }
return r, nil return r, nil

View File

@ -28,3 +28,6 @@ color-link color-column "#2D2F31"
#No extended types (bool in C, etc.) #No extended types (bool in C, etc.)
#color-link type.extended "default" #color-link type.extended "default"
#Plain brackets #Plain brackets
color-link match-brace "#1D1F21,#62B1FE"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -26,3 +26,6 @@ color-link color-column "254"
#No extended types (bool in C, &c.) and plain brackets #No extended types (bool in C, &c.) and plain brackets
color-link type.extended "241,231" color-link type.extended "241,231"
color-link symbol.brackets "241,231" color-link symbol.brackets "241,231"
color-link match-brace "231,28"
color-link tab-error "210"
color-link trailingws "210"

View File

@ -42,3 +42,6 @@ color-link gutter-warning "red"
color-link color-column "cyan" color-link color-column "cyan"
color-link underlined.url "underline blue, white" color-link underlined.url "underline blue, white"
color-link divider "blue" color-link divider "blue"
color-link match-brace "black,cyan"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -38,3 +38,6 @@ color-link color-column "#f26522"
color-link constant.bool "bold #55ffff" color-link constant.bool "bold #55ffff"
color-link constant.bool.true "bold #85ff85" color-link constant.bool.true "bold #85ff85"
color-link constant.bool.false "bold #ff8585" color-link constant.bool.false "bold #ff8585"
color-link match-brace "#1e2124,#55ffff"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -29,3 +29,6 @@ color-link color-column "#2C2C2C"
color-link type.extended "default" color-link type.extended "default"
#color-link symbol.brackets "default" #color-link symbol.brackets "default"
color-link symbol.tag "#AE81FF,#242424" color-link symbol.tag "#AE81FF,#242424"
color-link match-brace "#242424,#7A9EC2"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -1,31 +1 @@
color-link default "#F8F8F2,#282828" include "monokai"
color-link comment "#75715E,#282828"
color-link identifier "#66D9EF,#282828"
color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828"
color-link constant.string.char "#BDE6AD,#282828"
color-link statement "#F92672,#282828"
color-link symbol.operator "#F92671,#282828"
color-link preproc "#CB4B16,#282828"
color-link type "#66D9EF,#282828"
color-link special "#A6E22E,#282828"
color-link underlined "#D33682,#282828"
color-link error "bold #CB4B16,#282828"
color-link todo "bold #D33682,#282828"
color-link hlsearch "#282828,#E6DB74"
color-link statusline "#282828,#F8F8F2"
color-link tabbar "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"
color-link line-number "#AAAAAA,#323232"
color-link current-line-number "#AAAAAA,#282828"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#CB4B16,#282828"
color-link gutter-warning "#E6DB74,#282828"
color-link cursor-line "#323232"
color-link color-column "#323232"
#No extended types; Plain brackets.
color-link type.extended "default"
#color-link symbol.brackets "default"
color-link symbol.tag "#AE81FF,#282828"

View File

@ -43,3 +43,7 @@ color-link cursor-line "#44475A,#F8F8F2"
color-link color-column "#44475A" color-link color-column "#44475A"
color-link type.extended "default" color-link type.extended "default"
color-link match-brace "#282A36,#FF79C6"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#001e28"
color-link type.keyword "bold #5aaae6,#001e28" color-link type.keyword "bold #5aaae6,#001e28"
color-link type.extended "#ffffff,#001e28" color-link type.extended "#ffffff,#001e28"
color-link underlined "#608b4e,#001e28" color-link underlined "#608b4e,#001e28"
color-link match-brace "#001e28,#5aaae6"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -33,3 +33,6 @@ color-link type "bold #004080,#f0f0f0"
color-link type.keyword "bold #780050,#f0f0f0" color-link type.keyword "bold #780050,#f0f0f0"
color-link type.extended "#000000,#f0f0f0" color-link type.extended "#000000,#f0f0f0"
color-link underlined "#3f7f5f,#f0f0f0" color-link underlined "#3f7f5f,#f0f0f0"
color-link match-brace "#f0f0f0,#780050"
color-link tab-error "#ff8787"
color-link trailingws "#ff8787"

View File

@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#2d0023"
color-link type.keyword "bold #5aaae6,#2d0023" color-link type.keyword "bold #5aaae6,#2d0023"
color-link type.extended "#ffffff,#2d0023" color-link type.extended "#ffffff,#2d0023"
color-link underlined "#886484,#2d0023" color-link underlined "#886484,#2d0023"
color-link match-brace "#2d0023,#5aaae6"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -24,3 +24,6 @@ color-link diff-modified "yellow"
color-link diff-deleted "red" color-link diff-deleted "red"
color-link gutter-error ",red" color-link gutter-error ",red"
color-link gutter-warning "red" color-link gutter-warning "red"
color-link match-brace "black,cyan"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -24,3 +24,6 @@ color-link gutter-warning "#EDB443,#11151C"
color-link cursor-line "#091F2E" color-link cursor-line "#091F2E"
color-link color-column "#11151C" color-link color-column "#11151C"
color-link symbol "#99D1CE,#0C1014" color-link symbol "#99D1CE,#0C1014"
color-link match-brace "#0C1014,#D26937"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -24,3 +24,6 @@ color-link cursor-line "#3c3836"
color-link color-column "#79740e" color-link color-column "#79740e"
color-link statusline "#ebdbb2,#665c54" color-link statusline "#ebdbb2,#665c54"
color-link tabbar "#ebdbb2,#665c54" color-link tabbar "#ebdbb2,#665c54"
color-link match-brace "#282828,#d3869b"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -21,3 +21,6 @@ color-link cursor-line "237"
color-link color-column "237" color-link color-column "237"
color-link statusline "223,237" color-link statusline "223,237"
color-link tabbar "223,237" color-link tabbar "223,237"
color-link match-brace "235,72"
color-link tab-error "167"
color-link trailingws "167"

View File

@ -20,6 +20,7 @@ color-link identifier.macro "#FFCB6B,#263238"
color-link indent-char "#505050,#263238" color-link indent-char "#505050,#263238"
color-link line-number "#656866,#283942" color-link line-number "#656866,#283942"
color-link preproc "#C792EA,#263238" color-link preproc "#C792EA,#263238"
color-link scrollbar "#80DEEA,#283942"
color-link special "#C792EA,#263238" color-link special "#C792EA,#263238"
color-link statement "#C792EA,#263238" color-link statement "#C792EA,#263238"
color-link statusline "#80DEEA,#3b4d56" color-link statusline "#80DEEA,#3b4d56"
@ -30,3 +31,6 @@ color-link tabbar "#80DEEA,#3b4d56"
color-link todo "bold #C792EA,#263238" color-link todo "bold #C792EA,#263238"
color-link type "#FFCB6B,#263238" color-link type "#FFCB6B,#263238"
color-link underlined "underline #EEFFFF,#263238" color-link underlined "underline #EEFFFF,#263238"
color-link match-brace "#263238,#C792EA"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -23,3 +23,6 @@ color-link gutter-error "#CB4B16"
color-link gutter-warning "#E6DB74" color-link gutter-warning "#E6DB74"
color-link cursor-line "#323232" color-link cursor-line "#323232"
color-link color-column "#323232" color-link color-column "#323232"
color-link match-brace "#1D0000,#AE81FF"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -29,3 +29,6 @@ color-link color-column "#323232"
color-link type.extended "default" color-link type.extended "default"
#color-link symbol.brackets "default" #color-link symbol.brackets "default"
color-link symbol.tag "#AE81FF,#282828" color-link symbol.tag "#AE81FF,#282828"
color-link match-brace "#282828,#AE81FF"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -34,3 +34,6 @@ color-link todo "#8B98AB"
color-link type "#66D9EF" color-link type "#66D9EF"
color-link type.keyword "#C678DD" color-link type.keyword "#C678DD"
color-link underlined "#8996A8" color-link underlined "#8996A8"
color-link match-brace "#21252C,#C678DD"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -28,6 +28,10 @@ color-link tabbar "bold #b1b1b1,#232323"
color-link cursor-line "#353535" color-link cursor-line "#353535"
color-link color-column "#353535" color-link color-column "#353535"
color-link space "underline #e6e1dc,#2b2b2b" color-link space "underline #e6e1dc,#2b2b2b"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"
#the Python syntax definition are wrong. This is not how you should do decorators! #the Python syntax definition are wrong. This is not how you should do decorators!
color-link brightgreen "#edb753,#2b2b2b" color-link brightgreen "#edb753,#2b2b2b"
color-link match-brace "#2b2b2b,#a5c261"

View File

@ -10,6 +10,7 @@ color-link ignore "default"
color-link error ",brightred" color-link error ",brightred"
color-link todo ",brightyellow" color-link todo ",brightyellow"
color-link hlsearch "black,yellow" color-link hlsearch "black,yellow"
color-link statusline "black,white"
color-link indent-char "black" color-link indent-char "black"
color-link line-number "yellow" color-link line-number "yellow"
color-link current-line-number "red" color-link current-line-number "red"
@ -27,3 +28,6 @@ color-link type.extended "default"
color-link symbol.brackets "default" color-link symbol.brackets "default"
#Color shebangs the comment color #Color shebangs the comment color
color-link preproc.shebang "comment" color-link preproc.shebang "comment"
color-link match-brace ",magenta"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -26,3 +26,6 @@ color-link cursor-line "#003541"
color-link color-column "#003541" color-link color-column "#003541"
color-link type.extended "#839496,#002833" color-link type.extended "#839496,#002833"
color-link symbol.brackets "#839496,#002833" color-link symbol.brackets "#839496,#002833"
color-link match-brace "#002833,#268BD2"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -25,3 +25,6 @@ color-link cursor-line "black"
color-link color-column "black" color-link color-column "black"
color-link type.extended "default" color-link type.extended "default"
color-link symbol.brackets "default" color-link symbol.brackets "default"
color-link match-brace ",blue"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -24,3 +24,6 @@ color-link gutter-warning "88"
color-link cursor-line "229" color-link cursor-line "229"
#color-link color-column "196" #color-link color-column "196"
color-link current-line-number "246" color-link current-line-number "246"
color-line match-brace "230,22"
color-link tab-error "210"
color-link trailingws "210"

View File

@ -35,3 +35,6 @@ color-link todo "#8B98AB"
color-link type "#F9EE98" color-link type "#F9EE98"
color-link type.keyword "#CDA869" color-link type.keyword "#CDA869"
color-link underlined "#8996A8" color-link underlined "#8996A8"
color-link match-brace "#141414,#E0C589"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -25,3 +25,6 @@ color-link gutter-warning "174,237"
color-link cursor-line "238" color-link cursor-line "238"
color-link color-column "238" color-link color-column "238"
color-link current-line-number "188,237" color-link current-line-number "188,237"
color-link match-brace "237,223"
color-link tab-error "167"
color-link trailingws "167"

View File

@ -8,7 +8,7 @@ This help page aims to cover two aspects of micro's syntax highlighting engine:
## Colorschemes ## Colorschemes
To change your colorscheme, press Ctrl-e in micro to bring up the command To change your colorscheme, press `Ctrl-e` in micro to bring up the command
prompt, and type: prompt, and type:
``` ```
@ -93,7 +93,7 @@ and set this variable yourself.
* `solarized-tc`: this is the solarized colorscheme for true color. * `solarized-tc`: this is the solarized colorscheme for true color.
* `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme. * `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme.
* `cmc-tc`: A true colour variant of the cmc theme. It requires true color to * `cmc-tc`: A true colour variant of the cmc theme. It requires true color to
look its best. Use cmc-16 if your terminal doesn't support true color. look its best. Use cmc-16 if your terminal doesn't support true color.
* `gruvbox-tc`: The true color version of the gruvbox colorscheme * `gruvbox-tc`: The true color version of the gruvbox colorscheme
* `material-tc`: Colorscheme based off of Google's Material Design palette * `material-tc`: Colorscheme based off of Google's Material Design palette
@ -106,7 +106,7 @@ be found
Custom colorschemes should be placed in the `~/.config/micro/colorschemes` Custom colorschemes should be placed in the `~/.config/micro/colorschemes`
directory. directory.
A number of custom directives are placed in a `.micro` file. Colorschemes are A number of custom directives are placed in a `.micro` file. Colorschemes are
typically only 18-30 lines in total. typically only 18-30 lines in total.
To create the colorscheme you need to link highlight groups with To create the colorscheme you need to link highlight groups with
@ -152,7 +152,7 @@ Then you can use the terminals 256 colors by using their numbers 1-256 (numbers
If the user's terminal supports true color, then you can also specify colors If the user's terminal supports true color, then you can also specify colors
exactly using their hex codes. If the terminal is not true color but micro is exactly using their hex codes. If the terminal is not true color but micro is
told to use a true color colorscheme it will attempt to map the colors to the told to use a true color colorscheme it will attempt to map the colors to the
available 256 colors. available 256 colors.
Generally colorschemes which require true color terminals to look good are Generally colorschemes which require true color terminals to look good are
@ -177,10 +177,14 @@ Here is a list of the colorscheme groups that you can use:
* todo * todo
* selection (Color of the text selection) * selection (Color of the text selection)
* statusline (Color of the statusline) * statusline (Color of the statusline)
* statusline.inactive (Color of the statusline of inactive split panes)
* statusline.suggestions (Color of the autocomplete suggestions menu)
* tabbar (Color of the tabbar that lists open files) * tabbar (Color of the tabbar that lists open files)
* tabbar.active (Color of the active tab in the tabbar)
* indent-char (Color of the character which indicates tabs if the option is * indent-char (Color of the character which indicates tabs if the option is
enabled) enabled)
* line-number * line-number
* gutter-info
* gutter-error * gutter-error
* gutter-warning * gutter-warning
* diff-added * diff-added
@ -194,6 +198,10 @@ Here is a list of the colorscheme groups that you can use:
* divider (Color of the divider between vertical splits) * divider (Color of the divider between vertical splits)
* message (Color of messages in the bottom line of the screen) * message (Color of messages in the bottom line of the screen)
* error-message (Color of error messages in the bottom line of the screen) * error-message (Color of error messages in the bottom line of the screen)
* match-brace (Color of matching brackets when `matchbracestyle` is set to `highlight`)
* hlsearch (Color of highlighted search results when `hlsearch` is enabled)
* tab-error (Color of tab vs space errors when `hltaberrors` is enabled)
* trailingws (Color of trailing whitespaces when `hltrailingws` is enabled)
Colorschemes must be placed in the `~/.config/micro/colorschemes` directory to Colorschemes must be placed in the `~/.config/micro/colorschemes` directory to
be used. be used.
@ -210,9 +218,9 @@ safe and recommended to use subgroups in your custom syntax files.
For example if `constant.string` is found in your colorscheme, micro will us For example if `constant.string` is found in your colorscheme, micro will us
that for highlighting strings. If it's not found, it will use constant instead. that for highlighting strings. If it's not found, it will use constant instead.
Micro tries to match the largest set of groups it can find in the colorscheme Micro tries to match the largest set of groups it can find in the colorscheme
definitions, so if, for examle `constant.bool.true` is found then micro will definitions, so if, for example `constant.bool.true` is found then micro will
use that. If `constant.bool.true` is not found but `constant.bool` is found use that. If `constant.bool.true` is not found but `constant.bool` is found
micro will use `constant.bool`. If not, it uses `constant`. micro will use `constant.bool`. If not, it uses `constant`.
Here's a list of subgroups used in micro's built-in syntax files. Here's a list of subgroups used in micro's built-in syntax files.
@ -220,10 +228,10 @@ Here's a list of subgroups used in micro's built-in syntax files.
* constant.bool * constant.bool
* constant.bool.true * constant.bool.true
* constant.bool.false * constant.bool.false
* constant.number * constant.number
* constant.specialChar * constant.specialChar
* constant.string * constant.string
* constant.string.url * constant.string.url
* identifier.class (Also used for functions) * identifier.class (Also used for functions)
* identifier.macro * identifier.macro
* identifier.var * identifier.var
@ -236,6 +244,12 @@ Here's a list of subgroups used in micro's built-in syntax files.
In the future, plugins may also be able to use color groups for styling. In the future, plugins may also be able to use color groups for styling.
---
Last but not least it's even possible to use `include` followed by the
colorscheme name as string to include a different colorscheme within a new one.
Additionally the groups can then be extended or overwritten. The `default.micro`
theme can be seen as an example, which links to the chosen default colorscheme.
## Syntax files ## Syntax files
@ -244,7 +258,7 @@ languages.
Micro's builtin syntax highlighting tries very hard to be sane, sensible and Micro's builtin syntax highlighting tries very hard to be sane, sensible and
provide ample coverage of the meaningful elements of a language. Micro has provide ample coverage of the meaningful elements of a language. Micro has
syntax files built in for over 100 languages now! However, there may be syntax files built in for over 100 languages now! However, there may be
situations where you find Micro's highlighting to be insufficient or not to situations where you find Micro's highlighting to be insufficient or not to
your liking. The good news is that you can create your own syntax files, and your liking. The good news is that you can create your own syntax files, and
place them in `~/.config/micro/syntax` and Micro will use those instead. place them in `~/.config/micro/syntax` and Micro will use those instead.
@ -267,8 +281,9 @@ detect:
``` ```
Micro will match this regex against a given filename to detect the filetype. Micro will match this regex against a given filename to detect the filetype.
You may also provide an optional `header` regex that will check the first line
of the file. For example: In addition to the `filename` regex (or even instead of it) you can provide
a `header` regex that will check the first line of the file. For example:
``` ```
detect: detect:
@ -276,6 +291,32 @@ detect:
header: "%YAML" header: "%YAML"
``` ```
This is useful in cases when the given file name is not sufficient to determine
the filetype, e.g. with the above example, if a YAML file has no `.yaml`
extension but may contain a `%YAML` directive in its first line.
`filename` takes precedence over `header`, i.e. if there is a syntax file that
matches the file with a filetype by the `filename` and another syntax file that
matches the same file with another filetype by the `header`, the first filetype
will be used.
Finally, in addition to `filename` and/or `header` (but not instead of them)
you may also provide an optional `signature` regex which is useful for resolving
ambiguities when there are multiple syntax files matching the same file with
different filetypes. If a `signature` regex is given, micro will match a certain
amount of first lines in the file (this amount is determined by the `detectlimit`
option) against this regex, and if any of the lines match, this syntax file's
filetype will be preferred over other matching filetypes.
For example, to distinguish C++ header files from C and Objective-C header files
that have the same `.h` extension:
```
detect:
filename: "\\.c(c|pp|xx)$|\\.h(h|pp|xx)?$"
signature: "namespace|template|public|protected|private"
```
### Syntax rules ### Syntax rules
Next you must provide the syntax highlighting rules. There are two types of Next you must provide the syntax highlighting rules. There are two types of
@ -334,7 +375,6 @@ highlighted. For example:
start: "\"" start: "\""
end: "\"" end: "\""
skip: "\\." skip: "\\."
rules: []
``` ```
#### Includes #### Includes
@ -356,15 +396,28 @@ example, the following is possible for html:
- include: "css" - include: "css"
``` ```
## Syntax file headers Note that nested include (i.e. including syntax files that include other syntax
files) is not supported yet.
Syntax file headers are an optimization and it is likely you do not need to ### Default syntax highlighting
worry about them.
Syntax file headers are files that contain only the filetype and the detection If micro cannot detect the filetype of the file, it falls back to using the
regular expressions for a given syntax file. They have a `.hdr` suffix and are default syntax highlighting for it, which highlights just the bare minimum:
used by default only for the pre-installed syntax files. Header files allow email addresses, URLs etc.
micro to parse the syntax files much faster when checking the filetype of a
certain file. Custom syntax files may provide header files in Just like in other cases, you can override the default highlighting by adding
`~/.config/micro/syntax` as well but it is not necessary (only do this if you your own custom `default.yaml` file to `~/.config/micro/syntax`.
have many (100+) custom syntax files and want to improve performance).
For example, if you work with various config files that use the `#` sign to mark
the beginning of a comment, you can use the following custom `default.yaml` to
highlight those comments by default:
```
filetype: unknown
detect:
filename: ""
rules:
- comment: "(^|\\s)#.*$"
```

View File

@ -1,6 +1,6 @@
# Command bar # Command bar
The command bar is opened by pressing Ctrl-e. It is a single-line buffer, The command bar is opened by pressing `Ctrl-e`. It is a single-line buffer,
meaning that all keybindings from a normal buffer are supported (as well meaning that all keybindings from a normal buffer are supported (as well
as mouse and selection). as mouse and selection).
@ -21,32 +21,49 @@ quotes here but these are not necessary when entering the command in micro.
This command will modify `bindings.json` and overwrite any bindings to This command will modify `bindings.json` and overwrite any bindings to
`key` that already exist. `key` that already exist.
* `help 'topic'?`: opens the corresponding help topic. If no topic is provided * `help ['topic'] ['flags']`: opens the corresponding help topics.
opens the default help screen. Help topics are stored as `.md` files in the If no topic is provided opens the default help screen. If multiple topics are
`runtime/help` directory of the source tree, which is embedded in the final provided (separated via ` `) they are opened all as splits.
binary. Help topics are stored as `.md` files in the `runtime/help` directory of
the source tree, which is embedded in the final binary.
The `flags` are optional.
* `-hsplit`: Opens the help topic in a horizontal split
* `-vsplit`: Opens the help topic in a vertical split
* `save 'filename'?`: saves the current buffer. If the file is provided it The default split type is defined by the global `helpsplit` option.
* `save ['filename']`: saves the current buffer. If the file is provided it
will 'save as' the filename. will 'save as' the filename.
* `quit`: quits micro. * `quit`: quits micro.
* `goto 'line'`: jumps to the given line number. A negative number can be * `goto 'line[:col]'`: goes to the given absolute line (and optional column)
passed to jump inward from the end of the file; for example, -5 jumps number.
to the 5th-last line in the file. A negative number can be passed to go inward from the end of the file.
Example: -5 goes to the 5th-last line in the file.
* `replace 'search' 'value' 'flags'?`: This will replace `search` with `value`. * `jump 'line[:col]'`: goes to the given relative number from the current
line (and optional absolute column) number.
Example: -5 jumps 5 lines up in the file, while (+)3 jumps 3 lines down.
* `replace 'search' 'value' ['flags']`: This will replace `search` with `value`.
The `flags` are optional. Possible flags are: The `flags` are optional. Possible flags are:
* `-a`: Replace all occurrences at once * `-a`: Replace all occurrences at once
* `-l`: Do a literal search instead of a regex search * `-l`: Do a literal search instead of a regex search
Note that `search` must be a valid regex (unless `-l` is passed). If one Note that `search` must be a valid regex (unless `-l` is passed). If one
of the arguments does not have any spaces in it, you may omit the quotes. of the arguments does not have any spaces in it, you may omit the quotes.
In case the search is done non-literal (without `-l`), the 'value'
is interpreted as a template:
* `$3` or `${3}` substitutes the submatch of the 3rd (capturing group)
* `$foo` or `${foo}` substitutes the submatch of the (?P<foo>named group)
* You have to write `$$` to substitute a literal dollar.
* `replaceall 'search' 'value'`: this will replace all occurrences of `search` * `replaceall 'search' 'value'`: this will replace all occurrences of `search`
with `value` without user confirmation. with `value` without user confirmation.
See `replace` command for more information. See `replace` command for more information.
* `set 'option' 'value'`: sets the option to value. See the `options` help * `set 'option' 'value'`: sets the option to value. See the `options` help
topic for a list of options you can set. This will modify your topic for a list of options you can set. This will modify your
@ -57,18 +74,21 @@ quotes here but these are not necessary when entering the command in micro.
* `show 'option'`: shows the current value of the given option. * `show 'option'`: shows the current value of the given option.
* `run 'sh-command'`: runs the given shell command in the background. The * `run 'sh-command'`: runs the given shell command in the background. The
command's output will be displayed in one line when it finishes running. command's output will be displayed in one line when it finishes running.
* `vsplit 'filename'`: opens a vertical split with `filename`. If no filename * `vsplit ['filename']`: opens a vertical split with `filename`. If no filename
is provided, a vertical split is opened with an empty buffer. is provided, a vertical split is opened with an empty buffer. If multiple
files are provided (separated via ` `) they are opened all as splits.
* `hsplit 'filename'`: same as `vsplit` but opens a horizontal split instead * `hsplit ['filename']`: same as `vsplit` but opens a horizontal split instead
of a vertical split. of a vertical split.
* `tab 'filename'`: opens the given file in a new tab. * `tab ['filename']`: opens the given file in a new tab. If no filename
is provided, a tab is opened with an empty buffer. If multiple files are
provided (separated via ` `) they are opened all as tabs.
* `tabmove '[-+]?n'`: Moves the active tab to another slot. `n` is an integer. * `tabmove '[-+]n'`: Moves the active tab to another slot. `n` is an integer.
If `n` is prefixed with `-` or `+`, then it represents a relative position If `n` is prefixed with `-` or `+`, then it represents a relative position
(e.g. `tabmove +2` moves the tab to the right by `2`). If `n` has no prefix, (e.g. `tabmove +2` moves the tab to the right by `2`). If `n` has no prefix,
it represents an absolute position (e.g. `tabmove 2` moves the tab to slot `2`). it represents an absolute position (e.g. `tabmove 2` moves the tab to slot `2`).
@ -89,14 +109,17 @@ quotes here but these are not necessary when entering the command in micro.
* `plugin remove 'pl'`: remove a plugin. * `plugin remove 'pl'`: remove a plugin.
* `plugin update 'pl'`: update a plugin (if no arguments are provided * `plugin update ['pl']`: update a plugin (if no arguments are provided
updates all plugins). updates all plugins).
* `plugin search 'pl'`: search available plugins for a keyword. * `plugin search 'pl'`: search available plugins for a keyword.
* `plugin available`: show available plugins that can be installed. * `plugin available`: show available plugins that can be installed.
* `reload`: reloads all runtime files. * `reload`: reloads all runtime files (settings, keybindings, syntax files,
colorschemes, plugins). All plugins will be unloaded by running their
`deinit()` function (if it exists), and then loaded again by calling the
`preinit()`, `init()` and `postinit()` functions (if they exist).
* `cd 'path'`: Change the working directory to the given `path`. * `cd 'path'`: Change the working directory to the given `path`.
@ -104,6 +127,8 @@ quotes here but these are not necessary when entering the command in micro.
* `open 'filename'`: Open a file in the current buffer. * `open 'filename'`: Open a file in the current buffer.
* `reopen`: Reopens the current file from disk.
* `reset 'option'`: resets the given option to its default value * `reset 'option'`: resets the given option to its default value
* `retab`: Replaces all leading tabs with spaces or leading spaces with tabs * `retab`: Replaces all leading tabs with spaces or leading spaces with tabs
@ -114,10 +139,10 @@ quotes here but these are not necessary when entering the command in micro.
the terminal and helps you see which bindings aren't possible and why. This the terminal and helps you see which bindings aren't possible and why. This
is most useful for debugging keybindings. is most useful for debugging keybindings.
* `showkey`: Show the action(s) bound to a given key. For example * `showkey 'key'`: Show the action(s) bound to a given key. For example
running `> showkey Ctrl-c` will display `Copy`. running `> showkey Ctrl-c` will display `Copy`.
* `term exec?`: Open a terminal emulator running the given executable. If no * `term ['exec']`: Open a terminal emulator running the given executable. If no
executable is given, this will open the default shell in the terminal executable is given, this will open the default shell in the terminal
emulator. emulator.

View File

@ -12,13 +12,13 @@ is limited support among terminal emulators for the terminal clipboard
(which uses the OSC 52 protocol to communicate clipboard contents). (which uses the OSC 52 protocol to communicate clipboard contents).
Here is a list of terminal emulators and their status: Here is a list of terminal emulators and their status:
* Kitty: supported, but only writing is enabled by default. To enable * `Kitty`: supported, but only writing is enabled by default. To enable
reading, add `read-primary` and `read-clipboard` to the reading, add `read-primary` and `read-clipboard` to the
`clipboard_control` option. `clipboard_control` option.
* iTerm2: only copying (writing to clipboard) is supported. Must be enabled in * `iTerm2`: only copying (writing to clipboard) is supported. Must be enabled in
`Preferences->General-> Selection->Applications in terminal may access clipboard`. `Preferences->General-> Selection->Applications in terminal may access clipboard`.
You can use Command-v to paste. You can use `Command-v` to paste.
* `st`: supported. * `st`: supported.
@ -31,7 +31,8 @@ Here is a list of terminal emulators and their status:
* `gnome-terminal`: does not support OSC 52. * `gnome-terminal`: does not support OSC 52.
* `alacritty`: supported. * `alacritty`: supported. Since 0.13.0, reading has been disabled by default.
To reenable it, set the `terminal.osc52` option to `CopyPaste`.
* `foot`: supported. * `foot`: supported.
@ -48,12 +49,12 @@ supports OSC 52.
The recommended method of pasting is the following: The recommended method of pasting is the following:
* If you are not working over SSH, use the micro keybinding (Ctrl-v * If you are not working over SSH, use the micro keybinding (`Ctrl-v`
by default) to perform pastes. If on Linux, install `xclip` or by default) to perform pastes. If on Linux, install `xclip` or
`xsel` beforehand. `xsel` beforehand.
* If you are working over SSH, use the terminal keybinding * If you are working over SSH, use the terminal keybinding
(Ctrl-Shift-v or Command-v) to perform pastes. If your terminal (`Ctrl-Shift-v` or `Command-v`) to perform pastes. If your terminal
does not support bracketed paste, when performing a paste first does not support bracketed paste, when performing a paste first
enable the `paste` option, and when finished disable the option. enable the `paste` option, and when finished disable the option.
@ -62,8 +63,8 @@ The recommended method of pasting is the following:
Micro is an application that runs within the terminal. This means Micro is an application that runs within the terminal. This means
that the terminal sends micro events, such as key events, mouse that the terminal sends micro events, such as key events, mouse
events, resize events, and paste events. Micro's default keybinding events, resize events, and paste events. Micro's default keybinding
for paste is Ctrl-v. This means that when micro receives the key for paste is `Ctrl-v`. This means that when micro receives the key
event saying Ctrl-v has been pressed from the terminal, it will event saying `Ctrl-v` has been pressed from the terminal, it will
attempt to access the system clipboard and effect a paste. The attempt to access the system clipboard and effect a paste. The
system clipboard will be accessed through `pbpaste` on MacOS system clipboard will be accessed through `pbpaste` on MacOS
(installed by default), `xclip` or `xsel` on Linux (these (installed by default), `xclip` or `xsel` on Linux (these
@ -76,8 +77,8 @@ For certain keypresses, the terminal will not send an event to
micro and will instead do something itself. In this document, micro and will instead do something itself. In this document,
such keypresses will be called "terminal keybindings." Often such keypresses will be called "terminal keybindings." Often
there will be a terminal keybinding for pasting and copying. On there will be a terminal keybinding for pasting and copying. On
MacOS these are Command-v and Command-c and on Linux Ctrl-Shift-v MacOS these are Command-v and Command-c and on Linux `Ctrl-Shift-v`
and Ctrl-Shift-c. When the terminal keybinding for paste is and `Ctrl-Shift-c`. When the terminal keybinding for paste is
executed, your terminal will access the system clipboard, and send executed, your terminal will access the system clipboard, and send
micro either a paste event or a list of key events (one key for each micro either a paste event or a list of key events (one key for each
character in the paste), depending on whether or not your terminal character in the paste), depending on whether or not your terminal
@ -89,7 +90,7 @@ sends a list of key events, this can cause issues because micro
will think you manually entered each character and may add closing will think you manually entered each character and may add closing
brackets or automatic indentation, which will mess up the pasted brackets or automatic indentation, which will mess up the pasted
text. To avoid this, you can temporarily enable the `paste` option text. To avoid this, you can temporarily enable the `paste` option
while you perform the paste. When paste option is on, micro will while you perform the paste. When paste option is on, micro will
aggregate lists of multiple key events into larger paste events. aggregate lists of multiple key events into larger paste events.
It is a good idea to disable the `paste` option during normal use It is a good idea to disable the `paste` option during normal use
as occasionally if you are typing quickly, the terminal will send as occasionally if you are typing quickly, the terminal will send
@ -100,7 +101,7 @@ entered.
When working over SSH, micro is running on the remote machine and When working over SSH, micro is running on the remote machine and
your terminal is running on your local machine. Therefore if you your terminal is running on your local machine. Therefore if you
would like to paste, using Ctrl-v (micro's keybinding) will not would like to paste, using `Ctrl-v` (micro's keybinding) will not
work because when micro attempts to access the system clipboard, work because when micro attempts to access the system clipboard,
it will access the remote machine's clipboard rather than the local it will access the remote machine's clipboard rather than the local
machine's clipboard. On the other hand, the terminal keybinding machine's clipboard. On the other hand, the terminal keybinding
@ -113,12 +114,12 @@ the network as a paste event, which is what you want.
The recommended method of copying is the following: The recommended method of copying is the following:
* If you are not working over SSH, use the micro keybinding (Ctrl-c by * If you are not working over SSH, use the micro keybinding (`Ctrl-c` by
default) to perform copies. If on Linux, install `xclip` or `xsel` default) to perform copies. If on Linux, install `xclip` or `xsel`
beforehand. beforehand.
* If you are working over SSH, use the terminal keybinding * If you are working over SSH, use the terminal keybinding
(Ctrl-Shift-c or Command-c) to perform copies. You must first disable (`Ctrl-Shift-c` or `Command-c`) to perform copies. You must first disable
the `mouse` option to perform a terminal selection, and you may wish the `mouse` option to perform a terminal selection, and you may wish
to disable line numbers and diff indicators (`ruler` and `diffgutter` to disable line numbers and diff indicators (`ruler` and `diffgutter`
options) and close other splits. This method will only be able to copy options) and close other splits. This method will only be able to copy
@ -129,14 +130,14 @@ Copying follows a similar discussion to the one above about pasting.
The primary difference is before performing a copy, the application The primary difference is before performing a copy, the application
doing the copy must be told what text needs to be copied. doing the copy must be told what text needs to be copied.
Micro has a keybinding (Ctrl-c) for copying and will access the system Micro has a keybinding (`Ctrl-c`) for copying and will access the system
clipboard to perform the copy. The text that micro will copy into is clipboard to perform the copy. The text that micro will copy into is
the text that is currently selected in micro (usually such text is the text that is currently selected in micro (usually such text is
displayed with a white background). When the `mouse` option is enabled, displayed with a white background). When the `mouse` option is enabled,
the mouse can be used to select text, as well as other keybindings, the mouse can be used to select text, as well as other keybindings,
such as ShiftLeft, etc... such as ShiftLeft, etc...
The terminal also has a keybinding (Ctrl-Shift-c or Command-c) to perform The terminal also has a keybinding (`Ctrl-Shift-c` or `Command-c`) to perform
a copy, and the text that it copies is the text selected by the terminal's a copy, and the text that it copies is the text selected by the terminal's
selection (*not* micro's selection). To select text with the terminal selection (*not* micro's selection). To select text with the terminal
selection, micro's mouse support must first be disabled by turning the selection, micro's mouse support must first be disabled by turning the

View File

@ -52,9 +52,9 @@ can change it!
| Ctrl-n | Find next instance of current search | | Ctrl-n | Find next instance of current search |
| Ctrl-p | Find previous instance of current search | | Ctrl-p | Find previous instance of current search |
Note: Ctrl-n and Ctrl-p should be used from the main buffer, not from inside Note: `Ctrl-n` and `Ctrl-p` should be used from the main buffer, not from inside
the search prompt. After Ctrl-f, press enter to complete the search and then the search prompt. After `Ctrl-f`, press enter to complete the search and then
you can use Ctrl-n and Ctrl-p to cycle through matches. you can use `Ctrl-n` and `Ctrl-p` to cycle through matches.
### File Operations ### File Operations
@ -129,7 +129,7 @@ you can use Ctrl-n and Ctrl-p to cycle through matches.
### Function keys. ### Function keys.
Warning! The function keys may not work in all terminals! Warning! The function keys may not work in all terminals!
| Key | Description of function | | Key | Description of function |
|------ |-------------------------- | |------ |-------------------------- |

Some files were not shown because too many files have changed in this diff Show More