Compare commits

..

No commits in common. "master" and "v2.0.13" have entirely different histories.

252 changed files with 3198 additions and 5992 deletions

View File

@ -1,46 +0,0 @@
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

View File

@ -1,36 +0,0 @@
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,19 +4,14 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x, 1.23.x] go-version: [1.19.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@v5 - uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: false - uses: actions/checkout@v3
- 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/micro-editor/tcell/LICENSE (fork) github.com/zyedidia/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/micro-editor/json5/LICENSE (fork) github.com/zyedidia/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/micro-editor/terminal/LICENSE (fork) github.com/zyedidia/terminal/LICENSE (fork)
================ ================
Copyright (C) 2013 James Gray Copyright (C) 2013 James Gray

View File

@ -5,31 +5,24 @@ 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:
CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-dbg: build-dbg:
CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "$(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro go build -trimpath -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
build-tags: fetch-tags build build-tags: fetch-tags generate
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-all: build build-all: build
@ -39,7 +32,7 @@ install: generate
install-all: install install-all: install
fetch-tags: fetch-tags:
git fetch --tags --force git fetch --tags
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,9 +18,25 @@ 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
@ -47,12 +63,11 @@ You can also check out the website for Micro at https://micro-editor.github.io.
- Syntax highlighting for over [130 languages](runtime/syntax). - Syntax highlighting for over [130 languages](runtime/syntax).
- Color scheme support. - Color scheme support.
- By default, micro comes with 16, 256, and true color themes. - By default, micro comes with 16, 256, and true color themes.
- True color support. - True color support (set the `MICRO_TRUECOLOR` environment variable to 1 to enable it).
- Copy and paste with the system clipboard. - Copy and paste with the system clipboard.
- 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
@ -72,7 +87,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`.
#### Third-party quick-install script #### Quick-install script
```bash ```bash
curl https://getmic.ro | bash curl https://getmic.ro | bash
@ -122,33 +137,24 @@ 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: * Linux: Available in distro-specific package managers.
* distro-specific package managers: * `dnf install micro` (Fedora).
* `dnf install micro` (Fedora). * `apt install micro` (Ubuntu and Debian).
* `apt install micro` (Ubuntu and Debian). * `pacman -S micro` (Arch Linux).
* `pacman -S micro` (Arch Linux). * `emerge app-editors/micro` (Gentoo).
* `emerge app-editors/micro` (Gentoo). * `zypper install micro-editor` (SUSE)
* `zypper install micro-editor` (SUSE) * `eopkg install micro` (Solus).
* `eopkg install micro` (Solus). * `pacstall -I micro` (Pacstall).
* `pacstall -I micro` (Pacstall). * See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
* `apt-get install micro` (ALT Linux) * Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop).
* 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.
* `pkg_add -v micro`. * `pkd_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: Available in package managers. * macOS with [MacPorts](https://www.macports.org):
* `sudo port install micro` (with [MacPorts](https://www.macports.org)) * `sudo port install micro`
* `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:**
@ -162,7 +168,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.19 or greater and Go modules are enabled. Make sure that you have Go version 1.16 or greater and Go modules are enabled.
``` ```
git clone https://github.com/zyedidia/micro git clone https://github.com/zyedidia/micro
@ -180,20 +186,16 @@ 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 or dynamically linked binary ### Fully static binary
By default, the micro binary is linked statically to increase the portability of the prebuilt binaries. By default, the micro binary will dynamically link with core system libraries (this is generally
This behavior can simply be overriden by providing `CGO_ENABLED=1` to the build target. recommended for security and portability). However, there is a fully static prebuilt binary that
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
``` ```
CGO_ENABLED=1 make build CGO_ENABLED=0 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,4 +12,5 @@ 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,7 +12,6 @@ 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 {
@ -40,16 +39,7 @@ 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
@ -77,20 +67,16 @@ 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", settingsFile) fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json"))
if shouldContinue() { if shouldContinue() {
for _, s := range unusedOptions { for _, s := range unusedOptions {
delete(config.GlobalSettings, s) delete(config.GlobalSettings, s)
} }
err := config.OverwriteSettings(settingsFile) err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
if err != nil { if err != nil {
if errors.Is(err, util.ErrOverwrite) { fmt.Println("Error writing settings.json file: " + err.Error())
fmt.Println(err.Error())
} else {
fmt.Println("Error overwriting settings.json file: " + err.Error())
}
} }
fmt.Println("Removed unused options") fmt.Println("Removed unused options")
@ -99,13 +85,12 @@ func CleanConfig() {
} }
// detect incorrectly formatted buffer/ files // detect incorrectly formatted buffer/ files
buffersPath := filepath.Join(config.ConfigDir, "buffers") files, err := ioutil.ReadDir(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(buffersPath, f.Name()) fname := filepath.Join(config.ConfigDir, "buffers", f.Name())
file, e := os.Open(fname) file, e := os.Open(fname)
if e == nil { if e == nil {
@ -120,9 +105,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), buffersPath) fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers"))
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", buffersPath) fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers"))
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, util.FileMode) f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil { if err != nil {
log.Fatalf("error opening file: %v", err) log.Fatalf("error opening file: %v", err)
} }

View File

@ -2,7 +2,6 @@ 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"
@ -48,18 +47,14 @@ 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.BufPane { ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() action.Pane {
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, "After", luar.New(ulua.L, func(t time.Duration, f func()) { ulua.L.SetField(pkg, "Lock", luar.New(ulua.L, &ulua.Lock))
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"
@ -18,18 +18,22 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/action" "github.com/zyedidia/micro/v2/internal/action"
"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"
) )
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")
@ -40,9 +44,8 @@ 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
sighup chan os.Signal sigterm chan os.Signal
sighup chan os.Signal
timerChan chan func()
) )
func InitFlags() { func InitFlags() {
@ -65,7 +68,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 plugins can be managed at the command line with the following commands.\n") fmt.Print("\nMicro's plugin's 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]...")
@ -99,7 +102,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)
exit(0) os.Exit(0)
} }
if *flagOptions { if *flagOptions {
@ -115,7 +118,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)
} }
exit(0) os.Exit(0)
} }
if util.Debug == "OFF" && *flagDebug { if util.Debug == "OFF" && *flagDebug {
@ -136,7 +139,7 @@ func DoPluginFlags() {
CleanConfig() CleanConfig()
} }
exit(0) os.Exit(0)
} }
} }
@ -209,7 +212,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 = io.ReadAll(os.Stdin) input, err = ioutil.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{}
@ -223,55 +226,12 @@ 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())
} }
exit(0) os.Exit(0)
}() }()
var err error var err error
@ -296,15 +256,7 @@ func main() {
screen.TermMessage(err) screen.TermMessage(err)
} }
config.InitRuntimeFiles(true) config.InitRuntimeFiles()
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)
@ -322,12 +274,7 @@ 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
} }
} }
@ -337,7 +284,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.")
exit(1) os.Exit(1)
} }
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string)) m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
clipErr := clipboard.Initialize(m) clipErr := clipboard.Initialize(m)
@ -356,7 +303,7 @@ func main() {
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Backup() b.Backup()
} }
exit(1) os.Exit(1)
} }
}() }()
@ -365,12 +312,6 @@ 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()
@ -411,20 +352,18 @@ 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(a) config.SetAutoTime(int(a))
config.StartAutoSave()
} }
screen.Events = make(chan tcell.Event) screen.Events = make(chan tcell.Event)
util.Sigterm = make(chan os.Signal, 1) sigterm = make(chan os.Signal, 1)
sighup = make(chan os.Signal, 1) sighup = make(chan os.Signal, 1)
signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT) signal.Notify(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 {
@ -475,26 +414,39 @@ 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.AutoSave() b.Save()
} }
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:
exit(0) for _, b := range buffer.OpenBuffers {
case <-util.Sigterm: if !b.Modified() {
exit(0) b.Fini()
}
}
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 {
@ -502,25 +454,27 @@ 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
exit(0) for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
return return
} }
if event != nil { ulua.Lock.Lock()
_, resize := event.(*tcell.EventResize) // if event != nil {
if resize { if action.InfoBar.HasPrompt {
action.InfoBar.HandleEvent(event) action.InfoBar.HandleEvent(event)
action.Tabs.HandleEvent(event) } else {
} else if action.InfoBar.HasPrompt { action.Tabs.HandleEvent(event)
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,17 +2,18 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"testing" "testing"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zyedidia/micro/v2/internal/action" "github.com/zyedidia/micro/v2/internal/action"
"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"
) )
var tempDir string var tempDir string
@ -25,7 +26,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 = os.MkdirTemp("", "micro_test") tempDir, err = ioutil.TempDir("", "micro_test")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -34,9 +35,7 @@ func startup(args []string) (tcell.SimulationScreen, error) {
return nil, err return nil, err
} }
config.InitRuntimeFiles(true) config.InitRuntimeFiles()
config.InitPlugins()
err = config.ReadSettings() err = config.ReadSettings()
if err != nil { if err != nil {
return nil, err return nil, err
@ -108,10 +107,7 @@ 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) {
@ -153,32 +149,20 @@ func openFile(file string) {
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
} }
func findBuffer(file string) *buffer.Buffer { func createTestFile(name string, content string) (string, error) {
var buf *buffer.Buffer testf, err := ioutil.TempFile("", name)
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 {
t.Fatal(err) return "", err
}
defer func() {
if err := f.Close(); err != nil {
t.Fatal(err)
}
}()
if _, err := f.WriteString(content); err != nil {
t.Fatal(err)
} }
return f.Name() if _, err := testf.Write([]byte(content)); err != nil {
return "", err
}
if err := testf.Close(); err != nil {
return "", err
}
return testf.Name(), nil
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -195,12 +179,25 @@ func TestMain(m *testing.M) {
} }
func TestSimpleEdit(t *testing.T) { func TestSimpleEdit(t *testing.T) {
file := createTestFile(t, "base content") file, err := createTestFile("micro_simple_edit_test", "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
if findBuffer(file) == nil { var buf *buffer.Buffer
t.Fatalf("Could not find buffer %s", file) for _, b := range buffer.OpenBuffers {
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)
@ -218,23 +215,25 @@ func TestSimpleEdit(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := os.ReadFile(file) data, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
t.Fatal(err) t.Error(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 := createTestFile(t, "base content") file, err := createTestFile("micro_mouse_test", "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
@ -263,9 +262,10 @@ 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 := os.ReadFile(file) data, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
t.Fatal(err) t.Error(err)
return
} }
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data)) assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
@ -288,23 +288,25 @@ Ernleȝe test_string æðelen
` `
func TestSearchAndReplace(t *testing.T) { func TestSearchAndReplace(t *testing.T) {
file := createTestFile(t, srTestStart) file, err := createTestFile("micro_search_replace_test", 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 := os.ReadFile(file) data, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
t.Fatal(err) t.Error(err)
return
} }
assert.Equal(t, srTest2, string(data)) assert.Equal(t, srTest2, string(data))
@ -317,9 +319,10 @@ func TestSearchAndReplace(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err = os.ReadFile(file) data, err = ioutil.ReadFile(file)
if err != nil { if err != nil {
t.Fatal(err) t.Error(err)
return
} }
assert.Equal(t, srTest3, string(data)) assert.Equal(t, srTest3, string(data))

View File

@ -25,9 +25,6 @@
<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,19 +170,10 @@
"default": false "default": false
}, },
"matchbrace": { "matchbrace": {
"description": "Whether to show matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options", "description": "Whether to underline 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",
@ -305,8 +296,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": "boolean", "type": "string",
"default": true "default": "sudo"
}, },
"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",
@ -364,4 +355,4 @@
} }
}, },
"additionalProperties": false "additionalProperties": false
} }

33
go.mod
View File

@ -5,35 +5,26 @@ 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.20 github.com/mattn/go-isatty v0.0.11
github.com/mattn/go-runewidth v0.0.16 github.com/mattn/go-runewidth v0.0.7
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-20250324214352-e587e959c6b5
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 v1.1.1 github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
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
golang.org/x/text v0.4.0 github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d
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.11 layeh.com/gopher-luar v1.0.7
) )
require ( replace github.com/kballard/go-shellquote => github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655
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.28.0 // indirect
)
replace github.com/kballard/go-shellquote => github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5 replace github.com/mattn/go-runewidth => github.com/zyedidia/go-runewidth v0.0.12
replace layeh.com/gopher-luar v1.0.11 => github.com/layeh/gopher-luar v1.0.11 replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.7
go 1.19 go 1.16

80
go.sum
View File

@ -19,58 +19,84 @@ 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/layeh/gopher-luar v1.0.7 h1:wnfZhYiJM748y1A4qYBfcFeMY9HWbdERny+ZL0f/jWc=
github.com/layeh/gopher-luar v1.0.11 h1:ss6t9OtykOiETBScJylSMPhuYAtOmpH5rSX10/wCcis= github.com/layeh/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
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-20250324214352-e587e959c6b5 h1:czSkYUNmHuWS2lv8VreufENEXZNOCGZcXd744YKf8yM=
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5/go.mod h1:OszIG7ockt4osicVHq6gI2QmV4PBDK6H5/Bj8GDGv4Q=
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.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.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 v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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

@ -1,4 +1,4 @@
//go:build plan9 || nacl || windows // +build plan9 nacl windows
package action package action

View File

@ -1,4 +1,4 @@
//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd // +build linux darwin dragonfly solaris openbsd netbsd freebsd
package action package action

View File

@ -4,18 +4,17 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"unicode" "unicode"
"github.com/micro-editor/json5" "github.com/zyedidia/json5"
"github.com/micro-editor/tcell/v2"
"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/tcell/v2"
) )
var Binder = map[string]func(e Event, action string){ var Binder = map[string]func(e Event, action string){
@ -24,13 +23,9 @@ 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); errors.Is(e, fs.ErrNotExist) { if _, e := os.Stat(fname); os.IsNotExist(e) {
writeFile(fname, []byte("{}")) ioutil.WriteFile(fname, []byte("{}"), 0644)
} }
} }
@ -42,7 +37,7 @@ func InitBindings() {
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
input, err := os.ReadFile(filename) input, err := ioutil.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
@ -93,10 +88,6 @@ 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) {
@ -162,6 +153,7 @@ 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
@ -181,35 +173,39 @@ 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
} }
@ -243,24 +239,6 @@ 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) {
@ -270,7 +248,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 := os.ReadFile(filename) input, err := ioutil.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())
} }
@ -286,31 +264,28 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
} }
found := false found := false
var ev string for ev := range parsed {
for ev = range parsed {
if e, err := findEvent(ev); err == nil { if e, err := findEvent(ev); err == nil {
if eventsEqual(e, key) { if e == key {
if overwrite {
parsed[ev] = v
}
found = true found = true
break break
} }
} }
} }
if found { if found && !overwrite {
if overwrite { return true, nil
parsed[ev] = v } else if !found {
} 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, "", " ")
txt = append(txt, '\n') return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644)
return true, writeFile(filename, txt)
} }
return false, e return false, e
} }
@ -323,7 +298,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 := os.ReadFile(filename) input, err := ioutil.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())
} }
@ -340,17 +315,13 @@ 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 eventsEqual(e, key) { if 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"])
@ -360,8 +331,7 @@ func UnbindKey(k string) error {
} }
txt, _ := json.MarshalIndent(parsed, "", " ") txt, _ := json.MarshalIndent(parsed, "", " ")
txt = append(txt, '\n') return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
return writeFile(filename, txt)
} }
return e return e
} }

View File

@ -6,18 +6,17 @@ import (
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
"github.com/micro-editor/tcell/v2"
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"
) )
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
@ -45,9 +44,8 @@ func init() {
BufBindings = NewKeyTree() BufBindings = NewKeyTree()
} }
// LuaAction makes an action from a lua function. It returns either a BufKeyAction // LuaAction makes a BufKeyAction from a lua function.
// or a BufMouseAction depending on the event type. func LuaAction(fn string) func(*BufPane) bool {
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
@ -57,42 +55,33 @@ func LuaAction(fn string, k Event) BufAction {
if pl == nil { if pl == nil {
return nil return nil
} }
return func(h *BufPane) bool {
var action BufAction val, err := pl.Call(plFn, luar.New(ulua.L, h))
switch k.(type) { if err != nil {
case KeyEvent, KeySequenceEvent, RawEvent: screen.TermMessage(err)
action = BufKeyAction(func(h *BufPane) bool { }
val, err := pl.Call(plFn, luar.New(ulua.L, h)) if v, ok := val.(lua.LBool); !ok {
if err != nil { return false
screen.TermMessage(err) } else {
} 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
} }
// BufMapEvent maps an event to an action // BufMapKey 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
var actionfns []BufAction switch e := k.(type) {
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++ {
@ -100,7 +89,9 @@ func BufMapEvent(k Event, action string) {
break break
} }
idx := util.IndexAnyUnquoted(action, "&|,") // TODO: fix problem when complex bindings have these
// characters (escape them?)
idx := strings.IndexAny(action, "&|,")
a := action a := action
if idx >= 0 { if idx >= 0 {
a = action[:idx] a = action[:idx]
@ -111,7 +102,7 @@ func BufMapEvent(k Event, action string) {
action = "" action = ""
} }
var afn BufAction var afn func(*BufPane) bool
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)
@ -122,7 +113,7 @@ func BufMapEvent(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, k) afn = LuaAction(a)
if afn == nil { if afn == nil {
screen.TermMessage("Lua Error:", a, "does not exist") screen.TermMessage("Lua Error:", a, "does not exist")
continue continue
@ -138,52 +129,47 @@ func BufMapEvent(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, te *tcell.EventMouse) bool { bufAction := func(h *BufPane) bool {
cursors := h.Buf.GetCursors()
success := true
for i, a := range actionfns { for i, a := range actionfns {
var success bool innerSuccess := true
if _, ok := MultiActions[names[i]]; ok { for j, c := range cursors {
success = true if c == nil {
for _, c := range h.Buf.GetCursors() { continue
h.Buf.SetCurCursor(c.Num) }
h.Cursor = c h.Buf.SetCurCursor(c.Num)
success = success && h.execAction(a, names[i], te) h.Cursor = c
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()
if h == nil { success = innerSuccess
// stop, in case the current pane is not a BufPane
break
}
if (!success && types[i] == '&') || (success && types[i] == '|') {
break
}
} }
return true return true
} }
switch e := k.(type) { BufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(bufAction))
case KeyEvent, KeySequenceEvent, RawEvent: }
BufBindings.RegisterKeyBinding(e, BufKeyActionGeneral(func(h *BufPane) bool {
return bufAction(h, nil) // BufMapMouse maps a mouse event to an action
})) func bufMapMouse(k MouseEvent, action string) {
case MouseEvent: if f, ok := BufMouseActions[action]; ok {
BufBindings.RegisterMouseBinding(e, BufMouseActionGeneral(bufAction)) BufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f))
} else {
// TODO
// delete(BufMouseBindings, k)
bufMapKey(k, action)
} }
} }
@ -214,31 +200,32 @@ 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 press event // Since tcell doesn't differentiate between a mouse release event
// and a mouse move event with button pressed (nor between a mouse // and a mouse move event with no keys pressed, we need to keep
// release event and a mouse move event with no buttons pressed), // track of whether or not the mouse was pressed (or not released) last event to determine
// we need to keep track of whether or not the mouse was previously // mouse release events
// pressed, to determine mouse release and mouse drag events. mouseReleased bool
// 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
// freshClip returns true if one or more lines have been cut to the clipboard // lastCutTime stores when the last ctrl+k was issued.
// and have never been pasted yet. // It is used for clearing the clipboard to replace it with fresh cut lines.
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?
// Useful for detecting triple clicks -- if a double click is detected // Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click // but the last mouse event was actually a double click, it's a triple click
DoubleClick bool doubleClick bool
// Same here, just to keep track for mouse move events // Same here, just to keep track for mouse move events
TripleClick bool tripleClick bool
// Should the current multiple cursor selection search based on word or // Should the current multiple cursor selection search based on word or
// based on selection (false for selection, true for word) // based on selection (false for selection, true for word)
@ -263,7 +250,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.mousePressed = make(map[MouseEvent]bool) h.mouseReleased = true
return h return h
} }
@ -289,11 +276,7 @@ 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
@ -321,7 +304,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 aggregate boolean response // error if there is one and returns the aggregrate 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 {
@ -339,12 +322,6 @@ 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()
@ -355,7 +332,10 @@ 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.resetMouse() h.mouseReleased = true
// 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{}
} }
@ -415,12 +395,6 @@ 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)
@ -439,11 +413,11 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
if !yes || canceled { if !yes || canceled {
h.Buf.UpdateModTime() h.Buf.UpdateModTime()
} else { } else {
h.ReOpen() h.Buf.ReOpen()
} }
}) })
} else if reload == "auto" { } else if reload == "auto" {
h.ReOpen() h.Buf.ReOpen()
} else if reload == "disabled" { } else if reload == "disabled" {
h.Buf.DisableReload() h.Buf.DisableReload()
} else { } else {
@ -461,44 +435,61 @@ 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(e) ke := KeyEvent{
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:
if e.Buttons() != tcell.ButtonNone { cancel := false
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()
@ -518,14 +509,6 @@ 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.
@ -537,10 +520,7 @@ 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)
@ -554,33 +534,30 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
return more return more
} }
func (h *BufPane) execAction(action BufAction, name string, te *tcell.EventMouse) bool { func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
if name != "Autocomplete" && name != "CycleAutocompleteBack" { if name != "Autocomplete" && name != "CycleAutocompleteBack" {
h.Buf.HasSuggestions = false h.Buf.HasSuggestions = false
} }
if !h.PluginCB("pre" + name) { _, isMulti := MultiActions[name]
return false if (!isMulti && cursor == 0) || isMulti {
} if h.PluginCB("pre" + name) {
success := action(h)
success = success && h.PluginCB("on"+name)
var success bool if isMulti {
switch a := action.(type) { if recordingMacro {
case BufKeyAction: if name != "ToggleMacro" && name != "PlayMacro" {
success = a(h) curmacro = append(curmacro, action)
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 success return false
} }
func (h *BufPane) completeAction(action string) { func (h *BufPane) completeAction(action string) {
@ -634,7 +611,7 @@ func (h *BufPane) DoRuneInsert(r rune) {
c.ResetSelection() c.ResetSelection()
} }
if h.Buf.OverwriteMode { if h.isOverwriteMode {
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))
@ -652,28 +629,20 @@ func (h *BufPane) DoRuneInsert(r rune) {
// VSplitIndex opens the given buffer in a vertical split on the given side. // VSplitIndex opens the given buffer in a vertical split on the given side.
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 = h.tab.GetNode(h.splitID).VSplit(right) e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
currentPaneIdx := h.tab.GetPane(h.splitID) MainTab().Panes = append(MainTab().Panes, e)
if right { MainTab().Resize()
currentPaneIdx++ MainTab().SetActive(len(MainTab().Panes) - 1)
}
h.tab.AddPane(e, currentPaneIdx)
h.tab.Resize()
h.tab.SetActive(currentPaneIdx)
return e return e
} }
// HSplitIndex opens the given buffer in a horizontal split on the given side. // HSplitIndex opens the given buffer in a horizontal split on the given side.
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 = h.tab.GetNode(h.splitID).HSplit(bottom) e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
currentPaneIdx := h.tab.GetPane(h.splitID) MainTab().Panes = append(MainTab().Panes, e)
if bottom { MainTab().Resize()
currentPaneIdx++ MainTab().SetActive(len(MainTab().Panes) - 1)
}
h.tab.AddPane(e, currentPaneIdx)
h.tab.Resize()
h.tab.SetActive(currentPaneIdx)
return e return e
} }
@ -694,10 +663,6 @@ 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
@ -713,12 +678,8 @@ 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
@ -731,9 +692,6 @@ 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,
@ -742,16 +700,10 @@ 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,
@ -759,8 +711,6 @@ 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,
@ -781,7 +731,6 @@ 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,
@ -814,7 +763,6 @@ 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,
@ -826,12 +774,8 @@ 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,
@ -847,7 +791,6 @@ 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,
@ -861,8 +804,6 @@ 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,
} }
@ -887,16 +828,10 @@ 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,
@ -914,7 +849,6 @@ 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,7 +7,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -42,7 +41,6 @@ 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},
@ -139,25 +137,23 @@ func (h *BufPane) TextFilterCmd(args []string) {
InfoBar.Error("usage: textfilter arguments") InfoBar.Error("usage: textfilter arguments")
return return
} }
for _, c := range h.Buf.GetCursors() { sel := h.Cursor.GetSelection()
sel := c.GetSelection() if len(sel) == 0 {
if len(sel) == 0 { h.Cursor.SelectWord()
c.SelectWord() sel = h.Cursor.GetSelection()
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
@ -201,11 +197,10 @@ 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.Panes[0].ID()) Tabs.RemoveTab(activeTab.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))
@ -308,8 +303,15 @@ func (h *BufPane) OpenCmd(args []string) {
} }
h.OpenBuffer(b) h.OpenBuffer(b)
} }
if h.Buf.Modified() && !h.Buf.Shared() { if h.Buf.Modified() {
h.closePrompt("Save", open) InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
open()
} else if !canceled && yes {
h.Save()
open()
}
})
} else { } else {
open() open()
} }
@ -327,84 +329,31 @@ func (h *BufPane) ToggleLogCmd(args []string) {
} }
} }
// ReloadCmd reloads all files (syntax files, colorschemes, plugins...) // ReloadCmd reloads all files (syntax files, colorschemes...)
func (h *BufPane) ReloadCmd(args []string) { func (h *BufPane) ReloadCmd(args []string) {
reloadRuntime(true) ReloadConfig()
} }
// ReloadConfig reloads only the configuration
func ReloadConfig() { func ReloadConfig() {
reloadRuntime(false) config.InitRuntimeFiles()
}
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 reloadPlugins { if err != nil {
err = config.LoadAllPlugins() screen.TermMessage(err)
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.ReloadSettings(true) b.UpdateRules()
} }
} }
@ -414,93 +363,50 @@ 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.ReOpen() h.Buf.ReOpen()
} else if !canceled { } else if !canceled {
h.ReOpen() h.Buf.ReOpen()
} }
}) })
} else { } else {
h.ReOpen() h.Buf.ReOpen()
} }
} }
func (h *BufPane) openHelp(page string, hsplit bool, forceSplit bool) error { func (h *BufPane) openHelp(page string) 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.Sprintf("Unable to load help text for %s: %v", page, err)) return errors.New(fmt.Sprint("Unable to load help text", page, "\n", 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 && !forceSplit { if h.Buf.Type == buffer.BTHelp {
h.OpenBuffer(helpBuffer) h.OpenBuffer(helpBuffer)
} else if hsplit {
h.HSplitBuf(helpBuffer)
} else { } else {
h.VSplitBuf(helpBuffer) h.HSplitBuf(helpBuffer)
} }
} }
return nil return nil
} }
// HelpCmd tries to open the given help page according to the split type // HelpCmd tries to open the given help page in a horizontal split
// 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", hsplit, false) h.openHelp("help")
} else { } else {
var topics []string if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
forceSplit := false err := h.openHelp(args[0])
const errSplit = "hsplit and vsplit are not allowed at the same time" if err != nil {
for _, arg := range args { InfoBar.Error(err)
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 one or more vertical splits with the files given as arguments // VSplitCmd opens a vertical split with file given in the first argument
// 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 {
@ -509,18 +415,16 @@ func (h *BufPane) VSplitCmd(args []string) {
return return
} }
for _, a := range args { buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault) if err != nil {
if err != nil { InfoBar.Error(err)
InfoBar.Error(err) return
return
}
h.VSplitBuf(buf)
} }
h.VSplitBuf(buf)
} }
// HSplitCmd opens one or more horizontal splits with the files given as arguments // HSplitCmd opens a horizontal split with file given in the first argument
// 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 {
@ -529,15 +433,13 @@ func (h *BufPane) HSplitCmd(args []string) {
return return
} }
for _, a := range args { buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault) if err != nil {
if err != nil { InfoBar.Error(err)
InfoBar.Error(err) return
return
}
h.HSplitBuf(buf)
} }
h.HSplitBuf(buf)
} }
// EvalCmd evaluates a lua expression // EvalCmd evaluates a lua expression
@ -545,8 +447,7 @@ func (h *BufPane) EvalCmd(args []string) {
InfoBar.Error("Eval unsupported") InfoBar.Error("Eval unsupported")
} }
// NewTabCmd opens one or more tabs with the files given as arguments // NewTabCmd opens the given file in a new tab
// 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()
@ -569,98 +470,73 @@ func (h *BufPane) NewTabCmd(args []string) {
} }
} }
func doSetGlobalOptionNative(option string, nativeValue interface{}) error { func SetGlobalOptionNative(option string, nativeValue interface{}) error {
if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) { local := false
return nil for _, s := range config.LocalSettings {
if s == option {
local = true
break
}
} }
config.GlobalSettings[option] = nativeValue if !local {
config.ModifiedSettings[option] = true config.GlobalSettings[option] = nativeValue
delete(config.VolatileSettings, option) config.ModifiedSettings[option] = true
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 {
screen.Screen.EnableMouse() for _, pl := range config.Plugins {
} if option == pl.Name {
} else if option == "autosave" { if nativeValue.(bool) && !pl.Loaded {
if nativeValue.(float64) > 0 { pl.Load()
config.SetAutoTime(nativeValue.(float64)) _, err := pl.Call("init")
} else { if err != nil && err != config.ErrNoSuchFunction {
config.SetAutoTime(0) screen.TermMessage(err)
} }
} else if option == "paste" { } else if !nativeValue.(bool) && pl.Loaded {
screen.Screen.SetPaste(nativeValue.(bool)) _, err := pl.Call("deinit")
} else if option == "clipboard" { if err != nil && err != config.ErrNoSuchFunction {
m := clipboard.SetMethod(nativeValue.(string)) screen.TermMessage(err)
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)
} }
} }
} }
} }
} }
return nil
}
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
if err := config.OptionIsValid(option, nativeValue); err != nil {
return err
}
// 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 { for _, b := range buffer.OpenBuffers {
b.DoSetOptionNative(option, nativeValue) b.SetOptionNative(option, nativeValue)
delete(b.LocalSettings, option)
} }
err := config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) return 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 {
@ -684,10 +560,16 @@ func (h *BufPane) ResetCmd(args []string) {
} }
option := args[0] option := args[0]
defaults := config.DefaultAllSettings()
if _, ok := defaults[option]; ok { defaultGlobals := config.DefaultGlobalSettings()
SetGlobalOptionNative(option, defaults[option]) defaultLocals := config.DefaultCommonSettings()
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)
@ -752,11 +634,6 @@ 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 {
@ -764,7 +641,7 @@ func (h *BufPane) ShowKeyCmd(args []string) {
return return
} }
event, err := findEvent(parseKeyArg(args[0])) event, err := findEvent(args[0])
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
return return
@ -783,13 +660,9 @@ func (h *BufPane) BindCmd(args []string) {
return return
} }
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true) _, err := TryBindKey(args[0], args[1], true)
if err != nil { if err != nil {
if errors.Is(err, util.ErrOverwrite) { InfoBar.Error(err)
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -800,13 +673,9 @@ func (h *BufPane) UnbindCmd(args []string) {
return return
} }
err := UnbindKey(parseKeyArg(args[0])) err := UnbindKey(args[0])
if err != nil { if err != nil {
if errors.Is(err, util.ErrOverwrite) { InfoBar.Error(err)
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -832,67 +701,41 @@ 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 {
return 0, 0, errors.New("Not enough arguments") InfoBar.Error("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 {
line, err = strconv.Atoi(args[0]) h.RemoveAllMultiCursors()
if err != nil { if strings.Contains(args[0], ":") {
return 0, 0, err parts := strings.SplitN(args[0], ":", 2)
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
@ -900,7 +743,7 @@ func (h *BufPane) SaveCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
h.Save() h.Save()
} else { } else {
h.saveBufToFile(args[0], "SaveAs", nil) h.Buf.SaveAs(args[0])
} }
} }
@ -961,21 +804,19 @@ 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, !noRegex) nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
} 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)
} }
lastMatchEnd := buffer.Loc{-1, -1} searchLoc := h.Cursor.Loc
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)
@ -990,18 +831,6 @@ 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])
@ -1011,7 +840,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, !noRegex) _, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
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)
@ -1027,7 +856,6 @@ func (h *BufPane) ReplaceCmd(args []string) {
h.Buf.RelocateCursors() h.Buf.RelocateCursors()
return return
} }
lastMatchEnd = searchLoc
doReplacement() doReplacement()
}) })
} }
@ -1059,42 +887,10 @@ func (h *BufPane) ReplaceAllCmd(args []string) {
h.ReplaceCmd(append(args, "-a")) h.ReplaceCmd(append(args, "-a"))
} }
func (h *BufPane) openTerm(args []string, newtab bool) {
t := new(shell.Terminal)
err := t.Start(args, false, true, nil, nil)
if err != nil {
InfoBar.Error(err)
return
}
pane := 0
id := h.ID()
if newtab {
h.AddTab()
id = MainTab().Panes[pane].ID()
} else {
for i, p := range MainTab().Panes {
if p.IsActive() {
pane = i
id = p.ID()
p.Close()
break
}
}
}
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
InfoBar.Error(err)
return
}
MainTab().Panes[pane] = tp
MainTab().SetActive(pane)
}
// TermCmd opens a terminal in the current view // TermCmd opens a terminal in the current view
func (h *BufPane) TermCmd(args []string) { func (h *BufPane) TermCmd(args []string) {
ps := h.tab.Panes
if !TermEmuSupported { if !TermEmuSupported {
InfoBar.Error("Terminal emulator not supported on this system") InfoBar.Error("Terminal emulator not supported on this system")
return return
@ -1109,19 +905,56 @@ func (h *BufPane) TermCmd(args []string) {
args = []string{sh} args = []string{sh}
} }
term := func(i int, newtab bool) {
t := new(shell.Terminal)
err := t.Start(args, false, true, nil, nil)
if err != nil {
InfoBar.Error(err)
return
}
id := h.ID()
if newtab {
h.AddTab()
i = 0
id = MainTab().Panes[0].ID()
} else {
MainTab().Panes[i].Close()
}
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
InfoBar.Error(err)
return
}
MainTab().Panes[i] = tp
MainTab().SetActive(i)
}
// If there is only one open file we make a new tab instead of overwriting it // If there is only one open file we make a new tab instead of overwriting it
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1 newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
if newtab { if newtab {
h.openTerm(args, true) term(0, true)
return return
} }
if h.Buf.Modified() && !h.Buf.Shared() { for i, p := range ps {
h.closePrompt("Save", func() { if p.ID() == h.ID() {
h.openTerm(args, false) if h.Buf.Modified() {
}) InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
} else { if !canceled && !yes {
h.openTerm(args, false) term(i, false)
} else if !canceled && yes {
h.Save()
term(i, false)
}
})
} else {
term(i, false)
}
}
} }
} }

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|FirstSplit", "<Ctrl-w><Ctrl-w>": "NextSplit",
} }
// DefaultBindings returns a map containing micro's default keybindings // DefaultBindings returns a map containing micro's default keybindings

View File

@ -45,25 +45,23 @@ 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": "Copy|CopyLine", "Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut|CutLine", "Ctrl-x": "Cut",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "Duplicate|DuplicateLine", "Ctrl-d": "DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab|LastTab", "Alt-,": "PreviousTab",
"Alt-.": "NextTab|FirstTab", "Alt-.": "NextTab",
"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|LastTab", "CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab|FirstTab", "CtrlPageDown": "NextTab",
"ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
"Alt-g": "ToggleKeyMenu", "Alt-g": "ToggleKeyMenu",
"Ctrl-r": "ToggleRuler", "Ctrl-r": "ToggleRuler",
@ -72,7 +70,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|FirstSplit", "Ctrl-w": "NextSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -94,13 +92,11 @@ 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",
"MouseLeftDrag": "MouseDrag", "MouseMiddle": "PastePrimary",
"MouseLeftRelease": "MouseRelease", "Ctrl-MouseLeft": "MouseMultiCursor",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
"AltShiftUp": "SpawnMultiCursorUp", "AltShiftUp": "SpawnMultiCursorUp",
@ -146,8 +142,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "Copy|CopyLine", "Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut|CutLine", "Ctrl-x": "Cut",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
@ -179,10 +175,8 @@ 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",
"MouseLeftDrag": "MouseDrag", "MouseMiddle": "PastePrimary",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
} }

View File

@ -48,25 +48,23 @@ 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": "Copy|CopyLine", "Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut|CutLine", "Ctrl-x": "Cut",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "Duplicate|DuplicateLine", "Ctrl-d": "DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab|LastTab", "Alt-,": "PreviousTab",
"Alt-.": "NextTab|FirstTab", "Alt-.": "NextTab",
"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|LastTab", "CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab|FirstTab", "CtrlPageDown": "NextTab",
"ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
"Alt-g": "ToggleKeyMenu", "Alt-g": "ToggleKeyMenu",
"Ctrl-r": "ToggleRuler", "Ctrl-r": "ToggleRuler",
@ -75,7 +73,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|FirstSplit", "Ctrl-w": "NextSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -97,13 +95,11 @@ 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",
"MouseLeftDrag": "MouseDrag", "MouseMiddle": "PastePrimary",
"MouseLeftRelease": "MouseRelease", "Ctrl-MouseLeft": "MouseMultiCursor",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
"Alt-m": "SpawnMultiCursorSelect", "Alt-m": "SpawnMultiCursorSelect",
@ -149,8 +145,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "Copy|CopyLine", "Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut|CutLine", "Ctrl-x": "Cut",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
@ -182,10 +178,8 @@ 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",
"MouseLeftDrag": "MouseDrag", "MouseMiddle": "PastePrimary",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
} }

View File

@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/micro-editor/tcell/v2" "github.com/zyedidia/tcell/v2"
) )
type Event interface { type Event interface {
@ -44,17 +44,6 @@ 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>"
@ -79,7 +68,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]", k.code) s = fmt.Sprintf("Key[%d,%d]", k.code, int(k.r))
} }
} }
if len(m) != 0 { if len(m) != 0 {
@ -111,20 +100,11 @@ 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 {
@ -142,17 +122,9 @@ 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%s", mod, k, state) return fmt.Sprintf("%s%s", mod, k)
} }
} }
return "" return ""
@ -166,7 +138,11 @@ 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(e), nil return KeyEvent{
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,7 +8,6 @@ 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
@ -18,7 +17,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 := b.GetArg() input, argstart := buffer.GetArg(b)
var suggestions []string var suggestions []string
for cmd := range commands { for cmd := range commands {
@ -39,7 +38,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 := b.GetArg() input, argstart := buffer.GetArg(b)
var suggestions []string var suggestions []string
@ -78,67 +77,19 @@ func colorschemeComplete(input string) (string, []string) {
return chosen, suggestions return chosen, suggestions
} }
// filetypeComplete autocompletes filetype func contains(s []string, e string) bool {
func filetypeComplete(input string) (string, []string) { for _, a := range s {
var suggestions []string if a == e {
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)
} }
} }
headerLoop: return false
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 := b.GetArg() input, argstart := buffer.GetArg(b)
var suggestions []string var suggestions []string
for option := range config.GlobalSettings { for option := range config.GlobalSettings {
@ -165,7 +116,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 := b.GetArg() input, argstart := buffer.GetArg(b)
completeValue := false completeValue := false
args := bytes.Split(l, []byte{' '}) args := bytes.Split(l, []byte{' '})
@ -221,8 +172,13 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
switch inputOpt { switch inputOpt {
case "colorscheme": case "colorscheme":
_, suggestions = colorschemeComplete(input) _, suggestions = colorschemeComplete(input)
case "filetype": case "fileformat":
_, suggestions = filetypeComplete(input) if strings.HasPrefix("unix", 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")
@ -230,13 +186,15 @@ 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")
} }
default: case "clipboard":
if choices, ok := config.OptionChoices[inputOpt]; ok { if strings.HasPrefix("external", input) {
for _, choice := range choices { suggestions = append(suggestions, "external")
if strings.HasPrefix(choice, input) { }
suggestions = append(suggestions, choice) if strings.HasPrefix("internal", input) {
} suggestions = append(suggestions, "internal")
} }
if strings.HasPrefix("terminal", input) {
suggestions = append(suggestions, "terminal")
} }
} }
} }
@ -252,7 +210,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 := b.GetArg() input, argstart := buffer.GetArg(b)
var suggestions []string var suggestions []string
for _, cmd := range PluginCmds { for _, cmd := range PluginCmds {
@ -274,7 +232,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 := b.GetArg() input, argstart := buffer.GetArg(b)
completeValue := false completeValue := false
args := bytes.Split(l, []byte{' '}) args := bytes.Split(l, []byte{' '})

View File

@ -3,12 +3,12 @@ package action
import ( import (
"bytes" "bytes"
"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/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"
) )
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(e) ke := KeyEvent{
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 {
y := e.Rune() == 'y' || e.Rune() == 'Y' if (e.Rune() == 'y' || e.Rune() == 'Y') && hasYN {
n := e.Rune() == 'n' || e.Rune() == 'N' h.YNResp = true
if y || n { h.DonePrompt(false)
h.YNResp = y } else if (e.Rune() == 'n' || e.Rune() == 'N') && hasYN {
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,10 +122,7 @@ 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 {
@ -139,25 +136,11 @@ 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 {
action(h.BufPane) done := action(h.BufPane)
InfoBufBindings.ResetEvents() InfoBufBindings.ResetEvents()
return true return done
} 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/micro-editor/tcell/v2" "github.com/zyedidia/tcell/v2"
) )
type PaneKeyAction func(Pane) bool type PaneKeyAction func(Pane) bool

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"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/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/tcell/v2"
) )
type RawPane struct { type RawPane struct {

View File

@ -3,13 +3,13 @@ package action
import ( import (
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
"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/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/views" "github.com/zyedidia/micro/v2/internal/views"
"github.com/zyedidia/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,32 +107,30 @@ 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 && len(t.List) > 1 { if my == t.Y && mx == 0 {
if mx == 0 { t.Scroll(-4)
t.Scroll(-4) return
} else if mx == t.Width-1 { } else if my == t.Y && 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
} }
case tcell.ButtonNone: if len(t.List) > 1 {
if t.List[t.Active()].release { ind := t.LocFromVisual(buffer.Loc{mx, my})
// Mouse release received, while already released if ind != -1 {
t.ResetMouse() t.SetActive(ind)
return return
}
if my == 0 {
return
}
} }
case tcell.WheelUp: case tcell.WheelUp:
if my == t.Y && len(t.List) > 1 { if my == t.Y {
t.Scroll(4) t.Scroll(4)
return return
} }
case tcell.WheelDown: case tcell.WheelDown:
if my == t.Y && len(t.List) > 1 { if my == t.Y {
t.Scroll(-4) t.Scroll(-4)
return return
} }
@ -149,56 +147,6 @@ 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
@ -211,13 +159,11 @@ func InitTabs(bufs []*buffer.Buffer) {
for _, b := range bufs[1:] { for _, b := range bufs[1:] {
if multiopen == "vsplit" { if multiopen == "vsplit" {
MainTab().CurPane().VSplitBuf(b) MainTab().CurPane().VSplitBuf(b)
} else { // default hsplit } else { // default hsplit
MainTab().CurPane().HSplitBuf(b) MainTab().CurPane().HSplitBuf(b)
} }
} }
} }
screen.RestartCallback = Tabs.ResetMouse
} }
func MainTab() *Tab { func MainTab() *Tab {
@ -231,9 +177,6 @@ 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
@ -271,40 +214,34 @@ 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 press event in a pane, that pane will become active // If the event is a mouse event in a pane, that pane will become active and get
// and get the event // 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()
btn := e.Buttons() switch e.Buttons() {
switch { case tcell.Button1:
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 {
if btn == tcell.Button1 { var size int
if t.resizing != nil { if t.resizing.Kind == views.STVert {
var size int size = mx - t.resizing.X
if t.resizing.Kind == views.STVert { } else {
size = mx - t.resizing.X size = my - t.resizing.Y + 1
} 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
@ -314,15 +251,10 @@ func (t *Tab) HandleEvent(event tcell.Event) {
} }
} }
} }
case btn == tcell.ButtonNone: case tcell.ButtonNone:
// button release t.resizing = nil
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
@ -347,16 +279,11 @@ func (t *Tab) SetActive(i int) {
p.SetActive(false) p.SetActive(false)
} }
} }
}
// AddPane adds a pane at a given index err := config.RunPluginFn("onSetActive", luar.New(ulua.L, MainTab().CurPane()))
func (t *Tab) AddPane(pane Pane, i int) { if err != nil {
if len(t.Panes) == i { screen.TermMessage(err)
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

@ -1,4 +1,4 @@
//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd // +build linux darwin dragonfly openbsd_amd64 freebsd
package action package action

View File

@ -1,4 +1,4 @@
//go:build plan9 || nacl || windows // +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64
package action package action

View File

@ -4,13 +4,13 @@ import (
"errors" "errors"
"runtime" "runtime"
"github.com/micro-editor/tcell/v2"
"github.com/micro-editor/terminal"
"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"
"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/zyedidia/terminal"
) )
type TermKeyAction func(*TermPane) type TermKeyAction func(*TermPane)
@ -81,10 +81,6 @@ 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
} }
@ -125,7 +121,11 @@ 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(e) ke := KeyEvent{
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); !ok || t.State.Mode(terminal.ModeMouseMask) { } else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
// t.WriteString(event.EscSeq()) // t.WriteString(event.EscSeq())
} else { } else if e != nil {
x, y := e.Position() x, y := e.Position()
v := t.GetView() v := t.GetView()
x -= v.X x -= v.X
@ -188,12 +188,7 @@ 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/fs" "io/ioutil"
"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 (b *Buffer) GetWord() ([]byte, int) { func GetWord(b *Buffer) ([]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 (b *Buffer) GetWord() ([]byte, int) {
return []byte{}, -1 return []byte{}, -1
} }
if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) { if util.IsNonAlphaNumeric(b.RuneAt(c.Loc.Move(-1, b))) {
return []byte{}, c.X return []byte{}, c.X
} }
args := bytes.FieldsFunc(l, util.IsNonWordChar) args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
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 (b *Buffer) GetArg() (string, int) { func GetArg(b *Buffer) (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 (b *Buffer) GetArg() (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 := b.GetArg() input, argstart := GetArg(b)
sep := string(os.PathSeparator) sep := string(os.PathSeparator)
dirs := strings.Split(input, sep) dirs := strings.Split(input, sep)
var files []fs.DirEntry var files []os.FileInfo
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 = os.ReadDir(directories) files, err = ioutil.ReadDir(directories)
} else { } else {
files, err = os.ReadDir(".") files, err = ioutil.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 := b.GetWord() input, argstart := GetWord(b)
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.IsNonWordChar) words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
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.IsNonWordChar) words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
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,26 +1,24 @@
package buffer package buffer
import ( import (
"errors"
"fmt" "fmt"
"io/fs" "io"
"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: const backupMsg = `A backup was detected for this file. This likely means that micro
crashed while editing this file, or another instance of micro is currently
editing this file.
%s The backup was created on %s, and the file is
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
@ -32,80 +30,90 @@ The backup was created on %s and its path is:
Options: [r]ecover, [i]gnore, [a]bort: ` Options: [r]ecover, [i]gnore, [a]bort: `
const backupSeconds = 8 var backupRequestChan chan *Buffer
var BackupCompleteChan chan *Buffer func backupThread() {
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() {
BackupCompleteChan = make(chan *Buffer, 10) backupRequestChan = 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
} }
} }
func (b *Buffer) backupDir() string { // Backup saves the current buffer to ConfigDir/backups
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 := b.backupDir() backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) { if backupdir == "" || err != nil {
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 := util.DetermineEscapePath(backupdir, b.AbsPath) name := filepath.Join(backupdir, util.EscapePath(b.AbsPath))
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
_, err = b.overwriteFile(name) err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) {
if err == nil { if len(b.lines) == 0 {
BackupCompleteChan <- b return
} }
return err
}
tmp := util.AppendBackupSuffix(name) // end of line
_, err := b.overwriteFile(tmp) eol := []byte{'\n'}
if err != nil {
os.Remove(tmp)
return err
}
err = os.Rename(tmp, name)
if err != nil {
os.Remove(tmp)
return err
}
BackupCompleteChan <- b // write lines
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.keepBackup() || b.Path == "" || b.Type != BTDefault { if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault {
return return
} }
f := util.DetermineEscapePath(b.backupDir(), b.AbsPath) f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
os.Remove(f) os.Remove(f)
} }
@ -113,13 +121,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 := util.DetermineEscapePath(b.backupDir(), b.AbsPath) backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(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, b.Path, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile) msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath))
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,8 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -24,12 +25,13 @@ 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
@ -55,13 +57,17 @@ 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 a buffer that shows raw terminal events // BTRaw is 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
@ -84,10 +90,6 @@ 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
@ -101,8 +103,7 @@ 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
@ -210,11 +211,6 @@ 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
@ -238,20 +234,17 @@ 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 && !errors.Is(serr, fs.ErrNotExist) { if serr != nil && !os.IsNotExist(serr) {
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 {
@ -259,7 +252,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
} }
var buf *Buffer var buf *Buffer
if errors.Is(err, fs.ErrNotExist) { if os.IsNotExist(err) {
// 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 {
@ -329,19 +322,29 @@ 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.UpdatePathGlobLocals(b.Settings, absPath) config.InitLocalSettings(settings, absPath)
b.Settings["readonly"] = settings["readonly"]
b.Settings["filetype"] = settings["filetype"]
b.Settings["syntax"] = settings["syntax"]
b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string)) enc, err := htmlindex.Get(settings["encoding"].(string))
if err != nil { if err != nil {
b.encoding = unicode.UTF8 enc = unicode.UTF8
b.Settings["encoding"] = "utf-8" b.Settings["encoding"] = "utf-8"
} }
@ -352,22 +355,19 @@ 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, b.encoding.NewDecoder())) reader := bufio.NewReader(transform.NewReader(r, enc.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 b.Settings["fileformat"] { switch 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 +390,10 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
} }
b.UpdateRules() b.UpdateRules()
// we know the filetype now, so update per-filetype settings // init local settings again now that we know the filetype
config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string)) config.InitLocalSettings(b.Settings, b.Path)
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); errors.Is(err, fs.ErrNotExist) { if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) {
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm) os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
} }
@ -478,7 +478,7 @@ func (b *Buffer) GetName() string {
name = b.Path name = b.Path
} }
if b.Settings["basename"].(bool) { if b.Settings["basename"].(bool) {
return filepath.Base(name) return path.Base(name)
} }
return name return name
} }
@ -544,7 +544,7 @@ func (b *Buffer) ReOpen() error {
} }
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
data, err := io.ReadAll(reader) data, err := ioutil.ReadAll(reader)
txt := string(data) txt := string(data)
if err != nil { if err != nil {
@ -554,11 +554,7 @@ func (b *Buffer) ReOpen() error {
err = b.UpdateModTime() err = b.UpdateModTime()
if !b.Settings["fastdirty"].(bool) { if !b.Settings["fastdirty"].(bool) {
if len(data) > LargeFileThreshold { calcHash(b, &b.origHash)
b.Settings["fastdirty"] = true
} else {
calcHash(b, &b.origHash)
}
} }
b.isModified = false b.isModified = false
b.RelocateCursors() b.RelocateCursors()
@ -572,13 +568,6 @@ 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)
@ -619,16 +608,6 @@ func (b *Buffer) WordAt(loc Loc) []byte {
return b.Substr(start, end) return b.Substr(start, end)
} }
// Shared returns if there are other buffers with the same file as this buffer
func (b *Buffer) Shared() bool {
for _, buf := range OpenBuffers {
if buf != b && buf.SharedBuffer == b.SharedBuffer {
return true
}
}
return false
}
// Modified returns if this buffer has been modified since // Modified returns if this buffer has been modified since
// being opened // being opened
func (b *Buffer) Modified() bool { func (b *Buffer) Modified() bool {
@ -663,122 +642,39 @@ 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) { func calcHash(b *Buffer, out *[md5.Size]byte) error {
h := md5.New() h := md5.New()
size := 0
if len(b.lines) > 0 { if len(b.lines) > 0 {
h.Write(b.lines[0].data) n, e := 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:] {
if b.Endings == FFDos { n, e = h.Write([]byte{'\n'})
h.Write([]byte{'\r', '\n'}) if e != nil {
} else { return e
h.Write([]byte{'\n'})
} }
h.Write(l.data) size += n
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() {
@ -787,32 +683,13 @@ 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())
@ -824,145 +701,118 @@ 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)
matchedFileType := false if err != nil {
matchedFileName := false screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
matchedFileHeader := false continue
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 matchedFileType || matchedFileName || matchedFileHeader { if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
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
if matchedFileType { syntaxFile = f.Name()
b.SyntaxDef = syndef foundDef = true
syntaxFile = f.Name() break
foundDef = true
break
}
if matchedFileName {
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), syndef})
} else if matchedFileHeader {
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), syndef})
}
} }
} }
if !foundDef { // search in the default syntax files
// search for the syntax file in the built-in syntax files for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) { data, err := f.Data()
data, err := f.Data() if err != nil {
if err != nil { screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error()) continue
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 header.MatchFileName(b.Path) { if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
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
b.SyntaxDef = findRuntimeSyntaxDef(syntaxFile, header) for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
} if f.Name() == syntaxFile {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
if b.SyntaxDef != nil { file, err := highlight.ParseFile(data)
b.Settings["filetype"] = b.SyntaxDef.FileType if err != nil {
} else { screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
// search for the default file in the user's custom syntax files continue
b.SyntaxDef = findRealRuntimeSyntaxDef("default", nil) }
if b.SyntaxDef == nil {
// search for the default file in the built-in syntax files syndef, err := highlight.ParseDef(file, header)
b.SyntaxDef = findRuntimeSyntaxDef("default", nil) if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
b.SyntaxDef = syndef
break
}
} }
} }
if b.SyntaxDef != nil { if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
resolveIncludes(b.SyntaxDef) includes := highlight.GetIncludes(b.SyntaxDef)
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 {
b.SyntaxDef = &highlight.EmptyDef
} }
if b.SyntaxDef != nil { if b.SyntaxDef != nil {
@ -1064,7 +914,7 @@ func (b *Buffer) MergeCursors() {
b.EventHandler.active = b.curCursor b.EventHandler.active = b.curCursor
} }
// UpdateCursors updates all the cursors indices // UpdateCursors updates all the cursors indicies
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
@ -1089,7 +939,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().Deselect(true) b.GetActiveCursor().ResetSelection()
} }
// MoveLinesUp moves the range of lines up one row // MoveLinesUp moves the range of lines up one row
@ -1140,14 +990,34 @@ var BracePairs = [][2]rune{
{'[', ']'}, {'[', ']'},
} }
func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc, bool) { // FindMatchingBrace returns the location in the buffer of the matching bracket
// 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 char == braceType[0] { if startChar == braceType[0] || leftChar == 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 {
xInit = start.X if startChar == braceType[0] {
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]
@ -1156,76 +1026,42 @@ func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc
} else if r == braceType[1] { } else if r == braceType[1] {
i-- i--
if i == 0 { if i == 0 {
return Loc{x, y}, true if startChar == braceType[0] {
return Loc{x, y}, false, true
}
return Loc{x, y}, true, true
} }
} }
} }
} }
} else if char == braceType[1] { } else if startChar == braceType[1] || leftChar == 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 {
xInit = start.X if leftChar == braceType[1] {
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[1] { if r == braceType[0] {
i++
} else if r == braceType[0] {
i-- i--
if i == 0 { if i == 0 {
return Loc{x, y}, true if leftChar == braceType[1] {
return Loc{x, y}, true, true
}
return Loc{x, y}, false, true
} }
} else if r == braceType[1] {
i++
} }
} }
} }
} }
return start, false return start, true, 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
@ -1247,11 +1083,7 @@ 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
} }
@ -1294,7 +1126,7 @@ func (b *Buffer) Write(bytes []byte) (n int, err error) {
return len(bytes), nil return len(bytes), nil
} }
func (b *Buffer) updateDiff(synchronous bool) { func (b *Buffer) updateDiffSync() {
b.diffLock.Lock() b.diffLock.Lock()
defer b.diffLock.Unlock() defer b.diffLock.Unlock()
@ -1305,16 +1137,7 @@ func (b *Buffer) updateDiff(synchronous bool) {
} }
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
@ -1343,9 +1166,13 @@ func (b *Buffer) updateDiff(synchronous bool) {
// 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. // UpdateDiff does not schedule another update, in which case the callback
func (b *Buffer) UpdateDiff() { // is not called.
func (b *Buffer) UpdateDiff(callback func(bool)) {
if b.updateDiffTimer != nil { if b.updateDiffTimer != nil {
return return
} }
@ -1356,18 +1183,20 @@ func (b *Buffer) UpdateDiff() {
} }
if lineCount < 1000 { if lineCount < 1000 {
b.updateDiff(true) b.updateDiffSync()
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.updateDiff(false) b.updateDiffSync()
screen.Redraw() callback(false)
}) })
} 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)
} }
} }
@ -1379,7 +1208,9 @@ func (b *Buffer) SetDiffBase(diffBase []byte) {
} else { } else {
b.diffBaseLineCount = strings.Count(string(diffBase), "\n") b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
} }
b.UpdateDiff() b.UpdateDiff(func(synchronous bool) {
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,7 +20,6 @@ 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,14 +20,8 @@ type Cursor struct {
buf *Buffer buf *Buffer
Loc Loc
// Last visual x position of the cursor. Used in cursor up/down movements // Last cursor x position
// 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
@ -36,11 +30,6 @@ 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
} }
@ -49,8 +38,6 @@ 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
@ -67,9 +54,8 @@ 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 = b.X, b.Y c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
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
@ -80,8 +66,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(wrap bool) int { func (c *Cursor) GetVisualX() int {
if wrap && c.buf.GetVisualX != nil { if c.buf.GetVisualX != nil {
return c.buf.GetVisualX(c.Loc) return c.buf.GetVisualX(c.Loc)
} }
@ -107,7 +93,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.StoreVisualX() c.LastVisualX = c.GetVisualX()
} }
// StartOfText moves the cursor to the first non-whitespace rune of // StartOfText moves the cursor to the first non-whitespace rune of
@ -138,7 +124,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.StoreVisualX() c.LastVisualX = c.GetVisualX()
} }
// CopySelection copies the user's selection to either "primary" // CopySelection copies the user's selection to either "primary"
@ -193,7 +179,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] c.Loc = c.CurSelection[1].Move(-1, c.buf)
} }
c.ResetSelection() c.ResetSelection()
c.StoreVisualX() c.StoreVisualX()
@ -410,26 +396,13 @@ 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)) {
@ -441,10 +414,6 @@ 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 {
@ -452,17 +421,6 @@ 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 {
@ -473,132 +431,6 @@ 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)
@ -622,6 +454,5 @@ func (c *Cursor) RuneUnder(x int) rune {
} }
func (c *Cursor) StoreVisualX() { func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX(false) c.LastVisualX = c.GetVisualX()
c.LastWrappedVisualX = c.GetVisualX(true)
} }

View File

@ -104,11 +104,7 @@ 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.StoreVisualX() c.LastVisualX = c.GetVisualX()
}
if useUndo {
eh.updateTrailingWs(t)
} }
} }
@ -253,11 +249,11 @@ func (eh *EventHandler) Execute(t *TextEvent) {
ExecuteTextEvent(t, eh.buf) ExecuteTextEvent(t, eh.buf)
} }
// Undo the first event in the undo stack. Returns false if the stack is empty. // Undo the first event in the undo stack
func (eh *EventHandler) Undo() bool { func (eh *EventHandler) Undo() {
t := eh.UndoStack.Peek() t := eh.UndoStack.Peek()
if t == nil { if t == nil {
return false return
} }
startTime := t.Time.UnixNano() / int64(time.Millisecond) startTime := t.Time.UnixNano() / int64(time.Millisecond)
@ -266,16 +262,15 @@ func (eh *EventHandler) Undo() bool {
for { for {
t = eh.UndoStack.Peek() t = eh.UndoStack.Peek()
if t == nil { if t == nil {
break return
} }
if t.Time.UnixNano()/int64(time.Millisecond) < endTime { if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
break return
} }
eh.UndoOneEvent() eh.UndoOneEvent()
} }
return true
} }
// UndoOneEvent undoes one event // UndoOneEvent undoes one event
@ -291,20 +286,23 @@ func (eh *EventHandler) UndoOneEvent() {
eh.UndoTextEvent(t) eh.UndoTextEvent(t)
// Set the cursor in the right place // Set the cursor in the right place
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) { teCursor := t.C
eh.cursors[t.C.Num].Goto(t.C) if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY t.C = *eh.cursors[teCursor.Num]
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. Returns false if the stack is empty. // Redo the first event in the redo stack
func (eh *EventHandler) Redo() bool { func (eh *EventHandler) Redo() {
t := eh.RedoStack.Peek() t := eh.RedoStack.Peek()
if t == nil { if t == nil {
return false return
} }
startTime := t.Time.UnixNano() / int64(time.Millisecond) startTime := t.Time.UnixNano() / int64(time.Millisecond)
@ -313,16 +311,15 @@ func (eh *EventHandler) Redo() bool {
for { for {
t = eh.RedoStack.Peek() t = eh.RedoStack.Peek()
if t == nil { if t == nil {
break return
} }
if t.Time.UnixNano()/int64(time.Millisecond) > endTime { if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
break return
} }
eh.RedoOneEvent() eh.RedoOneEvent()
} }
return true
} }
// RedoOneEvent redoes one event // RedoOneEvent redoes one event
@ -332,9 +329,12 @@ func (eh *EventHandler) RedoOneEvent() {
return return
} }
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) { teCursor := t.C
eh.cursors[t.C.Num].Goto(t.C) if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY t.C = *eh.cursors[teCursor.Num]
eh.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
} }
// Modifies the text event // Modifies the text event
@ -342,58 +342,3 @@ 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,9 +46,10 @@ 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
lock sync.Mutex rehighlight bool
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.
@ -74,7 +75,6 @@ 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,18 +147,20 @@ 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++
@ -188,23 +190,22 @@ 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') {
@ -232,26 +233,24 @@ 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.lines[a].data = append(la.lines[a].data, la.lines[b].data...) la.insert(Loc{len(la.lines[a].data), a}, 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.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...) la.insert(Loc{0, pos.Y + 1}, 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)
@ -285,6 +284,11 @@ 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)
@ -323,11 +327,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(lineN int) []byte { func (la *LineArray) LineBytes(n int) []byte {
if lineN >= len(la.lines) || lineN < 0 { if n >= len(la.lines) || n < 0 {
return []byte{} return []byte{}
} }
return la.lines[lineN].data return la.lines[n].data
} }
// State gets the highlight state for the given line number // State gets the highlight state for the given line number
@ -358,14 +362,16 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch {
return la.lines[lineN].match return la.lines[lineN].match
} }
// Locks the whole LineArray func (la *LineArray) Rehighlight(lineN int) bool {
func (la *LineArray) Lock() { la.lines[lineN].lock.Lock()
la.lock.Lock() defer la.lines[lineN].lock.Unlock()
return la.lines[lineN].rehighlight
} }
// Unlocks the whole LineArray func (la *LineArray) SetRehighlight(lineN int, on bool) {
func (la *LineArray) Unlock() { la.lines[lineN].lock.Lock()
la.lock.Unlock() defer la.lines[lineN].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,16 +47,6 @@ 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
@ -149,5 +139,10 @@ 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 {
return pos.Clamp(la.Start(), la.End()) if pos.GreaterEqual(la.End()) {
return la.End()
} else if pos.LessThan(la.Start()) {
return la.Start()
}
return pos
} }

View File

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

View File

@ -5,19 +5,18 @@ 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"
) )
@ -25,178 +24,68 @@ import (
// because hashing is too slow // because hashing is too slow
const LargeFileThreshold = 50000 const LargeFileThreshold = 50000
type wrappedFile struct { // overwriteFile opens the given file for writing, truncating if one exists, and then calls
writeCloser io.WriteCloser // the supplied function with the file as io.Writer object, also making sure the file is
withSudo bool // closed afterwards.
screenb bool func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) {
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 err != nil { if writeCloser, err = cmd.StdinPipe(); err != nil {
return wrappedFile{}, err return
} }
sigChan = make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Reset(os.Interrupt) signal.Notify(c, os.Interrupt)
signal.Notify(sigChan, os.Interrupt) go func() {
<-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
err = cmd.Start() if e := cmd.Start(); e != nil && err == nil {
if err != nil {
screen.TempStart(screenb) screen.TempStart(screenb)
return err
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(sigChan)
return wrappedFile{}, err
} }
} else { } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode) return
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 {
return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil err = err2
}
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
} }
// end of line if withSudo {
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 := wf.cmd.Wait() err := cmd.Wait()
screen.TempStart(wf.screenb) screen.TempStart(screenb)
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(wf.sigChan)
if err != nil { if err != nil {
return err return err
} }
} }
return err
}
func (b *Buffer) overwriteFile(name string) (int, error) { return
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
@ -204,19 +93,9 @@ 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, false) return b.saveToFile(filename, false)
} }
func (b *Buffer) SaveWithSudo() error { func (b *Buffer) SaveWithSudo() error {
@ -224,10 +103,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, false) return b.saveToFile(filename, true)
} }
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error { func (b *Buffer) saveToFile(filename string, withSudo 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")
@ -239,7 +118,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
return errors.New("Save with sudo not supported on Windows") return errors.New("Save with sudo not supported on Windows")
} }
if !autoSave && b.Settings["rmtrailingws"].(bool) { if 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))
@ -257,35 +136,19 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
filename, err = util.ReplaceHome(filename) // Update the last time this file was updated after saving
if err != nil { defer func() {
return err b.ModTime, _ = util.GetModTime(filename)
} err = b.Serialize()
}()
newFile := false // Removes any tilde and replaces with the absolute path to home
fileInfo, err := os.Stat(filename) absFilename, _ := util.ReplaceHome(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); errors.Is(statErr, fs.ErrNotExist) { if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
// 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
@ -299,22 +162,49 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
saveResponseChan := make(chan saveResponse) var fileSize int
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
result := <-saveResponseChan
err = result.err
if err != nil {
if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
err = errors.Unwrap(err)
b.UpdateModTime() enc, err := htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
return err
}
fwriter := func(file io.Writer) (e error) {
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 result.size > LargeFileThreshold { if fileSize > 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 {
@ -322,70 +212,10 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
newPath := b.Path != filename
b.Path = filename b.Path = filename
b.AbsPath = absFilename absPath, _ := filepath.Abs(filename)
b.AbsPath = absPath
b.isModified = false b.isModified = false
b.UpdateModTime() b.UpdateRules()
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,56 +2,10 @@ 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 {
@ -68,19 +22,30 @@ 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, charpos, padMode, rPadded := findLineParams(b, start, end, i, r) l := b.LineBytes(i)
charpos := 0
match := rPadded.FindIndex(l) 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)
}
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
@ -105,39 +70,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-- {
charCount := util.CharacterCount(b.LineBytes(i)) l := b.LineBytes(i)
from := Loc{0, i}.Clamp(start, end) charpos := 0
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]
return [2]Loc{match[0], match[1]}, true start := Loc{charpos + util.RunePos(l, match[0]), i}
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
@ -181,58 +146,49 @@ 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 characters // and returns the number of replacements made and the number of runes
// 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, captureGroups bool) (int, int) { func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) {
if start.GreaterThan(end) { if start.GreaterThan(end) {
start, end = end, start start, end = end, start
} }
charsEnd := util.CharacterCount(b.LineBytes(end.Y)) netrunes := 0
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.LineBytes(i) l := b.lines[i].data
charCount := util.CharacterCount(l) charpos := 0
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
from := Loc{0, i}.Clamp(start, end) if start.Y == end.Y && i == start.Y {
to := Loc{charCount, i}.Clamp(start, end) l = util.SliceStart(l, end.X)
matches := b.findAll(search, from, to) l = util.SliceEnd(l, start.X)
found += len(matches) charpos = start.X
} else if i == start.Y {
for j := len(matches) - 1; j >= 0; j-- { l = util.SliceEnd(l, start.X)
// if we counted upwards, the different deltas would interfere charpos = start.X
match := matches[j] } else if i == end.Y {
var newText []byte l = util.SliceStart(l, end.X)
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, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd return found, netrunes
} }

View File

@ -1,13 +1,15 @@
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"
) )
@ -29,18 +31,16 @@ func (b *Buffer) Serialize() error {
return nil return nil
} }
var buf bytes.Buffer name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
b.EventHandler,
b.GetActiveCursor().Loc,
b.ModTime,
})
if err != nil {
return err
}
name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) return overwriteFile(name, encoding.Nop, func(file io.Writer) error {
return util.SafeWrite(name, buf.Bytes(), true) err := gob.NewEncoder(file).Encode(SerializedBuffer{
b.EventHandler,
b.GetActiveCursor().Loc,
b.ModTime,
})
return err
}, false)
} }
// 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(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)) file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)))
if err == nil { if err == nil {
defer file.Close() defer file.Close()
var buffer SerializedBuffer var buffer SerializedBuffer

View File

@ -1,89 +1,36 @@
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) ReloadSettings(reloadFiletype bool) { func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
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.Size() > LargeFileThreshold { if !b.Modified() {
b.Settings["fastdirty"] = true e := calcHash(b, &b.origHash)
} else { if e == ErrFileTooLarge {
if !b.isModified { b.Settings["fastdirty"] = false
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" {
b.ReloadSettings(false) config.InitRuntimeFiles()
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":
@ -99,12 +46,6 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
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)
@ -133,19 +74,12 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
} }
} }
} }
}
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
} }
@ -162,15 +96,3 @@ 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

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

View File

@ -1,49 +1,44 @@
package config package config
import ( import (
"sync"
"time" "time"
) )
var Autosave chan bool var Autosave chan bool
var autotime chan float64 var autotime int
// 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 float64) { func SetAutoTime(a int) {
autotime <- a autolock.Lock()
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 {
select { autolock.Lock()
case a = <-autotime: a := autotime
if t != nil { autolock.Unlock()
t.Stop() if a < 1 {
for len(elapsed) > 0 { break
<-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/micro-editor/tcell/v2" "github.com/zyedidia/tcell/v2"
) )
// DefStyle is Micro's default style // DefStyle is Micro's default style
@ -52,55 +52,43 @@ func InitColorscheme() error {
Colorscheme = make(map[string]tcell.Style) Colorscheme = make(map[string]tcell.Style)
DefStyle = tcell.StyleDefault DefStyle = tcell.StyleDefault
c, err := LoadDefaultColorscheme() return 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() (map[string]tcell.Style, error) { func LoadDefaultColorscheme() error {
var parsedColorschemes []string return LoadColorscheme(GlobalSettings["colorscheme"].(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, parsedColorschemes *[]string) (map[string]tcell.Style, error) { func LoadColorscheme(colorschemeName string) error {
c := make(map[string]tcell.Style)
file := FindRuntimeFile(RTColorscheme, colorschemeName) file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil { if file == nil {
return c, errors.New(colorschemeName + " is not a valid colorscheme") return errors.New(colorschemeName + " is not a valid colorscheme")
} }
if data, err := file.Data(); err != nil { if data, err := file.Data(); err != nil {
return c, errors.New("Error loading colorscheme: " + err.Error()) return errors.New("Error loading colorscheme: " + err.Error())
} else { } else {
var err error Colorscheme, err = ParseColorscheme(string(data))
c, err = ParseColorscheme(file.Name(), string(data), parsedColorschemes)
if err != nil { if err != nil {
return c, err return err
} }
} }
return c, nil return 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(name string, text string, parsedColorschemes *[]string) (map[string]tcell.Style, error) { func ParseColorscheme(text string) (map[string]tcell.Style, error) {
var err error var err error
colorParser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`) parser := 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] == '#' {
@ -108,30 +96,7 @@ lineLoop:
continue continue
} }
matches := includeParser.FindSubmatch([]byte(line)) matches := parser.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

@ -3,8 +3,8 @@ package config
import ( import (
"testing" "testing"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zyedidia/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", testColorscheme, nil) c, err := ParseColorscheme(testColorscheme)
assert.Nil(t, err) assert.Nil(t, err)
fg, bg, _ := c["comment"].Decompose() fg, bg, _ := c["comment"].Decompose()

View File

@ -71,7 +71,7 @@ type Plugin struct {
Info *PluginInfo // json file containing info Info *PluginInfo // json file containing info
Srcs []RuntimeFile // lua files Srcs []RuntimeFile // lua files
Loaded bool Loaded bool
Default bool // pre-installed plugin Default bool // pre-installed plugin
} }
// IsLoaded returns if a plugin is enabled // IsLoaded returns if a plugin is enabled
@ -143,3 +143,15 @@ 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,6 +5,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -13,8 +14,8 @@ import (
"sync" "sync"
"github.com/blang/semver" "github.com/blang/semver"
"github.com/micro-editor/json5"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/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"
) )
@ -395,7 +396,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
data, err := io.ReadAll(resp.Body) data, err := ioutil.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/micro-editor/json5" "github.com/zyedidia/json5"
) )
func TestDependencyResolving(t *testing.T) { func TestDependencyResolving(t *testing.T) {

View File

@ -2,8 +2,10 @@ package config
import ( import (
"errors" "errors"
"io/ioutil"
"log" "log"
"os" "os"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@ -38,10 +40,6 @@ 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)
} }
@ -60,6 +58,12 @@ 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
@ -79,18 +83,22 @@ func (rf realFile) Name() string {
} }
func (rf realFile) Data() ([]byte, error) { func (rf realFile) Data() ([]byte, error) {
return os.ReadFile(string(rf)) return ioutil.ReadFile(string(rf))
} }
func (af assetFile) Name() string { func (af assetFile) Name() string {
fn := filepath.Base(string(af)) fn := path.Base(string(af))
return fn[:len(fn)-len(filepath.Ext(fn))] return fn[:len(fn)-len(path.Ext(fn))]
} }
func (af assetFile) Data() ([]byte, error) { 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)
@ -105,7 +113,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, _ := os.ReadDir(directory) files, _ := ioutil.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())
@ -121,17 +129,9 @@ 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, _ := filepath.Match(pattern, f); ok { if ok, _ := path.Match(pattern, f); ok {
af := assetFile(filepath.Join(directory, f)) AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
for _, rf := range realFiles[fileType] {
if af.Name() == rf.Name() {
continue assetLoop
}
}
AddRuntimeFile(fileType, af)
} }
} }
} }
@ -158,30 +158,19 @@ func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return realFiles[fileType] return realFiles[fileType]
} }
// InitRuntimeFiles initializes all assets files and the config directory. // InitRuntimeFiles initializes all assets file and the config directory
// If `user` is false, InitRuntimeFiles ignores the config directory and func InitRuntimeFiles() {
// initializes asset files only.
func InitRuntimeFiles(user bool) {
add := func(fileType RTFiletype, dir, pattern string) { add := func(fileType RTFiletype, dir, pattern string) {
if user { AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern) AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
}
AddRuntimeFilesFromAssets(fileType, filepath.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"
@ -192,14 +181,14 @@ func InitPlugins() {
// Search ConfigDir for plugin-scripts // Search ConfigDir for plugin-scripts
plugdir := filepath.Join(ConfigDir, "plug") plugdir := filepath.Join(ConfigDir, "plug")
files, _ := os.ReadDir(plugdir) files, _ := ioutil.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, _ := os.ReadDir(plugpath) srcs, _ := ioutil.ReadDir(plugpath)
p := new(Plugin) p := new(Plugin)
p.Name = d.Name() p.Name = d.Name()
p.DirName = d.Name() p.DirName = d.Name()
@ -207,7 +196,7 @@ func InitPlugins() {
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 := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name())) data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
if err != nil { if err != nil {
continue continue
} }
@ -229,15 +218,7 @@ func InitPlugins() {
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
@ -299,7 +280,7 @@ func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) e
if _, err := os.Stat(fullpath); err == nil { if _, err := os.Stat(fullpath); err == nil {
AddRealRuntimeFile(filetype, realFile(fullpath)) AddRealRuntimeFile(filetype, realFile(fullpath))
} else { } else {
fullpath = filepath.Join("runtime", "plugins", pldir, filePath) fullpath = path.Join("runtime", "plugins", pldir, filePath)
AddRuntimeFile(filetype, assetFile(fullpath)) AddRuntimeFile(filetype, assetFile(fullpath))
} }
return nil return nil
@ -316,7 +297,7 @@ func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, dire
if _, err := os.Stat(fullpath); err == nil { if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern) AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else { } else {
fullpath = filepath.Join("runtime", "plugins", pldir, directory) fullpath = path.Join("runtime", "plugins", pldir, directory)
AddRuntimeFilesFromAssets(filetype, fullpath, pattern) AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
} }
return nil return nil

View File

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

View File

@ -4,106 +4,340 @@ 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/micro-editor/json5"
"github.com/zyedidia/glob" "github.com/zyedidia/glob"
"github.com/zyedidia/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
// a list of settings that need option validators var (
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": validateChoice, "clipboard": validateClipboard,
"colorcolumn": validateNonNegativeValue, "tabsize": validatePositiveValue,
"colorscheme": validateColorscheme, "scrollmargin": validateNonNegativeValue,
"detectlimit": validateNonNegativeValue, "scrollspeed": validateNonNegativeValue,
"encoding": validateEncoding, "colorscheme": validateColorscheme,
"fileformat": validateChoice, "colorcolumn": validateNonNegativeValue,
"helpsplit": validateChoice, "fileformat": validateLineEnding,
"matchbracestyle": validateChoice, "encoding": validateEncoding,
"multiopen": validateChoice, "multiopen": validateMultiOpen,
"pageoverlap": validateNonNegativeValue, "reload": validateReload,
"reload": validateChoice,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"tabsize": validatePositiveValue,
"truecolor": validateChoice,
} }
// a list of settings with pre-defined choices func ReadSettings() error {
var OptionChoices = map[string][]string{ filename := filepath.Join(ConfigDir, "settings.json")
"clipboard": {"internal", "external", "terminal"}, if _, e := os.Stat(filename); e == nil {
"fileformat": {"unix", "dos"}, input, err := ioutil.ReadFile(filename)
"helpsplit": {"hsplit", "vsplit"}, if err != nil {
"matchbracestyle": {"underline", "highlight"}, settingsParseError = true
"multiopen": {"tab", "hsplit", "vsplit"}, return errors.New("Error reading settings.json file: " + err.Error())
"reload": {"prompt", "auto", "disabled"}, }
"truecolor": {"auto", "on", "off"}, 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,
"detectlimit": float64(100), "diffgutter": false,
"diffgutter": false, "encoding": "utf-8",
"encoding": "utf-8", "eofnewline": true,
"eofnewline": true, "fastdirty": false,
"fastdirty": false, "fileformat": "unix",
"fileformat": defaultFileFormat(), "filetype": "unknown",
"filetype": "unknown", "hlsearch": false,
"hlsearch": false, "incsearch": true,
"hltaberrors": false, "ignorecase": true,
"hltrailingws": false, "indentchar": " ",
"ignorecase": true, "keepautoindent": false,
"incsearch": true, "matchbrace": true,
"indentchar": " ", "mkparents": false,
"keepautoindent": false, "permbackup": false,
"matchbrace": true, "readonly": false,
"matchbraceleft": true, "reload": "prompt",
"matchbracestyle": "underline", "rmtrailingws": false,
"mkparents": false, "ruler": true,
"pageoverlap": float64(2), "relativeruler": false,
"permbackup": false, "savecursor": false,
"readonly": false, "saveundo": false,
"relativeruler": false, "scrollbar": false,
"reload": "prompt", "scrollmargin": float64(3),
"rmtrailingws": false, "scrollspeed": float64(2),
"ruler": true, "smartpaste": true,
"savecursor": false, "softwrap": false,
"saveundo": false, "splitbottom": true,
"scrollbar": false, "splitright": true,
"scrollmargin": float64(3), "statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
"scrollspeed": float64(2), "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
"smartpaste": true, "statusline": true,
"softwrap": false, "syntax": true,
"splitbottom": true, "tabmovement": false,
"splitright": true, "tabsize": float64(4),
"statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)", "tabstospaces": false,
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help", "useprimary": true,
"statusline": true, "wordwrap": false,
"syntax": true, }
"tabmovement": false,
"tabsize": float64(4), func GetInfoBarOffset() int {
"tabstospaces": false, offset := 0
"truecolor": "auto", 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
@ -115,7 +349,6 @@ 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,
@ -138,322 +371,21 @@ var LocalSettings = []string{
"readonly", "readonly",
} }
var ( // DefaultGlobalSettings returns the default global settings for micro
ErrInvalidOption = errors.New("Invalid option") // Note that colorscheme is a global only option
ErrInvalidValue = errors.New("Invalid value") func DefaultGlobalSettings() map[string]interface{} {
globalsettings := make(map[string]interface{})
// 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
// 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)
}
func init() {
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 { for k, v := range defaultCommonSettings {
commonsettings[k] = v globalsettings[k] = v
} }
return commonsettings for k, v := range DefaultGlobalOnlySettings {
globalsettings[k] = v
}
return globalsettings
} }
// DefaultAllSettings returns a map of all common buffer & global-only settings // DefaultAllSettings returns a map of all settings and their
// and their default values // default values (both common and global settings)
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 {
@ -478,15 +410,18 @@ 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 {
f, err := strconv.ParseFloat(value, 64) i, err := strconv.Atoi(value)
if err != nil { if err != nil {
return nil, ErrInvalidValue return nil, ErrInvalidValue
} }
native = f native = float64(i)
} else { } else {
return nil, ErrInvalidValue return nil, ErrInvalidValue
} }
if err := OptionIsValid(option, native); err != nil {
return nil, err
}
return native, nil return native, nil
} }
@ -502,13 +437,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 {
nativeValue, ok := value.(float64) tabsize, ok := value.(float64)
if !ok { if !ok {
return errors.New("Expected numeric type for " + option) return errors.New("Expected numeric type for " + option)
} }
if nativeValue < 1 { if tabsize < 1 {
return errors.New(option + " must be greater than 0") return errors.New(option + " must be greater than 0")
} }
@ -529,26 +464,6 @@ 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)
@ -563,7 +478,69 @@ 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

@ -4,11 +4,11 @@ import (
"strconv" "strconv"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"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"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/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,15 +58,9 @@ 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.LastWrappedVisualX = c.GetVisualX(true) c.LastVisualX = c.GetVisualX()
} }
} }
if option == "diffgutter" || option == "ruler" || option == "scrollbar" ||
option == "statusline" {
w.updateDisplayInfo()
w.Relocate()
}
} }
b.GetVisualX = func(loc buffer.Loc) int { b.GetVisualX = func(loc buffer.Loc) int {
return w.VLocFromLoc(loc).VisualX return w.VLocFromLoc(loc).VisualX
@ -135,11 +129,6 @@ 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
@ -157,16 +146,16 @@ func (w *BufWindow) updateDisplayInfo() {
w.gutterOffset += w.maxLineNumLength + 1 w.gutterOffset += w.maxLineNumLength + 1
} }
if w.gutterOffset > w.Width-scrollbarWidth {
w.gutterOffset = w.Width - scrollbarWidth
}
prevBufWidth := w.bufWidth prevBufWidth := w.bufWidth
w.bufWidth = w.Width - w.gutterOffset - scrollbarWidth
w.bufWidth = w.Width - w.gutterOffset
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
w.bufWidth--
}
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.LastWrappedVisualX = c.GetVisualX(true) c.LastVisualX = c.GetVisualX()
} }
} }
} }
@ -244,7 +233,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(false) cx := activeC.GetVisualX()
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
@ -254,8 +243,8 @@ func (w *BufWindow) Relocate() bool {
w.StartCol = cx w.StartCol = cx
ret = true ret = true
} }
if cx+rw > w.StartCol+w.bufWidth { if cx+w.gutterOffset+rw > w.StartCol+w.Width {
w.StartCol = cx - w.bufWidth + rw w.StartCol = cx - w.Width + w.gutterOffset + rw
ret = true ret = true
} }
} }
@ -288,17 +277,13 @@ func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
break break
} }
} }
for i := 0; i < 2 && vloc.X < w.gutterOffset; i++ { 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++ screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
} 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 := ""
@ -334,28 +319,26 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc
} else { } else {
lineInt = bloc.Y - cursorLine lineInt = bloc.Y - cursorLine
} }
lineNum := []rune(strconv.Itoa(util.Abs(lineInt))) lineNum := 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) && vloc.X < w.gutterOffset; i++ { for i := 0; i < w.maxLineNumLength-len(lineNum); 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 i := 0; i < len(lineNum) && vloc.X < w.gutterOffset; i++ { for _, ch := range lineNum {
if softwrapped || (w.bufWidth == 0 && w.Buf.Settings["softwrap"] == true) { if softwrapped {
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, lineNum[i], nil, lineNumStyle) screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
} }
vloc.X++ vloc.X++
} }
// Write the extra space // Write the extra space
if vloc.X < w.gutterOffset { 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++
}
} }
// getStyle returns the highlight style for the given character position // getStyle returns the highlight style for the given character position
@ -390,7 +373,18 @@ func (w *BufWindow) displayBuffer() {
if b.ModifiedThisFrame { if b.ModifiedThisFrame {
if b.Settings["diffgutter"].(bool) { if b.Settings["diffgutter"].(bool) {
b.UpdateDiff() b.UpdateDiff(func(synchronous bool) {
// 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
} }
@ -398,20 +392,26 @@ 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 _, c := range b.GetCursors() { for _, bp := range buffer.BracePairs {
if c.HasSelection() { for _, c := range b.GetCursors() {
continue if c.HasSelection() {
} continue
}
curX := c.X
curLoc := c.Loc
mb, left, found := b.FindMatchingBrace(c.Loc) r := c.RuneUnder(curX)
if found { rl := c.RuneUnder(curX - 1)
matchingBraces = append(matchingBraces, mb) if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
if !left { mb, left, found := b.FindMatchingBrace(bp, curLoc)
if b.Settings["matchbracestyle"].(string) != "highlight" { if found {
matchingBraces = append(matchingBraces, c.Loc) matchingBraces = append(matchingBraces, mb)
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 +455,7 @@ func (w *BufWindow) displayBuffer() {
currentLine := false currentLine := false
for _, c := range cursors { for _, c := range cursors {
if !c.HasSelection() && bloc.Y == c.Y && w.active { if bloc.Y == c.Y && w.active {
currentLine = true currentLine = true
break break
} }
@ -482,12 +482,6 @@ 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
@ -511,37 +505,6 @@ 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]) ||
@ -594,15 +557,7 @@ 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 {
if b.Settings["matchbracestyle"].(string) == "highlight" { style = style.Underline(true)
if s, ok := config.Colorscheme["match-brace"]; ok {
style = s
} else {
style = style.Reverse(true)
}
} else {
style = style.Underline(true)
}
} }
} }
} }
@ -625,21 +580,16 @@ func (w *BufWindow) displayBuffer() {
wrap := func() { wrap := func() {
vloc.X = 0 vloc.X = 0
if w.hasMessage {
w.drawGutter(&vloc, &bloc)
}
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
}
if vloc.Y >= 0 { // This will draw an empty line number because the current line is wrapped
if w.hasMessage { if b.Settings["ruler"].(bool) {
w.drawGutter(&vloc, &bloc) w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
}
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
}
// This will draw an empty line number because the current line is wrapped
if b.Settings["ruler"].(bool) {
w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
}
} else {
vloc.X = w.gutterOffset
} }
} }
@ -659,7 +609,7 @@ func (w *BufWindow) displayBuffer() {
wordwidth := 0 wordwidth := 0
totalwidth := w.StartCol - nColsBeforeStart totalwidth := w.StartCol - nColsBeforeStart
for len(line) > 0 && vloc.X < maxWidth { for len(line) > 0 {
r, combc, size := util.DecodeCharacter(line) r, combc, size := util.DecodeCharacter(line)
line = line[size:] line = line[size:]

View File

@ -2,12 +2,12 @@ package display
import ( import (
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"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/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"
) )
type InfoWindow struct { type InfoWindow struct {

View File

@ -291,7 +291,13 @@ 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 = util.Clamp(s.Line+n, 0, w.Buf.LinesNum()-1) s.Line += n
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,12 +47,6 @@ 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/micro-editor/tcell/v2" "github.com/zyedidia/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"
@ -112,10 +112,10 @@ func (w *TabWindow) Display() {
} }
return tabBarStyle, tabBarActiveStyle return tabBarStyle, tabBarActiveStyle
} }
draw := func(r rune, n int, active bool, reversed bool) { draw := func(r rune, n int, active bool, reversed bool) {
tabBarStyle, tabBarActiveStyle := reverseStyles(reversed) tabBarStyle, tabBarActiveStyle := reverseStyles(reversed)
style := tabBarStyle style := tabBarStyle
if active { if active {
style = tabBarActiveStyle style = tabBarActiveStyle
@ -147,15 +147,15 @@ func (w *TabWindow) Display() {
} else { } else {
draw(' ', 1, false, tabCharHighlight) draw(' ', 1, false, tabCharHighlight)
} }
for _, c := range n { for _, c := range n {
draw(c, 1, i == w.active, tabCharHighlight) draw(c, 1, i == w.active, tabCharHighlight)
} }
if i == len(w.Names)-1 { if i == len(w.Names)-1 {
done = true done = true
} }
if i == w.active { if i == w.active {
draw(']', 1, true, tabCharHighlight) draw(']', 1, true, tabCharHighlight)
draw(' ', 2, true, globalTabReverse) draw(' ', 2, true, globalTabReverse)
@ -163,7 +163,7 @@ func (w *TabWindow) Display() {
draw(' ', 1, false, tabCharHighlight) draw(' ', 1, false, tabCharHighlight)
draw(' ', 2, false, globalTabReverse) draw(' ', 2, false, globalTabReverse)
} }
if x >= w.Width { if x >= w.Width {
break break
} }

View File

@ -1,13 +1,13 @@
package display package display
import ( import (
"github.com/micro-editor/tcell/v2"
"github.com/micro-editor/terminal"
"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/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/zyedidia/terminal"
) )
type TermWindow struct { type TermWindow struct {
@ -110,8 +110,6 @@ 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()
if curx < w.Width && cury < w.Height { screen.ShowCursor(curx+w.X, cury+w.Y)
screen.ShowCursor(curx+w.X, cury+w.Y)
}
} }
} }

View File

@ -1,16 +1,12 @@
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"
) )
@ -21,23 +17,24 @@ 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 {
if !errors.Is(err, fs.ErrNotExist) { defer file.Close()
i.Error("Error loading history: ", err) decoder := gob.NewDecoder(file)
} err = decoder.Decode(&decodedMap)
return
}
defer file.Close() if err != nil {
err = gob.NewDecoder(file).Decode(&decodedMap) i.Error("Error loading history:", err)
if err != nil { return
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)
} }
} }
@ -52,18 +49,16 @@ func (i *InfoBuf) SaveHistory() {
} }
} }
var buf bytes.Buffer file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history"))
err := gob.NewEncoder(&buf).Encode(i.History) if err == nil {
if err != nil { defer file.Close()
screen.TermMessage("Error encoding history: ", err) encoder := gob.NewEncoder(file)
return
}
filename := filepath.Join(config.ConfigDir, "buffers", "history") err = encoder.Encode(i.History)
err = util.SafeWrite(filename, buf.Bytes(), true) if err != nil {
if err != nil { i.Error("Error saving history:", err)
screen.TermMessage("Error saving history: ", err) return
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 represented as a buffer and a message with a style. // It is respresented as a buffer and a message with a style.
type InfoBuf struct { type InfoBuf struct {
*buffer.Buffer *buffer.Buffer
@ -143,12 +143,13 @@ 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
@ -159,8 +160,6 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
break break
} }
} }
i.PromptCallback(resp, false)
} }
// i.PromptCallback = nil // i.PromptCallback = nil
} }

View File

@ -17,6 +17,7 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
"sync"
"time" "time"
"unicode/utf8" "unicode/utf8"
@ -26,6 +27,7 @@ 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 {
@ -125,7 +127,6 @@ 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))
@ -371,8 +372,6 @@ 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))
@ -391,7 +390,6 @@ 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
} }
@ -427,6 +425,7 @@ func importPath() *lua.LTable {
func importFilePath() *lua.LTable { func importFilePath() *lua.LTable {
pkg := L.NewTable() pkg := L.NewTable()
L.SetField(pkg, "Join", luar.New(L, filepath.Join))
L.SetField(pkg, "Abs", luar.New(L, filepath.Abs)) L.SetField(pkg, "Abs", luar.New(L, filepath.Abs))
L.SetField(pkg, "Base", luar.New(L, filepath.Base)) L.SetField(pkg, "Base", luar.New(L, filepath.Base))
L.SetField(pkg, "Clean", luar.New(L, filepath.Clean)) L.SetField(pkg, "Clean", luar.New(L, filepath.Clean))
@ -524,16 +523,21 @@ 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))
@ -541,15 +545,6 @@ 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

@ -6,9 +6,9 @@ import (
"os" "os"
"sync" "sync"
"github.com/micro-editor/tcell/v2"
"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"
) )
// 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,10 +22,6 @@ 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
@ -33,12 +29,6 @@ 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()
@ -127,34 +117,6 @@ 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
@ -172,10 +134,6 @@ func TempStart(screenWasNil bool) {
if !screenWasNil { if !screenWasNil {
Init() Init()
Unlock() Unlock()
if RestartCallback != nil {
RestartCallback()
}
} }
} }
@ -184,13 +142,10 @@ func Init() error {
drawChan = make(chan bool, 8) drawChan = make(chan bool, 8)
// Should we enable true color? // Should we enable true color?
truecolor := config.GetGlobalOption("truecolor").(string) truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
if truecolor == "on" || (truecolor == "auto" && os.Getenv("MICRO_TRUECOLOR") == "1") {
os.Setenv("TCELL_TRUECOLOR", "enable") if !truecolor {
} else if truecolor == "off" {
os.Setenv("TCELL_TRUECOLOR", "disable") os.Setenv("TCELL_TRUECOLOR", "disable")
} else {
// For "auto", tcell already autodetects truecolor by default
} }
var oldTerm string var oldTerm string
@ -232,10 +187,6 @@ func Init() error {
Screen.EnableMouse() Screen.EnableMouse()
} }
for _, r := range rawSeq {
Screen.RegisterRawSeq(r)
}
return nil return nil
} }

View File

@ -78,10 +78,8 @@ 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()
if onExit != nil { jobFunc := JobFunction{onExit, outbuf.String(), userargs}
jobFunc := JobFunction{onExit, outbuf.String(), userargs} Jobs <- jobFunc
Jobs <- jobFunc
}
}() }()
return &Job{proc, stdin} return &Job{proc, stdin}

View File

@ -11,7 +11,6 @@ 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
@ -96,30 +95,28 @@ 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
// micro is killed if the signal is ignored only on Windows, so it is // Instead we trap Ctrl-C to kill the program we're running
// 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)
err = cmd.Start() go func() {
if err == nil { for range c {
err = cmd.Wait() cmd.Process.Kill()
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

@ -5,9 +5,9 @@ import (
"os/exec" "os/exec"
"strconv" "strconv"
"github.com/micro-editor/terminal"
"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"
) )
type TermType int type TermType int

View File

@ -6,9 +6,7 @@ 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"
@ -18,7 +16,6 @@ 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"
@ -43,46 +40,8 @@ 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)
@ -258,56 +217,10 @@ func FSize(f *os.File) int64 {
return fi.Size() return fi.Size()
} }
// IsWordChar returns whether or not a rune is a 'word character' // IsWordChar returns whether or not the string is a 'word character'
// Word characters are defined as numbers, letters or sub-word delimiters // Word characters are defined as numbers, letters, or '_'
func IsWordChar(r rune) bool { func IsWordChar(r rune) bool {
return IsAlphanumeric(r) || IsSubwordDelimiter(r) return unicode.IsLetter(r) || unicode.IsNumber(r) || 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
@ -358,28 +271,6 @@ 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 {
@ -424,7 +315,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 {
@ -446,17 +337,8 @@ func GetModTime(path string) (time.Time, error) {
return info.ModTime(), nil return info.ModTime(), nil
} }
func AppendBackupSuffix(path string) string { // EscapePath replaces every path separator in a given path with a %
return path + ".micro-backup" func EscapePath(path string) string {
}
// 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
@ -465,24 +347,6 @@ func EscapePathLegacy(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{}
@ -499,28 +363,6 @@ 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))
@ -580,9 +422,19 @@ 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 == '.' || IsWordChar(c) return c == '.' || !IsNonAlphaNumeric(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)
@ -655,77 +507,3 @@ 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,10 +185,6 @@ 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
@ -205,7 +201,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 accordingly // Resize sets this node's size and resizes all children accordlingly
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
@ -439,12 +435,11 @@ func (n *Node) VSplit(right bool) uint64 {
} }
// unsplits the child of a split // unsplits the child of a split
func (n *Node) unsplit(i int) { func (n *Node) unsplit(i int, h bool) {
copy(n.children[i:], n.children[i+1:]) copy(n.children[i:], n.children[i+1:])
n.children[len(n.children)-1] = nil n.children[len(n.children)-1] = nil
n.children = n.children[:len(n.children)-1] n.children = n.children[:len(n.children)-1]
h := n.Kind == STVert
nonrs, numr := n.getResizeInfo(h) nonrs, numr := n.getResizeInfo(h)
if numr == 0 { if numr == 0 {
// This means that this was the last child // This means that this was the last child
@ -471,62 +466,18 @@ func (n *Node) Unsplit() bool {
ind = i ind = i
} }
} }
n.parent.unsplit(ind) if n.parent.Kind == STVert {
n.parent.unsplit(ind, true)
} else {
n.parent.unsplit(ind, false)
}
if n.parent.IsLeaf() { if n.parent.IsLeaf() {
return n.parent.Unsplit() return n.parent.Unsplit()
} }
n.parent.flatten()
return true return true
} }
// flattens the tree by removing unnecessary intermediate parents that have only one child
// and handles the side effect of it
func (n *Node) flatten() {
if n.parent == nil || len(n.children) != 1 {
return
}
ind := 0
for i, c := range n.parent.children {
if c.id == n.id {
ind = i
}
}
// Replace current node with its child node to remove chained parent
successor := n.children[0]
n.parent.children[ind] = successor
successor.parent = n.parent
// Maintain the tree in a consistent state: any child node's kind (horiz vs vert)
// should be the opposite of its parent's kind.
if successor.IsLeaf() {
successor.Kind = n.Kind
} else {
// If the successor node has children, that means it is a chained parent as well.
// Therefore it can be replaced by its own children.
origsize := len(n.parent.children)
// Let's say we have 5 children and want to replace [2] with its children [a] [b] [c]
// [0] [1] [2] [3] [4] --> [0] [1] [a] [b] [c] [3] [4]
// insertcount will be `3 - 1 = 2` in this case
insertcount := len(successor.children) - 1
n.parent.children = append(n.parent.children, make([]*Node, insertcount)...)
copy(n.parent.children[ind+insertcount+1:], n.parent.children[ind+1:origsize])
copy(n.parent.children[ind:], successor.children)
for i := 0; i < len(successor.children); i++ {
n.parent.children[ind+i].parent = n.parent
}
}
// Update propW and propH since the parent of the children has been updated,
// so the children have new siblings
n.parent.markSizes()
}
// String returns the string form of the node and all children (used for debugging) // String returns the string form of the node and all children (used for debugging)
func (n *Node) String() string { func (n *Node) String() string {
var strf func(n *Node, ident int) string var strf func(n *Node, ident int) string

18
pkg/highlight/ftdetect.go Normal file
View File

@ -0,0 +1,18 @@
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,9 +51,25 @@ 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
@ -61,8 +77,6 @@ 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
@ -162,7 +176,7 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
if curRegion.group == curRegion.limitGroup || p.group == curRegion.limitGroup { if curRegion.group == curRegion.limitGroup || p.group == curRegion.limitGroup {
matches := findAllIndex(p.regex, line) matches := findAllIndex(p.regex, line)
for _, m := range matches { for _, m := range matches {
if (endLoc == nil) || (m[0] < endLoc[0]) { if ((endLoc == nil) || (m[0] < endLoc[0])) {
for i := m[0]; i < m[1]; i++ { for i := m[0]; i < m[1]; i++ {
fullHighlights[i] = p.group fullHighlights[i] = p.group
} }
@ -289,13 +303,7 @@ 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++ { for i := 0; i < input.LinesNum(); i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
line := input.LineBytes(i) line := input.LineBytes(i)
// highlights := make(LineMatch) // highlights := make(LineMatch)
@ -308,7 +316,6 @@ func (h *Highlighter) HighlightStates(input LineStates) {
curState := h.lastRegion curState := h.lastRegion
input.SetState(i, curState) input.SetState(i, curState)
input.Unlock()
} }
} }
@ -317,9 +324,7 @@ 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
} }
@ -334,7 +339,6 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int)
} }
input.SetMatch(i, match) input.SetMatch(i, match)
input.Unlock()
} }
} }
@ -346,19 +350,9 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
h.lastRegion = nil h.lastRegion = nil
if startline > 0 { if startline > 0 {
input.Lock() h.lastRegion = input.State(startline - 1)
if startline-1 < input.LinesNum() {
h.lastRegion = input.State(startline - 1)
}
input.Unlock()
} }
for i := startline; ; i++ { for i := startline; i < input.LinesNum(); i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
line := input.LineBytes(i) line := input.LineBytes(i)
// highlights := make(LineMatch) // highlights := make(LineMatch)
@ -372,7 +366,6 @@ 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
@ -384,9 +377,6 @@ 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,28 +33,27 @@ 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
FileNameRegex *regexp.Regexp FtDetect [2]*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 {
FNameRegexStr string `yaml:"filename"` FNameRgx string `yaml:"filename"`
HeaderRegexStr string `yaml:"header"` HeaderRgx 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
@ -98,24 +97,20 @@ 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) < 4 { if len(lines) < 3 {
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])
fnameRegexStr := string(lines[1]) fnameRgx := string(lines[1])
headerRegexStr := string(lines[2]) headerRgx := string(lines[2])
signatureRegexStr := string(lines[3])
if fnameRegexStr != "" { if fnameRgx != "" {
header.FileNameRegex, err = regexp.Compile(fnameRegexStr) header.FtDetect[0], err = regexp.Compile(fnameRgx)
} }
if err == nil && headerRegexStr != "" { if err == nil && headerRgx != "" {
header.HeaderRegex, err = regexp.Compile(headerRegexStr) header.FtDetect[1], err = regexp.Compile(headerRgx)
}
if err == nil && signatureRegexStr != "" {
header.SignatureRegex, err = regexp.Compile(signatureRegexStr)
} }
if err != nil { if err != nil {
@ -137,14 +132,11 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
header := new(Header) header := new(Header)
header.FileType = hdrYaml.FileType header.FileType = hdrYaml.FileType
if hdrYaml.Detect.FNameRegexStr != "" { if hdrYaml.Detect.FNameRgx != "" {
header.FileNameRegex, err = regexp.Compile(hdrYaml.Detect.FNameRegexStr) header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx)
} }
if err == nil && hdrYaml.Detect.HeaderRegexStr != "" { if err == nil && hdrYaml.Detect.HeaderRgx != "" {
header.HeaderRegex, err = regexp.Compile(hdrYaml.Detect.HeaderRegexStr) header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx)
}
if err == nil && hdrYaml.Detect.SignatureRegexStr != "" {
header.SignatureRegex, err = regexp.Compile(hdrYaml.Detect.SignatureRegexStr)
} }
if err != nil { if err != nil {
@ -154,37 +146,6 @@ 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() {
@ -209,19 +170,11 @@ 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
} }
@ -238,12 +191,12 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
} }
}() }()
src := f.yamlSrc rules := f.yamlSrc
s = new(Def) s = new(Def)
s.Header = header s.Header = header
for k, v := range src { for k, v := range rules {
if k == "rules" { if k == "rules" {
inputRules := v.([]interface{}) inputRules := v.([]interface{})
@ -256,11 +209,6 @@ 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
} }
@ -355,10 +303,6 @@ 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 {
@ -412,56 +356,30 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
r.group = groupNum r.group = groupNum
r.parent = prevRegion r.parent = prevRegion
// start is mandatory r.start, err = regexp.Compile(regionInfo["start"].(string))
if start, ok := regionInfo["start"]; ok {
start := start.(string)
if start == "" {
return nil, fmt.Errorf("Empty start in %s", group)
}
r.start, err = regexp.Compile(start) if err != nil {
if err != nil { return nil, err
return nil, err
}
} else {
return nil, fmt.Errorf("Missing start in %s", group)
} }
// end is mandatory r.end, err = regexp.Compile(regionInfo["end"].(string))
if end, ok := regionInfo["end"]; ok {
end := end.(string)
if end == "" {
return nil, fmt.Errorf("Empty end in %s", group)
}
r.end, err = regexp.Compile(end) if err != nil {
if err != nil { return nil, err
return nil, err
}
} else {
return nil, fmt.Errorf("Missing end in %s", group)
} }
// skip is optional // skip is optional
if skip, ok := regionInfo["skip"]; ok { if _, ok := regionInfo["skip"]; ok {
skip := skip.(string) r.skip, err = regexp.Compile(regionInfo["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 groupStr, ok := regionInfo["limit-group"]; ok { if _, ok := regionInfo["limit-group"]; ok {
groupStr := groupStr.(string) groupStr := regionInfo["limit-group"].(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
@ -476,17 +394,10 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
r.limitGroup = r.group r.limitGroup = r.group
} }
// rules are optional r.rules, err = parseRules(regionInfo["rules"].([]interface{}), r)
if rules, ok := regionInfo["rules"]; ok {
r.rules, err = parseRules(rules.([]interface{}), r)
if err != nil {
return nil, err
}
}
if r.rules == nil { if err != nil {
// allow empty rules return nil, err
r.rules = &rules{}
} }
return r, nil return r, nil

View File

@ -28,6 +28,3 @@ 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,6 +26,3 @@ 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,6 +42,3 @@ 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,6 +38,3 @@ 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,6 +29,3 @@ 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 +1,31 @@
include "monokai" color-link default "#F8F8F2,#282828"
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,7 +43,3 @@ 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,6 +33,3 @@ 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,6 +33,3 @@ 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,6 +33,3 @@ 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,6 +24,3 @@ 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,6 +24,3 @@ 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,6 +24,3 @@ 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,6 +21,3 @@ 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,7 +20,6 @@ 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"
@ -31,6 +30,3 @@ 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,6 +23,3 @@ 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,6 +29,3 @@ 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,6 +34,3 @@ 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,10 +28,6 @@ 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,7 +10,6 @@ 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"
@ -28,6 +27,3 @@ 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,6 +26,3 @@ 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,6 +25,3 @@ 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,6 +24,3 @@ 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-link match-brace "230,22"
color-link tab-error "210"
color-link trailingws "210"

View File

@ -35,6 +35,3 @@ 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,6 +25,3 @@ 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"

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