mirror of
https://github.com/zyedidia/micro.git
synced 2025-06-19 07:15:34 -04:00
Merge branch 'master' into update-justfile-syntax
This commit is contained in:
commit
6474fd03da
46
.github/workflows/nightly.yaml
vendored
Normal file
46
.github/workflows/nightly.yaml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: Nightly builds
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # Allows manual trigger
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
jobs:
|
||||||
|
nightly:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.23.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Setup
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: tools/cross-compile.sh nightly
|
||||||
|
|
||||||
|
- name: Tag
|
||||||
|
uses: rickstaa/action-create-tag@v1
|
||||||
|
with:
|
||||||
|
tag: nightly
|
||||||
|
force_push_tag: true
|
||||||
|
message: "Pre-release nightly"
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: nightly
|
||||||
|
tag_name: nightly
|
||||||
|
files: binaries/*
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm -rf binaries
|
36
.github/workflows/release.yaml
vendored
Normal file
36
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
name: Release builds
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # Allows manual trigger
|
||||||
|
# push:
|
||||||
|
# tags:
|
||||||
|
# - 'v*.*.*' # automatically react on semantic versioned tags
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.23.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Setup
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: tools/cross-compile.sh
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: binaries/*
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
run: rm -rf binaries
|
11
.github/workflows/test.yaml
vendored
11
.github/workflows/test.yaml
vendored
@ -4,14 +4,19 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.19.x, 1.23.x]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
- uses: actions/checkout@v3
|
cache: false
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
@ -430,7 +430,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
|
|
||||||
github.com/gdamore/tcell/LICENSE
|
github.com/gdamore/tcell/LICENSE
|
||||||
================
|
================
|
||||||
github.com/zyedidia/tcell/LICENSE (fork)
|
github.com/micro-editor/tcell/LICENSE (fork)
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
||||||
@ -1048,7 +1048,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|||||||
|
|
||||||
github.com/flynn/json5/LICENSE
|
github.com/flynn/json5/LICENSE
|
||||||
================
|
================
|
||||||
github.com/zyedidia/json5/LICENSE (fork)
|
github.com/micro-editor/json5/LICENSE (fork)
|
||||||
================
|
================
|
||||||
|
|
||||||
Decoder code based on package encoding/json from the Go language.
|
Decoder code based on package encoding/json from the Go language.
|
||||||
@ -1108,7 +1108,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
github.com/james4k/terminal/LICENSE
|
github.com/james4k/terminal/LICENSE
|
||||||
================
|
================
|
||||||
github.com/zyedidia/terminal/LICENSE (fork)
|
github.com/micro-editor/terminal/LICENSE (fork)
|
||||||
================
|
================
|
||||||
|
|
||||||
Copyright (C) 2013 James Gray
|
Copyright (C) 2013 James Gray
|
||||||
|
23
Makefile
23
Makefile
@ -5,24 +5,31 @@ VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH
|
|||||||
HASH = $(shell git rev-parse --short HEAD)
|
HASH = $(shell git rev-parse --short HEAD)
|
||||||
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||||
go run tools/build-date.go)
|
go run tools/build-date.go)
|
||||||
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
|
|
||||||
GOARCH=$(shell go env GOHOSTARCH) \
|
|
||||||
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
|
|
||||||
GOBIN ?= $(shell go env GOPATH)/bin
|
GOBIN ?= $(shell go env GOPATH)/bin
|
||||||
GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)'
|
GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)'
|
||||||
DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON
|
DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON
|
||||||
VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/'
|
VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/'
|
||||||
|
CGO_ENABLED := $(if $(CGO_ENABLED),$(CGO_ENABLED),0)
|
||||||
|
|
||||||
|
ADDITIONAL_GO_LINKER_FLAGS := ""
|
||||||
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
|
ifeq ($(GOHOSTOS), darwin)
|
||||||
|
# Native darwin resp. macOS builds need external and dynamic linking
|
||||||
|
ADDITIONAL_GO_LINKER_FLAGS += $(shell GOOS=$(GOHOSTOS) \
|
||||||
|
GOARCH=$(shell go env GOHOSTARCH) \
|
||||||
|
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
|
||||||
|
CGO_ENABLED = 1
|
||||||
|
endif
|
||||||
|
|
||||||
build: generate build-quick
|
build: generate build-quick
|
||||||
|
|
||||||
build-quick:
|
build-quick:
|
||||||
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||||
|
|
||||||
build-dbg:
|
build-dbg:
|
||||||
go build -trimpath -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
|
CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "$(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
|
||||||
|
|
||||||
build-tags: fetch-tags generate
|
build-tags: fetch-tags build
|
||||||
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
|
||||||
|
|
||||||
build-all: build
|
build-all: build
|
||||||
|
|
||||||
@ -32,7 +39,7 @@ install: generate
|
|||||||
install-all: install
|
install-all: install
|
||||||
|
|
||||||
fetch-tags:
|
fetch-tags:
|
||||||
git fetch --tags
|
git fetch --tags --force
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime
|
GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime
|
||||||
|
72
README.md
72
README.md
@ -18,25 +18,9 @@ Here is a picture of micro editing its source code.
|
|||||||

|

|
||||||
|
|
||||||
To see more screenshots of micro, showcasing some of the default color schemes, see [here](https://micro-editor.github.io).
|
To see more screenshots of micro, showcasing some of the default color schemes, see [here](https://micro-editor.github.io).
|
||||||
|
|
||||||
You can also check out the website for Micro at https://micro-editor.github.io.
|
You can also check out the website for Micro at https://micro-editor.github.io.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Features](#features)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Prebuilt binaries](#pre-built-binaries)
|
|
||||||
- [Package Managers](#package-managers)
|
|
||||||
- [Building from source](#building-from-source)
|
|
||||||
- [Fully static binary](#fully-static-binary)
|
|
||||||
- [macOS terminal](#macos-terminal)
|
|
||||||
- [Linux clipboard support](#linux-clipboard-support)
|
|
||||||
- [Colors and syntax highlighting](#colors-and-syntax-highlighting)
|
|
||||||
- [Cygwin, Mingw, Plan9](#cygwin-mingw-plan9)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [Documentation and Help](#documentation-and-help)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
|
|
||||||
- - -
|
- - -
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -68,6 +52,7 @@ You can also check out the website for Micro at https://micro-editor.github.io.
|
|||||||
- Small and simple.
|
- Small and simple.
|
||||||
- Easily configurable.
|
- Easily configurable.
|
||||||
- Macros.
|
- Macros.
|
||||||
|
- Smart highlighting of trailing whitespace and tab vs space errors.
|
||||||
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
|
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@ -87,7 +72,7 @@ Pre-built binaries are distributed in [releases](https://github.com/zyedidia/mic
|
|||||||
|
|
||||||
To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`.
|
To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`.
|
||||||
|
|
||||||
#### Quick-install script
|
#### Third-party quick-install script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl https://getmic.ro | bash
|
curl https://getmic.ro | bash
|
||||||
@ -137,24 +122,33 @@ for other operating systems. These packages are not guaranteed to be up-to-date.
|
|||||||
|
|
||||||
<!-- * `apt install micro` (Ubuntu 20.04 `focal`, and Debian `unstable | testing | buster-backports`). At the moment, this package (2.0.1-1) is outdated and has a known bug where debug mode is enabled. -->
|
<!-- * `apt install micro` (Ubuntu 20.04 `focal`, and Debian `unstable | testing | buster-backports`). At the moment, this package (2.0.1-1) is outdated and has a known bug where debug mode is enabled. -->
|
||||||
|
|
||||||
* Linux: Available in distro-specific package managers.
|
* Linux:
|
||||||
* `dnf install micro` (Fedora).
|
* distro-specific package managers:
|
||||||
* `apt install micro` (Ubuntu and Debian).
|
* `dnf install micro` (Fedora).
|
||||||
* `pacman -S micro` (Arch Linux).
|
* `apt install micro` (Ubuntu and Debian).
|
||||||
* `emerge app-editors/micro` (Gentoo).
|
* `pacman -S micro` (Arch Linux).
|
||||||
* `zypper install micro-editor` (SUSE)
|
* `emerge app-editors/micro` (Gentoo).
|
||||||
* `eopkg install micro` (Solus).
|
* `zypper install micro-editor` (SUSE)
|
||||||
* `pacstall -I micro` (Pacstall).
|
* `eopkg install micro` (Solus).
|
||||||
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
|
* `pacstall -I micro` (Pacstall).
|
||||||
* Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop).
|
* `apt-get install micro` (ALT Linux)
|
||||||
|
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
|
||||||
|
* distro-agnostic package managers:
|
||||||
|
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
|
||||||
|
* `flox install micro` (with [Flox](https://flox.dev))
|
||||||
|
* Windows: [Chocolatey](https://chocolatey.org), [Scoop](https://scoop.sh/) and [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/winget/).
|
||||||
* `choco install micro`.
|
* `choco install micro`.
|
||||||
* `scoop install micro`.
|
* `scoop install micro`.
|
||||||
|
* `winget install zyedidia.micro`
|
||||||
* OpenBSD: Available in the ports tree and also available as a binary package.
|
* OpenBSD: Available in the ports tree and also available as a binary package.
|
||||||
* `pkd_add -v micro`.
|
* `pkg_add -v micro`.
|
||||||
* NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](http://www.pkgsrc.org/)-current:
|
* NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](http://www.pkgsrc.org/)-current:
|
||||||
* `pkg_add micro`
|
* `pkg_add micro`
|
||||||
* macOS with [MacPorts](https://www.macports.org):
|
* macOS: Available in package managers.
|
||||||
* `sudo port install micro`
|
* `sudo port install micro` (with [MacPorts](https://www.macports.org))
|
||||||
|
* `brew install micro` (with [Homebrew](https://brew.sh/))
|
||||||
|
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
|
||||||
|
* `flox install micro` (with [Flox](https://flox.dev))
|
||||||
|
|
||||||
**Note for Linux desktop environments:**
|
**Note for Linux desktop environments:**
|
||||||
|
|
||||||
@ -168,7 +162,7 @@ Without these tools installed, micro will use an internal clipboard for copy and
|
|||||||
|
|
||||||
If your operating system does not have a binary release, but does run Go, you can build from source.
|
If your operating system does not have a binary release, but does run Go, you can build from source.
|
||||||
|
|
||||||
Make sure that you have Go version 1.16 or greater and Go modules are enabled.
|
Make sure that you have Go version 1.19 or greater and Go modules are enabled.
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/zyedidia/micro
|
git clone https://github.com/zyedidia/micro
|
||||||
@ -186,16 +180,20 @@ You can install directly with `go get` (`go get github.com/zyedidia/micro/cmd/mi
|
|||||||
recommended because it doesn't build micro with version information (necessary for the plugin manager),
|
recommended because it doesn't build micro with version information (necessary for the plugin manager),
|
||||||
and doesn't disable debug mode.
|
and doesn't disable debug mode.
|
||||||
|
|
||||||
### Fully static binary
|
### Fully static or dynamically linked binary
|
||||||
|
|
||||||
By default, the micro binary will dynamically link with core system libraries (this is generally
|
By default, the micro binary is linked statically to increase the portability of the prebuilt binaries.
|
||||||
recommended for security and portability). However, there is a fully static prebuilt binary that
|
This behavior can simply be overriden by providing `CGO_ENABLED=1` to the build target.
|
||||||
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
|
|
||||||
|
|
||||||
```
|
```
|
||||||
CGO_ENABLED=0 make build
|
CGO_ENABLED=1 make build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Afterwards the micro binary will dynamically link with the present core system libraries.
|
||||||
|
|
||||||
|
**Note for Mac:**
|
||||||
|
Native macOS builds are done with `CGO_ENABLED=1` forced set to support adding the "Information Property List" in the linker step.
|
||||||
|
|
||||||
### macOS terminal
|
### macOS terminal
|
||||||
|
|
||||||
If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Presets...`, and select `Esc+` for `Left Option Key` in the same menu. The newest versions also support true color.
|
If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Presets...`, and select `Esc+` for `Left Option Key` in the same menu. The newest versions also support true color.
|
||||||
|
@ -12,5 +12,4 @@ Keywords=text;editor;syntax;terminal;
|
|||||||
Exec=micro %F
|
Exec=micro %F
|
||||||
StartupNotify=false
|
StartupNotify=false
|
||||||
Terminal=true
|
Terminal=true
|
||||||
NoDisplay=true
|
|
||||||
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;
|
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;
|
||||||
|
@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func shouldContinue() bool {
|
func shouldContinue() bool {
|
||||||
@ -39,7 +40,16 @@ func CleanConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Cleaning default settings")
|
fmt.Println("Cleaning default settings")
|
||||||
config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
|
||||||
|
settingsFile := filepath.Join(config.ConfigDir, "settings.json")
|
||||||
|
err := config.WriteSettings(settingsFile)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("Error writing settings.json file: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// detect unused options
|
// detect unused options
|
||||||
var unusedOptions []string
|
var unusedOptions []string
|
||||||
@ -67,16 +77,20 @@ func CleanConfig() {
|
|||||||
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
|
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json"))
|
fmt.Printf("These options will be removed from %s\n", settingsFile)
|
||||||
|
|
||||||
if shouldContinue() {
|
if shouldContinue() {
|
||||||
for _, s := range unusedOptions {
|
for _, s := range unusedOptions {
|
||||||
delete(config.GlobalSettings, s)
|
delete(config.GlobalSettings, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
err := config.OverwriteSettings(settingsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error writing settings.json file: " + err.Error())
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("Error overwriting settings.json file: " + err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Removed unused options")
|
fmt.Println("Removed unused options")
|
||||||
@ -85,12 +99,13 @@ func CleanConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detect incorrectly formatted buffer/ files
|
// detect incorrectly formatted buffer/ files
|
||||||
files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers"))
|
buffersPath := filepath.Join(config.ConfigDir, "buffers")
|
||||||
|
files, err := os.ReadDir(buffersPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var badFiles []string
|
var badFiles []string
|
||||||
var buffer buffer.SerializedBuffer
|
var buffer buffer.SerializedBuffer
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
fname := filepath.Join(config.ConfigDir, "buffers", f.Name())
|
fname := filepath.Join(buffersPath, f.Name())
|
||||||
file, e := os.Open(fname)
|
file, e := os.Open(fname)
|
||||||
|
|
||||||
if e == nil {
|
if e == nil {
|
||||||
@ -105,9 +120,9 @@ func CleanConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(badFiles) > 0 {
|
if len(badFiles) > 0 {
|
||||||
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers"))
|
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), buffersPath)
|
||||||
fmt.Println("These files store cursor and undo history.")
|
fmt.Println("These files store cursor and undo history.")
|
||||||
fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers"))
|
fmt.Printf("Removing badly formatted files in %s\n", buffersPath)
|
||||||
|
|
||||||
if shouldContinue() {
|
if shouldContinue() {
|
||||||
removed := 0
|
removed := 0
|
||||||
|
@ -18,7 +18,7 @@ func (NullWriter) Write(data []byte) (n int, err error) {
|
|||||||
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
||||||
func InitLog() {
|
func InitLog() {
|
||||||
if util.Debug == "ON" {
|
if util.Debug == "ON" {
|
||||||
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, util.FileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error opening file: %v", err)
|
log.Fatalf("error opening file: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
luar "layeh.com/gopher-luar"
|
luar "layeh.com/gopher-luar"
|
||||||
@ -47,14 +48,18 @@ func luaImportMicro() *lua.LTable {
|
|||||||
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
|
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
|
||||||
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
|
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
|
||||||
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
|
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
|
||||||
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() action.Pane {
|
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() *action.BufPane {
|
||||||
return action.MainTab().CurPane()
|
return action.MainTab().CurPane()
|
||||||
}))
|
}))
|
||||||
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab))
|
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab))
|
||||||
ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList {
|
ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList {
|
||||||
return action.Tabs
|
return action.Tabs
|
||||||
}))
|
}))
|
||||||
ulua.L.SetField(pkg, "Lock", luar.New(ulua.L, &ulua.Lock))
|
ulua.L.SetField(pkg, "After", luar.New(ulua.L, func(t time.Duration, f func()) {
|
||||||
|
time.AfterFunc(t, func() {
|
||||||
|
timerChan <- f
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
return pkg
|
return pkg
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
@ -23,17 +23,13 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/clipboard"
|
"github.com/zyedidia/micro/v2/internal/clipboard"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/shell"
|
"github.com/zyedidia/micro/v2/internal/shell"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Event channel
|
|
||||||
autosave chan bool
|
|
||||||
|
|
||||||
// Command line flags
|
// Command line flags
|
||||||
flagVersion = flag.Bool("version", false, "Show the version number and information")
|
flagVersion = flag.Bool("version", false, "Show the version number and information")
|
||||||
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
|
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
|
||||||
@ -44,8 +40,9 @@ var (
|
|||||||
flagClean = flag.Bool("clean", false, "Clean configuration directory")
|
flagClean = flag.Bool("clean", false, "Clean configuration directory")
|
||||||
optionFlags map[string]*string
|
optionFlags map[string]*string
|
||||||
|
|
||||||
sigterm chan os.Signal
|
sighup chan os.Signal
|
||||||
sighup chan os.Signal
|
|
||||||
|
timerChan chan func()
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitFlags() {
|
func InitFlags() {
|
||||||
@ -68,7 +65,7 @@ func InitFlags() {
|
|||||||
fmt.Println("-version")
|
fmt.Println("-version")
|
||||||
fmt.Println(" \tShow the version number and information")
|
fmt.Println(" \tShow the version number and information")
|
||||||
|
|
||||||
fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n")
|
fmt.Print("\nMicro's plugins can be managed at the command line with the following commands.\n")
|
||||||
fmt.Println("-plugin install [PLUGIN]...")
|
fmt.Println("-plugin install [PLUGIN]...")
|
||||||
fmt.Println(" \tInstall plugin(s)")
|
fmt.Println(" \tInstall plugin(s)")
|
||||||
fmt.Println("-plugin remove [PLUGIN]...")
|
fmt.Println("-plugin remove [PLUGIN]...")
|
||||||
@ -102,7 +99,7 @@ func InitFlags() {
|
|||||||
fmt.Println("Version:", util.Version)
|
fmt.Println("Version:", util.Version)
|
||||||
fmt.Println("Commit hash:", util.CommitHash)
|
fmt.Println("Commit hash:", util.CommitHash)
|
||||||
fmt.Println("Compiled on", util.CompileDate)
|
fmt.Println("Compiled on", util.CompileDate)
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flagOptions {
|
if *flagOptions {
|
||||||
@ -118,7 +115,7 @@ func InitFlags() {
|
|||||||
fmt.Printf("-%s value\n", k)
|
fmt.Printf("-%s value\n", k)
|
||||||
fmt.Printf(" \tDefault value: '%v'\n", v)
|
fmt.Printf(" \tDefault value: '%v'\n", v)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.Debug == "OFF" && *flagDebug {
|
if util.Debug == "OFF" && *flagDebug {
|
||||||
@ -139,7 +136,7 @@ func DoPluginFlags() {
|
|||||||
CleanConfig()
|
CleanConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +209,7 @@ func LoadInput(args []string) []*buffer.Buffer {
|
|||||||
// Option 2
|
// Option 2
|
||||||
// The input is not a terminal, so something is being piped in
|
// The input is not a terminal, so something is being piped in
|
||||||
// and we should read from stdin
|
// and we should read from stdin
|
||||||
input, err = ioutil.ReadAll(os.Stdin)
|
input, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error reading from stdin: ", err)
|
screen.TermMessage("Error reading from stdin: ", err)
|
||||||
input = []byte{}
|
input = []byte{}
|
||||||
@ -226,12 +223,55 @@ func LoadInput(args []string) []*buffer.Buffer {
|
|||||||
return buffers
|
return buffers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkBackup(name string) error {
|
||||||
|
target := filepath.Join(config.ConfigDir, name)
|
||||||
|
backup := util.AppendBackupSuffix(target)
|
||||||
|
if info, err := os.Stat(backup); err == nil {
|
||||||
|
input, err := os.ReadFile(backup)
|
||||||
|
if err == nil {
|
||||||
|
t := info.ModTime()
|
||||||
|
msg := fmt.Sprintf(buffer.BackupMsg, target, t.Format("Mon Jan _2 at 15:04, 2006"), backup)
|
||||||
|
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
|
||||||
|
|
||||||
|
if choice%3 == 0 {
|
||||||
|
// recover
|
||||||
|
err := os.WriteFile(target, input, util.FileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Remove(backup)
|
||||||
|
} else if choice%3 == 1 {
|
||||||
|
// delete
|
||||||
|
return os.Remove(backup)
|
||||||
|
} else if choice%3 == 2 {
|
||||||
|
// abort
|
||||||
|
return errors.New("Aborted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exit(rc int) {
|
||||||
|
for _, b := range buffer.OpenBuffers {
|
||||||
|
if !b.Modified() {
|
||||||
|
b.Fini()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if screen.Screen != nil {
|
||||||
|
screen.Screen.Fini()
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(rc)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if util.Stdout.Len() > 0 {
|
if util.Stdout.Len() > 0 {
|
||||||
fmt.Fprint(os.Stdout, util.Stdout.String())
|
fmt.Fprint(os.Stdout, util.Stdout.String())
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -256,7 +296,15 @@ func main() {
|
|||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.InitRuntimeFiles()
|
config.InitRuntimeFiles(true)
|
||||||
|
config.InitPlugins()
|
||||||
|
|
||||||
|
err = checkBackup("settings.json")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
err = config.ReadSettings()
|
err = config.ReadSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
@ -274,7 +322,12 @@ func main() {
|
|||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if err = config.OptionIsValid(k, nativeValue); err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
config.GlobalSettings[k] = nativeValue
|
config.GlobalSettings[k] = nativeValue
|
||||||
|
config.VolatileSettings[k] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +337,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
fmt.Println("Fatal: Micro could not initialize a Screen.")
|
fmt.Println("Fatal: Micro could not initialize a Screen.")
|
||||||
os.Exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
|
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
|
||||||
clipErr := clipboard.Initialize(m)
|
clipErr := clipboard.Initialize(m)
|
||||||
@ -303,7 +356,7 @@ func main() {
|
|||||||
for _, b := range buffer.OpenBuffers {
|
for _, b := range buffer.OpenBuffers {
|
||||||
b.Backup()
|
b.Backup()
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -312,6 +365,12 @@ func main() {
|
|||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = checkBackup("bindings.json")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
action.InitBindings()
|
action.InitBindings()
|
||||||
action.InitCommands()
|
action.InitCommands()
|
||||||
|
|
||||||
@ -352,18 +411,20 @@ func main() {
|
|||||||
log.Println(clipErr, " or change 'clipboard' option")
|
log.Println(clipErr, " or change 'clipboard' option")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.StartAutoSave()
|
||||||
if a := config.GetGlobalOption("autosave").(float64); a > 0 {
|
if a := config.GetGlobalOption("autosave").(float64); a > 0 {
|
||||||
config.SetAutoTime(int(a))
|
config.SetAutoTime(a)
|
||||||
config.StartAutoSave()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.Events = make(chan tcell.Event)
|
screen.Events = make(chan tcell.Event)
|
||||||
|
|
||||||
sigterm = make(chan os.Signal, 1)
|
util.Sigterm = make(chan os.Signal, 1)
|
||||||
sighup = make(chan os.Signal, 1)
|
sighup = make(chan os.Signal, 1)
|
||||||
signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
|
signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
|
||||||
signal.Notify(sighup, syscall.SIGHUP)
|
signal.Notify(sighup, syscall.SIGHUP)
|
||||||
|
|
||||||
|
timerChan = make(chan func())
|
||||||
|
|
||||||
// Here is the event loop which runs in a separate thread
|
// Here is the event loop which runs in a separate thread
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@ -414,39 +475,26 @@ func DoEvent() {
|
|||||||
select {
|
select {
|
||||||
case f := <-shell.Jobs:
|
case f := <-shell.Jobs:
|
||||||
// If a new job has finished while running in the background we should execute the callback
|
// If a new job has finished while running in the background we should execute the callback
|
||||||
ulua.Lock.Lock()
|
|
||||||
f.Function(f.Output, f.Args)
|
f.Function(f.Output, f.Args)
|
||||||
ulua.Lock.Unlock()
|
|
||||||
case <-config.Autosave:
|
case <-config.Autosave:
|
||||||
ulua.Lock.Lock()
|
|
||||||
for _, b := range buffer.OpenBuffers {
|
for _, b := range buffer.OpenBuffers {
|
||||||
b.Save()
|
b.AutoSave()
|
||||||
}
|
}
|
||||||
ulua.Lock.Unlock()
|
|
||||||
case <-shell.CloseTerms:
|
case <-shell.CloseTerms:
|
||||||
|
action.Tabs.CloseTerms()
|
||||||
case event = <-screen.Events:
|
case event = <-screen.Events:
|
||||||
case <-screen.DrawChan():
|
case <-screen.DrawChan():
|
||||||
for len(screen.DrawChan()) > 0 {
|
for len(screen.DrawChan()) > 0 {
|
||||||
<-screen.DrawChan()
|
<-screen.DrawChan()
|
||||||
}
|
}
|
||||||
|
case f := <-timerChan:
|
||||||
|
f()
|
||||||
|
case b := <-buffer.BackupCompleteChan:
|
||||||
|
b.RequestedBackup = false
|
||||||
case <-sighup:
|
case <-sighup:
|
||||||
for _, b := range buffer.OpenBuffers {
|
exit(0)
|
||||||
if !b.Modified() {
|
case <-util.Sigterm:
|
||||||
b.Fini()
|
exit(0)
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
case <-sigterm:
|
|
||||||
for _, b := range buffer.OpenBuffers {
|
|
||||||
if !b.Modified() {
|
|
||||||
b.Fini()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if screen.Screen != nil {
|
|
||||||
screen.Screen.Fini()
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, ok := event.(*tcell.EventError); ok {
|
if e, ok := event.(*tcell.EventError); ok {
|
||||||
@ -454,27 +502,25 @@ func DoEvent() {
|
|||||||
|
|
||||||
if e.Err() == io.EOF {
|
if e.Err() == io.EOF {
|
||||||
// shutdown due to terminal closing/becoming inaccessible
|
// shutdown due to terminal closing/becoming inaccessible
|
||||||
for _, b := range buffer.OpenBuffers {
|
exit(0)
|
||||||
if !b.Modified() {
|
|
||||||
b.Fini()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if screen.Screen != nil {
|
|
||||||
screen.Screen.Fini()
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ulua.Lock.Lock()
|
if event != nil {
|
||||||
// if event != nil {
|
_, resize := event.(*tcell.EventResize)
|
||||||
if action.InfoBar.HasPrompt {
|
if resize {
|
||||||
action.InfoBar.HandleEvent(event)
|
action.InfoBar.HandleEvent(event)
|
||||||
} else {
|
action.Tabs.HandleEvent(event)
|
||||||
action.Tabs.HandleEvent(event)
|
} else if action.InfoBar.HasPrompt {
|
||||||
|
action.InfoBar.HandleEvent(event)
|
||||||
|
} else {
|
||||||
|
action.Tabs.HandleEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.RunPluginFn("onAnyEvent")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
ulua.Lock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@ -13,7 +12,7 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var tempDir string
|
var tempDir string
|
||||||
@ -26,7 +25,7 @@ func init() {
|
|||||||
func startup(args []string) (tcell.SimulationScreen, error) {
|
func startup(args []string) (tcell.SimulationScreen, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
tempDir, err = ioutil.TempDir("", "micro_test")
|
tempDir, err = os.MkdirTemp("", "micro_test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -35,7 +34,9 @@ func startup(args []string) (tcell.SimulationScreen, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.InitRuntimeFiles()
|
config.InitRuntimeFiles(true)
|
||||||
|
config.InitPlugins()
|
||||||
|
|
||||||
err = config.ReadSettings()
|
err = config.ReadSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -107,7 +108,10 @@ func handleEvent() {
|
|||||||
if e != nil {
|
if e != nil {
|
||||||
screen.Events <- e
|
screen.Events <- e
|
||||||
}
|
}
|
||||||
DoEvent()
|
|
||||||
|
for len(screen.DrawChan()) > 0 || len(screen.Events) > 0 {
|
||||||
|
DoEvent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
|
func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
|
||||||
@ -149,20 +153,32 @@ func openFile(file string) {
|
|||||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestFile(name string, content string) (string, error) {
|
func findBuffer(file string) *buffer.Buffer {
|
||||||
testf, err := ioutil.TempFile("", name)
|
var buf *buffer.Buffer
|
||||||
|
for _, b := range buffer.OpenBuffers {
|
||||||
|
if b.Path == file {
|
||||||
|
buf = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestFile(t *testing.T, content string) string {
|
||||||
|
f, err := os.CreateTemp(t.TempDir(), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := f.WriteString(content); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := testf.Write([]byte(content)); err != nil {
|
return f.Name()
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := testf.Close(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return testf.Name(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@ -179,25 +195,12 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSimpleEdit(t *testing.T) {
|
func TestSimpleEdit(t *testing.T) {
|
||||||
file, err := createTestFile("micro_simple_edit_test", "base content")
|
file := createTestFile(t, "base content")
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(file)
|
|
||||||
|
|
||||||
openFile(file)
|
openFile(file)
|
||||||
|
|
||||||
var buf *buffer.Buffer
|
if findBuffer(file) == nil {
|
||||||
for _, b := range buffer.OpenBuffers {
|
t.Fatalf("Could not find buffer %s", file)
|
||||||
if b.Path == file {
|
|
||||||
buf = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf == nil {
|
|
||||||
t.Errorf("Could not find buffer %s", file)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||||
@ -215,25 +218,23 @@ func TestSimpleEdit(t *testing.T) {
|
|||||||
|
|
||||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "firstfoobar\nbase content\n", string(data))
|
assert.Equal(t, "firstfoobar\nbase content\n", string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMouse(t *testing.T) {
|
func TestMouse(t *testing.T) {
|
||||||
file, err := createTestFile("micro_mouse_test", "base content")
|
file := createTestFile(t, "base content")
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(file)
|
|
||||||
|
|
||||||
openFile(file)
|
openFile(file)
|
||||||
|
|
||||||
|
if findBuffer(file) == nil {
|
||||||
|
t.Fatalf("Could not find buffer %s", file)
|
||||||
|
}
|
||||||
|
|
||||||
// buffer:
|
// buffer:
|
||||||
// base content
|
// base content
|
||||||
// the selections need to happen at different locations to avoid a double click
|
// the selections need to happen at different locations to avoid a double click
|
||||||
@ -262,10 +263,9 @@ func TestMouse(t *testing.T) {
|
|||||||
// base content
|
// base content
|
||||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
|
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
|
||||||
@ -288,25 +288,23 @@ Ernleȝe test_string æðelen
|
|||||||
`
|
`
|
||||||
|
|
||||||
func TestSearchAndReplace(t *testing.T) {
|
func TestSearchAndReplace(t *testing.T) {
|
||||||
file, err := createTestFile("micro_search_replace_test", srTestStart)
|
file := createTestFile(t, srTestStart)
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(file)
|
|
||||||
|
|
||||||
openFile(file)
|
openFile(file)
|
||||||
|
|
||||||
|
if findBuffer(file) == nil {
|
||||||
|
t.Fatalf("Could not find buffer %s", file)
|
||||||
|
}
|
||||||
|
|
||||||
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
|
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
|
||||||
injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string"))
|
injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string"))
|
||||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||||
|
|
||||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, srTest2, string(data))
|
assert.Equal(t, srTest2, string(data))
|
||||||
@ -319,10 +317,9 @@ func TestSearchAndReplace(t *testing.T) {
|
|||||||
|
|
||||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||||
|
|
||||||
data, err = ioutil.ReadFile(file)
|
data, err = os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, srTest3, string(data))
|
assert.Equal(t, srTest3, string(data))
|
||||||
|
@ -25,6 +25,9 @@
|
|||||||
<category>TextEditor</category>
|
<category>TextEditor</category>
|
||||||
</categories>
|
</categories>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.0.14" date="2024-08-27"/>
|
||||||
|
<release version="2.0.13" date="2023-10-22"/>
|
||||||
|
<release version="2.0.12" date="2023-09-06"/>
|
||||||
<release version="2.0.11" date="2022-08-01"/>
|
<release version="2.0.11" date="2022-08-01"/>
|
||||||
</releases>
|
</releases>
|
||||||
<provides>
|
<provides>
|
||||||
|
@ -170,10 +170,19 @@
|
|||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"matchbrace": {
|
"matchbrace": {
|
||||||
"description": "Whether to underline matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
"description": "Whether to show matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
|
"matchbracestyle": {
|
||||||
|
"description": "Whether to underline or highlight matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"underline",
|
||||||
|
"highlight"
|
||||||
|
],
|
||||||
|
"default": "underline"
|
||||||
|
},
|
||||||
"mkparents": {
|
"mkparents": {
|
||||||
"description": "Whether to create missing directories\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
"description": "Whether to create missing directories\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@ -296,8 +305,8 @@
|
|||||||
},
|
},
|
||||||
"statusline": {
|
"statusline": {
|
||||||
"description": "Whether to display a status line\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
"description": "Whether to display a status line\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||||
"type": "string",
|
"type": "boolean",
|
||||||
"default": "sudo"
|
"default": true
|
||||||
},
|
},
|
||||||
"sucmd": {
|
"sucmd": {
|
||||||
"description": "A super user command\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
"description": "A super user command\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||||
@ -355,4 +364,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
33
go.mod
33
go.mod
@ -5,26 +5,35 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/go-errors/errors v1.0.1
|
github.com/go-errors/errors v1.0.1
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/mattn/go-isatty v0.0.11
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mattn/go-runewidth v0.0.7
|
github.com/mattn/go-runewidth v0.0.16
|
||||||
|
github.com/micro-editor/json5 v1.0.1-micro
|
||||||
|
github.com/micro-editor/tcell/v2 v2.0.11
|
||||||
|
github.com/micro-editor/terminal v0.0.0-20250105114944-ffd0fc59e777
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/sergi/go-diff v1.1.0
|
github.com/sergi/go-diff v1.1.0
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.4.0
|
||||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
github.com/zyedidia/clipper v0.1.1
|
github.com/zyedidia/clipper v0.1.1
|
||||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
|
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
|
||||||
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d
|
golang.org/x/text v0.4.0
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10 // indirect
|
|
||||||
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef
|
|
||||||
golang.org/x/text v0.3.8
|
|
||||||
gopkg.in/yaml.v2 v2.2.8
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
layeh.com/gopher-luar v1.0.7
|
layeh.com/gopher-luar v1.0.11
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/kballard/go-shellquote => github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655
|
require (
|
||||||
|
github.com/creack/pty v1.1.18 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/gdamore/encoding v1.0.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/zyedidia/poller v1.0.1 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
replace github.com/mattn/go-runewidth => github.com/zyedidia/go-runewidth v0.0.12
|
replace github.com/kballard/go-shellquote => github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5
|
||||||
|
|
||||||
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.7
|
replace layeh.com/gopher-luar v1.0.11 => github.com/layeh/gopher-luar v1.0.11
|
||||||
|
|
||||||
go 1.16
|
go 1.19
|
||||||
|
79
go.sum
79
go.sum
@ -19,84 +19,57 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/layeh/gopher-luar v1.0.7 h1:wnfZhYiJM748y1A4qYBfcFeMY9HWbdERny+ZL0f/jWc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/layeh/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
|
github.com/layeh/gopher-luar v1.0.11 h1:ss6t9OtykOiETBScJylSMPhuYAtOmpH5rSX10/wCcis=
|
||||||
|
github.com/layeh/gopher-luar v1.0.11/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5 h1:D7BPnsedXiKo/e8RTFX419/52ICNhU8UKPQGZ/0yiLc=
|
||||||
|
github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5/go.mod h1:zaPgW/fDiW4MUfEwxpC+GB/bhvX44NJaNHmRAC9auHQ=
|
||||||
|
github.com/micro-editor/json5 v1.0.1-micro h1:5Y4MuzhkmW0sQQNPvrIVevIOKi557qsznwjRr4iq1AI=
|
||||||
|
github.com/micro-editor/json5 v1.0.1-micro/go.mod h1:cmlPHZ1JKOXNse0/3zwwKj/GUpzAVkzx4lZDkpHl4q0=
|
||||||
|
github.com/micro-editor/tcell/v2 v2.0.11 h1:USjdpBSmbocx2yPARbY19KcUSj+ZerScrdmBqGjzoX4=
|
||||||
|
github.com/micro-editor/tcell/v2 v2.0.11/go.mod h1:kVYk6NOwYJrboL/7IA7cCupk4o2NzyF/0UMLjeEJN/s=
|
||||||
|
github.com/micro-editor/terminal v0.0.0-20250105114944-ffd0fc59e777 h1:ddEDJwVqnxd8Yxtr6ZlUBBH3Kyf90jLHMJAbmU1bjuA=
|
||||||
|
github.com/micro-editor/terminal v0.0.0-20250105114944-ffd0fc59e777/go.mod h1:soCc8JsxxfCZd3y08pX67F16Bni5L/xtcAnqEswzs44=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8 h1:woqigIZtZUZxws1zZA99nAvuz2mQrxtWsuZSR9c8I/A=
|
|
||||||
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8/go.mod h1:6Yhx5ZJl5942QrNRWLwITArVT9okUXc5c3brgWJMoDc=
|
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8/go.mod h1:6Yhx5ZJl5942QrNRWLwITArVT9okUXc5c3brgWJMoDc=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw=
|
github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw=
|
||||||
github.com/zyedidia/clipper v0.1.1/go.mod h1:7YApPNiiTZTXdKKZG92G50qj6mnWEX975Sdu65J7YpQ=
|
github.com/zyedidia/clipper v0.1.1/go.mod h1:7YApPNiiTZTXdKKZG92G50qj6mnWEX975Sdu65J7YpQ=
|
||||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
|
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
|
||||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
|
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
|
||||||
github.com/zyedidia/go-runewidth v0.0.12 h1:aHWj8qL3aH7caRzoPBJXe1pEaZBXHpKtfTuiBo5p74Q=
|
|
||||||
github.com/zyedidia/go-runewidth v0.0.12/go.mod h1:vF8djYdLmG8BJaUZ4CznFYCJ3pFR8m4B4VinTvTTarU=
|
|
||||||
github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655 h1:Z3RhH6hvcSx7eX6Q/pP6YVsgea/1eMDG99vtWwi3nK4=
|
|
||||||
github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655/go.mod h1:1sTqqO+kcYzZp43M5VsJe1tns9IzlSeC9jB6c2+o/5Y=
|
|
||||||
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d h1:zmDMkh22zXOB7gz8jFaI4GpI7llsPgzm38/jG0UgxjE=
|
|
||||||
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d/go.mod h1:NDJSTTYWivnza6zkRapeX2/LwhKPEMQ7bJxqgDVT78I=
|
|
||||||
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
|
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
|
||||||
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
||||||
github.com/zyedidia/tcell/v2 v2.0.9 h1:FxXRkE62N0GPHES7EMLtp2rteYqC9r1kVid8vJN1kOE=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.9/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10-0.20221007181625-f562052bccb8 h1:53ULv4mmLyQDnqbjVxanckP57WSreWHwTmlLJrJEutY=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10-0.20221007181625-f562052bccb8/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10-0.20230320201625-54f6acdada4a h1:W4TWa++Wk6uRGxZoxr2nPX1TpIEl+Wxv0mTtocG4TYc=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10-0.20230320201625-54f6acdada4a/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10-0.20230831153116-061c5b2c7260 h1:SCAmAacT5BxZsmOFdFy5zwwi6nj1MjA60gydjKdTgXo=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10-0.20230831153116-061c5b2c7260/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10 h1:6fbbYAx/DYc9A//4jU1OeBrxtc9qJxYCZXCtGQbtTWU=
|
|
||||||
github.com/zyedidia/tcell/v2 v2.0.10/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
|
|
||||||
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef h1:LeB4Qs0Tss4r/Qh8pfsTTqagDYHysfKJLYzAH3MVfu0=
|
|
||||||
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef/go.mod h1:zeb8MJdcCObFKVvur3n2B4BANIPuo2Q8r4iiNs9Enx0=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -4,17 +4,18 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/zyedidia/json5"
|
"github.com/micro-editor/json5"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Binder = map[string]func(e Event, action string){
|
var Binder = map[string]func(e Event, action string){
|
||||||
@ -23,9 +24,13 @@ var Binder = map[string]func(e Event, action string){
|
|||||||
"terminal": TermMapEvent,
|
"terminal": TermMapEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeFile(name string, txt []byte) error {
|
||||||
|
return util.SafeWrite(name, txt, false)
|
||||||
|
}
|
||||||
|
|
||||||
func createBindingsIfNotExist(fname string) {
|
func createBindingsIfNotExist(fname string) {
|
||||||
if _, e := os.Stat(fname); os.IsNotExist(e) {
|
if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) {
|
||||||
ioutil.WriteFile(fname, []byte("{}"), 0644)
|
writeFile(fname, []byte("{}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +42,7 @@ func InitBindings() {
|
|||||||
createBindingsIfNotExist(filename)
|
createBindingsIfNotExist(filename)
|
||||||
|
|
||||||
if _, e := os.Stat(filename); e == nil {
|
if _, e := os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
||||||
return
|
return
|
||||||
@ -88,6 +93,10 @@ func BindKey(k, v string, bind func(e Event, a string)) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(k, "\x1b") {
|
||||||
|
screen.RegisterRawSeq(k)
|
||||||
|
}
|
||||||
|
|
||||||
bind(event, v)
|
bind(event, v)
|
||||||
|
|
||||||
// switch e := event.(type) {
|
// switch e := event.(type) {
|
||||||
@ -153,7 +162,6 @@ modSearch:
|
|||||||
k = k[5:]
|
k = k[5:]
|
||||||
modifiers |= tcell.ModShift
|
modifiers |= tcell.ModShift
|
||||||
case strings.HasPrefix(k, "\x1b"):
|
case strings.HasPrefix(k, "\x1b"):
|
||||||
screen.Screen.RegisterRawSeq(k)
|
|
||||||
return RawEvent{
|
return RawEvent{
|
||||||
esc: k,
|
esc: k,
|
||||||
}, true
|
}, true
|
||||||
@ -173,39 +181,35 @@ modSearch:
|
|||||||
// see if the key is in bindingKeys with the Ctrl prefix.
|
// see if the key is in bindingKeys with the Ctrl prefix.
|
||||||
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
|
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
|
||||||
if code, ok := keyEvents["Ctrl"+k]; ok {
|
if code, ok := keyEvents["Ctrl"+k]; ok {
|
||||||
var r tcell.Key
|
|
||||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
|
||||||
if code < 256 && code != 27 {
|
|
||||||
r = code
|
|
||||||
}
|
|
||||||
// It is, we're done.
|
|
||||||
return KeyEvent{
|
return KeyEvent{
|
||||||
code: code,
|
code: code,
|
||||||
mod: modifiers,
|
mod: modifiers,
|
||||||
r: rune(r),
|
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if we can find the key in bindingKeys
|
// See if we can find the key in bindingKeys
|
||||||
if code, ok := keyEvents[k]; ok {
|
if code, ok := keyEvents[k]; ok {
|
||||||
var r tcell.Key
|
|
||||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
|
||||||
if code < 256 && code != 27 {
|
|
||||||
r = code
|
|
||||||
}
|
|
||||||
return KeyEvent{
|
return KeyEvent{
|
||||||
code: code,
|
code: code,
|
||||||
mod: modifiers,
|
mod: modifiers,
|
||||||
r: rune(r),
|
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mstate MouseState = MousePress
|
||||||
|
if strings.HasSuffix(k, "Drag") {
|
||||||
|
k = k[:len(k)-4]
|
||||||
|
mstate = MouseDrag
|
||||||
|
} else if strings.HasSuffix(k, "Release") {
|
||||||
|
k = k[:len(k)-7]
|
||||||
|
mstate = MouseRelease
|
||||||
|
}
|
||||||
// See if we can find the key in bindingMouse
|
// See if we can find the key in bindingMouse
|
||||||
if code, ok := mouseEvents[k]; ok {
|
if code, ok := mouseEvents[k]; ok {
|
||||||
return MouseEvent{
|
return MouseEvent{
|
||||||
btn: code,
|
btn: code,
|
||||||
mod: modifiers,
|
mod: modifiers,
|
||||||
|
state: mstate,
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +243,24 @@ func findEvent(k string) (Event, error) {
|
|||||||
return event, nil
|
return event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func eventsEqual(e1 Event, e2 Event) bool {
|
||||||
|
seq1, ok1 := e1.(KeySequenceEvent)
|
||||||
|
seq2, ok2 := e2.(KeySequenceEvent)
|
||||||
|
if ok1 && ok2 {
|
||||||
|
if len(seq1.keys) != len(seq2.keys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(seq1.keys); i++ {
|
||||||
|
if seq1.keys[i] != seq2.keys[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return e1 == e2
|
||||||
|
}
|
||||||
|
|
||||||
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
|
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
|
||||||
// Returns true if the keybinding already existed and a possible error
|
// Returns true if the keybinding already existed and a possible error
|
||||||
func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
||||||
@ -248,7 +270,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
|||||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||||
createBindingsIfNotExist(filename)
|
createBindingsIfNotExist(filename)
|
||||||
if _, e = os.Stat(filename); e == nil {
|
if _, e = os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -264,28 +286,31 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for ev := range parsed {
|
var ev string
|
||||||
|
for ev = range parsed {
|
||||||
if e, err := findEvent(ev); err == nil {
|
if e, err := findEvent(ev); err == nil {
|
||||||
if e == key {
|
if eventsEqual(e, key) {
|
||||||
if overwrite {
|
|
||||||
parsed[ev] = v
|
|
||||||
}
|
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if found && !overwrite {
|
if found {
|
||||||
return true, nil
|
if overwrite {
|
||||||
} else if !found {
|
parsed[ev] = v
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
parsed[k] = v
|
parsed[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
BindKey(k, v, Binder["buffer"])
|
BindKey(k, v, Binder["buffer"])
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||||
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
txt = append(txt, '\n')
|
||||||
|
return true, writeFile(filename, txt)
|
||||||
}
|
}
|
||||||
return false, e
|
return false, e
|
||||||
}
|
}
|
||||||
@ -298,7 +323,7 @@ func UnbindKey(k string) error {
|
|||||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||||
createBindingsIfNotExist(filename)
|
createBindingsIfNotExist(filename)
|
||||||
if _, e = os.Stat(filename); e == nil {
|
if _, e = os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Error reading bindings.json file: " + err.Error())
|
return errors.New("Error reading bindings.json file: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -315,13 +340,17 @@ func UnbindKey(k string) error {
|
|||||||
|
|
||||||
for ev := range parsed {
|
for ev := range parsed {
|
||||||
if e, err := findEvent(ev); err == nil {
|
if e, err := findEvent(ev); err == nil {
|
||||||
if e == key {
|
if eventsEqual(e, key) {
|
||||||
delete(parsed, ev)
|
delete(parsed, ev)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(k, "\x1b") {
|
||||||
|
screen.UnregisterRawSeq(k)
|
||||||
|
}
|
||||||
|
|
||||||
defaults := DefaultBindings("buffer")
|
defaults := DefaultBindings("buffer")
|
||||||
if a, ok := defaults[k]; ok {
|
if a, ok := defaults[k]; ok {
|
||||||
BindKey(k, a, Binder["buffer"])
|
BindKey(k, a, Binder["buffer"])
|
||||||
@ -331,7 +360,8 @@ func UnbindKey(k string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||||
return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
txt = append(txt, '\n')
|
||||||
|
return writeFile(filename, txt)
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,16 @@ import (
|
|||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/clipboard"
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/display"
|
"github.com/zyedidia/micro/v2/internal/display"
|
||||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type BufAction interface{}
|
||||||
|
|
||||||
// BufKeyAction represents an action bound to a key.
|
// BufKeyAction represents an action bound to a key.
|
||||||
type BufKeyAction func(*BufPane) bool
|
type BufKeyAction func(*BufPane) bool
|
||||||
|
|
||||||
@ -44,8 +45,9 @@ func init() {
|
|||||||
BufBindings = NewKeyTree()
|
BufBindings = NewKeyTree()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LuaAction makes a BufKeyAction from a lua function.
|
// LuaAction makes an action from a lua function. It returns either a BufKeyAction
|
||||||
func LuaAction(fn string) func(*BufPane) bool {
|
// or a BufMouseAction depending on the event type.
|
||||||
|
func LuaAction(fn string, k Event) BufAction {
|
||||||
luaFn := strings.Split(fn, ".")
|
luaFn := strings.Split(fn, ".")
|
||||||
if len(luaFn) <= 1 {
|
if len(luaFn) <= 1 {
|
||||||
return nil
|
return nil
|
||||||
@ -55,33 +57,42 @@ func LuaAction(fn string) func(*BufPane) bool {
|
|||||||
if pl == nil {
|
if pl == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return func(h *BufPane) bool {
|
|
||||||
val, err := pl.Call(plFn, luar.New(ulua.L, h))
|
var action BufAction
|
||||||
if err != nil {
|
switch k.(type) {
|
||||||
screen.TermMessage(err)
|
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||||
}
|
action = BufKeyAction(func(h *BufPane) bool {
|
||||||
if v, ok := val.(lua.LBool); !ok {
|
val, err := pl.Call(plFn, luar.New(ulua.L, h))
|
||||||
return false
|
if err != nil {
|
||||||
} else {
|
screen.TermMessage(err)
|
||||||
return bool(v)
|
}
|
||||||
}
|
if v, ok := val.(lua.LBool); !ok {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return bool(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case MouseEvent:
|
||||||
|
action = BufMouseAction(func(h *BufPane, te *tcell.EventMouse) bool {
|
||||||
|
val, err := pl.Call(plFn, luar.New(ulua.L, h), luar.New(ulua.L, te))
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
if v, ok := val.(lua.LBool); !ok {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return bool(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return action
|
||||||
}
|
}
|
||||||
|
|
||||||
// BufMapKey maps an event to an action
|
// BufMapEvent maps an event to an action
|
||||||
func BufMapEvent(k Event, action string) {
|
func BufMapEvent(k Event, action string) {
|
||||||
config.Bindings["buffer"][k.Name()] = action
|
config.Bindings["buffer"][k.Name()] = action
|
||||||
|
|
||||||
switch e := k.(type) {
|
var actionfns []BufAction
|
||||||
case KeyEvent, KeySequenceEvent, RawEvent:
|
|
||||||
bufMapKey(e, action)
|
|
||||||
case MouseEvent:
|
|
||||||
bufMapMouse(e, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bufMapKey(k Event, action string) {
|
|
||||||
var actionfns []func(*BufPane) bool
|
|
||||||
var names []string
|
var names []string
|
||||||
var types []byte
|
var types []byte
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
@ -89,9 +100,7 @@ func bufMapKey(k Event, action string) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix problem when complex bindings have these
|
idx := util.IndexAnyUnquoted(action, "&|,")
|
||||||
// characters (escape them?)
|
|
||||||
idx := strings.IndexAny(action, "&|,")
|
|
||||||
a := action
|
a := action
|
||||||
if idx >= 0 {
|
if idx >= 0 {
|
||||||
a = action[:idx]
|
a = action[:idx]
|
||||||
@ -102,7 +111,7 @@ func bufMapKey(k Event, action string) {
|
|||||||
action = ""
|
action = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var afn func(*BufPane) bool
|
var afn BufAction
|
||||||
if strings.HasPrefix(a, "command:") {
|
if strings.HasPrefix(a, "command:") {
|
||||||
a = strings.SplitN(a, ":", 2)[1]
|
a = strings.SplitN(a, ":", 2)[1]
|
||||||
afn = CommandAction(a)
|
afn = CommandAction(a)
|
||||||
@ -113,7 +122,7 @@ func bufMapKey(k Event, action string) {
|
|||||||
names = append(names, "")
|
names = append(names, "")
|
||||||
} else if strings.HasPrefix(a, "lua:") {
|
} else if strings.HasPrefix(a, "lua:") {
|
||||||
a = strings.SplitN(a, ":", 2)[1]
|
a = strings.SplitN(a, ":", 2)[1]
|
||||||
afn = LuaAction(a)
|
afn = LuaAction(a, k)
|
||||||
if afn == nil {
|
if afn == nil {
|
||||||
screen.TermMessage("Lua Error:", a, "does not exist")
|
screen.TermMessage("Lua Error:", a, "does not exist")
|
||||||
continue
|
continue
|
||||||
@ -129,47 +138,52 @@ func bufMapKey(k Event, action string) {
|
|||||||
} else if f, ok := BufKeyActions[a]; ok {
|
} else if f, ok := BufKeyActions[a]; ok {
|
||||||
afn = f
|
afn = f
|
||||||
names = append(names, a)
|
names = append(names, a)
|
||||||
|
} else if f, ok := BufMouseActions[a]; ok {
|
||||||
|
afn = f
|
||||||
|
names = append(names, a)
|
||||||
} else {
|
} else {
|
||||||
screen.TermMessage("Error in bindings: action", a, "does not exist")
|
screen.TermMessage("Error in bindings: action", a, "does not exist")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
actionfns = append(actionfns, afn)
|
actionfns = append(actionfns, afn)
|
||||||
}
|
}
|
||||||
bufAction := func(h *BufPane) bool {
|
bufAction := func(h *BufPane, te *tcell.EventMouse) bool {
|
||||||
cursors := h.Buf.GetCursors()
|
|
||||||
success := true
|
|
||||||
for i, a := range actionfns {
|
for i, a := range actionfns {
|
||||||
innerSuccess := true
|
var success bool
|
||||||
for j, c := range cursors {
|
if _, ok := MultiActions[names[i]]; ok {
|
||||||
if c == nil {
|
success = true
|
||||||
continue
|
for _, c := range h.Buf.GetCursors() {
|
||||||
}
|
h.Buf.SetCurCursor(c.Num)
|
||||||
h.Buf.SetCurCursor(c.Num)
|
h.Cursor = c
|
||||||
h.Cursor = c
|
success = success && h.execAction(a, names[i], te)
|
||||||
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
|
|
||||||
innerSuccess = innerSuccess && h.execAction(a, names[i], j)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
h.Buf.SetCurCursor(0)
|
||||||
|
h.Cursor = h.Buf.GetActiveCursor()
|
||||||
|
success = h.execAction(a, names[i], te)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the action changed the current pane, update the reference
|
// if the action changed the current pane, update the reference
|
||||||
h = MainTab().CurPane()
|
h = MainTab().CurPane()
|
||||||
success = innerSuccess
|
if h == nil {
|
||||||
|
// stop, in case the current pane is not a BufPane
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success && types[i] == '&') || (success && types[i] == '|') {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
BufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(bufAction))
|
switch e := k.(type) {
|
||||||
}
|
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||||
|
BufBindings.RegisterKeyBinding(e, BufKeyActionGeneral(func(h *BufPane) bool {
|
||||||
// BufMapMouse maps a mouse event to an action
|
return bufAction(h, nil)
|
||||||
func bufMapMouse(k MouseEvent, action string) {
|
}))
|
||||||
if f, ok := BufMouseActions[action]; ok {
|
case MouseEvent:
|
||||||
BufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f))
|
BufBindings.RegisterMouseBinding(e, BufMouseActionGeneral(bufAction))
|
||||||
} else {
|
|
||||||
// TODO
|
|
||||||
// delete(BufMouseBindings, k)
|
|
||||||
bufMapKey(k, action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,24 +214,23 @@ type BufPane struct {
|
|||||||
// Cursor is the currently active buffer cursor
|
// Cursor is the currently active buffer cursor
|
||||||
Cursor *buffer.Cursor
|
Cursor *buffer.Cursor
|
||||||
|
|
||||||
// Since tcell doesn't differentiate between a mouse release event
|
// Since tcell doesn't differentiate between a mouse press event
|
||||||
// and a mouse move event with no keys pressed, we need to keep
|
// and a mouse move event with button pressed (nor between a mouse
|
||||||
// track of whether or not the mouse was pressed (or not released) last event to determine
|
// release event and a mouse move event with no buttons pressed),
|
||||||
// mouse release events
|
// we need to keep track of whether or not the mouse was previously
|
||||||
mouseReleased bool
|
// pressed, to determine mouse release and mouse drag events.
|
||||||
|
// Moreover, since in case of a release event tcell doesn't tell us
|
||||||
|
// which button was released, we need to keep track of which
|
||||||
|
// (possibly multiple) buttons were pressed previously.
|
||||||
|
mousePressed map[MouseEvent]bool
|
||||||
|
|
||||||
// We need to keep track of insert key press toggle
|
|
||||||
isOverwriteMode bool
|
|
||||||
// This stores when the last click was
|
// This stores when the last click was
|
||||||
// This is useful for detecting double and triple clicks
|
// This is useful for detecting double and triple clicks
|
||||||
lastClickTime time.Time
|
lastClickTime time.Time
|
||||||
lastLoc buffer.Loc
|
lastLoc buffer.Loc
|
||||||
|
|
||||||
// lastCutTime stores when the last ctrl+k was issued.
|
// freshClip returns true if one or more lines have been cut to the clipboard
|
||||||
// It is used for clearing the clipboard to replace it with fresh cut lines.
|
// and have never been pasted yet.
|
||||||
lastCutTime time.Time
|
|
||||||
|
|
||||||
// freshClip returns true if the clipboard has never been pasted.
|
|
||||||
freshClip bool
|
freshClip bool
|
||||||
|
|
||||||
// Was the last mouse event actually a double click?
|
// Was the last mouse event actually a double click?
|
||||||
@ -250,7 +263,7 @@ func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
|
|||||||
h.tab = tab
|
h.tab = tab
|
||||||
|
|
||||||
h.Cursor = h.Buf.GetActiveCursor()
|
h.Cursor = h.Buf.GetActiveCursor()
|
||||||
h.mouseReleased = true
|
h.mousePressed = make(map[MouseEvent]bool)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
@ -276,7 +289,11 @@ func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
|
|||||||
func (h *BufPane) finishInitialize() {
|
func (h *BufPane) finishInitialize() {
|
||||||
h.initialRelocate()
|
h.initialRelocate()
|
||||||
h.initialized = true
|
h.initialized = true
|
||||||
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
|
||||||
|
err := config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize resizes the pane
|
// Resize resizes the pane
|
||||||
@ -304,7 +321,7 @@ func (h *BufPane) ResizePane(size int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PluginCB calls all plugin callbacks with a certain name and displays an
|
// PluginCB calls all plugin callbacks with a certain name and displays an
|
||||||
// error if there is one and returns the aggregrate boolean response
|
// error if there is one and returns the aggregate boolean response
|
||||||
func (h *BufPane) PluginCB(cb string) bool {
|
func (h *BufPane) PluginCB(cb string) bool {
|
||||||
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
|
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -322,6 +339,12 @@ func (h *BufPane) PluginCBRune(cb string, r rune) bool {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *BufPane) resetMouse() {
|
||||||
|
for me := range h.mousePressed {
|
||||||
|
delete(h.mousePressed, me)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// OpenBuffer opens the given buffer in this pane.
|
// OpenBuffer opens the given buffer in this pane.
|
||||||
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
||||||
h.Buf.Close()
|
h.Buf.Close()
|
||||||
@ -332,10 +355,7 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
|||||||
h.initialRelocate()
|
h.initialRelocate()
|
||||||
// Set mouseReleased to true because we assume the mouse is not being
|
// Set mouseReleased to true because we assume the mouse is not being
|
||||||
// pressed when the editor is opened
|
// pressed when the editor is opened
|
||||||
h.mouseReleased = true
|
h.resetMouse()
|
||||||
// Set isOverwriteMode to false, because we assume we are in the default
|
|
||||||
// mode when editor is opened
|
|
||||||
h.isOverwriteMode = false
|
|
||||||
h.lastClickTime = time.Time{}
|
h.lastClickTime = time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,6 +415,12 @@ func (h *BufPane) Name() string {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReOpen reloads the file opened in the bufpane from disk
|
||||||
|
func (h *BufPane) ReOpen() {
|
||||||
|
h.Buf.ReOpen()
|
||||||
|
h.Relocate()
|
||||||
|
}
|
||||||
|
|
||||||
func (h *BufPane) getReloadSetting() string {
|
func (h *BufPane) getReloadSetting() string {
|
||||||
reloadSetting := h.Buf.Settings["reload"]
|
reloadSetting := h.Buf.Settings["reload"]
|
||||||
return reloadSetting.(string)
|
return reloadSetting.(string)
|
||||||
@ -413,11 +439,11 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
|
|||||||
if !yes || canceled {
|
if !yes || canceled {
|
||||||
h.Buf.UpdateModTime()
|
h.Buf.UpdateModTime()
|
||||||
} else {
|
} else {
|
||||||
h.Buf.ReOpen()
|
h.ReOpen()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if reload == "auto" {
|
} else if reload == "auto" {
|
||||||
h.Buf.ReOpen()
|
h.ReOpen()
|
||||||
} else if reload == "disabled" {
|
} else if reload == "disabled" {
|
||||||
h.Buf.DisableReload()
|
h.Buf.DisableReload()
|
||||||
} else {
|
} else {
|
||||||
@ -435,61 +461,44 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
|
|||||||
h.paste(e.Text())
|
h.paste(e.Text())
|
||||||
h.Relocate()
|
h.Relocate()
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
ke := KeyEvent{
|
ke := keyEvent(e)
|
||||||
code: e.Key(),
|
|
||||||
mod: metaToAlt(e.Modifiers()),
|
|
||||||
r: e.Rune(),
|
|
||||||
}
|
|
||||||
|
|
||||||
done := h.DoKeyEvent(ke)
|
done := h.DoKeyEvent(ke)
|
||||||
if !done && e.Key() == tcell.KeyRune {
|
if !done && e.Key() == tcell.KeyRune {
|
||||||
h.DoRuneInsert(e.Rune())
|
h.DoRuneInsert(e.Rune())
|
||||||
}
|
}
|
||||||
case *tcell.EventMouse:
|
case *tcell.EventMouse:
|
||||||
cancel := false
|
if e.Buttons() != tcell.ButtonNone {
|
||||||
switch e.Buttons() {
|
|
||||||
case tcell.Button1:
|
|
||||||
_, my := e.Position()
|
|
||||||
if h.Buf.Type.Kind != buffer.BTInfo.Kind && h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
|
|
||||||
cancel = true
|
|
||||||
}
|
|
||||||
case tcell.ButtonNone:
|
|
||||||
// Mouse event with no click
|
|
||||||
if !h.mouseReleased {
|
|
||||||
// Mouse was just released
|
|
||||||
|
|
||||||
// mx, my := e.Position()
|
|
||||||
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
|
|
||||||
|
|
||||||
// we could finish the selection based on the release location as described
|
|
||||||
// below but when the mouse click is within the scroll margin this will
|
|
||||||
// cause a scroll and selection even for a simple mouse click which is
|
|
||||||
// not good
|
|
||||||
// for terminals that don't support mouse motion events, selection via
|
|
||||||
// the mouse won't work but this is ok
|
|
||||||
|
|
||||||
// Relocating here isn't really necessary because the cursor will
|
|
||||||
// be in the right place from the last mouse event
|
|
||||||
// However, if we are running in a terminal that doesn't support mouse motion
|
|
||||||
// events, this still allows the user to make selections, except only after they
|
|
||||||
// release the mouse
|
|
||||||
|
|
||||||
// if !h.doubleClick && !h.tripleClick {
|
|
||||||
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
|
|
||||||
// }
|
|
||||||
if h.Cursor.HasSelection() {
|
|
||||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
|
||||||
}
|
|
||||||
h.mouseReleased = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cancel {
|
|
||||||
me := MouseEvent{
|
me := MouseEvent{
|
||||||
btn: e.Buttons(),
|
btn: e.Buttons(),
|
||||||
mod: metaToAlt(e.Modifiers()),
|
mod: metaToAlt(e.Modifiers()),
|
||||||
|
state: MousePress,
|
||||||
|
}
|
||||||
|
isDrag := len(h.mousePressed) > 0
|
||||||
|
|
||||||
|
if e.Buttons() & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone {
|
||||||
|
h.mousePressed[me] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDrag {
|
||||||
|
me.state = MouseDrag
|
||||||
}
|
}
|
||||||
h.DoMouseEvent(me, e)
|
h.DoMouseEvent(me, e)
|
||||||
|
} else {
|
||||||
|
// Mouse event with no click - mouse was just released.
|
||||||
|
// If there were multiple mouse buttons pressed, we don't know which one
|
||||||
|
// was actually released, so we assume they all were released.
|
||||||
|
pressed := len(h.mousePressed) > 0
|
||||||
|
for me := range h.mousePressed {
|
||||||
|
delete(h.mousePressed, me)
|
||||||
|
|
||||||
|
me.state = MouseRelease
|
||||||
|
h.DoMouseEvent(me, e)
|
||||||
|
}
|
||||||
|
if !pressed {
|
||||||
|
// Propagate the mouse release in case the press wasn't for this BufPane
|
||||||
|
Tabs.ResetMouse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.Buf.MergeCursors()
|
h.Buf.MergeCursors()
|
||||||
@ -509,6 +518,14 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
|
|||||||
InfoBar.ClearGutter()
|
InfoBar.ClearGutter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursors := h.Buf.GetCursors()
|
||||||
|
for _, c := range cursors {
|
||||||
|
if c.NewTrailingWsY != c.Y && (!c.HasSelection() ||
|
||||||
|
(c.NewTrailingWsY != c.CurSelection[0].Y && c.NewTrailingWsY != c.CurSelection[1].Y)) {
|
||||||
|
c.NewTrailingWsY = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bindings returns the current bindings tree for this buffer.
|
// Bindings returns the current bindings tree for this buffer.
|
||||||
@ -520,7 +537,10 @@ func (h *BufPane) Bindings() *KeyTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DoKeyEvent executes a key event by finding the action it is bound
|
// DoKeyEvent executes a key event by finding the action it is bound
|
||||||
// to and executing it (possibly multiple times for multiple cursors)
|
// to and executing it (possibly multiple times for multiple cursors).
|
||||||
|
// Returns true if the action was executed OR if there are more keys
|
||||||
|
// remaining to process before executing an action (if this is a key
|
||||||
|
// sequence event). Returns false if no action found.
|
||||||
func (h *BufPane) DoKeyEvent(e Event) bool {
|
func (h *BufPane) DoKeyEvent(e Event) bool {
|
||||||
binds := h.Bindings()
|
binds := h.Bindings()
|
||||||
action, more := binds.NextEvent(e, nil)
|
action, more := binds.NextEvent(e, nil)
|
||||||
@ -534,30 +554,33 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
|
|||||||
return more
|
return more
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
|
func (h *BufPane) execAction(action BufAction, name string, te *tcell.EventMouse) bool {
|
||||||
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
|
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
|
||||||
h.Buf.HasSuggestions = false
|
h.Buf.HasSuggestions = false
|
||||||
}
|
}
|
||||||
|
|
||||||
_, isMulti := MultiActions[name]
|
if !h.PluginCB("pre" + name) {
|
||||||
if (!isMulti && cursor == 0) || isMulti {
|
return false
|
||||||
if h.PluginCB("pre" + name) {
|
}
|
||||||
success := action(h)
|
|
||||||
success = success && h.PluginCB("on"+name)
|
|
||||||
|
|
||||||
if isMulti {
|
var success bool
|
||||||
if recordingMacro {
|
switch a := action.(type) {
|
||||||
if name != "ToggleMacro" && name != "PlayMacro" {
|
case BufKeyAction:
|
||||||
curmacro = append(curmacro, action)
|
success = a(h)
|
||||||
}
|
case BufMouseAction:
|
||||||
}
|
success = a(h, te)
|
||||||
|
}
|
||||||
|
success = success && h.PluginCB("on"+name)
|
||||||
|
|
||||||
|
if _, ok := MultiActions[name]; ok {
|
||||||
|
if recordingMacro {
|
||||||
|
if name != "ToggleMacro" && name != "PlayMacro" {
|
||||||
|
curmacro = append(curmacro, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
return success
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BufPane) completeAction(action string) {
|
func (h *BufPane) completeAction(action string) {
|
||||||
@ -611,7 +634,7 @@ func (h *BufPane) DoRuneInsert(r rune) {
|
|||||||
c.ResetSelection()
|
c.ResetSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.isOverwriteMode {
|
if h.Buf.OverwriteMode {
|
||||||
next := c.Loc
|
next := c.Loc
|
||||||
next.X++
|
next.X++
|
||||||
h.Buf.Replace(c.Loc, next, string(r))
|
h.Buf.Replace(c.Loc, next, string(r))
|
||||||
@ -630,9 +653,13 @@ func (h *BufPane) DoRuneInsert(r rune) {
|
|||||||
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
|
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
|
||||||
e := NewBufPaneFromBuf(buf, h.tab)
|
e := NewBufPaneFromBuf(buf, h.tab)
|
||||||
e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
|
e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
|
||||||
MainTab().Panes = append(MainTab().Panes, e)
|
currentPaneIdx := MainTab().GetPane(h.splitID)
|
||||||
|
if right {
|
||||||
|
currentPaneIdx++
|
||||||
|
}
|
||||||
|
MainTab().AddPane(e, currentPaneIdx)
|
||||||
MainTab().Resize()
|
MainTab().Resize()
|
||||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
MainTab().SetActive(currentPaneIdx)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,9 +667,13 @@ func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
|
|||||||
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
|
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
|
||||||
e := NewBufPaneFromBuf(buf, h.tab)
|
e := NewBufPaneFromBuf(buf, h.tab)
|
||||||
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
|
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
|
||||||
MainTab().Panes = append(MainTab().Panes, e)
|
currentPaneIdx := MainTab().GetPane(h.splitID)
|
||||||
|
if bottom {
|
||||||
|
currentPaneIdx++
|
||||||
|
}
|
||||||
|
MainTab().AddPane(e, currentPaneIdx)
|
||||||
MainTab().Resize()
|
MainTab().Resize()
|
||||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
MainTab().SetActive(currentPaneIdx)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,6 +694,10 @@ func (h *BufPane) Close() {
|
|||||||
|
|
||||||
// SetActive marks this pane as active.
|
// SetActive marks this pane as active.
|
||||||
func (h *BufPane) SetActive(b bool) {
|
func (h *BufPane) SetActive(b bool) {
|
||||||
|
if h.IsActive() == b {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
h.BWindow.SetActive(b)
|
h.BWindow.SetActive(b)
|
||||||
if b {
|
if b {
|
||||||
// Display any gutter messages for this line
|
// Display any gutter messages for this line
|
||||||
@ -678,8 +713,12 @@ func (h *BufPane) SetActive(b bool) {
|
|||||||
if none && InfoBar.HasGutter {
|
if none && InfoBar.HasGutter {
|
||||||
InfoBar.ClearGutter()
|
InfoBar.ClearGutter()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, h))
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
|
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
|
||||||
@ -692,6 +731,9 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"CursorRight": (*BufPane).CursorRight,
|
"CursorRight": (*BufPane).CursorRight,
|
||||||
"CursorStart": (*BufPane).CursorStart,
|
"CursorStart": (*BufPane).CursorStart,
|
||||||
"CursorEnd": (*BufPane).CursorEnd,
|
"CursorEnd": (*BufPane).CursorEnd,
|
||||||
|
"CursorToViewTop": (*BufPane).CursorToViewTop,
|
||||||
|
"CursorToViewCenter": (*BufPane).CursorToViewCenter,
|
||||||
|
"CursorToViewBottom": (*BufPane).CursorToViewBottom,
|
||||||
"SelectToStart": (*BufPane).SelectToStart,
|
"SelectToStart": (*BufPane).SelectToStart,
|
||||||
"SelectToEnd": (*BufPane).SelectToEnd,
|
"SelectToEnd": (*BufPane).SelectToEnd,
|
||||||
"SelectUp": (*BufPane).SelectUp,
|
"SelectUp": (*BufPane).SelectUp,
|
||||||
@ -700,10 +742,16 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"SelectRight": (*BufPane).SelectRight,
|
"SelectRight": (*BufPane).SelectRight,
|
||||||
"WordRight": (*BufPane).WordRight,
|
"WordRight": (*BufPane).WordRight,
|
||||||
"WordLeft": (*BufPane).WordLeft,
|
"WordLeft": (*BufPane).WordLeft,
|
||||||
|
"SubWordRight": (*BufPane).SubWordRight,
|
||||||
|
"SubWordLeft": (*BufPane).SubWordLeft,
|
||||||
"SelectWordRight": (*BufPane).SelectWordRight,
|
"SelectWordRight": (*BufPane).SelectWordRight,
|
||||||
"SelectWordLeft": (*BufPane).SelectWordLeft,
|
"SelectWordLeft": (*BufPane).SelectWordLeft,
|
||||||
|
"SelectSubWordRight": (*BufPane).SelectSubWordRight,
|
||||||
|
"SelectSubWordLeft": (*BufPane).SelectSubWordLeft,
|
||||||
"DeleteWordRight": (*BufPane).DeleteWordRight,
|
"DeleteWordRight": (*BufPane).DeleteWordRight,
|
||||||
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
|
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
|
||||||
|
"DeleteSubWordRight": (*BufPane).DeleteSubWordRight,
|
||||||
|
"DeleteSubWordLeft": (*BufPane).DeleteSubWordLeft,
|
||||||
"SelectLine": (*BufPane).SelectLine,
|
"SelectLine": (*BufPane).SelectLine,
|
||||||
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
|
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
|
||||||
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
|
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
|
||||||
@ -711,6 +759,8 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
|
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
|
||||||
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
|
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
|
||||||
"ParagraphNext": (*BufPane).ParagraphNext,
|
"ParagraphNext": (*BufPane).ParagraphNext,
|
||||||
|
"SelectToParagraphPrevious": (*BufPane).SelectToParagraphPrevious,
|
||||||
|
"SelectToParagraphNext": (*BufPane).SelectToParagraphNext,
|
||||||
"InsertNewline": (*BufPane).InsertNewline,
|
"InsertNewline": (*BufPane).InsertNewline,
|
||||||
"Backspace": (*BufPane).Backspace,
|
"Backspace": (*BufPane).Backspace,
|
||||||
"Delete": (*BufPane).Delete,
|
"Delete": (*BufPane).Delete,
|
||||||
@ -731,6 +781,7 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"CopyLine": (*BufPane).CopyLine,
|
"CopyLine": (*BufPane).CopyLine,
|
||||||
"Cut": (*BufPane).Cut,
|
"Cut": (*BufPane).Cut,
|
||||||
"CutLine": (*BufPane).CutLine,
|
"CutLine": (*BufPane).CutLine,
|
||||||
|
"Duplicate": (*BufPane).Duplicate,
|
||||||
"DuplicateLine": (*BufPane).DuplicateLine,
|
"DuplicateLine": (*BufPane).DuplicateLine,
|
||||||
"DeleteLine": (*BufPane).DeleteLine,
|
"DeleteLine": (*BufPane).DeleteLine,
|
||||||
"MoveLinesUp": (*BufPane).MoveLinesUp,
|
"MoveLinesUp": (*BufPane).MoveLinesUp,
|
||||||
@ -763,6 +814,7 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"ToggleRuler": (*BufPane).ToggleRuler,
|
"ToggleRuler": (*BufPane).ToggleRuler,
|
||||||
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
|
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
|
||||||
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
|
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
|
||||||
|
"ResetSearch": (*BufPane).ResetSearch,
|
||||||
"ClearStatus": (*BufPane).ClearStatus,
|
"ClearStatus": (*BufPane).ClearStatus,
|
||||||
"ShellMode": (*BufPane).ShellMode,
|
"ShellMode": (*BufPane).ShellMode,
|
||||||
"CommandMode": (*BufPane).CommandMode,
|
"CommandMode": (*BufPane).CommandMode,
|
||||||
@ -774,8 +826,12 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"AddTab": (*BufPane).AddTab,
|
"AddTab": (*BufPane).AddTab,
|
||||||
"PreviousTab": (*BufPane).PreviousTab,
|
"PreviousTab": (*BufPane).PreviousTab,
|
||||||
"NextTab": (*BufPane).NextTab,
|
"NextTab": (*BufPane).NextTab,
|
||||||
|
"FirstTab": (*BufPane).FirstTab,
|
||||||
|
"LastTab": (*BufPane).LastTab,
|
||||||
"NextSplit": (*BufPane).NextSplit,
|
"NextSplit": (*BufPane).NextSplit,
|
||||||
"PreviousSplit": (*BufPane).PreviousSplit,
|
"PreviousSplit": (*BufPane).PreviousSplit,
|
||||||
|
"FirstSplit": (*BufPane).FirstSplit,
|
||||||
|
"LastSplit": (*BufPane).LastSplit,
|
||||||
"Unsplit": (*BufPane).Unsplit,
|
"Unsplit": (*BufPane).Unsplit,
|
||||||
"VSplit": (*BufPane).VSplitAction,
|
"VSplit": (*BufPane).VSplitAction,
|
||||||
"HSplit": (*BufPane).HSplitAction,
|
"HSplit": (*BufPane).HSplitAction,
|
||||||
@ -791,6 +847,7 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
|
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
|
||||||
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
|
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
|
||||||
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
|
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
|
||||||
|
"SkipMultiCursorBack": (*BufPane).SkipMultiCursorBack,
|
||||||
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
|
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
|
||||||
"JumpLine": (*BufPane).JumpLine,
|
"JumpLine": (*BufPane).JumpLine,
|
||||||
"Deselect": (*BufPane).Deselect,
|
"Deselect": (*BufPane).Deselect,
|
||||||
@ -804,6 +861,8 @@ var BufKeyActions = map[string]BufKeyAction{
|
|||||||
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
|
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
|
||||||
var BufMouseActions = map[string]BufMouseAction{
|
var BufMouseActions = map[string]BufMouseAction{
|
||||||
"MousePress": (*BufPane).MousePress,
|
"MousePress": (*BufPane).MousePress,
|
||||||
|
"MouseDrag": (*BufPane).MouseDrag,
|
||||||
|
"MouseRelease": (*BufPane).MouseRelease,
|
||||||
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
|
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -828,10 +887,16 @@ var MultiActions = map[string]bool{
|
|||||||
"SelectRight": true,
|
"SelectRight": true,
|
||||||
"WordRight": true,
|
"WordRight": true,
|
||||||
"WordLeft": true,
|
"WordLeft": true,
|
||||||
|
"SubWordRight": true,
|
||||||
|
"SubWordLeft": true,
|
||||||
"SelectWordRight": true,
|
"SelectWordRight": true,
|
||||||
"SelectWordLeft": true,
|
"SelectWordLeft": true,
|
||||||
|
"SelectSubWordRight": true,
|
||||||
|
"SelectSubWordLeft": true,
|
||||||
"DeleteWordRight": true,
|
"DeleteWordRight": true,
|
||||||
"DeleteWordLeft": true,
|
"DeleteWordLeft": true,
|
||||||
|
"DeleteSubWordRight": true,
|
||||||
|
"DeleteSubWordLeft": true,
|
||||||
"SelectLine": true,
|
"SelectLine": true,
|
||||||
"SelectToStartOfLine": true,
|
"SelectToStartOfLine": true,
|
||||||
"SelectToStartOfText": true,
|
"SelectToStartOfText": true,
|
||||||
@ -849,6 +914,7 @@ var MultiActions = map[string]bool{
|
|||||||
"Copy": true,
|
"Copy": true,
|
||||||
"Cut": true,
|
"Cut": true,
|
||||||
"CutLine": true,
|
"CutLine": true,
|
||||||
|
"Duplicate": true,
|
||||||
"DuplicateLine": true,
|
"DuplicateLine": true,
|
||||||
"DeleteLine": true,
|
"DeleteLine": true,
|
||||||
"MoveLinesUp": true,
|
"MoveLinesUp": true,
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -41,6 +42,7 @@ func InitCommands() {
|
|||||||
"unbind": {(*BufPane).UnbindCmd, nil},
|
"unbind": {(*BufPane).UnbindCmd, nil},
|
||||||
"quit": {(*BufPane).QuitCmd, nil},
|
"quit": {(*BufPane).QuitCmd, nil},
|
||||||
"goto": {(*BufPane).GotoCmd, nil},
|
"goto": {(*BufPane).GotoCmd, nil},
|
||||||
|
"jump": {(*BufPane).JumpCmd, nil},
|
||||||
"save": {(*BufPane).SaveCmd, nil},
|
"save": {(*BufPane).SaveCmd, nil},
|
||||||
"replace": {(*BufPane).ReplaceCmd, nil},
|
"replace": {(*BufPane).ReplaceCmd, nil},
|
||||||
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
|
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
|
||||||
@ -137,23 +139,25 @@ func (h *BufPane) TextFilterCmd(args []string) {
|
|||||||
InfoBar.Error("usage: textfilter arguments")
|
InfoBar.Error("usage: textfilter arguments")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sel := h.Cursor.GetSelection()
|
for _, c := range h.Buf.GetCursors() {
|
||||||
if len(sel) == 0 {
|
sel := c.GetSelection()
|
||||||
h.Cursor.SelectWord()
|
if len(sel) == 0 {
|
||||||
sel = h.Cursor.GetSelection()
|
c.SelectWord()
|
||||||
|
sel = c.GetSelection()
|
||||||
|
}
|
||||||
|
var bout, berr bytes.Buffer
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = strings.NewReader(string(sel))
|
||||||
|
cmd.Stderr = &berr
|
||||||
|
cmd.Stdout = &bout
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
InfoBar.Error(err.Error() + " " + berr.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.DeleteSelection()
|
||||||
|
h.Buf.Insert(c.Loc, bout.String())
|
||||||
}
|
}
|
||||||
var bout, berr bytes.Buffer
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = strings.NewReader(string(sel))
|
|
||||||
cmd.Stderr = &berr
|
|
||||||
cmd.Stdout = &bout
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
InfoBar.Error(err.Error() + " " + berr.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.Cursor.DeleteSelection()
|
|
||||||
h.Buf.Insert(h.Cursor.Loc, bout.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TabMoveCmd moves the current tab to a given index (starts at 1). The
|
// TabMoveCmd moves the current tab to a given index (starts at 1). The
|
||||||
@ -197,10 +201,11 @@ func (h *BufPane) TabMoveCmd(args []string) {
|
|||||||
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
|
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
|
||||||
|
|
||||||
activeTab := Tabs.List[idxFrom]
|
activeTab := Tabs.List[idxFrom]
|
||||||
Tabs.RemoveTab(activeTab.ID())
|
Tabs.RemoveTab(activeTab.Panes[0].ID())
|
||||||
Tabs.List = append(Tabs.List, nil)
|
Tabs.List = append(Tabs.List, nil)
|
||||||
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
|
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
|
||||||
Tabs.List[idxTo] = activeTab
|
Tabs.List[idxTo] = activeTab
|
||||||
|
Tabs.Resize()
|
||||||
Tabs.UpdateNames()
|
Tabs.UpdateNames()
|
||||||
Tabs.SetActive(idxTo)
|
Tabs.SetActive(idxTo)
|
||||||
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
|
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
|
||||||
@ -329,31 +334,84 @@ func (h *BufPane) ToggleLogCmd(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadCmd reloads all files (syntax files, colorschemes...)
|
// ReloadCmd reloads all files (syntax files, colorschemes, plugins...)
|
||||||
func (h *BufPane) ReloadCmd(args []string) {
|
func (h *BufPane) ReloadCmd(args []string) {
|
||||||
ReloadConfig()
|
reloadRuntime(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReloadConfig reloads only the configuration
|
||||||
func ReloadConfig() {
|
func ReloadConfig() {
|
||||||
config.InitRuntimeFiles()
|
reloadRuntime(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadRuntime(reloadPlugins bool) {
|
||||||
|
if reloadPlugins {
|
||||||
|
err := config.RunPluginFn("deinit")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.InitRuntimeFiles(true)
|
||||||
|
|
||||||
|
if reloadPlugins {
|
||||||
|
config.InitPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
err := config.ReadSettings()
|
err := config.ReadSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
|
} else {
|
||||||
|
parsedSettings := config.ParsedSettings()
|
||||||
|
defaultSettings := config.DefaultAllSettings()
|
||||||
|
for k := range defaultSettings {
|
||||||
|
if _, ok := config.VolatileSettings[k]; ok {
|
||||||
|
// reload should not override volatile settings
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := parsedSettings[k]; ok {
|
||||||
|
err = doSetGlobalOptionNative(k, parsedSettings[k])
|
||||||
|
} else {
|
||||||
|
err = doSetGlobalOptionNative(k, defaultSettings[k])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = config.InitGlobalSettings()
|
|
||||||
if err != nil {
|
if reloadPlugins {
|
||||||
screen.TermMessage(err)
|
err = config.LoadAllPlugins()
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InitBindings()
|
InitBindings()
|
||||||
InitCommands()
|
InitCommands()
|
||||||
|
|
||||||
|
if reloadPlugins {
|
||||||
|
err = config.RunPluginFn("preinit")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
err = config.RunPluginFn("init")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
err = config.RunPluginFn("postinit")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = config.InitColorscheme()
|
err = config.InitColorscheme()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range buffer.OpenBuffers {
|
for _, b := range buffer.OpenBuffers {
|
||||||
b.UpdateRules()
|
b.ReloadSettings(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,50 +421,93 @@ func (h *BufPane) ReopenCmd(args []string) {
|
|||||||
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
|
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
|
||||||
if !canceled && yes {
|
if !canceled && yes {
|
||||||
h.Save()
|
h.Save()
|
||||||
h.Buf.ReOpen()
|
h.ReOpen()
|
||||||
} else if !canceled {
|
} else if !canceled {
|
||||||
h.Buf.ReOpen()
|
h.ReOpen()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
h.Buf.ReOpen()
|
h.ReOpen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BufPane) openHelp(page string) error {
|
func (h *BufPane) openHelp(page string, hsplit bool, forceSplit bool) error {
|
||||||
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
||||||
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
|
return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err))
|
||||||
} else {
|
} else {
|
||||||
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
||||||
helpBuffer.SetName("Help " + page)
|
helpBuffer.SetName("Help " + page)
|
||||||
|
helpBuffer.SetOptionNative("hltaberrors", false)
|
||||||
|
helpBuffer.SetOptionNative("hltrailingws", false)
|
||||||
|
|
||||||
if h.Buf.Type == buffer.BTHelp {
|
if h.Buf.Type == buffer.BTHelp && !forceSplit {
|
||||||
h.OpenBuffer(helpBuffer)
|
h.OpenBuffer(helpBuffer)
|
||||||
} else {
|
} else if hsplit {
|
||||||
h.HSplitBuf(helpBuffer)
|
h.HSplitBuf(helpBuffer)
|
||||||
|
} else {
|
||||||
|
h.VSplitBuf(helpBuffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HelpCmd tries to open the given help page in a horizontal split
|
// HelpCmd tries to open the given help page according to the split type
|
||||||
|
// configured with the "helpsplit" option. It can be overriden by the optional
|
||||||
|
// arguments "-vpslit" or "-hsplit". In case more than one help page is given
|
||||||
|
// as argument then it opens all of them with the defined split type.
|
||||||
func (h *BufPane) HelpCmd(args []string) {
|
func (h *BufPane) HelpCmd(args []string) {
|
||||||
|
hsplit := config.GlobalSettings["helpsplit"] == "hsplit"
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
// Open the default help if the user just typed "> help"
|
// Open the default help if the user just typed "> help"
|
||||||
h.openHelp("help")
|
h.openHelp("help", hsplit, false)
|
||||||
} else {
|
} else {
|
||||||
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
|
var topics []string
|
||||||
err := h.openHelp(args[0])
|
forceSplit := false
|
||||||
if err != nil {
|
const errSplit = "hsplit and vsplit are not allowed at the same time"
|
||||||
InfoBar.Error(err)
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "-vsplit":
|
||||||
|
if forceSplit {
|
||||||
|
InfoBar.Error(errSplit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hsplit = false
|
||||||
|
forceSplit = true
|
||||||
|
case "-hsplit":
|
||||||
|
if forceSplit {
|
||||||
|
InfoBar.Error(errSplit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hsplit = true
|
||||||
|
forceSplit = true
|
||||||
|
default:
|
||||||
|
topics = append(topics, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(topics) < 1 {
|
||||||
|
// Do the same as without arg
|
||||||
|
h.openHelp("help", hsplit, forceSplit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(topics) > 1 {
|
||||||
|
forceSplit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, topic := range topics {
|
||||||
|
if config.FindRuntimeFile(config.RTHelp, topic) != nil {
|
||||||
|
err := h.openHelp(topic, hsplit, forceSplit)
|
||||||
|
if err != nil {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InfoBar.Error("Sorry, no help for ", topic)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
InfoBar.Error("Sorry, no help for ", args[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VSplitCmd opens a vertical split with file given in the first argument
|
// VSplitCmd opens one or more vertical splits with the files given as arguments
|
||||||
// If no file is given, it opens an empty buffer in a new split
|
// If no file is given, it opens an empty buffer in a new split
|
||||||
func (h *BufPane) VSplitCmd(args []string) {
|
func (h *BufPane) VSplitCmd(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@ -415,16 +516,18 @@ func (h *BufPane) VSplitCmd(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
for _, a := range args {
|
||||||
if err != nil {
|
buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
|
||||||
InfoBar.Error(err)
|
if err != nil {
|
||||||
return
|
InfoBar.Error(err)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
h.VSplitBuf(buf)
|
h.VSplitBuf(buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HSplitCmd opens a horizontal split with file given in the first argument
|
// HSplitCmd opens one or more horizontal splits with the files given as arguments
|
||||||
// If no file is given, it opens an empty buffer in a new split
|
// If no file is given, it opens an empty buffer in a new split
|
||||||
func (h *BufPane) HSplitCmd(args []string) {
|
func (h *BufPane) HSplitCmd(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@ -433,13 +536,15 @@ func (h *BufPane) HSplitCmd(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
for _, a := range args {
|
||||||
if err != nil {
|
buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
|
||||||
InfoBar.Error(err)
|
if err != nil {
|
||||||
return
|
InfoBar.Error(err)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
h.HSplitBuf(buf)
|
h.HSplitBuf(buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvalCmd evaluates a lua expression
|
// EvalCmd evaluates a lua expression
|
||||||
@ -447,7 +552,8 @@ func (h *BufPane) EvalCmd(args []string) {
|
|||||||
InfoBar.Error("Eval unsupported")
|
InfoBar.Error("Eval unsupported")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTabCmd opens the given file in a new tab
|
// NewTabCmd opens one or more tabs with the files given as arguments
|
||||||
|
// If no file is given, it opens an empty buffer in a new tab
|
||||||
func (h *BufPane) NewTabCmd(args []string) {
|
func (h *BufPane) NewTabCmd(args []string) {
|
||||||
width, height := screen.Screen.Size()
|
width, height := screen.Screen.Size()
|
||||||
iOffset := config.GetInfoBarOffset()
|
iOffset := config.GetInfoBarOffset()
|
||||||
@ -470,73 +576,98 @@ func (h *BufPane) NewTabCmd(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
func doSetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||||
local := false
|
if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) {
|
||||||
for _, s := range config.LocalSettings {
|
return nil
|
||||||
if s == option {
|
|
||||||
local = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !local {
|
config.GlobalSettings[option] = nativeValue
|
||||||
config.GlobalSettings[option] = nativeValue
|
config.ModifiedSettings[option] = true
|
||||||
config.ModifiedSettings[option] = true
|
delete(config.VolatileSettings, option)
|
||||||
|
|
||||||
if option == "colorscheme" {
|
if option == "colorscheme" {
|
||||||
// LoadSyntaxFiles()
|
// LoadSyntaxFiles()
|
||||||
config.InitColorscheme()
|
config.InitColorscheme()
|
||||||
for _, b := range buffer.OpenBuffers {
|
for _, b := range buffer.OpenBuffers {
|
||||||
b.UpdateRules()
|
b.UpdateRules()
|
||||||
}
|
}
|
||||||
} else if option == "infobar" || option == "keymenu" {
|
} else if option == "infobar" || option == "keymenu" {
|
||||||
Tabs.Resize()
|
Tabs.Resize()
|
||||||
} else if option == "mouse" {
|
} else if option == "mouse" {
|
||||||
if !nativeValue.(bool) {
|
if !nativeValue.(bool) {
|
||||||
screen.Screen.DisableMouse()
|
screen.Screen.DisableMouse()
|
||||||
} else {
|
|
||||||
screen.Screen.EnableMouse()
|
|
||||||
}
|
|
||||||
} else if option == "autosave" {
|
|
||||||
if nativeValue.(float64) > 0 {
|
|
||||||
config.SetAutoTime(int(nativeValue.(float64)))
|
|
||||||
config.StartAutoSave()
|
|
||||||
} else {
|
|
||||||
config.SetAutoTime(0)
|
|
||||||
}
|
|
||||||
} else if option == "paste" {
|
|
||||||
screen.Screen.SetPaste(nativeValue.(bool))
|
|
||||||
} else if option == "clipboard" {
|
|
||||||
m := clipboard.SetMethod(nativeValue.(string))
|
|
||||||
err := clipboard.Initialize(m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for _, pl := range config.Plugins {
|
screen.Screen.EnableMouse()
|
||||||
if option == pl.Name {
|
}
|
||||||
if nativeValue.(bool) && !pl.Loaded {
|
} else if option == "autosave" {
|
||||||
pl.Load()
|
if nativeValue.(float64) > 0 {
|
||||||
_, err := pl.Call("init")
|
config.SetAutoTime(nativeValue.(float64))
|
||||||
if err != nil && err != config.ErrNoSuchFunction {
|
} else {
|
||||||
screen.TermMessage(err)
|
config.SetAutoTime(0)
|
||||||
}
|
}
|
||||||
} else if !nativeValue.(bool) && pl.Loaded {
|
} else if option == "paste" {
|
||||||
_, err := pl.Call("deinit")
|
screen.Screen.SetPaste(nativeValue.(bool))
|
||||||
if err != nil && err != config.ErrNoSuchFunction {
|
} else if option == "clipboard" {
|
||||||
screen.TermMessage(err)
|
m := clipboard.SetMethod(nativeValue.(string))
|
||||||
}
|
err := clipboard.Initialize(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, pl := range config.Plugins {
|
||||||
|
if option == pl.Name {
|
||||||
|
if nativeValue.(bool) && !pl.Loaded {
|
||||||
|
pl.Load()
|
||||||
|
_, err := pl.Call("init")
|
||||||
|
if err != nil && err != config.ErrNoSuchFunction {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
} else if !nativeValue.(bool) && pl.Loaded {
|
||||||
|
_, err := pl.Call("deinit")
|
||||||
|
if err != nil && err != config.ErrNoSuchFunction {
|
||||||
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range buffer.OpenBuffers {
|
return nil
|
||||||
b.SetOptionNative(option, nativeValue)
|
}
|
||||||
|
|
||||||
|
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||||
|
if err := config.OptionIsValid(option, nativeValue); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
// check for local option first...
|
||||||
|
for _, s := range config.LocalSettings {
|
||||||
|
if s == option {
|
||||||
|
return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...if it's not local continue with the globals...
|
||||||
|
if err := doSetGlobalOptionNative(option, nativeValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...at last check the buffer locals
|
||||||
|
for _, b := range buffer.OpenBuffers {
|
||||||
|
b.DoSetOptionNative(option, nativeValue)
|
||||||
|
delete(b.LocalSettings, option)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
err = errors.Unwrap(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalOption(option, value string) error {
|
func SetGlobalOption(option, value string) error {
|
||||||
@ -560,16 +691,10 @@ func (h *BufPane) ResetCmd(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
option := args[0]
|
option := args[0]
|
||||||
|
defaults := config.DefaultAllSettings()
|
||||||
|
|
||||||
defaultGlobals := config.DefaultGlobalSettings()
|
if _, ok := defaults[option]; ok {
|
||||||
defaultLocals := config.DefaultCommonSettings()
|
SetGlobalOptionNative(option, defaults[option])
|
||||||
|
|
||||||
if _, ok := defaultGlobals[option]; ok {
|
|
||||||
SetGlobalOptionNative(option, defaultGlobals[option])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, ok := defaultLocals[option]; ok {
|
|
||||||
h.Buf.SetOptionNative(option, defaultLocals[option])
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
InfoBar.Error(config.ErrInvalidOption)
|
InfoBar.Error(config.ErrInvalidOption)
|
||||||
@ -634,6 +759,11 @@ func (h *BufPane) ShowCmd(args []string) {
|
|||||||
InfoBar.Message(option)
|
InfoBar.Message(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseKeyArg(arg string) string {
|
||||||
|
// If this is a raw escape sequence, convert it to its raw byte form
|
||||||
|
return strings.ReplaceAll(arg, "\\x1b", "\x1b")
|
||||||
|
}
|
||||||
|
|
||||||
// ShowKeyCmd displays the action that a key is bound to
|
// ShowKeyCmd displays the action that a key is bound to
|
||||||
func (h *BufPane) ShowKeyCmd(args []string) {
|
func (h *BufPane) ShowKeyCmd(args []string) {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
@ -641,7 +771,7 @@ func (h *BufPane) ShowKeyCmd(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
event, err := findEvent(args[0])
|
event, err := findEvent(parseKeyArg(args[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InfoBar.Error(err)
|
InfoBar.Error(err)
|
||||||
return
|
return
|
||||||
@ -660,9 +790,13 @@ func (h *BufPane) BindCmd(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := TryBindKey(args[0], args[1], true)
|
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InfoBar.Error(err)
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
} else {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -673,9 +807,13 @@ func (h *BufPane) UnbindCmd(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := UnbindKey(args[0])
|
err := UnbindKey(parseKeyArg(args[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InfoBar.Error(err)
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
} else {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -701,41 +839,67 @@ func (h *BufPane) QuitCmd(args []string) {
|
|||||||
// position in the buffer
|
// position in the buffer
|
||||||
// For example: `goto line`, or `goto line:col`
|
// For example: `goto line`, or `goto line:col`
|
||||||
func (h *BufPane) GotoCmd(args []string) {
|
func (h *BufPane) GotoCmd(args []string) {
|
||||||
|
line, col, err := h.parseLineCol(args)
|
||||||
|
if err != nil {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if line < 0 {
|
||||||
|
line = h.Buf.LinesNum() + 1 + line
|
||||||
|
}
|
||||||
|
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||||
|
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
||||||
|
|
||||||
|
h.RemoveAllMultiCursors()
|
||||||
|
h.Cursor.Deselect(true)
|
||||||
|
h.GotoLoc(buffer.Loc{col, line})
|
||||||
|
}
|
||||||
|
|
||||||
|
// JumpCmd is a command that will send the cursor to a certain relative
|
||||||
|
// position in the buffer
|
||||||
|
// For example: `jump line`, `jump -line`, or `jump -line:col`
|
||||||
|
func (h *BufPane) JumpCmd(args []string) {
|
||||||
|
line, col, err := h.parseLineCol(args)
|
||||||
|
if err != nil {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line = h.Buf.GetActiveCursor().Y + 1 + line
|
||||||
|
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||||
|
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
||||||
|
|
||||||
|
h.RemoveAllMultiCursors()
|
||||||
|
h.Cursor.Deselect(true)
|
||||||
|
h.GotoLoc(buffer.Loc{col, line})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLineCol is a helper to parse the input of GotoCmd and JumpCmd
|
||||||
|
func (h *BufPane) parseLineCol(args []string) (line int, col int, err error) {
|
||||||
if len(args) <= 0 {
|
if len(args) <= 0 {
|
||||||
InfoBar.Error("Not enough arguments")
|
return 0, 0, errors.New("Not enough arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
line, col = 0, 0
|
||||||
|
if strings.Contains(args[0], ":") {
|
||||||
|
parts := strings.SplitN(args[0], ":", 2)
|
||||||
|
line, err = strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
col, err = strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
h.RemoveAllMultiCursors()
|
line, err = strconv.Atoi(args[0])
|
||||||
if strings.Contains(args[0], ":") {
|
if err != nil {
|
||||||
parts := strings.SplitN(args[0], ":", 2)
|
return 0, 0, err
|
||||||
line, err := strconv.Atoi(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
InfoBar.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
col, err := strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
InfoBar.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if line < 0 {
|
|
||||||
line = h.Buf.LinesNum() + 1 + line
|
|
||||||
}
|
|
||||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
|
||||||
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
|
||||||
h.GotoLoc(buffer.Loc{col, line})
|
|
||||||
} else {
|
|
||||||
line, err := strconv.Atoi(args[0])
|
|
||||||
if err != nil {
|
|
||||||
InfoBar.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if line < 0 {
|
|
||||||
line = h.Buf.LinesNum() + 1 + line
|
|
||||||
}
|
|
||||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
|
||||||
h.GotoLoc(buffer.Loc{0, line})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return line, col, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveCmd saves the buffer optionally with an argument file name
|
// SaveCmd saves the buffer optionally with an argument file name
|
||||||
@ -743,7 +907,7 @@ func (h *BufPane) SaveCmd(args []string) {
|
|||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
h.Save()
|
h.Save()
|
||||||
} else {
|
} else {
|
||||||
h.Buf.SaveAs(args[0])
|
h.saveBufToFile(args[0], "SaveAs", nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -804,19 +968,21 @@ func (h *BufPane) ReplaceCmd(args []string) {
|
|||||||
nreplaced := 0
|
nreplaced := 0
|
||||||
start := h.Buf.Start()
|
start := h.Buf.Start()
|
||||||
end := h.Buf.End()
|
end := h.Buf.End()
|
||||||
|
searchLoc := h.Cursor.Loc
|
||||||
selection := h.Cursor.HasSelection()
|
selection := h.Cursor.HasSelection()
|
||||||
if selection {
|
if selection {
|
||||||
start = h.Cursor.CurSelection[0]
|
start = h.Cursor.CurSelection[0]
|
||||||
end = h.Cursor.CurSelection[1]
|
end = h.Cursor.CurSelection[1]
|
||||||
|
searchLoc = start // otherwise me might start at the end
|
||||||
}
|
}
|
||||||
if all {
|
if all {
|
||||||
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
|
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex)
|
||||||
} else {
|
} else {
|
||||||
inRange := func(l buffer.Loc) bool {
|
inRange := func(l buffer.Loc) bool {
|
||||||
return l.GreaterEqual(start) && l.LessEqual(end)
|
return l.GreaterEqual(start) && l.LessEqual(end)
|
||||||
}
|
}
|
||||||
|
|
||||||
searchLoc := h.Cursor.Loc
|
lastMatchEnd := buffer.Loc{-1, -1}
|
||||||
var doReplacement func()
|
var doReplacement func()
|
||||||
doReplacement = func() {
|
doReplacement = func() {
|
||||||
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
|
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
|
||||||
@ -831,6 +997,18 @@ func (h *BufPane) ReplaceCmd(args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lastMatchEnd == locs[1] {
|
||||||
|
// skip empty match right after previous match
|
||||||
|
if searchLoc == end {
|
||||||
|
searchLoc = start
|
||||||
|
lastMatchEnd = buffer.Loc{-1, -1}
|
||||||
|
} else {
|
||||||
|
searchLoc = searchLoc.Move(1, h.Buf)
|
||||||
|
}
|
||||||
|
doReplacement()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
h.Cursor.SetSelectionStart(locs[0])
|
h.Cursor.SetSelectionStart(locs[0])
|
||||||
h.Cursor.SetSelectionEnd(locs[1])
|
h.Cursor.SetSelectionEnd(locs[1])
|
||||||
h.GotoLoc(locs[0])
|
h.GotoLoc(locs[0])
|
||||||
@ -840,7 +1018,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
|
|||||||
|
|
||||||
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
|
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
|
||||||
if !canceled && yes {
|
if !canceled && yes {
|
||||||
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
|
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace, !noRegex)
|
||||||
|
|
||||||
searchLoc = locs[0]
|
searchLoc = locs[0]
|
||||||
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
|
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
|
||||||
@ -856,6 +1034,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
|
|||||||
h.Buf.RelocateCursors()
|
h.Buf.RelocateCursors()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
lastMatchEnd = searchLoc
|
||||||
doReplacement()
|
doReplacement()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package action
|
|||||||
var termdefaults = map[string]string{
|
var termdefaults = map[string]string{
|
||||||
"<Ctrl-q><Ctrl-q>": "Exit",
|
"<Ctrl-q><Ctrl-q>": "Exit",
|
||||||
"<Ctrl-e><Ctrl-e>": "CommandMode",
|
"<Ctrl-e><Ctrl-e>": "CommandMode",
|
||||||
"<Ctrl-w><Ctrl-w>": "NextSplit",
|
"<Ctrl-w><Ctrl-w>": "NextSplit|FirstSplit",
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultBindings returns a map containing micro's default keybindings
|
// DefaultBindings returns a map containing micro's default keybindings
|
||||||
|
@ -45,23 +45,25 @@ var bufdefaults = map[string]string{
|
|||||||
"Alt-]": "DiffNext|CursorEnd",
|
"Alt-]": "DiffNext|CursorEnd",
|
||||||
"Ctrl-z": "Undo",
|
"Ctrl-z": "Undo",
|
||||||
"Ctrl-y": "Redo",
|
"Ctrl-y": "Redo",
|
||||||
"Ctrl-c": "CopyLine|Copy",
|
"Ctrl-c": "Copy|CopyLine",
|
||||||
"Ctrl-x": "Cut",
|
"Ctrl-x": "Cut|CutLine",
|
||||||
"Ctrl-k": "CutLine",
|
"Ctrl-k": "CutLine",
|
||||||
"Ctrl-d": "DuplicateLine",
|
"Ctrl-d": "Duplicate|DuplicateLine",
|
||||||
"Ctrl-v": "Paste",
|
"Ctrl-v": "Paste",
|
||||||
"Ctrl-a": "SelectAll",
|
"Ctrl-a": "SelectAll",
|
||||||
"Ctrl-t": "AddTab",
|
"Ctrl-t": "AddTab",
|
||||||
"Alt-,": "PreviousTab",
|
"Alt-,": "PreviousTab|LastTab",
|
||||||
"Alt-.": "NextTab",
|
"Alt-.": "NextTab|FirstTab",
|
||||||
"Home": "StartOfTextToggle",
|
"Home": "StartOfTextToggle",
|
||||||
"End": "EndOfLine",
|
"End": "EndOfLine",
|
||||||
"CtrlHome": "CursorStart",
|
"CtrlHome": "CursorStart",
|
||||||
"CtrlEnd": "CursorEnd",
|
"CtrlEnd": "CursorEnd",
|
||||||
"PageUp": "CursorPageUp",
|
"PageUp": "CursorPageUp",
|
||||||
"PageDown": "CursorPageDown",
|
"PageDown": "CursorPageDown",
|
||||||
"CtrlPageUp": "PreviousTab",
|
"CtrlPageUp": "PreviousTab|LastTab",
|
||||||
"CtrlPageDown": "NextTab",
|
"CtrlPageDown": "NextTab|FirstTab",
|
||||||
|
"ShiftPageUp": "SelectPageUp",
|
||||||
|
"ShiftPageDown": "SelectPageDown",
|
||||||
"Ctrl-g": "ToggleHelp",
|
"Ctrl-g": "ToggleHelp",
|
||||||
"Alt-g": "ToggleKeyMenu",
|
"Alt-g": "ToggleKeyMenu",
|
||||||
"Ctrl-r": "ToggleRuler",
|
"Ctrl-r": "ToggleRuler",
|
||||||
@ -70,7 +72,7 @@ var bufdefaults = map[string]string{
|
|||||||
"Ctrl-b": "ShellMode",
|
"Ctrl-b": "ShellMode",
|
||||||
"Ctrl-q": "Quit",
|
"Ctrl-q": "Quit",
|
||||||
"Ctrl-e": "CommandMode",
|
"Ctrl-e": "CommandMode",
|
||||||
"Ctrl-w": "NextSplit",
|
"Ctrl-w": "NextSplit|FirstSplit",
|
||||||
"Ctrl-u": "ToggleMacro",
|
"Ctrl-u": "ToggleMacro",
|
||||||
"Ctrl-j": "PlayMacro",
|
"Ctrl-j": "PlayMacro",
|
||||||
"Insert": "ToggleOverwriteMode",
|
"Insert": "ToggleOverwriteMode",
|
||||||
@ -92,11 +94,13 @@ var bufdefaults = map[string]string{
|
|||||||
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
||||||
|
|
||||||
// Mouse bindings
|
// Mouse bindings
|
||||||
"MouseWheelUp": "ScrollUp",
|
"MouseWheelUp": "ScrollUp",
|
||||||
"MouseWheelDown": "ScrollDown",
|
"MouseWheelDown": "ScrollDown",
|
||||||
"MouseLeft": "MousePress",
|
"MouseLeft": "MousePress",
|
||||||
"MouseMiddle": "PastePrimary",
|
"MouseLeftDrag": "MouseDrag",
|
||||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
"MouseLeftRelease": "MouseRelease",
|
||||||
|
"MouseMiddle": "PastePrimary",
|
||||||
|
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||||
|
|
||||||
"Alt-n": "SpawnMultiCursor",
|
"Alt-n": "SpawnMultiCursor",
|
||||||
"AltShiftUp": "SpawnMultiCursorUp",
|
"AltShiftUp": "SpawnMultiCursorUp",
|
||||||
@ -142,8 +146,8 @@ var infodefaults = map[string]string{
|
|||||||
"Backtab": "CycleAutocompleteBack",
|
"Backtab": "CycleAutocompleteBack",
|
||||||
"Ctrl-z": "Undo",
|
"Ctrl-z": "Undo",
|
||||||
"Ctrl-y": "Redo",
|
"Ctrl-y": "Redo",
|
||||||
"Ctrl-c": "CopyLine|Copy",
|
"Ctrl-c": "Copy|CopyLine",
|
||||||
"Ctrl-x": "Cut",
|
"Ctrl-x": "Cut|CutLine",
|
||||||
"Ctrl-k": "CutLine",
|
"Ctrl-k": "CutLine",
|
||||||
"Ctrl-v": "Paste",
|
"Ctrl-v": "Paste",
|
||||||
"Home": "StartOfTextToggle",
|
"Home": "StartOfTextToggle",
|
||||||
@ -175,8 +179,10 @@ var infodefaults = map[string]string{
|
|||||||
"Esc": "AbortCommand",
|
"Esc": "AbortCommand",
|
||||||
|
|
||||||
// Mouse bindings
|
// Mouse bindings
|
||||||
"MouseWheelUp": "HistoryUp",
|
"MouseWheelUp": "HistoryUp",
|
||||||
"MouseWheelDown": "HistoryDown",
|
"MouseWheelDown": "HistoryDown",
|
||||||
"MouseLeft": "MousePress",
|
"MouseLeft": "MousePress",
|
||||||
"MouseMiddle": "PastePrimary",
|
"MouseLeftDrag": "MouseDrag",
|
||||||
|
"MouseLeftRelease": "MouseRelease",
|
||||||
|
"MouseMiddle": "PastePrimary",
|
||||||
}
|
}
|
||||||
|
@ -48,23 +48,25 @@ var bufdefaults = map[string]string{
|
|||||||
"Alt-]": "DiffNext|CursorEnd",
|
"Alt-]": "DiffNext|CursorEnd",
|
||||||
"Ctrl-z": "Undo",
|
"Ctrl-z": "Undo",
|
||||||
"Ctrl-y": "Redo",
|
"Ctrl-y": "Redo",
|
||||||
"Ctrl-c": "CopyLine|Copy",
|
"Ctrl-c": "Copy|CopyLine",
|
||||||
"Ctrl-x": "Cut",
|
"Ctrl-x": "Cut|CutLine",
|
||||||
"Ctrl-k": "CutLine",
|
"Ctrl-k": "CutLine",
|
||||||
"Ctrl-d": "DuplicateLine",
|
"Ctrl-d": "Duplicate|DuplicateLine",
|
||||||
"Ctrl-v": "Paste",
|
"Ctrl-v": "Paste",
|
||||||
"Ctrl-a": "SelectAll",
|
"Ctrl-a": "SelectAll",
|
||||||
"Ctrl-t": "AddTab",
|
"Ctrl-t": "AddTab",
|
||||||
"Alt-,": "PreviousTab",
|
"Alt-,": "PreviousTab|LastTab",
|
||||||
"Alt-.": "NextTab",
|
"Alt-.": "NextTab|FirstTab",
|
||||||
"Home": "StartOfTextToggle",
|
"Home": "StartOfTextToggle",
|
||||||
"End": "EndOfLine",
|
"End": "EndOfLine",
|
||||||
"CtrlHome": "CursorStart",
|
"CtrlHome": "CursorStart",
|
||||||
"CtrlEnd": "CursorEnd",
|
"CtrlEnd": "CursorEnd",
|
||||||
"PageUp": "CursorPageUp",
|
"PageUp": "CursorPageUp",
|
||||||
"PageDown": "CursorPageDown",
|
"PageDown": "CursorPageDown",
|
||||||
"CtrlPageUp": "PreviousTab",
|
"CtrlPageUp": "PreviousTab|LastTab",
|
||||||
"CtrlPageDown": "NextTab",
|
"CtrlPageDown": "NextTab|FirstTab",
|
||||||
|
"ShiftPageUp": "SelectPageUp",
|
||||||
|
"ShiftPageDown": "SelectPageDown",
|
||||||
"Ctrl-g": "ToggleHelp",
|
"Ctrl-g": "ToggleHelp",
|
||||||
"Alt-g": "ToggleKeyMenu",
|
"Alt-g": "ToggleKeyMenu",
|
||||||
"Ctrl-r": "ToggleRuler",
|
"Ctrl-r": "ToggleRuler",
|
||||||
@ -73,7 +75,7 @@ var bufdefaults = map[string]string{
|
|||||||
"Ctrl-b": "ShellMode",
|
"Ctrl-b": "ShellMode",
|
||||||
"Ctrl-q": "Quit",
|
"Ctrl-q": "Quit",
|
||||||
"Ctrl-e": "CommandMode",
|
"Ctrl-e": "CommandMode",
|
||||||
"Ctrl-w": "NextSplit",
|
"Ctrl-w": "NextSplit|FirstSplit",
|
||||||
"Ctrl-u": "ToggleMacro",
|
"Ctrl-u": "ToggleMacro",
|
||||||
"Ctrl-j": "PlayMacro",
|
"Ctrl-j": "PlayMacro",
|
||||||
"Insert": "ToggleOverwriteMode",
|
"Insert": "ToggleOverwriteMode",
|
||||||
@ -95,11 +97,13 @@ var bufdefaults = map[string]string{
|
|||||||
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
||||||
|
|
||||||
// Mouse bindings
|
// Mouse bindings
|
||||||
"MouseWheelUp": "ScrollUp",
|
"MouseWheelUp": "ScrollUp",
|
||||||
"MouseWheelDown": "ScrollDown",
|
"MouseWheelDown": "ScrollDown",
|
||||||
"MouseLeft": "MousePress",
|
"MouseLeft": "MousePress",
|
||||||
"MouseMiddle": "PastePrimary",
|
"MouseLeftDrag": "MouseDrag",
|
||||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
"MouseLeftRelease": "MouseRelease",
|
||||||
|
"MouseMiddle": "PastePrimary",
|
||||||
|
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||||
|
|
||||||
"Alt-n": "SpawnMultiCursor",
|
"Alt-n": "SpawnMultiCursor",
|
||||||
"Alt-m": "SpawnMultiCursorSelect",
|
"Alt-m": "SpawnMultiCursorSelect",
|
||||||
@ -145,8 +149,8 @@ var infodefaults = map[string]string{
|
|||||||
"Backtab": "CycleAutocompleteBack",
|
"Backtab": "CycleAutocompleteBack",
|
||||||
"Ctrl-z": "Undo",
|
"Ctrl-z": "Undo",
|
||||||
"Ctrl-y": "Redo",
|
"Ctrl-y": "Redo",
|
||||||
"Ctrl-c": "CopyLine|Copy",
|
"Ctrl-c": "Copy|CopyLine",
|
||||||
"Ctrl-x": "Cut",
|
"Ctrl-x": "Cut|CutLine",
|
||||||
"Ctrl-k": "CutLine",
|
"Ctrl-k": "CutLine",
|
||||||
"Ctrl-v": "Paste",
|
"Ctrl-v": "Paste",
|
||||||
"Home": "StartOfTextToggle",
|
"Home": "StartOfTextToggle",
|
||||||
@ -178,8 +182,10 @@ var infodefaults = map[string]string{
|
|||||||
"Esc": "AbortCommand",
|
"Esc": "AbortCommand",
|
||||||
|
|
||||||
// Mouse bindings
|
// Mouse bindings
|
||||||
"MouseWheelUp": "HistoryUp",
|
"MouseWheelUp": "HistoryUp",
|
||||||
"MouseWheelDown": "HistoryDown",
|
"MouseWheelDown": "HistoryDown",
|
||||||
"MouseLeft": "MousePress",
|
"MouseLeft": "MousePress",
|
||||||
"MouseMiddle": "PastePrimary",
|
"MouseLeftDrag": "MouseDrag",
|
||||||
|
"MouseLeftRelease": "MouseRelease",
|
||||||
|
"MouseMiddle": "PastePrimary",
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event interface {
|
type Event interface {
|
||||||
@ -44,6 +44,17 @@ func metaToAlt(mod tcell.ModMask) tcell.ModMask {
|
|||||||
return mod
|
return mod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func keyEvent(e *tcell.EventKey) KeyEvent {
|
||||||
|
ke := KeyEvent{
|
||||||
|
code: e.Key(),
|
||||||
|
mod: metaToAlt(e.Modifiers()),
|
||||||
|
}
|
||||||
|
if e.Key() == tcell.KeyRune {
|
||||||
|
ke.r = e.Rune()
|
||||||
|
}
|
||||||
|
return ke
|
||||||
|
}
|
||||||
|
|
||||||
func (k KeyEvent) Name() string {
|
func (k KeyEvent) Name() string {
|
||||||
if k.any {
|
if k.any {
|
||||||
return "<any>"
|
return "<any>"
|
||||||
@ -68,7 +79,7 @@ func (k KeyEvent) Name() string {
|
|||||||
if k.code == tcell.KeyRune {
|
if k.code == tcell.KeyRune {
|
||||||
s = string(k.r)
|
s = string(k.r)
|
||||||
} else {
|
} else {
|
||||||
s = fmt.Sprintf("Key[%d,%d]", k.code, int(k.r))
|
s = fmt.Sprintf("Key[%d]", k.code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(m) != 0 {
|
if len(m) != 0 {
|
||||||
@ -100,11 +111,20 @@ func (k KeySequenceEvent) Name() string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MouseState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MousePress = iota
|
||||||
|
MouseDrag
|
||||||
|
MouseRelease
|
||||||
|
)
|
||||||
|
|
||||||
// MouseEvent is a mouse event with a mouse button and
|
// MouseEvent is a mouse event with a mouse button and
|
||||||
// any possible key modifiers
|
// any possible key modifiers
|
||||||
type MouseEvent struct {
|
type MouseEvent struct {
|
||||||
btn tcell.ButtonMask
|
btn tcell.ButtonMask
|
||||||
mod tcell.ModMask
|
mod tcell.ModMask
|
||||||
|
state MouseState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m MouseEvent) Name() string {
|
func (m MouseEvent) Name() string {
|
||||||
@ -122,9 +142,17 @@ func (m MouseEvent) Name() string {
|
|||||||
mod = "Ctrl-"
|
mod = "Ctrl-"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state := ""
|
||||||
|
switch m.state {
|
||||||
|
case MouseDrag:
|
||||||
|
state = "Drag"
|
||||||
|
case MouseRelease:
|
||||||
|
state = "Release"
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range mouseEvents {
|
for k, v := range mouseEvents {
|
||||||
if v == m.btn {
|
if v == m.btn {
|
||||||
return fmt.Sprintf("%s%s", mod, k)
|
return fmt.Sprintf("%s%s%s", mod, k, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@ -138,11 +166,7 @@ func (m MouseEvent) Name() string {
|
|||||||
func ConstructEvent(event tcell.Event) (Event, error) {
|
func ConstructEvent(event tcell.Event) (Event, error) {
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
return KeyEvent{
|
return keyEvent(e), nil
|
||||||
code: e.Key(),
|
|
||||||
mod: metaToAlt(e.Modifiers()),
|
|
||||||
r: e.Rune(),
|
|
||||||
}, nil
|
|
||||||
case *tcell.EventRaw:
|
case *tcell.EventRaw:
|
||||||
return RawEvent{
|
return RawEvent{
|
||||||
esc: e.EscSeq(),
|
esc: e.EscSeq(),
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
|
"github.com/zyedidia/micro/v2/pkg/highlight"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file is meant (for now) for autocompletion in command mode, not
|
// This file is meant (for now) for autocompletion in command mode, not
|
||||||
@ -17,7 +18,7 @@ import (
|
|||||||
// CommandComplete autocompletes commands
|
// CommandComplete autocompletes commands
|
||||||
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
input, argstart := buffer.GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
var suggestions []string
|
var suggestions []string
|
||||||
for cmd := range commands {
|
for cmd := range commands {
|
||||||
@ -38,7 +39,7 @@ func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
|||||||
// HelpComplete autocompletes help topics
|
// HelpComplete autocompletes help topics
|
||||||
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
|
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
input, argstart := buffer.GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
var suggestions []string
|
var suggestions []string
|
||||||
|
|
||||||
@ -77,19 +78,67 @@ func colorschemeComplete(input string) (string, []string) {
|
|||||||
return chosen, suggestions
|
return chosen, suggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(s []string, e string) bool {
|
// filetypeComplete autocompletes filetype
|
||||||
for _, a := range s {
|
func filetypeComplete(input string) (string, []string) {
|
||||||
if a == e {
|
var suggestions []string
|
||||||
return true
|
|
||||||
|
// We cannot match filetypes just by names of syntax files,
|
||||||
|
// since those names may be different from the actual filetype values
|
||||||
|
// specified inside syntax files (e.g. "c++" filetype in cpp.yaml).
|
||||||
|
// So we need to parse filetype values out of those files.
|
||||||
|
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||||
|
data, err := f.Data()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
header, err := highlight.MakeHeaderYaml(data)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Prevent duplicated defaults
|
||||||
|
if header.FileType == "off" || header.FileType == "unknown" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(header.FileType, input) {
|
||||||
|
suggestions = append(suggestions, header.FileType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
headerLoop:
|
||||||
|
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
|
||||||
|
data, err := f.Data()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
header, err := highlight.MakeHeader(data)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range suggestions {
|
||||||
|
if v == header.FileType {
|
||||||
|
continue headerLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(header.FileType, input) {
|
||||||
|
suggestions = append(suggestions, header.FileType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix("off", input) {
|
||||||
|
suggestions = append(suggestions, "off")
|
||||||
|
}
|
||||||
|
|
||||||
|
var chosen string
|
||||||
|
if len(suggestions) == 1 {
|
||||||
|
chosen = suggestions[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosen, suggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionComplete autocompletes options
|
// OptionComplete autocompletes options
|
||||||
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
|
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
input, argstart := buffer.GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
var suggestions []string
|
var suggestions []string
|
||||||
for option := range config.GlobalSettings {
|
for option := range config.GlobalSettings {
|
||||||
@ -116,7 +165,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
|||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
l := b.LineBytes(c.Y)
|
l := b.LineBytes(c.Y)
|
||||||
l = util.SliceStart(l, c.X)
|
l = util.SliceStart(l, c.X)
|
||||||
input, argstart := buffer.GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
completeValue := false
|
completeValue := false
|
||||||
args := bytes.Split(l, []byte{' '})
|
args := bytes.Split(l, []byte{' '})
|
||||||
@ -172,13 +221,8 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
|||||||
switch inputOpt {
|
switch inputOpt {
|
||||||
case "colorscheme":
|
case "colorscheme":
|
||||||
_, suggestions = colorschemeComplete(input)
|
_, suggestions = colorschemeComplete(input)
|
||||||
case "fileformat":
|
case "filetype":
|
||||||
if strings.HasPrefix("unix", input) {
|
_, suggestions = filetypeComplete(input)
|
||||||
suggestions = append(suggestions, "unix")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix("dos", input) {
|
|
||||||
suggestions = append(suggestions, "dos")
|
|
||||||
}
|
|
||||||
case "sucmd":
|
case "sucmd":
|
||||||
if strings.HasPrefix("sudo", input) {
|
if strings.HasPrefix("sudo", input) {
|
||||||
suggestions = append(suggestions, "sudo")
|
suggestions = append(suggestions, "sudo")
|
||||||
@ -186,15 +230,13 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
|||||||
if strings.HasPrefix("doas", input) {
|
if strings.HasPrefix("doas", input) {
|
||||||
suggestions = append(suggestions, "doas")
|
suggestions = append(suggestions, "doas")
|
||||||
}
|
}
|
||||||
case "clipboard":
|
default:
|
||||||
if strings.HasPrefix("external", input) {
|
if choices, ok := config.OptionChoices[inputOpt]; ok {
|
||||||
suggestions = append(suggestions, "external")
|
for _, choice := range choices {
|
||||||
}
|
if strings.HasPrefix(choice, input) {
|
||||||
if strings.HasPrefix("internal", input) {
|
suggestions = append(suggestions, choice)
|
||||||
suggestions = append(suggestions, "internal")
|
}
|
||||||
}
|
}
|
||||||
if strings.HasPrefix("terminal", input) {
|
|
||||||
suggestions = append(suggestions, "terminal")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,7 +252,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
|||||||
// PluginCmdComplete autocompletes the plugin command
|
// PluginCmdComplete autocompletes the plugin command
|
||||||
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
|
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
input, argstart := buffer.GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
var suggestions []string
|
var suggestions []string
|
||||||
for _, cmd := range PluginCmds {
|
for _, cmd := range PluginCmds {
|
||||||
@ -232,7 +274,7 @@ func PluginComplete(b *buffer.Buffer) ([]string, []string) {
|
|||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
l := b.LineBytes(c.Y)
|
l := b.LineBytes(c.Y)
|
||||||
l = util.SliceStart(l, c.X)
|
l = util.SliceStart(l, c.X)
|
||||||
input, argstart := buffer.GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
completeValue := false
|
completeValue := false
|
||||||
args := bytes.Split(l, []byte{' '})
|
args := bytes.Split(l, []byte{' '})
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/display"
|
"github.com/zyedidia/micro/v2/internal/display"
|
||||||
"github.com/zyedidia/micro/v2/internal/info"
|
"github.com/zyedidia/micro/v2/internal/info"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InfoKeyAction func(*InfoPane)
|
type InfoKeyAction func(*InfoPane)
|
||||||
@ -83,22 +83,22 @@ func (h *InfoPane) Close() {
|
|||||||
|
|
||||||
func (h *InfoPane) HandleEvent(event tcell.Event) {
|
func (h *InfoPane) HandleEvent(event tcell.Event) {
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
|
case *tcell.EventResize:
|
||||||
|
// TODO
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
ke := KeyEvent{
|
ke := keyEvent(e)
|
||||||
code: e.Key(),
|
|
||||||
mod: metaToAlt(e.Modifiers()),
|
|
||||||
r: e.Rune(),
|
|
||||||
}
|
|
||||||
|
|
||||||
done := h.DoKeyEvent(ke)
|
done := h.DoKeyEvent(ke)
|
||||||
hasYN := h.HasYN
|
hasYN := h.HasYN
|
||||||
if e.Key() == tcell.KeyRune && hasYN {
|
if e.Key() == tcell.KeyRune && hasYN {
|
||||||
if (e.Rune() == 'y' || e.Rune() == 'Y') && hasYN {
|
y := e.Rune() == 'y' || e.Rune() == 'Y'
|
||||||
h.YNResp = true
|
n := e.Rune() == 'n' || e.Rune() == 'N'
|
||||||
h.DonePrompt(false)
|
if y || n {
|
||||||
} else if (e.Rune() == 'n' || e.Rune() == 'N') && hasYN {
|
h.YNResp = y
|
||||||
h.YNResp = false
|
|
||||||
h.DonePrompt(false)
|
h.DonePrompt(false)
|
||||||
|
|
||||||
|
InfoBindings.ResetEvents()
|
||||||
|
InfoBufBindings.ResetEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.Key() == tcell.KeyRune && !done && !hasYN {
|
if e.Key() == tcell.KeyRune && !done && !hasYN {
|
||||||
@ -122,7 +122,10 @@ func (h *InfoPane) HandleEvent(event tcell.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoKeyEvent executes a key event for the command bar, doing any overridden actions
|
// DoKeyEvent executes a key event for the command bar, doing any overridden actions.
|
||||||
|
// Returns true if the action was executed OR if there are more keys remaining
|
||||||
|
// to process before executing an action (if this is a key sequence event).
|
||||||
|
// Returns false if no action found.
|
||||||
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
||||||
action, more := InfoBindings.NextEvent(e, nil)
|
action, more := InfoBindings.NextEvent(e, nil)
|
||||||
if action != nil && !more {
|
if action != nil && !more {
|
||||||
@ -136,11 +139,25 @@ func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !more {
|
if !more {
|
||||||
|
// If no infopane action found, try to find a bufpane action.
|
||||||
|
//
|
||||||
|
// TODO: this is buggy. For example, if the command bar has the following
|
||||||
|
// two bindings:
|
||||||
|
//
|
||||||
|
// "<Ctrl-x><Ctrl-p>": "HistoryUp",
|
||||||
|
// "<Ctrl-x><Ctrl-v>": "Paste",
|
||||||
|
//
|
||||||
|
// the 2nd binding (with a bufpane action) doesn't work, since <Ctrl-x>
|
||||||
|
// has been already consumed by the 1st binding (with an infopane action).
|
||||||
|
//
|
||||||
|
// We should either iterate both InfoBindings and InfoBufBindings keytrees
|
||||||
|
// together, or just use the same keytree for both infopane and bufpane
|
||||||
|
// bindings.
|
||||||
action, more = InfoBufBindings.NextEvent(e, nil)
|
action, more = InfoBufBindings.NextEvent(e, nil)
|
||||||
if action != nil && !more {
|
if action != nil && !more {
|
||||||
done := action(h.BufPane)
|
action(h.BufPane)
|
||||||
InfoBufBindings.ResetEvents()
|
InfoBufBindings.ResetEvents()
|
||||||
return done
|
return true
|
||||||
} else if action == nil && !more {
|
} else if action == nil && !more {
|
||||||
InfoBufBindings.ResetEvents()
|
InfoBufBindings.ResetEvents()
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package action
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaneKeyAction func(Pane) bool
|
type PaneKeyAction func(Pane) bool
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/display"
|
"github.com/zyedidia/micro/v2/internal/display"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RawPane struct {
|
type RawPane struct {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/views"
|
"github.com/zyedidia/micro/v2/internal/views"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The TabList is a list of tabs and a window to display the tab bar
|
// The TabList is a list of tabs and a window to display the tab bar
|
||||||
@ -107,30 +107,32 @@ func (t *TabList) HandleEvent(event tcell.Event) {
|
|||||||
mx, my := e.Position()
|
mx, my := e.Position()
|
||||||
switch e.Buttons() {
|
switch e.Buttons() {
|
||||||
case tcell.Button1:
|
case tcell.Button1:
|
||||||
if my == t.Y && mx == 0 {
|
if my == t.Y && len(t.List) > 1 {
|
||||||
t.Scroll(-4)
|
if mx == 0 {
|
||||||
return
|
t.Scroll(-4)
|
||||||
} else if my == t.Y && mx == t.Width-1 {
|
} else if mx == t.Width-1 {
|
||||||
t.Scroll(4)
|
t.Scroll(4)
|
||||||
|
} else {
|
||||||
|
ind := t.LocFromVisual(buffer.Loc{mx, my})
|
||||||
|
if ind != -1 {
|
||||||
|
t.SetActive(ind)
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(t.List) > 1 {
|
case tcell.ButtonNone:
|
||||||
ind := t.LocFromVisual(buffer.Loc{mx, my})
|
if t.List[t.Active()].release {
|
||||||
if ind != -1 {
|
// Mouse release received, while already released
|
||||||
t.SetActive(ind)
|
t.ResetMouse()
|
||||||
return
|
return
|
||||||
}
|
|
||||||
if my == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case tcell.WheelUp:
|
case tcell.WheelUp:
|
||||||
if my == t.Y {
|
if my == t.Y && len(t.List) > 1 {
|
||||||
t.Scroll(4)
|
t.Scroll(4)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case tcell.WheelDown:
|
case tcell.WheelDown:
|
||||||
if my == t.Y {
|
if my == t.Y && len(t.List) > 1 {
|
||||||
t.Scroll(-4)
|
t.Scroll(-4)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -147,6 +149,56 @@ func (t *TabList) Display() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TabList) SetActive(a int) {
|
||||||
|
t.TabWindow.SetActive(a)
|
||||||
|
|
||||||
|
for i, p := range t.List {
|
||||||
|
if i == a {
|
||||||
|
if !p.isActive {
|
||||||
|
p.isActive = true
|
||||||
|
|
||||||
|
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, p.CurPane()))
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.isActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetMouse resets the mouse release state after the screen was stopped
|
||||||
|
// or the pane changed.
|
||||||
|
// This prevents situations in which mouse releases are received at the wrong place
|
||||||
|
// and the mouse state is still pressed.
|
||||||
|
func (t *TabList) ResetMouse() {
|
||||||
|
for _, tab := range t.List {
|
||||||
|
if !tab.release && tab.resizing != nil {
|
||||||
|
tab.resizing = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.release = true
|
||||||
|
|
||||||
|
for _, p := range tab.Panes {
|
||||||
|
if bp, ok := p.(*BufPane); ok {
|
||||||
|
bp.resetMouse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseTerms notifies term panes that a terminal job has finished.
|
||||||
|
func (t *TabList) CloseTerms() {
|
||||||
|
for _, tab := range t.List {
|
||||||
|
for _, p := range tab.Panes {
|
||||||
|
if tp, ok := p.(*TermPane); ok {
|
||||||
|
tp.HandleTermClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tabs is the global tab list
|
// Tabs is the global tab list
|
||||||
var Tabs *TabList
|
var Tabs *TabList
|
||||||
|
|
||||||
@ -164,6 +216,8 @@ func InitTabs(bufs []*buffer.Buffer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen.RestartCallback = Tabs.ResetMouse
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainTab() *Tab {
|
func MainTab() *Tab {
|
||||||
@ -177,6 +231,9 @@ func MainTab() *Tab {
|
|||||||
type Tab struct {
|
type Tab struct {
|
||||||
*views.Node
|
*views.Node
|
||||||
*display.UIWindow
|
*display.UIWindow
|
||||||
|
|
||||||
|
isActive bool
|
||||||
|
|
||||||
Panes []Pane
|
Panes []Pane
|
||||||
active int
|
active int
|
||||||
|
|
||||||
@ -214,34 +271,40 @@ func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
|
|||||||
// HandleEvent takes a tcell event and usually dispatches it to the current
|
// HandleEvent takes a tcell event and usually dispatches it to the current
|
||||||
// active pane. However if the event is a resize or a mouse event where the user
|
// active pane. However if the event is a resize or a mouse event where the user
|
||||||
// is interacting with the UI (resizing splits) then the event is consumed here
|
// is interacting with the UI (resizing splits) then the event is consumed here
|
||||||
// If the event is a mouse event in a pane, that pane will become active and get
|
// If the event is a mouse press event in a pane, that pane will become active
|
||||||
// the event
|
// and get the event
|
||||||
func (t *Tab) HandleEvent(event tcell.Event) {
|
func (t *Tab) HandleEvent(event tcell.Event) {
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *tcell.EventMouse:
|
case *tcell.EventMouse:
|
||||||
mx, my := e.Position()
|
mx, my := e.Position()
|
||||||
switch e.Buttons() {
|
btn := e.Buttons()
|
||||||
case tcell.Button1:
|
switch {
|
||||||
|
case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone:
|
||||||
|
// button press or drag
|
||||||
wasReleased := t.release
|
wasReleased := t.release
|
||||||
t.release = false
|
t.release = false
|
||||||
if t.resizing != nil {
|
|
||||||
var size int
|
if btn == tcell.Button1 {
|
||||||
if t.resizing.Kind == views.STVert {
|
if t.resizing != nil {
|
||||||
size = mx - t.resizing.X
|
var size int
|
||||||
} else {
|
if t.resizing.Kind == views.STVert {
|
||||||
size = my - t.resizing.Y + 1
|
size = mx - t.resizing.X
|
||||||
|
} else {
|
||||||
|
size = my - t.resizing.Y + 1
|
||||||
|
}
|
||||||
|
t.resizing.ResizeSplit(size)
|
||||||
|
t.Resize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if wasReleased {
|
||||||
|
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
|
||||||
|
if t.resizing != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.resizing.ResizeSplit(size)
|
|
||||||
t.Resize()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if wasReleased {
|
if wasReleased {
|
||||||
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
|
|
||||||
if t.resizing != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, p := range t.Panes {
|
for i, p := range t.Panes {
|
||||||
v := p.GetView()
|
v := p.GetView()
|
||||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||||
@ -251,10 +314,15 @@ func (t *Tab) HandleEvent(event tcell.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tcell.ButtonNone:
|
case btn == tcell.ButtonNone:
|
||||||
t.resizing = nil
|
// button release
|
||||||
t.release = true
|
t.release = true
|
||||||
|
if t.resizing != nil {
|
||||||
|
t.resizing = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
// wheel move
|
||||||
for _, p := range t.Panes {
|
for _, p := range t.Panes {
|
||||||
v := p.GetView()
|
v := p.GetView()
|
||||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||||
@ -279,11 +347,16 @@ func (t *Tab) SetActive(i int) {
|
|||||||
p.SetActive(false)
|
p.SetActive(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, MainTab().CurPane()))
|
// AddPane adds a pane at a given index
|
||||||
if err != nil {
|
func (t *Tab) AddPane(pane Pane, i int) {
|
||||||
screen.TermMessage(err)
|
if len(t.Panes) == i {
|
||||||
|
t.Panes = append(t.Panes, pane)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
t.Panes = append(t.Panes[:i+1], t.Panes[i:]...)
|
||||||
|
t.Panes[i] = pane
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPane returns the pane with the given split index
|
// GetPane returns the pane with the given split index
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/display"
|
"github.com/zyedidia/micro/v2/internal/display"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/shell"
|
"github.com/zyedidia/micro/v2/internal/shell"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
"github.com/zyedidia/terminal"
|
"github.com/micro-editor/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TermKeyAction func(*TermPane)
|
type TermKeyAction func(*TermPane)
|
||||||
@ -81,6 +81,10 @@ func (t *TermPane) SetID(i uint64) {
|
|||||||
t.id = i
|
t.id = i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TermPane) Name() string {
|
||||||
|
return t.Terminal.Name()
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TermPane) SetTab(tab *Tab) {
|
func (t *TermPane) SetTab(tab *Tab) {
|
||||||
t.tab = tab
|
t.tab = tab
|
||||||
}
|
}
|
||||||
@ -121,11 +125,7 @@ func (t *TermPane) Unsplit() {
|
|||||||
// copy-paste
|
// copy-paste
|
||||||
func (t *TermPane) HandleEvent(event tcell.Event) {
|
func (t *TermPane) HandleEvent(event tcell.Event) {
|
||||||
if e, ok := event.(*tcell.EventKey); ok {
|
if e, ok := event.(*tcell.EventKey); ok {
|
||||||
ke := KeyEvent{
|
ke := keyEvent(e)
|
||||||
code: e.Key(),
|
|
||||||
mod: metaToAlt(e.Modifiers()),
|
|
||||||
r: e.Rune(),
|
|
||||||
}
|
|
||||||
action, more := TermBindings.NextEvent(ke, nil)
|
action, more := TermBindings.NextEvent(ke, nil)
|
||||||
|
|
||||||
if !more {
|
if !more {
|
||||||
@ -159,9 +159,9 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
|
|||||||
if t.Status != shell.TTDone {
|
if t.Status != shell.TTDone {
|
||||||
t.WriteString(event.EscSeq())
|
t.WriteString(event.EscSeq())
|
||||||
}
|
}
|
||||||
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
|
} else if e, ok := event.(*tcell.EventMouse); !ok || t.State.Mode(terminal.ModeMouseMask) {
|
||||||
// t.WriteString(event.EscSeq())
|
// t.WriteString(event.EscSeq())
|
||||||
} else if e != nil {
|
} else {
|
||||||
x, y := e.Position()
|
x, y := e.Position()
|
||||||
v := t.GetView()
|
v := t.GetView()
|
||||||
x -= v.X
|
x -= v.X
|
||||||
@ -188,7 +188,12 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
|
|||||||
t.mouseReleased = true
|
t.mouseReleased = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleTermClose is called when a terminal has finished its job
|
||||||
|
// and should be closed. If that terminal is this termpane's terminal,
|
||||||
|
// HandleTermClose will close the terminal and the termpane itself.
|
||||||
|
func (t *TermPane) HandleTermClose() {
|
||||||
if t.Status == shell.TTClose {
|
if t.Status == shell.TTClose {
|
||||||
t.Quit()
|
t.Quit()
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package buffer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -64,7 +64,7 @@ func (b *Buffer) CycleAutocomplete(forward bool) {
|
|||||||
|
|
||||||
// GetWord gets the most recent word separated by any separator
|
// GetWord gets the most recent word separated by any separator
|
||||||
// (whitespace, punctuation, any non alphanumeric character)
|
// (whitespace, punctuation, any non alphanumeric character)
|
||||||
func GetWord(b *Buffer) ([]byte, int) {
|
func (b *Buffer) GetWord() ([]byte, int) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
l := b.LineBytes(c.Y)
|
l := b.LineBytes(c.Y)
|
||||||
l = util.SliceStart(l, c.X)
|
l = util.SliceStart(l, c.X)
|
||||||
@ -73,17 +73,17 @@ func GetWord(b *Buffer) ([]byte, int) {
|
|||||||
return []byte{}, -1
|
return []byte{}, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc.Move(-1, b))) {
|
if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) {
|
||||||
return []byte{}, c.X
|
return []byte{}, c.X
|
||||||
}
|
}
|
||||||
|
|
||||||
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
args := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||||
input := args[len(args)-1]
|
input := args[len(args)-1]
|
||||||
return input, c.X - util.CharacterCount(input)
|
return input, c.X - util.CharacterCount(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetArg gets the most recent word (separated by ' ' only)
|
// GetArg gets the most recent word (separated by ' ' only)
|
||||||
func GetArg(b *Buffer) (string, int) {
|
func (b *Buffer) GetArg() (string, int) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
l := b.LineBytes(c.Y)
|
l := b.LineBytes(c.Y)
|
||||||
l = util.SliceStart(l, c.X)
|
l = util.SliceStart(l, c.X)
|
||||||
@ -104,20 +104,20 @@ func GetArg(b *Buffer) (string, int) {
|
|||||||
// FileComplete autocompletes filenames
|
// FileComplete autocompletes filenames
|
||||||
func FileComplete(b *Buffer) ([]string, []string) {
|
func FileComplete(b *Buffer) ([]string, []string) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
input, argstart := GetArg(b)
|
input, argstart := b.GetArg()
|
||||||
|
|
||||||
sep := string(os.PathSeparator)
|
sep := string(os.PathSeparator)
|
||||||
dirs := strings.Split(input, sep)
|
dirs := strings.Split(input, sep)
|
||||||
|
|
||||||
var files []os.FileInfo
|
var files []fs.DirEntry
|
||||||
var err error
|
var err error
|
||||||
if len(dirs) > 1 {
|
if len(dirs) > 1 {
|
||||||
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
||||||
|
|
||||||
directories, _ = util.ReplaceHome(directories)
|
directories, _ = util.ReplaceHome(directories)
|
||||||
files, err = ioutil.ReadDir(directories)
|
files, err = os.ReadDir(directories)
|
||||||
} else {
|
} else {
|
||||||
files, err = ioutil.ReadDir(".")
|
files, err = os.ReadDir(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -153,7 +153,7 @@ func FileComplete(b *Buffer) ([]string, []string) {
|
|||||||
// BufferComplete autocompletes based on previous words in the buffer
|
// BufferComplete autocompletes based on previous words in the buffer
|
||||||
func BufferComplete(b *Buffer) ([]string, []string) {
|
func BufferComplete(b *Buffer) ([]string, []string) {
|
||||||
c := b.GetActiveCursor()
|
c := b.GetActiveCursor()
|
||||||
input, argstart := GetWord(b)
|
input, argstart := b.GetWord()
|
||||||
|
|
||||||
if argstart == -1 {
|
if argstart == -1 {
|
||||||
return []string{}, []string{}
|
return []string{}, []string{}
|
||||||
@ -166,7 +166,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
|
|||||||
var suggestions []string
|
var suggestions []string
|
||||||
for i := c.Y; i >= 0; i-- {
|
for i := c.Y; i >= 0; i-- {
|
||||||
l := b.LineBytes(i)
|
l := b.LineBytes(i)
|
||||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
words := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||||
for _, w := range words {
|
for _, w := range words {
|
||||||
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
||||||
strw := string(w)
|
strw := string(w)
|
||||||
@ -179,7 +179,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
|
|||||||
}
|
}
|
||||||
for i := c.Y + 1; i < b.LinesNum(); i++ {
|
for i := c.Y + 1; i < b.LinesNum(); i++ {
|
||||||
l := b.LineBytes(i)
|
l := b.LineBytes(i)
|
||||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
words := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||||
for _, w := range words {
|
for _, w := range words {
|
||||||
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
||||||
strw := string(w)
|
strw := string(w)
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"golang.org/x/text/encoding"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupMsg = `A backup was detected for this file. This likely means that micro
|
const BackupMsg = `A backup was detected for:
|
||||||
crashed while editing this file, or another instance of micro is currently
|
|
||||||
editing this file.
|
|
||||||
|
|
||||||
The backup was created on %s, and the file is
|
%s
|
||||||
|
|
||||||
|
This likely means that micro crashed while editing this file,
|
||||||
|
or another instance of micro is currently editing this file,
|
||||||
|
or an error occurred while saving this file so it may be corrupted.
|
||||||
|
|
||||||
|
The backup was created on %s and its path is:
|
||||||
|
|
||||||
%s
|
%s
|
||||||
|
|
||||||
@ -30,90 +32,80 @@ The backup was created on %s, and the file is
|
|||||||
|
|
||||||
Options: [r]ecover, [i]gnore, [a]bort: `
|
Options: [r]ecover, [i]gnore, [a]bort: `
|
||||||
|
|
||||||
var backupRequestChan chan *Buffer
|
const backupSeconds = 8
|
||||||
|
|
||||||
func backupThread() {
|
var BackupCompleteChan chan *Buffer
|
||||||
for {
|
|
||||||
time.Sleep(time.Second * 8)
|
|
||||||
|
|
||||||
for len(backupRequestChan) > 0 {
|
|
||||||
b := <-backupRequestChan
|
|
||||||
bfini := atomic.LoadInt32(&(b.fini)) != 0
|
|
||||||
if !bfini {
|
|
||||||
b.Backup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
backupRequestChan = make(chan *Buffer, 10)
|
BackupCompleteChan = make(chan *Buffer, 10)
|
||||||
|
|
||||||
go backupThread()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) RequestBackup() {
|
func (b *Buffer) RequestBackup() {
|
||||||
if !b.requestedBackup {
|
if !b.RequestedBackup {
|
||||||
select {
|
select {
|
||||||
case backupRequestChan <- b:
|
case backupRequestChan <- b:
|
||||||
default:
|
default:
|
||||||
// channel is full
|
// channel is full
|
||||||
}
|
}
|
||||||
b.requestedBackup = true
|
b.RequestedBackup = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup saves the current buffer to ConfigDir/backups
|
func (b *Buffer) backupDir() string {
|
||||||
|
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
|
||||||
|
if backupdir == "" || err != nil {
|
||||||
|
backupdir = filepath.Join(config.ConfigDir, "backups")
|
||||||
|
}
|
||||||
|
return backupdir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) keepBackup() bool {
|
||||||
|
return b.forceKeepBackup || b.Settings["permbackup"].(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup saves the current buffer to the backups directory
|
||||||
func (b *Buffer) Backup() error {
|
func (b *Buffer) Backup() error {
|
||||||
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
|
backupdir := b.backupDir()
|
||||||
if backupdir == "" || err != nil {
|
if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) {
|
||||||
backupdir = filepath.Join(config.ConfigDir, "backups")
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
|
|
||||||
os.Mkdir(backupdir, os.ModePerm)
|
os.Mkdir(backupdir, os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath))
|
name := util.DetermineEscapePath(backupdir, b.AbsPath)
|
||||||
|
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
|
||||||
err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) {
|
_, err = b.overwriteFile(name)
|
||||||
if len(b.lines) == 0 {
|
if err == nil {
|
||||||
return
|
BackupCompleteChan <- b
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// end of line
|
tmp := util.AppendBackupSuffix(name)
|
||||||
eol := []byte{'\n'}
|
_, err := b.overwriteFile(tmp)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.Rename(tmp, name)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// write lines
|
BackupCompleteChan <- b
|
||||||
if _, e = file.Write(b.lines[0].data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range b.lines[1:] {
|
|
||||||
if _, e = file.Write(eol); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, e = file.Write(l.data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
b.requestedBackup = false
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveBackup removes any backup file associated with this buffer
|
// RemoveBackup removes any backup file associated with this buffer
|
||||||
func (b *Buffer) RemoveBackup() {
|
func (b *Buffer) RemoveBackup() {
|
||||||
if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
|
f := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
|
||||||
os.Remove(f)
|
os.Remove(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,13 +113,13 @@ func (b *Buffer) RemoveBackup() {
|
|||||||
// Returns true if a backup was applied
|
// Returns true if a backup was applied
|
||||||
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
|
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
|
||||||
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
|
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
|
||||||
backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
|
backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
|
||||||
if info, err := os.Stat(backupfile); err == nil {
|
if info, err := os.Stat(backupfile); err == nil {
|
||||||
backup, err := os.Open(backupfile)
|
backup, err := os.Open(backupfile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer backup.Close()
|
defer backup.Close()
|
||||||
t := info.ModTime()
|
t := info.ModTime()
|
||||||
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath))
|
msg := fmt.Sprintf(BackupMsg, b.Path, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile)
|
||||||
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
|
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
|
||||||
|
|
||||||
if choice%3 == 0 {
|
if choice%3 == 0 {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -25,13 +25,12 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/micro/v2/pkg/highlight"
|
"github.com/zyedidia/micro/v2/pkg/highlight"
|
||||||
|
"golang.org/x/text/encoding"
|
||||||
"golang.org/x/text/encoding/htmlindex"
|
"golang.org/x/text/encoding/htmlindex"
|
||||||
"golang.org/x/text/encoding/unicode"
|
"golang.org/x/text/encoding/unicode"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupTime = 8000
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// OpenBuffers is a list of the currently open buffers
|
// OpenBuffers is a list of the currently open buffers
|
||||||
OpenBuffers []*Buffer
|
OpenBuffers []*Buffer
|
||||||
@ -57,17 +56,13 @@ var (
|
|||||||
BTLog = BufType{2, true, true, false}
|
BTLog = BufType{2, true, true, false}
|
||||||
// BTScratch is a buffer that cannot be saved (for scratch work)
|
// BTScratch is a buffer that cannot be saved (for scratch work)
|
||||||
BTScratch = BufType{3, false, true, false}
|
BTScratch = BufType{3, false, true, false}
|
||||||
// BTRaw is is a buffer that shows raw terminal events
|
// BTRaw is a buffer that shows raw terminal events
|
||||||
BTRaw = BufType{4, false, true, false}
|
BTRaw = BufType{4, false, true, false}
|
||||||
// BTInfo is a buffer for inputting information
|
// BTInfo is a buffer for inputting information
|
||||||
BTInfo = BufType{5, false, true, false}
|
BTInfo = BufType{5, false, true, false}
|
||||||
// BTStdout is a buffer that only writes to stdout
|
// BTStdout is a buffer that only writes to stdout
|
||||||
// when closed
|
// when closed
|
||||||
BTStdout = BufType{6, false, true, true}
|
BTStdout = BufType{6, false, true, true}
|
||||||
|
|
||||||
// ErrFileTooLarge is returned when the file is too large to hash
|
|
||||||
// (fastdirty is automatically enabled)
|
|
||||||
ErrFileTooLarge = errors.New("File is too large to hash")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SharedBuffer is a struct containing info that is shared among buffers
|
// SharedBuffer is a struct containing info that is shared among buffers
|
||||||
@ -90,6 +85,10 @@ type SharedBuffer struct {
|
|||||||
|
|
||||||
// Settings customized by the user
|
// Settings customized by the user
|
||||||
Settings map[string]interface{}
|
Settings map[string]interface{}
|
||||||
|
// LocalSettings customized by the user for this buffer only
|
||||||
|
LocalSettings map[string]bool
|
||||||
|
|
||||||
|
encoding encoding.Encoding
|
||||||
|
|
||||||
Suggestions []string
|
Suggestions []string
|
||||||
Completions []string
|
Completions []string
|
||||||
@ -103,7 +102,8 @@ type SharedBuffer struct {
|
|||||||
diffLock sync.RWMutex
|
diffLock sync.RWMutex
|
||||||
diff map[int]DiffStatus
|
diff map[int]DiffStatus
|
||||||
|
|
||||||
requestedBackup bool
|
RequestedBackup bool
|
||||||
|
forceKeepBackup bool
|
||||||
|
|
||||||
// ReloadDisabled allows the user to disable reloads if they
|
// ReloadDisabled allows the user to disable reloads if they
|
||||||
// are viewing a file that is constantly changing
|
// are viewing a file that is constantly changing
|
||||||
@ -211,6 +211,11 @@ type Buffer struct {
|
|||||||
LastSearchRegex bool
|
LastSearchRegex bool
|
||||||
// HighlightSearch enables highlighting all instances of the last successful search
|
// HighlightSearch enables highlighting all instances of the last successful search
|
||||||
HighlightSearch bool
|
HighlightSearch bool
|
||||||
|
|
||||||
|
// OverwriteMode indicates that we are in overwrite mode (toggled by
|
||||||
|
// Insert key by default) i.e. that typing a character shall replace the
|
||||||
|
// character under the cursor instead of inserting a character before it.
|
||||||
|
OverwriteMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBufferFromFileAtLoc opens a new buffer with a given cursor location
|
// NewBufferFromFileAtLoc opens a new buffer with a given cursor location
|
||||||
@ -234,17 +239,20 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
|
|
||||||
readonly := os.IsPermission(err)
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
fileInfo, serr := os.Stat(filename)
|
fileInfo, serr := os.Stat(filename)
|
||||||
if serr != nil && !os.IsNotExist(serr) {
|
if serr != nil && !errors.Is(serr, fs.ErrNotExist) {
|
||||||
return nil, serr
|
return nil, serr
|
||||||
}
|
}
|
||||||
if serr == nil && fileInfo.IsDir() {
|
if serr == nil && fileInfo.IsDir() {
|
||||||
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
|
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
|
||||||
}
|
}
|
||||||
|
if serr == nil && !fileInfo.Mode().IsRegular() {
|
||||||
|
return nil, errors.New("Error: " + filename + " is not a regular file and cannot be opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
|
||||||
|
readonly := errors.Is(err, fs.ErrPermission)
|
||||||
|
f.Close()
|
||||||
|
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -252,7 +260,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf *Buffer
|
var buf *Buffer
|
||||||
if os.IsNotExist(err) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
// File does not exist -- create an empty buffer with that name
|
// File does not exist -- create an empty buffer with that name
|
||||||
buf = NewBufferFromString("", filename, btype)
|
buf = NewBufferFromString("", filename, btype)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -322,29 +330,19 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
|||||||
b.AbsPath = absPath
|
b.AbsPath = absPath
|
||||||
b.Path = path
|
b.Path = path
|
||||||
|
|
||||||
// this is a little messy since we need to know some settings to read
|
|
||||||
// the file properly, but some settings depend on the filetype, which
|
|
||||||
// we don't know until reading the file. We first read the settings
|
|
||||||
// into a local variable and then use that to determine the encoding,
|
|
||||||
// readonly, and fileformat necessary for reading the file and
|
|
||||||
// assigning the filetype.
|
|
||||||
settings := config.DefaultCommonSettings()
|
|
||||||
b.Settings = config.DefaultCommonSettings()
|
b.Settings = config.DefaultCommonSettings()
|
||||||
|
b.LocalSettings = make(map[string]bool)
|
||||||
for k, v := range config.GlobalSettings {
|
for k, v := range config.GlobalSettings {
|
||||||
if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
|
if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
|
||||||
// make sure setting is not global-only
|
// make sure setting is not global-only
|
||||||
settings[k] = v
|
|
||||||
b.Settings[k] = v
|
b.Settings[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.InitLocalSettings(settings, absPath)
|
config.UpdatePathGlobLocals(b.Settings, absPath)
|
||||||
b.Settings["readonly"] = settings["readonly"]
|
|
||||||
b.Settings["filetype"] = settings["filetype"]
|
|
||||||
b.Settings["syntax"] = settings["syntax"]
|
|
||||||
|
|
||||||
enc, err := htmlindex.Get(settings["encoding"].(string))
|
b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
enc = unicode.UTF8
|
b.encoding = unicode.UTF8
|
||||||
b.Settings["encoding"] = "utf-8"
|
b.Settings["encoding"] = "utf-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,19 +353,22 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
|||||||
return NewBufferFromString("", "", btype)
|
return NewBufferFromString("", "", btype)
|
||||||
}
|
}
|
||||||
if !hasBackup {
|
if !hasBackup {
|
||||||
reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
|
reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder()))
|
||||||
|
|
||||||
var ff FileFormat = FFAuto
|
var ff FileFormat = FFAuto
|
||||||
|
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
// for empty files, use the fileformat setting instead of
|
// for empty files, use the fileformat setting instead of
|
||||||
// autodetection
|
// autodetection
|
||||||
switch settings["fileformat"] {
|
switch b.Settings["fileformat"] {
|
||||||
case "unix":
|
case "unix":
|
||||||
ff = FFUnix
|
ff = FFUnix
|
||||||
case "dos":
|
case "dos":
|
||||||
ff = FFDos
|
ff = FFDos
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// in case of autodetection treat as locally set
|
||||||
|
b.LocalSettings["fileformat"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
b.LineArray = NewLineArray(uint64(size), ff, reader)
|
b.LineArray = NewLineArray(uint64(size), ff, reader)
|
||||||
@ -390,10 +391,10 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.UpdateRules()
|
b.UpdateRules()
|
||||||
// init local settings again now that we know the filetype
|
// we know the filetype now, so update per-filetype settings
|
||||||
config.InitLocalSettings(b.Settings, b.Path)
|
config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string))
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); errors.Is(err, fs.ErrNotExist) {
|
||||||
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
|
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,7 +545,7 @@ func (b *Buffer) ReOpen() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
|
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
|
||||||
data, err := ioutil.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
txt := string(data)
|
txt := string(data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -554,7 +555,11 @@ func (b *Buffer) ReOpen() error {
|
|||||||
|
|
||||||
err = b.UpdateModTime()
|
err = b.UpdateModTime()
|
||||||
if !b.Settings["fastdirty"].(bool) {
|
if !b.Settings["fastdirty"].(bool) {
|
||||||
calcHash(b, &b.origHash)
|
if len(data) > LargeFileThreshold {
|
||||||
|
b.Settings["fastdirty"] = true
|
||||||
|
} else {
|
||||||
|
calcHash(b, &b.origHash)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.isModified = false
|
b.isModified = false
|
||||||
b.RelocateCursors()
|
b.RelocateCursors()
|
||||||
@ -568,6 +573,13 @@ func (b *Buffer) RelocateCursors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeselectCursors removes selection from all cursors
|
||||||
|
func (b *Buffer) DeselectCursors() {
|
||||||
|
for _, c := range b.cursors {
|
||||||
|
c.Deselect(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RuneAt returns the rune at a given location in the buffer
|
// RuneAt returns the rune at a given location in the buffer
|
||||||
func (b *Buffer) RuneAt(loc Loc) rune {
|
func (b *Buffer) RuneAt(loc Loc) rune {
|
||||||
line := b.LineBytes(loc.Y)
|
line := b.LineBytes(loc.Y)
|
||||||
@ -642,39 +654,122 @@ func (b *Buffer) Size() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calcHash calculates md5 hash of all lines in the buffer
|
// calcHash calculates md5 hash of all lines in the buffer
|
||||||
func calcHash(b *Buffer, out *[md5.Size]byte) error {
|
func calcHash(b *Buffer, out *[md5.Size]byte) {
|
||||||
h := md5.New()
|
h := md5.New()
|
||||||
|
|
||||||
size := 0
|
|
||||||
if len(b.lines) > 0 {
|
if len(b.lines) > 0 {
|
||||||
n, e := h.Write(b.lines[0].data)
|
h.Write(b.lines[0].data)
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
size += n
|
|
||||||
|
|
||||||
for _, l := range b.lines[1:] {
|
for _, l := range b.lines[1:] {
|
||||||
n, e = h.Write([]byte{'\n'})
|
if b.Endings == FFDos {
|
||||||
if e != nil {
|
h.Write([]byte{'\r', '\n'})
|
||||||
return e
|
} else {
|
||||||
|
h.Write([]byte{'\n'})
|
||||||
}
|
}
|
||||||
size += n
|
h.Write(l.data)
|
||||||
n, e = h.Write(l.data)
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
size += n
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if size > LargeFileThreshold {
|
|
||||||
return ErrFileTooLarge
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Sum((*out)[:0])
|
h.Sum((*out)[:0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def {
|
||||||
|
data, err := f.Data()
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == nil {
|
||||||
|
header, err = highlight.MakeHeaderYaml(data)
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := highlight.ParseFile(data)
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
syndef, err := highlight.ParseDef(file, header)
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return syndef
|
||||||
|
}
|
||||||
|
|
||||||
|
// findRealRuntimeSyntaxDef finds a specific syntax definition
|
||||||
|
// in the user's custom syntax files
|
||||||
|
func findRealRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
|
||||||
|
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||||
|
if f.Name() == name {
|
||||||
|
syndef := parseDefFromFile(f, header)
|
||||||
|
if syndef != nil {
|
||||||
|
return syndef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findRuntimeSyntaxDef finds a specific syntax definition
|
||||||
|
// in the built-in syntax files
|
||||||
|
func findRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
|
||||||
|
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||||
|
if f.Name() == name {
|
||||||
|
syndef := parseDefFromFile(f, header)
|
||||||
|
if syndef != nil {
|
||||||
|
return syndef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveIncludes(syndef *highlight.Def) {
|
||||||
|
includes := highlight.GetIncludes(syndef)
|
||||||
|
if len(includes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []*highlight.File
|
||||||
|
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||||
|
data, err := f.Data()
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := highlight.MakeHeaderYaml(data)
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range includes {
|
||||||
|
if header.FileType == i {
|
||||||
|
file, err := highlight.ParseFile(data)
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(files) >= len(includes) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight.ResolveIncludes(syndef, files)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||||
// This is called when the colorscheme changes
|
// This is called when the colorscheme changes
|
||||||
func (b *Buffer) UpdateRules() {
|
func (b *Buffer) UpdateRules() {
|
||||||
@ -683,13 +778,32 @@ func (b *Buffer) UpdateRules() {
|
|||||||
}
|
}
|
||||||
ft := b.Settings["filetype"].(string)
|
ft := b.Settings["filetype"].(string)
|
||||||
if ft == "off" {
|
if ft == "off" {
|
||||||
|
b.ClearMatches()
|
||||||
|
b.SyntaxDef = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.SyntaxDef = nil
|
||||||
|
|
||||||
|
// syntaxFileInfo is an internal helper structure
|
||||||
|
// to store properties of one single syntax file
|
||||||
|
type syntaxFileInfo struct {
|
||||||
|
header *highlight.Header
|
||||||
|
fileName string
|
||||||
|
syntaxDef *highlight.Def
|
||||||
|
}
|
||||||
|
|
||||||
|
fnameMatches := []syntaxFileInfo{}
|
||||||
|
headerMatches := []syntaxFileInfo{}
|
||||||
syntaxFile := ""
|
syntaxFile := ""
|
||||||
foundDef := false
|
foundDef := false
|
||||||
var header *highlight.Header
|
var header *highlight.Header
|
||||||
// search for the syntax file in the user's custom syntax files
|
// search for the syntax file in the user's custom syntax files
|
||||||
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||||
|
if f.Name() == "default" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
data, err := f.Data()
|
data, err := f.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||||
@ -701,118 +815,145 @@ func (b *Buffer) UpdateRules() {
|
|||||||
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
|
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
file, err := highlight.ParseFile(data)
|
|
||||||
if err != nil {
|
matchedFileType := false
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
matchedFileName := false
|
||||||
continue
|
matchedFileHeader := false
|
||||||
|
|
||||||
|
if ft == "unknown" || ft == "" {
|
||||||
|
if header.MatchFileName(b.Path) {
|
||||||
|
matchedFileName = true
|
||||||
|
}
|
||||||
|
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
|
||||||
|
matchedFileHeader = true
|
||||||
|
}
|
||||||
|
} else if header.FileType == ft {
|
||||||
|
matchedFileType = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
|
if matchedFileType || matchedFileName || matchedFileHeader {
|
||||||
|
file, err := highlight.ParseFile(data)
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
syndef, err := highlight.ParseDef(file, header)
|
syndef, err := highlight.ParseDef(file, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.SyntaxDef = syndef
|
|
||||||
syntaxFile = f.Name()
|
if matchedFileType {
|
||||||
foundDef = true
|
b.SyntaxDef = syndef
|
||||||
break
|
syntaxFile = f.Name()
|
||||||
|
foundDef = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchedFileName {
|
||||||
|
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), syndef})
|
||||||
|
} else if matchedFileHeader {
|
||||||
|
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), syndef})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// search in the default syntax files
|
if !foundDef {
|
||||||
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
|
// search for the syntax file in the built-in syntax files
|
||||||
data, err := f.Data()
|
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
|
||||||
if err != nil {
|
data, err := f.Data()
|
||||||
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
|
if err != nil {
|
||||||
continue
|
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
header, err = highlight.MakeHeader(data)
|
header, err = highlight.MakeHeader(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error reading syntax header file", f.Name(), err)
|
screen.TermMessage("Error reading syntax header file", f.Name(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ft == "unknown" || ft == "" {
|
if ft == "unknown" || ft == "" {
|
||||||
if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
|
if header.MatchFileName(b.Path) {
|
||||||
|
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), nil})
|
||||||
|
}
|
||||||
|
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
|
||||||
|
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), nil})
|
||||||
|
}
|
||||||
|
} else if header.FileType == ft {
|
||||||
syntaxFile = f.Name()
|
syntaxFile = f.Name()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if header.FileType == ft {
|
}
|
||||||
syntaxFile = f.Name()
|
}
|
||||||
break
|
|
||||||
|
if syntaxFile == "" {
|
||||||
|
matches := fnameMatches
|
||||||
|
if len(matches) == 0 {
|
||||||
|
matches = headerMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
length := len(matches)
|
||||||
|
if length > 0 {
|
||||||
|
signatureMatch := false
|
||||||
|
if length > 1 {
|
||||||
|
// multiple matching syntax files found, try to resolve the ambiguity
|
||||||
|
// using signatures
|
||||||
|
detectlimit := util.IntOpt(b.Settings["detectlimit"])
|
||||||
|
lineCount := len(b.lines)
|
||||||
|
limit := lineCount
|
||||||
|
if detectlimit > 0 && lineCount > detectlimit {
|
||||||
|
limit = detectlimit
|
||||||
|
}
|
||||||
|
|
||||||
|
matchLoop:
|
||||||
|
for _, m := range matches {
|
||||||
|
if m.header.HasFileSignature() {
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
if m.header.MatchFileSignature(b.lines[i].data) {
|
||||||
|
syntaxFile = m.fileName
|
||||||
|
if m.syntaxDef != nil {
|
||||||
|
b.SyntaxDef = m.syntaxDef
|
||||||
|
foundDef = true
|
||||||
|
}
|
||||||
|
header = m.header
|
||||||
|
signatureMatch = true
|
||||||
|
break matchLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if length == 1 || !signatureMatch {
|
||||||
|
syntaxFile = matches[0].fileName
|
||||||
|
if matches[0].syntaxDef != nil {
|
||||||
|
b.SyntaxDef = matches[0].syntaxDef
|
||||||
|
foundDef = true
|
||||||
|
}
|
||||||
|
header = matches[0].header
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if syntaxFile != "" && !foundDef {
|
if syntaxFile != "" && !foundDef {
|
||||||
// we found a syntax file using a syntax header file
|
// we found a syntax file using a syntax header file
|
||||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
b.SyntaxDef = findRuntimeSyntaxDef(syntaxFile, header)
|
||||||
if f.Name() == syntaxFile {
|
|
||||||
data, err := f.Data()
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := highlight.ParseFile(data)
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
syndef, err := highlight.ParseDef(file, header)
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b.SyntaxDef = syndef
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
|
if b.SyntaxDef != nil {
|
||||||
includes := highlight.GetIncludes(b.SyntaxDef)
|
b.Settings["filetype"] = b.SyntaxDef.FileType
|
||||||
|
|
||||||
var files []*highlight.File
|
|
||||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
|
||||||
data, err := f.Data()
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
header, err := highlight.MakeHeaderYaml(data)
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, i := range includes {
|
|
||||||
if header.FileType == i {
|
|
||||||
file, err := highlight.ParseFile(data)
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
files = append(files, file)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(files) >= len(includes) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight.ResolveIncludes(b.SyntaxDef, files)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Highlighter == nil || syntaxFile != "" {
|
|
||||||
if b.SyntaxDef != nil {
|
|
||||||
b.Settings["filetype"] = b.SyntaxDef.FileType
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
b.SyntaxDef = &highlight.EmptyDef
|
// search for the default file in the user's custom syntax files
|
||||||
|
b.SyntaxDef = findRealRuntimeSyntaxDef("default", nil)
|
||||||
|
if b.SyntaxDef == nil {
|
||||||
|
// search for the default file in the built-in syntax files
|
||||||
|
b.SyntaxDef = findRuntimeSyntaxDef("default", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.SyntaxDef != nil {
|
||||||
|
resolveIncludes(b.SyntaxDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.SyntaxDef != nil {
|
if b.SyntaxDef != nil {
|
||||||
@ -914,7 +1055,7 @@ func (b *Buffer) MergeCursors() {
|
|||||||
b.EventHandler.active = b.curCursor
|
b.EventHandler.active = b.curCursor
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCursors updates all the cursors indicies
|
// UpdateCursors updates all the cursors indices
|
||||||
func (b *Buffer) UpdateCursors() {
|
func (b *Buffer) UpdateCursors() {
|
||||||
b.EventHandler.cursors = b.cursors
|
b.EventHandler.cursors = b.cursors
|
||||||
b.EventHandler.active = b.curCursor
|
b.EventHandler.active = b.curCursor
|
||||||
@ -939,7 +1080,7 @@ func (b *Buffer) ClearCursors() {
|
|||||||
b.cursors = b.cursors[:1]
|
b.cursors = b.cursors[:1]
|
||||||
b.UpdateCursors()
|
b.UpdateCursors()
|
||||||
b.curCursor = 0
|
b.curCursor = 0
|
||||||
b.GetActiveCursor().ResetSelection()
|
b.GetActiveCursor().Deselect(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveLinesUp moves the range of lines up one row
|
// MoveLinesUp moves the range of lines up one row
|
||||||
@ -990,34 +1131,14 @@ var BracePairs = [][2]rune{
|
|||||||
{'[', ']'},
|
{'[', ']'},
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindMatchingBrace returns the location in the buffer of the matching bracket
|
func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc, bool) {
|
||||||
// It is given a brace type containing the open and closing character, (for example
|
|
||||||
// '{' and '}') as well as the location to match from
|
|
||||||
// TODO: maybe can be more efficient with utf8 package
|
|
||||||
// returns the location of the matching brace
|
|
||||||
// if the boolean returned is true then the original matching brace is one character left
|
|
||||||
// of the starting location
|
|
||||||
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
|
|
||||||
curLine := []rune(string(b.LineBytes(start.Y)))
|
|
||||||
startChar := ' '
|
|
||||||
if start.X >= 0 && start.X < len(curLine) {
|
|
||||||
startChar = curLine[start.X]
|
|
||||||
}
|
|
||||||
leftChar := ' '
|
|
||||||
if start.X-1 >= 0 && start.X-1 < len(curLine) {
|
|
||||||
leftChar = curLine[start.X-1]
|
|
||||||
}
|
|
||||||
var i int
|
var i int
|
||||||
if startChar == braceType[0] || leftChar == braceType[0] {
|
if char == braceType[0] {
|
||||||
for y := start.Y; y < b.LinesNum(); y++ {
|
for y := start.Y; y < b.LinesNum(); y++ {
|
||||||
l := []rune(string(b.LineBytes(y)))
|
l := []rune(string(b.LineBytes(y)))
|
||||||
xInit := 0
|
xInit := 0
|
||||||
if y == start.Y {
|
if y == start.Y {
|
||||||
if startChar == braceType[0] {
|
xInit = start.X
|
||||||
xInit = start.X
|
|
||||||
} else {
|
|
||||||
xInit = start.X - 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for x := xInit; x < len(l); x++ {
|
for x := xInit; x < len(l); x++ {
|
||||||
r := l[x]
|
r := l[x]
|
||||||
@ -1026,42 +1147,76 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo
|
|||||||
} else if r == braceType[1] {
|
} else if r == braceType[1] {
|
||||||
i--
|
i--
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
if startChar == braceType[0] {
|
return Loc{x, y}, true
|
||||||
return Loc{x, y}, false, true
|
|
||||||
}
|
|
||||||
return Loc{x, y}, true, true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if startChar == braceType[1] || leftChar == braceType[1] {
|
} else if char == braceType[1] {
|
||||||
for y := start.Y; y >= 0; y-- {
|
for y := start.Y; y >= 0; y-- {
|
||||||
l := []rune(string(b.lines[y].data))
|
l := []rune(string(b.lines[y].data))
|
||||||
xInit := len(l) - 1
|
xInit := len(l) - 1
|
||||||
if y == start.Y {
|
if y == start.Y {
|
||||||
if leftChar == braceType[1] {
|
xInit = start.X
|
||||||
xInit = start.X - 1
|
|
||||||
} else {
|
|
||||||
xInit = start.X
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for x := xInit; x >= 0; x-- {
|
for x := xInit; x >= 0; x-- {
|
||||||
r := l[x]
|
r := l[x]
|
||||||
if r == braceType[0] {
|
if r == braceType[1] {
|
||||||
|
i++
|
||||||
|
} else if r == braceType[0] {
|
||||||
i--
|
i--
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
if leftChar == braceType[1] {
|
return Loc{x, y}, true
|
||||||
return Loc{x, y}, true, true
|
|
||||||
}
|
|
||||||
return Loc{x, y}, false, true
|
|
||||||
}
|
}
|
||||||
} else if r == braceType[1] {
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return start, true, false
|
return start, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a brace character (for example '{' or ']') at the given start location,
|
||||||
|
// FindMatchingBrace returns the location of the matching brace for it (for example '}'
|
||||||
|
// or '['). The second returned value is true if there was no matching brace found
|
||||||
|
// for given starting location but it was found for the location one character left
|
||||||
|
// of it. The third returned value is true if the matching brace was found at all.
|
||||||
|
func (b *Buffer) FindMatchingBrace(start Loc) (Loc, bool, bool) {
|
||||||
|
// TODO: maybe can be more efficient with utf8 package
|
||||||
|
curLine := []rune(string(b.LineBytes(start.Y)))
|
||||||
|
|
||||||
|
// first try to find matching brace for the given location (it has higher priority)
|
||||||
|
if start.X >= 0 && start.X < len(curLine) {
|
||||||
|
startChar := curLine[start.X]
|
||||||
|
|
||||||
|
for _, bp := range BracePairs {
|
||||||
|
if startChar == bp[0] || startChar == bp[1] {
|
||||||
|
mb, found := b.findMatchingBrace(bp, start, startChar)
|
||||||
|
if found {
|
||||||
|
return mb, false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Settings["matchbraceleft"].(bool) {
|
||||||
|
// failed to find matching brace for the given location, so try to find matching
|
||||||
|
// brace for the location one character left of it
|
||||||
|
if start.X-1 >= 0 && start.X-1 < len(curLine) {
|
||||||
|
leftChar := curLine[start.X-1]
|
||||||
|
left := Loc{start.X - 1, start.Y}
|
||||||
|
|
||||||
|
for _, bp := range BracePairs {
|
||||||
|
if leftChar == bp[0] || leftChar == bp[1] {
|
||||||
|
mb, found := b.findMatchingBrace(bp, left, leftChar)
|
||||||
|
if found {
|
||||||
|
return mb, true, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retab changes all tabs to spaces or vice versa
|
// Retab changes all tabs to spaces or vice versa
|
||||||
@ -1083,7 +1238,11 @@ func (b *Buffer) Retab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
l = bytes.TrimLeft(l, " \t")
|
l = bytes.TrimLeft(l, " \t")
|
||||||
|
|
||||||
|
b.Lock()
|
||||||
b.lines[i].data = append(ws, l...)
|
b.lines[i].data = append(ws, l...)
|
||||||
|
b.Unlock()
|
||||||
|
|
||||||
b.MarkModified(i, i)
|
b.MarkModified(i, i)
|
||||||
dirty = true
|
dirty = true
|
||||||
}
|
}
|
||||||
@ -1126,7 +1285,7 @@ func (b *Buffer) Write(bytes []byte) (n int, err error) {
|
|||||||
return len(bytes), nil
|
return len(bytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) updateDiffSync() {
|
func (b *Buffer) updateDiff(synchronous bool) {
|
||||||
b.diffLock.Lock()
|
b.diffLock.Lock()
|
||||||
defer b.diffLock.Unlock()
|
defer b.diffLock.Unlock()
|
||||||
|
|
||||||
@ -1137,7 +1296,16 @@ func (b *Buffer) updateDiffSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
differ := dmp.New()
|
differ := dmp.New()
|
||||||
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
|
|
||||||
|
if !synchronous {
|
||||||
|
b.Lock()
|
||||||
|
}
|
||||||
|
bytes := b.Bytes()
|
||||||
|
if !synchronous {
|
||||||
|
b.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(bytes))
|
||||||
diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
|
diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
|
||||||
lineN := 0
|
lineN := 0
|
||||||
|
|
||||||
@ -1166,13 +1334,9 @@ func (b *Buffer) updateDiffSync() {
|
|||||||
|
|
||||||
// UpdateDiff computes the diff between the diff base and the buffer content.
|
// UpdateDiff computes the diff between the diff base and the buffer content.
|
||||||
// The update may be performed synchronously or asynchronously.
|
// The update may be performed synchronously or asynchronously.
|
||||||
// UpdateDiff calls the supplied callback when the update is complete.
|
|
||||||
// The argument passed to the callback is set to true if and only if
|
|
||||||
// the update was performed synchronously.
|
|
||||||
// If an asynchronous update is already pending when UpdateDiff is called,
|
// If an asynchronous update is already pending when UpdateDiff is called,
|
||||||
// UpdateDiff does not schedule another update, in which case the callback
|
// UpdateDiff does not schedule another update.
|
||||||
// is not called.
|
func (b *Buffer) UpdateDiff() {
|
||||||
func (b *Buffer) UpdateDiff(callback func(bool)) {
|
|
||||||
if b.updateDiffTimer != nil {
|
if b.updateDiffTimer != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1183,20 +1347,18 @@ func (b *Buffer) UpdateDiff(callback func(bool)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if lineCount < 1000 {
|
if lineCount < 1000 {
|
||||||
b.updateDiffSync()
|
b.updateDiff(true)
|
||||||
callback(true)
|
|
||||||
} else if lineCount < 30000 {
|
} else if lineCount < 30000 {
|
||||||
b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
|
b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
|
||||||
b.updateDiffTimer = nil
|
b.updateDiffTimer = nil
|
||||||
b.updateDiffSync()
|
b.updateDiff(false)
|
||||||
callback(false)
|
screen.Redraw()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Don't compute diffs for very large files
|
// Don't compute diffs for very large files
|
||||||
b.diffLock.Lock()
|
b.diffLock.Lock()
|
||||||
b.diff = make(map[int]DiffStatus)
|
b.diff = make(map[int]DiffStatus)
|
||||||
b.diffLock.Unlock()
|
b.diffLock.Unlock()
|
||||||
callback(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1208,9 +1370,7 @@ func (b *Buffer) SetDiffBase(diffBase []byte) {
|
|||||||
} else {
|
} else {
|
||||||
b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
|
b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
|
||||||
}
|
}
|
||||||
b.UpdateDiff(func(synchronous bool) {
|
b.UpdateDiff()
|
||||||
screen.Redraw()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffStatus returns the diff status for a line in the buffer
|
// DiffStatus returns the diff status for a line in the buffer
|
||||||
|
@ -20,6 +20,7 @@ type operation struct {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ulua.L = lua.NewState()
|
ulua.L = lua.NewState()
|
||||||
|
config.InitRuntimeFiles(false)
|
||||||
config.InitGlobalSettings()
|
config.InitGlobalSettings()
|
||||||
config.GlobalSettings["backup"] = false
|
config.GlobalSettings["backup"] = false
|
||||||
config.GlobalSettings["fastdirty"] = true
|
config.GlobalSettings["fastdirty"] = true
|
||||||
|
@ -20,8 +20,14 @@ type Cursor struct {
|
|||||||
buf *Buffer
|
buf *Buffer
|
||||||
Loc
|
Loc
|
||||||
|
|
||||||
// Last cursor x position
|
// Last visual x position of the cursor. Used in cursor up/down movements
|
||||||
|
// for remembering the original x position when moving to a line that is
|
||||||
|
// shorter than current x position.
|
||||||
LastVisualX int
|
LastVisualX int
|
||||||
|
// Similar to LastVisualX but takes softwrapping into account, i.e. last
|
||||||
|
// visual x position in a visual (wrapped) line on the screen, which may be
|
||||||
|
// different from the line in the buffer.
|
||||||
|
LastWrappedVisualX int
|
||||||
|
|
||||||
// The current selection as a range of character numbers (inclusive)
|
// The current selection as a range of character numbers (inclusive)
|
||||||
CurSelection [2]Loc
|
CurSelection [2]Loc
|
||||||
@ -30,6 +36,11 @@ type Cursor struct {
|
|||||||
// to know what the original selection was
|
// to know what the original selection was
|
||||||
OrigSelection [2]Loc
|
OrigSelection [2]Loc
|
||||||
|
|
||||||
|
// The line number where a new trailing whitespace has been added
|
||||||
|
// or -1 if there is no new trailing whitespace at this cursor.
|
||||||
|
// This is used for checking if a trailing whitespace should be highlighted
|
||||||
|
NewTrailingWsY int
|
||||||
|
|
||||||
// Which cursor index is this (for multiple cursors)
|
// Which cursor index is this (for multiple cursors)
|
||||||
Num int
|
Num int
|
||||||
}
|
}
|
||||||
@ -38,6 +49,8 @@ func NewCursor(b *Buffer, l Loc) *Cursor {
|
|||||||
c := &Cursor{
|
c := &Cursor{
|
||||||
buf: b,
|
buf: b,
|
||||||
Loc: l,
|
Loc: l,
|
||||||
|
|
||||||
|
NewTrailingWsY: -1,
|
||||||
}
|
}
|
||||||
c.StoreVisualX()
|
c.StoreVisualX()
|
||||||
return c
|
return c
|
||||||
@ -54,8 +67,9 @@ func (c *Cursor) Buf() *Buffer {
|
|||||||
// Goto puts the cursor at the given cursor's location and gives
|
// Goto puts the cursor at the given cursor's location and gives
|
||||||
// the current cursor its selection too
|
// the current cursor its selection too
|
||||||
func (c *Cursor) Goto(b Cursor) {
|
func (c *Cursor) Goto(b Cursor) {
|
||||||
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
|
c.X, c.Y = b.X, b.Y
|
||||||
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
||||||
|
c.StoreVisualX()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GotoLoc puts the cursor at the given cursor's location and gives
|
// GotoLoc puts the cursor at the given cursor's location and gives
|
||||||
@ -66,8 +80,8 @@ func (c *Cursor) GotoLoc(l Loc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetVisualX returns the x value of the cursor in visual spaces
|
// GetVisualX returns the x value of the cursor in visual spaces
|
||||||
func (c *Cursor) GetVisualX() int {
|
func (c *Cursor) GetVisualX(wrap bool) int {
|
||||||
if c.buf.GetVisualX != nil {
|
if wrap && c.buf.GetVisualX != nil {
|
||||||
return c.buf.GetVisualX(c.Loc)
|
return c.buf.GetVisualX(c.Loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +107,7 @@ func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
|
|||||||
// Start moves the cursor to the start of the line it is on
|
// Start moves the cursor to the start of the line it is on
|
||||||
func (c *Cursor) Start() {
|
func (c *Cursor) Start() {
|
||||||
c.X = 0
|
c.X = 0
|
||||||
c.LastVisualX = c.GetVisualX()
|
c.StoreVisualX()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartOfText moves the cursor to the first non-whitespace rune of
|
// StartOfText moves the cursor to the first non-whitespace rune of
|
||||||
@ -124,7 +138,7 @@ func (c *Cursor) IsStartOfText() bool {
|
|||||||
// End moves the cursor to the end of the line it is on
|
// End moves the cursor to the end of the line it is on
|
||||||
func (c *Cursor) End() {
|
func (c *Cursor) End() {
|
||||||
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
|
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
|
||||||
c.LastVisualX = c.GetVisualX()
|
c.StoreVisualX()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopySelection copies the user's selection to either "primary"
|
// CopySelection copies the user's selection to either "primary"
|
||||||
@ -179,7 +193,7 @@ func (c *Cursor) Deselect(start bool) {
|
|||||||
if start {
|
if start {
|
||||||
c.Loc = c.CurSelection[0]
|
c.Loc = c.CurSelection[0]
|
||||||
} else {
|
} else {
|
||||||
c.Loc = c.CurSelection[1].Move(-1, c.buf)
|
c.Loc = c.CurSelection[1]
|
||||||
}
|
}
|
||||||
c.ResetSelection()
|
c.ResetSelection()
|
||||||
c.StoreVisualX()
|
c.StoreVisualX()
|
||||||
@ -396,13 +410,26 @@ func (c *Cursor) SelectTo(loc Loc) {
|
|||||||
|
|
||||||
// WordRight moves the cursor one word to the right
|
// WordRight moves the cursor one word to the right
|
||||||
func (c *Cursor) WordRight() {
|
func (c *Cursor) WordRight() {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
c.Right()
|
||||||
|
return
|
||||||
|
}
|
||||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
c.Right()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Right()
|
c.Right()
|
||||||
}
|
}
|
||||||
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
|
||||||
|
util.IsNonWordChar(c.RuneUnder(c.X+1)) {
|
||||||
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Right()
|
c.Right()
|
||||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||||
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
@ -414,6 +441,10 @@ func (c *Cursor) WordRight() {
|
|||||||
|
|
||||||
// WordLeft moves the cursor one word to the left
|
// WordLeft moves the cursor one word to the left
|
||||||
func (c *Cursor) WordLeft() {
|
func (c *Cursor) WordLeft() {
|
||||||
|
if c.X == 0 {
|
||||||
|
c.Left()
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Left()
|
c.Left()
|
||||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
if c.X == 0 {
|
if c.X == 0 {
|
||||||
@ -421,6 +452,17 @@ func (c *Cursor) WordLeft() {
|
|||||||
}
|
}
|
||||||
c.Left()
|
c.Left()
|
||||||
}
|
}
|
||||||
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
|
||||||
|
util.IsNonWordChar(c.RuneUnder(c.X-1)) {
|
||||||
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Left()
|
c.Left()
|
||||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||||
if c.X == 0 {
|
if c.X == 0 {
|
||||||
@ -431,6 +473,132 @@ func (c *Cursor) WordLeft() {
|
|||||||
c.Right()
|
c.Right()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubWordRight moves the cursor one sub-word to the right
|
||||||
|
func (c *Cursor) SubWordRight() {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
c.Right()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
||||||
|
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
|
||||||
|
util.IsUpperLetter(c.RuneUnder(c.X+1)) {
|
||||||
|
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
if util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.Right()
|
||||||
|
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubWordLeft moves the cursor one sub-word to the left
|
||||||
|
func (c *Cursor) SubWordLeft() {
|
||||||
|
if c.X == 0 {
|
||||||
|
c.Left()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
c.Right()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
||||||
|
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
if util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||||
|
c.Right()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
|
||||||
|
util.IsUpperLetter(c.RuneUnder(c.X-1)) {
|
||||||
|
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
if !util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
if c.X == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Left()
|
||||||
|
}
|
||||||
|
if !util.IsAlphanumeric(c.RuneUnder(c.X)) {
|
||||||
|
c.Right()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RuneUnder returns the rune under the given x position
|
// RuneUnder returns the rune under the given x position
|
||||||
func (c *Cursor) RuneUnder(x int) rune {
|
func (c *Cursor) RuneUnder(x int) rune {
|
||||||
line := c.buf.LineBytes(c.Y)
|
line := c.buf.LineBytes(c.Y)
|
||||||
@ -454,5 +622,6 @@ func (c *Cursor) RuneUnder(x int) rune {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cursor) StoreVisualX() {
|
func (c *Cursor) StoreVisualX() {
|
||||||
c.LastVisualX = c.GetVisualX()
|
c.LastVisualX = c.GetVisualX(false)
|
||||||
|
c.LastWrappedVisualX = c.GetVisualX(true)
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,11 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
|
|||||||
c.OrigSelection[0] = move(c.OrigSelection[0])
|
c.OrigSelection[0] = move(c.OrigSelection[0])
|
||||||
c.OrigSelection[1] = move(c.OrigSelection[1])
|
c.OrigSelection[1] = move(c.OrigSelection[1])
|
||||||
c.Relocate()
|
c.Relocate()
|
||||||
c.LastVisualX = c.GetVisualX()
|
c.StoreVisualX()
|
||||||
|
}
|
||||||
|
|
||||||
|
if useUndo {
|
||||||
|
eh.updateTrailingWs(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,11 +253,11 @@ func (eh *EventHandler) Execute(t *TextEvent) {
|
|||||||
ExecuteTextEvent(t, eh.buf)
|
ExecuteTextEvent(t, eh.buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undo the first event in the undo stack
|
// Undo the first event in the undo stack. Returns false if the stack is empty.
|
||||||
func (eh *EventHandler) Undo() {
|
func (eh *EventHandler) Undo() bool {
|
||||||
t := eh.UndoStack.Peek()
|
t := eh.UndoStack.Peek()
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||||
@ -262,15 +266,16 @@ func (eh *EventHandler) Undo() {
|
|||||||
for {
|
for {
|
||||||
t = eh.UndoStack.Peek()
|
t = eh.UndoStack.Peek()
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
eh.UndoOneEvent()
|
eh.UndoOneEvent()
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// UndoOneEvent undoes one event
|
// UndoOneEvent undoes one event
|
||||||
@ -286,23 +291,20 @@ func (eh *EventHandler) UndoOneEvent() {
|
|||||||
eh.UndoTextEvent(t)
|
eh.UndoTextEvent(t)
|
||||||
|
|
||||||
// Set the cursor in the right place
|
// Set the cursor in the right place
|
||||||
teCursor := t.C
|
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
|
||||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
eh.cursors[t.C.Num].Goto(t.C)
|
||||||
t.C = *eh.cursors[teCursor.Num]
|
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
|
||||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
|
||||||
} else {
|
|
||||||
teCursor.Num = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push it to the redo stack
|
// Push it to the redo stack
|
||||||
eh.RedoStack.Push(t)
|
eh.RedoStack.Push(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redo the first event in the redo stack
|
// Redo the first event in the redo stack. Returns false if the stack is empty.
|
||||||
func (eh *EventHandler) Redo() {
|
func (eh *EventHandler) Redo() bool {
|
||||||
t := eh.RedoStack.Peek()
|
t := eh.RedoStack.Peek()
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||||
@ -311,15 +313,16 @@ func (eh *EventHandler) Redo() {
|
|||||||
for {
|
for {
|
||||||
t = eh.RedoStack.Peek()
|
t = eh.RedoStack.Peek()
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
eh.RedoOneEvent()
|
eh.RedoOneEvent()
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedoOneEvent redoes one event
|
// RedoOneEvent redoes one event
|
||||||
@ -329,12 +332,9 @@ func (eh *EventHandler) RedoOneEvent() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
teCursor := t.C
|
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
|
||||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
eh.cursors[t.C.Num].Goto(t.C)
|
||||||
t.C = *eh.cursors[teCursor.Num]
|
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
|
||||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
|
||||||
} else {
|
|
||||||
teCursor.Num = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifies the text event
|
// Modifies the text event
|
||||||
@ -342,3 +342,58 @@ func (eh *EventHandler) RedoOneEvent() {
|
|||||||
|
|
||||||
eh.UndoStack.Push(t)
|
eh.UndoStack.Push(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTrailingWs updates the cursor's trailing whitespace status after a text event
|
||||||
|
func (eh *EventHandler) updateTrailingWs(t *TextEvent) {
|
||||||
|
if len(t.Deltas) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text := t.Deltas[0].Text
|
||||||
|
start := t.Deltas[0].Start
|
||||||
|
end := t.Deltas[0].End
|
||||||
|
|
||||||
|
c := eh.cursors[eh.active]
|
||||||
|
isEol := func(loc Loc) bool {
|
||||||
|
return loc.X == util.CharacterCount(eh.buf.LineBytes(loc.Y))
|
||||||
|
}
|
||||||
|
if t.EventType == TextEventInsert && c.Loc == end && isEol(end) {
|
||||||
|
var addedTrailingWs bool
|
||||||
|
addedAfterWs := false
|
||||||
|
addedWsOnly := false
|
||||||
|
if start.Y == end.Y {
|
||||||
|
addedTrailingWs = util.HasTrailingWhitespace(text)
|
||||||
|
addedWsOnly = util.IsBytesWhitespace(text)
|
||||||
|
addedAfterWs = start.X > 0 && util.IsWhitespace(c.buf.RuneAt(Loc{start.X - 1, start.Y}))
|
||||||
|
} else {
|
||||||
|
lastnl := bytes.LastIndex(text, []byte{'\n'})
|
||||||
|
addedTrailingWs = util.HasTrailingWhitespace(text[lastnl+1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
if addedTrailingWs && !(addedAfterWs && addedWsOnly) {
|
||||||
|
c.NewTrailingWsY = c.Y
|
||||||
|
} else if !addedTrailingWs {
|
||||||
|
c.NewTrailingWsY = -1
|
||||||
|
}
|
||||||
|
} else if t.EventType == TextEventRemove && c.Loc == start && isEol(start) {
|
||||||
|
removedAfterWs := util.HasTrailingWhitespace(eh.buf.LineBytes(start.Y))
|
||||||
|
var removedWsOnly bool
|
||||||
|
if start.Y == end.Y {
|
||||||
|
removedWsOnly = util.IsBytesWhitespace(text)
|
||||||
|
} else {
|
||||||
|
firstnl := bytes.Index(text, []byte{'\n'})
|
||||||
|
removedWsOnly = util.IsBytesWhitespace(text[:firstnl])
|
||||||
|
}
|
||||||
|
|
||||||
|
if removedAfterWs && !removedWsOnly {
|
||||||
|
c.NewTrailingWsY = c.Y
|
||||||
|
} else if !removedAfterWs {
|
||||||
|
c.NewTrailingWsY = -1
|
||||||
|
}
|
||||||
|
} else if c.NewTrailingWsY != -1 && start.Y != end.Y && c.Loc.GreaterThan(start) &&
|
||||||
|
((t.EventType == TextEventInsert && c.Y == c.NewTrailingWsY+(end.Y-start.Y)) ||
|
||||||
|
(t.EventType == TextEventRemove && c.Y == c.NewTrailingWsY-(end.Y-start.Y))) {
|
||||||
|
// The cursor still has its new trailingws
|
||||||
|
// but its line number was shifted by insert or remove of lines above
|
||||||
|
c.NewTrailingWsY = c.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -46,10 +46,9 @@ type searchState struct {
|
|||||||
type Line struct {
|
type Line struct {
|
||||||
data []byte
|
data []byte
|
||||||
|
|
||||||
state highlight.State
|
state highlight.State
|
||||||
match highlight.LineMatch
|
match highlight.LineMatch
|
||||||
rehighlight bool
|
lock sync.Mutex
|
||||||
lock sync.Mutex
|
|
||||||
|
|
||||||
// The search states for the line, used for highlighting of search matches,
|
// The search states for the line, used for highlighting of search matches,
|
||||||
// separately from the syntax highlighting.
|
// separately from the syntax highlighting.
|
||||||
@ -75,6 +74,7 @@ type LineArray struct {
|
|||||||
lines []Line
|
lines []Line
|
||||||
Endings FileFormat
|
Endings FileFormat
|
||||||
initsize uint64
|
initsize uint64
|
||||||
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append efficiently appends lines together
|
// Append efficiently appends lines together
|
||||||
@ -147,20 +147,18 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
la.lines = Append(la.lines, Line{
|
la.lines = Append(la.lines, Line{
|
||||||
data: data,
|
data: data,
|
||||||
state: nil,
|
state: nil,
|
||||||
match: nil,
|
match: nil,
|
||||||
rehighlight: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Last line was read
|
// Last line was read
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
la.lines = Append(la.lines, Line{
|
la.lines = Append(la.lines, Line{
|
||||||
data: data[:dlen-1],
|
data: data[:dlen-1],
|
||||||
state: nil,
|
state: nil,
|
||||||
match: nil,
|
match: nil,
|
||||||
rehighlight: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
n++
|
n++
|
||||||
@ -190,22 +188,23 @@ func (la *LineArray) Bytes() []byte {
|
|||||||
// newlineBelow adds a newline below the given line number
|
// newlineBelow adds a newline below the given line number
|
||||||
func (la *LineArray) newlineBelow(y int) {
|
func (la *LineArray) newlineBelow(y int) {
|
||||||
la.lines = append(la.lines, Line{
|
la.lines = append(la.lines, Line{
|
||||||
data: []byte{' '},
|
data: []byte{' '},
|
||||||
state: nil,
|
state: nil,
|
||||||
match: nil,
|
match: nil,
|
||||||
rehighlight: false,
|
|
||||||
})
|
})
|
||||||
copy(la.lines[y+2:], la.lines[y+1:])
|
copy(la.lines[y+2:], la.lines[y+1:])
|
||||||
la.lines[y+1] = Line{
|
la.lines[y+1] = Line{
|
||||||
data: []byte{},
|
data: []byte{},
|
||||||
state: la.lines[y].state,
|
state: la.lines[y].state,
|
||||||
match: nil,
|
match: nil,
|
||||||
rehighlight: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inserts a byte array at a given location
|
// Inserts a byte array at a given location
|
||||||
func (la *LineArray) insert(pos Loc, value []byte) {
|
func (la *LineArray) insert(pos Loc, value []byte) {
|
||||||
|
la.lock.Lock()
|
||||||
|
defer la.lock.Unlock()
|
||||||
|
|
||||||
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
|
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
|
||||||
for i := 0; i < len(value); i++ {
|
for i := 0; i < len(value); i++ {
|
||||||
if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') {
|
if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') {
|
||||||
@ -233,24 +232,26 @@ func (la *LineArray) insertByte(pos Loc, value byte) {
|
|||||||
|
|
||||||
// joinLines joins the two lines a and b
|
// joinLines joins the two lines a and b
|
||||||
func (la *LineArray) joinLines(a, b int) {
|
func (la *LineArray) joinLines(a, b int) {
|
||||||
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
|
la.lines[a].data = append(la.lines[a].data, la.lines[b].data...)
|
||||||
la.deleteLine(b)
|
la.deleteLine(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// split splits a line at a given position
|
// split splits a line at a given position
|
||||||
func (la *LineArray) split(pos Loc) {
|
func (la *LineArray) split(pos Loc) {
|
||||||
la.newlineBelow(pos.Y)
|
la.newlineBelow(pos.Y)
|
||||||
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:])
|
la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...)
|
||||||
la.lines[pos.Y+1].state = la.lines[pos.Y].state
|
la.lines[pos.Y+1].state = la.lines[pos.Y].state
|
||||||
la.lines[pos.Y].state = nil
|
la.lines[pos.Y].state = nil
|
||||||
la.lines[pos.Y].match = nil
|
la.lines[pos.Y].match = nil
|
||||||
la.lines[pos.Y+1].match = nil
|
la.lines[pos.Y+1].match = nil
|
||||||
la.lines[pos.Y].rehighlight = true
|
|
||||||
la.deleteToEnd(Loc{pos.X, pos.Y})
|
la.deleteToEnd(Loc{pos.X, pos.Y})
|
||||||
}
|
}
|
||||||
|
|
||||||
// removes from start to end
|
// removes from start to end
|
||||||
func (la *LineArray) remove(start, end Loc) []byte {
|
func (la *LineArray) remove(start, end Loc) []byte {
|
||||||
|
la.lock.Lock()
|
||||||
|
defer la.lock.Unlock()
|
||||||
|
|
||||||
sub := la.Substr(start, end)
|
sub := la.Substr(start, end)
|
||||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||||
@ -284,11 +285,6 @@ func (la *LineArray) deleteLines(y1, y2 int) {
|
|||||||
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
|
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteByte deletes the byte at a position
|
|
||||||
func (la *LineArray) deleteByte(pos Loc) {
|
|
||||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substr returns the string representation between two locations
|
// Substr returns the string representation between two locations
|
||||||
func (la *LineArray) Substr(start, end Loc) []byte {
|
func (la *LineArray) Substr(start, end Loc) []byte {
|
||||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||||
@ -327,11 +323,11 @@ func (la *LineArray) End() Loc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LineBytes returns line n as an array of bytes
|
// LineBytes returns line n as an array of bytes
|
||||||
func (la *LineArray) LineBytes(n int) []byte {
|
func (la *LineArray) LineBytes(lineN int) []byte {
|
||||||
if n >= len(la.lines) || n < 0 {
|
if lineN >= len(la.lines) || lineN < 0 {
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
return la.lines[n].data
|
return la.lines[lineN].data
|
||||||
}
|
}
|
||||||
|
|
||||||
// State gets the highlight state for the given line number
|
// State gets the highlight state for the given line number
|
||||||
@ -362,16 +358,14 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch {
|
|||||||
return la.lines[lineN].match
|
return la.lines[lineN].match
|
||||||
}
|
}
|
||||||
|
|
||||||
func (la *LineArray) Rehighlight(lineN int) bool {
|
// Locks the whole LineArray
|
||||||
la.lines[lineN].lock.Lock()
|
func (la *LineArray) Lock() {
|
||||||
defer la.lines[lineN].lock.Unlock()
|
la.lock.Lock()
|
||||||
return la.lines[lineN].rehighlight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (la *LineArray) SetRehighlight(lineN int, on bool) {
|
// Unlocks the whole LineArray
|
||||||
la.lines[lineN].lock.Lock()
|
func (la *LineArray) Unlock() {
|
||||||
defer la.lines[lineN].lock.Unlock()
|
la.lock.Unlock()
|
||||||
la.lines[lineN].rehighlight = on
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchMatch returns true if the location `pos` is within a match
|
// SearchMatch returns true if the location `pos` is within a match
|
||||||
|
@ -47,6 +47,16 @@ func (l Loc) LessEqual(b Loc) bool {
|
|||||||
return l == b
|
return l == b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clamp clamps a loc between start and end
|
||||||
|
func (l Loc) Clamp(start, end Loc) Loc {
|
||||||
|
if l.GreaterEqual(end) {
|
||||||
|
return end
|
||||||
|
} else if l.LessThan(start) {
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// The following functions require a buffer to know where newlines are
|
// The following functions require a buffer to know where newlines are
|
||||||
|
|
||||||
// Diff returns the distance between two locations
|
// Diff returns the distance between two locations
|
||||||
@ -139,10 +149,5 @@ func ByteOffset(pos Loc, buf *Buffer) int {
|
|||||||
|
|
||||||
// clamps a loc within a buffer
|
// clamps a loc within a buffer
|
||||||
func clamp(pos Loc, la *LineArray) Loc {
|
func clamp(pos Loc, la *LineArray) Loc {
|
||||||
if pos.GreaterEqual(la.End()) {
|
return pos.Clamp(la.Start(), la.End())
|
||||||
return la.End()
|
|
||||||
} else if pos.LessThan(la.Start()) {
|
|
||||||
return la.Start()
|
|
||||||
}
|
|
||||||
return pos
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package buffer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MsgType int
|
type MsgType int
|
||||||
|
@ -5,18 +5,19 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"golang.org/x/text/encoding"
|
|
||||||
"golang.org/x/text/encoding/htmlindex"
|
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,68 +25,178 @@ import (
|
|||||||
// because hashing is too slow
|
// because hashing is too slow
|
||||||
const LargeFileThreshold = 50000
|
const LargeFileThreshold = 50000
|
||||||
|
|
||||||
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
|
type wrappedFile struct {
|
||||||
// the supplied function with the file as io.Writer object, also making sure the file is
|
writeCloser io.WriteCloser
|
||||||
// closed afterwards.
|
withSudo bool
|
||||||
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) {
|
screenb bool
|
||||||
|
cmd *exec.Cmd
|
||||||
|
sigChan chan os.Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
type saveResponse struct {
|
||||||
|
size int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type saveRequest struct {
|
||||||
|
buf *Buffer
|
||||||
|
path string
|
||||||
|
withSudo bool
|
||||||
|
newFile bool
|
||||||
|
saveResponseChan chan saveResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveRequestChan chan saveRequest
|
||||||
|
var backupRequestChan chan *Buffer
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
saveRequestChan = make(chan saveRequest, 10)
|
||||||
|
backupRequestChan = make(chan *Buffer, 10)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
duration := backupSeconds * float64(time.Second)
|
||||||
|
backupTicker := time.NewTicker(time.Duration(duration))
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sr := <-saveRequestChan:
|
||||||
|
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
|
||||||
|
sr.saveResponseChan <- saveResponse{size, err}
|
||||||
|
case <-backupTicker.C:
|
||||||
|
for len(backupRequestChan) > 0 {
|
||||||
|
b := <-backupRequestChan
|
||||||
|
bfini := atomic.LoadInt32(&(b.fini)) != 0
|
||||||
|
if !bfini {
|
||||||
|
b.Backup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFile(name string, withSudo bool) (wrappedFile, error) {
|
||||||
|
var err error
|
||||||
var writeCloser io.WriteCloser
|
var writeCloser io.WriteCloser
|
||||||
var screenb bool
|
var screenb bool
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
|
var sigChan chan os.Signal
|
||||||
|
|
||||||
if withSudo {
|
if withSudo {
|
||||||
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
||||||
|
writeCloser, err = cmd.StdinPipe()
|
||||||
if writeCloser, err = cmd.StdinPipe(); err != nil {
|
if err != nil {
|
||||||
return
|
return wrappedFile{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
sigChan = make(chan os.Signal, 1)
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Reset(os.Interrupt)
|
||||||
go func() {
|
signal.Notify(sigChan, os.Interrupt)
|
||||||
<-c
|
|
||||||
cmd.Process.Kill()
|
|
||||||
}()
|
|
||||||
|
|
||||||
screenb = screen.TempFini()
|
screenb = screen.TempFini()
|
||||||
// need to start the process now, otherwise when we flush the file
|
// need to start the process now, otherwise when we flush the file
|
||||||
// contents to its stdin it might hang because the kernel's pipe size
|
// contents to its stdin it might hang because the kernel's pipe size
|
||||||
// is too small to handle the full file contents all at once
|
// is too small to handle the full file contents all at once
|
||||||
if e := cmd.Start(); e != nil && err == nil {
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
screen.TempStart(screenb)
|
screen.TempStart(screenb)
|
||||||
return err
|
|
||||||
|
signal.Notify(util.Sigterm, os.Interrupt)
|
||||||
|
signal.Stop(sigChan)
|
||||||
|
|
||||||
|
return wrappedFile{}, err
|
||||||
}
|
}
|
||||||
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
} else {
|
||||||
return
|
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
|
||||||
}
|
if err != nil {
|
||||||
|
return wrappedFile{}, err
|
||||||
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
|
|
||||||
err = fn(w)
|
|
||||||
|
|
||||||
if err2 := w.Flush(); err2 != nil && err == nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
// Call Sync() on the file to make sure the content is safely on disk.
|
|
||||||
// Does not work with sudo as we don't have direct access to the file.
|
|
||||||
if !withSudo {
|
|
||||||
f := writeCloser.(*os.File)
|
|
||||||
if err2 := f.Sync(); err2 != nil && err == nil {
|
|
||||||
err = err2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err2 := writeCloser.Close(); err2 != nil && err == nil {
|
|
||||||
err = err2
|
return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf wrappedFile) Write(b *Buffer) (int, error) {
|
||||||
|
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
|
||||||
|
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
if len(b.lines) == 0 {
|
||||||
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if withSudo {
|
// end of line
|
||||||
|
var eol []byte
|
||||||
|
if b.Endings == FFDos {
|
||||||
|
eol = []byte{'\r', '\n'}
|
||||||
|
} else {
|
||||||
|
eol = []byte{'\n'}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wf.withSudo {
|
||||||
|
f := wf.writeCloser.(*os.File)
|
||||||
|
err := f.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write lines
|
||||||
|
size, err := file.Write(b.lines[0].data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range b.lines[1:] {
|
||||||
|
if _, err = file.Write(eol); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err = file.Write(l.data); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
size += len(eol) + len(l.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Flush()
|
||||||
|
if err == nil && !wf.withSudo {
|
||||||
|
// Call Sync() on the file to make sure the content is safely on disk.
|
||||||
|
f := wf.writeCloser.(*os.File)
|
||||||
|
err = f.Sync()
|
||||||
|
}
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf wrappedFile) Close() error {
|
||||||
|
err := wf.writeCloser.Close()
|
||||||
|
if wf.withSudo {
|
||||||
// wait for dd to finish and restart the screen if we used sudo
|
// wait for dd to finish and restart the screen if we used sudo
|
||||||
err := cmd.Wait()
|
err := wf.cmd.Wait()
|
||||||
screen.TempStart(screenb)
|
screen.TempStart(wf.screenb)
|
||||||
|
|
||||||
|
signal.Notify(util.Sigterm, os.Interrupt)
|
||||||
|
signal.Stop(wf.sigChan)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return
|
func (b *Buffer) overwriteFile(name string) (int, error) {
|
||||||
|
file, err := openFile(name, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := file.Write(b)
|
||||||
|
|
||||||
|
err2 := file.Close()
|
||||||
|
if err2 != nil && err == nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
return size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the buffer to its default path
|
// Save saves the buffer to its default path
|
||||||
@ -93,9 +204,19 @@ func (b *Buffer) Save() error {
|
|||||||
return b.SaveAs(b.Path)
|
return b.SaveAs(b.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AutoSave saves the buffer to its default path
|
||||||
|
func (b *Buffer) AutoSave() error {
|
||||||
|
// Doing full b.Modified() check every time would be costly, due to the hash
|
||||||
|
// calculation. So use just isModified even if fastdirty is not set.
|
||||||
|
if !b.isModified {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.saveToFile(b.Path, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||||
func (b *Buffer) SaveAs(filename string) error {
|
func (b *Buffer) SaveAs(filename string) error {
|
||||||
return b.saveToFile(filename, false)
|
return b.saveToFile(filename, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) SaveWithSudo() error {
|
func (b *Buffer) SaveWithSudo() error {
|
||||||
@ -103,10 +224,10 @@ func (b *Buffer) SaveWithSudo() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||||
return b.saveToFile(filename, true)
|
return b.saveToFile(filename, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error {
|
||||||
var err error
|
var err error
|
||||||
if b.Type.Readonly {
|
if b.Type.Readonly {
|
||||||
return errors.New("Cannot save readonly buffer")
|
return errors.New("Cannot save readonly buffer")
|
||||||
@ -118,7 +239,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
|||||||
return errors.New("Save with sudo not supported on Windows")
|
return errors.New("Save with sudo not supported on Windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.Settings["rmtrailingws"].(bool) {
|
if !autoSave && b.Settings["rmtrailingws"].(bool) {
|
||||||
for i, l := range b.lines {
|
for i, l := range b.lines {
|
||||||
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||||
|
|
||||||
@ -136,19 +257,35 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the last time this file was updated after saving
|
filename, err = util.ReplaceHome(filename)
|
||||||
defer func() {
|
if err != nil {
|
||||||
b.ModTime, _ = util.GetModTime(filename)
|
return err
|
||||||
err = b.Serialize()
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Removes any tilde and replaces with the absolute path to home
|
newFile := false
|
||||||
absFilename, _ := util.ReplaceHome(filename)
|
fileInfo, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newFile = true
|
||||||
|
}
|
||||||
|
if err == nil && fileInfo.IsDir() {
|
||||||
|
return errors.New("Error: " + filename + " is a directory and cannot be saved")
|
||||||
|
}
|
||||||
|
if err == nil && !fileInfo.Mode().IsRegular() {
|
||||||
|
return errors.New("Error: " + filename + " is not a regular file and cannot be saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
absFilename, err := filepath.Abs(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the leading path to the file | "." is returned if there's no leading path provided
|
// Get the leading path to the file | "." is returned if there's no leading path provided
|
||||||
if dirname := filepath.Dir(absFilename); dirname != "." {
|
if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||||
// Check if the parent dirs don't exist
|
// Check if the parent dirs don't exist
|
||||||
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
|
if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) {
|
||||||
// Prompt to make sure they want to create the dirs that are missing
|
// Prompt to make sure they want to create the dirs that are missing
|
||||||
if b.Settings["mkparents"].(bool) {
|
if b.Settings["mkparents"].(bool) {
|
||||||
// Create all leading dir(s) since they don't exist
|
// Create all leading dir(s) since they don't exist
|
||||||
@ -162,49 +299,22 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSize int
|
saveResponseChan := make(chan saveResponse)
|
||||||
|
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
|
||||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
result := <-saveResponseChan
|
||||||
|
err = result.err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
}
|
screen.TermMessage(err)
|
||||||
|
err = errors.Unwrap(err)
|
||||||
|
|
||||||
fwriter := func(file io.Writer) (e error) {
|
b.UpdateModTime()
|
||||||
if len(b.lines) == 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// end of line
|
|
||||||
var eol []byte
|
|
||||||
if b.Endings == FFDos {
|
|
||||||
eol = []byte{'\r', '\n'}
|
|
||||||
} else {
|
|
||||||
eol = []byte{'\n'}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write lines
|
|
||||||
if fileSize, e = file.Write(b.lines[0].data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range b.lines[1:] {
|
|
||||||
if _, e = file.Write(eol); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, e = file.Write(l.data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileSize += len(eol) + len(l.data)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !b.Settings["fastdirty"].(bool) {
|
if !b.Settings["fastdirty"].(bool) {
|
||||||
if fileSize > LargeFileThreshold {
|
if result.size > LargeFileThreshold {
|
||||||
// For large files 'fastdirty' needs to be on
|
// For large files 'fastdirty' needs to be on
|
||||||
b.Settings["fastdirty"] = true
|
b.Settings["fastdirty"] = true
|
||||||
} else {
|
} else {
|
||||||
@ -212,10 +322,70 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newPath := b.Path != filename
|
||||||
b.Path = filename
|
b.Path = filename
|
||||||
absPath, _ := filepath.Abs(filename)
|
b.AbsPath = absFilename
|
||||||
b.AbsPath = absPath
|
|
||||||
b.isModified = false
|
b.isModified = false
|
||||||
b.UpdateRules()
|
b.UpdateModTime()
|
||||||
|
|
||||||
|
if newPath {
|
||||||
|
// need to update glob-based and filetype-based settings
|
||||||
|
b.ReloadSettings(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.Serialize()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safeWrite writes the buffer to a file in a "safe" way, preventing loss of the
|
||||||
|
// contents of the file if it fails to write the new contents.
|
||||||
|
// This means that the file is not overwritten directly but by writing to the
|
||||||
|
// backup file first.
|
||||||
|
func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) {
|
||||||
|
file, err := openFile(path, withSudo)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if newFile && err != nil {
|
||||||
|
os.Remove(path)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
backupDir := b.backupDir()
|
||||||
|
if _, err := os.Stat(backupDir); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backupName := util.DetermineEscapePath(backupDir, path)
|
||||||
|
_, err = b.overwriteFile(backupName)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(backupName)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.forceKeepBackup = true
|
||||||
|
size, err := file.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
err = util.OverwriteError{err, backupName}
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
b.forceKeepBackup = false
|
||||||
|
|
||||||
|
if !b.keepBackup() {
|
||||||
|
os.Remove(backupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
err2 := file.Close()
|
||||||
|
if err2 != nil && err == nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
@ -2,10 +2,56 @@ package buffer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// We want "^" and "$" to match only the beginning/end of a line, not the
|
||||||
|
// beginning/end of the search region if it is in the middle of a line.
|
||||||
|
// In that case we use padded regexps to require a rune before or after
|
||||||
|
// the match. (This also affects other empty-string patters like "\\b".)
|
||||||
|
// The following two flags indicate the padding used.
|
||||||
|
const (
|
||||||
|
padStart = 1 << iota
|
||||||
|
padEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
func findLineParams(b *Buffer, start, end Loc, i int, r *regexp.Regexp) ([]byte, int, int, *regexp.Regexp) {
|
||||||
|
l := b.LineBytes(i)
|
||||||
|
charpos := 0
|
||||||
|
padMode := 0
|
||||||
|
|
||||||
|
if i == end.Y {
|
||||||
|
nchars := util.CharacterCount(l)
|
||||||
|
end.X = util.Clamp(end.X, 0, nchars)
|
||||||
|
if end.X < nchars {
|
||||||
|
l = util.SliceStart(l, end.X+1)
|
||||||
|
padMode |= padEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == start.Y {
|
||||||
|
nchars := util.CharacterCount(l)
|
||||||
|
start.X = util.Clamp(start.X, 0, nchars)
|
||||||
|
if start.X > 0 {
|
||||||
|
charpos = start.X - 1
|
||||||
|
l = util.SliceEnd(l, charpos)
|
||||||
|
padMode |= padStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if padMode == padStart {
|
||||||
|
r = regexp.MustCompile(".(?:" + r.String() + ")")
|
||||||
|
} else if padMode == padEnd {
|
||||||
|
r = regexp.MustCompile("(?:" + r.String() + ").")
|
||||||
|
} else if padMode == padStart|padEnd {
|
||||||
|
r = regexp.MustCompile(".(?:" + r.String() + ").")
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, charpos, padMode, r
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||||
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
|
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
|
||||||
if start.Y > b.LinesNum()-1 {
|
if start.Y > b.LinesNum()-1 {
|
||||||
@ -22,30 +68,19 @@ func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := start.Y; i <= end.Y; i++ {
|
for i := start.Y; i <= end.Y; i++ {
|
||||||
l := b.LineBytes(i)
|
l, charpos, padMode, rPadded := findLineParams(b, start, end, i, r)
|
||||||
charpos := 0
|
|
||||||
|
|
||||||
if i == start.Y && start.Y == end.Y {
|
match := rPadded.FindIndex(l)
|
||||||
nchars := util.CharacterCount(l)
|
|
||||||
start.X = util.Clamp(start.X, 0, nchars)
|
|
||||||
end.X = util.Clamp(end.X, 0, nchars)
|
|
||||||
l = util.SliceStart(l, end.X)
|
|
||||||
l = util.SliceEnd(l, start.X)
|
|
||||||
charpos = start.X
|
|
||||||
} else if i == start.Y {
|
|
||||||
nchars := util.CharacterCount(l)
|
|
||||||
start.X = util.Clamp(start.X, 0, nchars)
|
|
||||||
l = util.SliceEnd(l, start.X)
|
|
||||||
charpos = start.X
|
|
||||||
} else if i == end.Y {
|
|
||||||
nchars := util.CharacterCount(l)
|
|
||||||
end.X = util.Clamp(end.X, 0, nchars)
|
|
||||||
l = util.SliceStart(l, end.X)
|
|
||||||
}
|
|
||||||
|
|
||||||
match := r.FindIndex(l)
|
|
||||||
|
|
||||||
if match != nil {
|
if match != nil {
|
||||||
|
if padMode&padStart != 0 {
|
||||||
|
_, size := utf8.DecodeRune(l[match[0]:])
|
||||||
|
match[0] += size
|
||||||
|
}
|
||||||
|
if padMode&padEnd != 0 {
|
||||||
|
_, size := utf8.DecodeLastRune(l[:match[1]])
|
||||||
|
match[1] -= size
|
||||||
|
}
|
||||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||||
return [2]Loc{start, end}, true
|
return [2]Loc{start, end}, true
|
||||||
@ -70,39 +105,39 @@ func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := end.Y; i >= start.Y; i-- {
|
for i := end.Y; i >= start.Y; i-- {
|
||||||
l := b.LineBytes(i)
|
charCount := util.CharacterCount(b.LineBytes(i))
|
||||||
charpos := 0
|
from := Loc{0, i}.Clamp(start, end)
|
||||||
|
to := Loc{charCount, i}.Clamp(start, end)
|
||||||
if i == start.Y && start.Y == end.Y {
|
|
||||||
nchars := util.CharacterCount(l)
|
|
||||||
start.X = util.Clamp(start.X, 0, nchars)
|
|
||||||
end.X = util.Clamp(end.X, 0, nchars)
|
|
||||||
l = util.SliceStart(l, end.X)
|
|
||||||
l = util.SliceEnd(l, start.X)
|
|
||||||
charpos = start.X
|
|
||||||
} else if i == start.Y {
|
|
||||||
nchars := util.CharacterCount(l)
|
|
||||||
start.X = util.Clamp(start.X, 0, nchars)
|
|
||||||
l = util.SliceEnd(l, start.X)
|
|
||||||
charpos = start.X
|
|
||||||
} else if i == end.Y {
|
|
||||||
nchars := util.CharacterCount(l)
|
|
||||||
end.X = util.Clamp(end.X, 0, nchars)
|
|
||||||
l = util.SliceStart(l, end.X)
|
|
||||||
}
|
|
||||||
|
|
||||||
allMatches := r.FindAllIndex(l, -1)
|
|
||||||
|
|
||||||
|
allMatches := b.findAll(r, from, to)
|
||||||
if allMatches != nil {
|
if allMatches != nil {
|
||||||
match := allMatches[len(allMatches)-1]
|
match := allMatches[len(allMatches)-1]
|
||||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
return [2]Loc{match[0], match[1]}, true
|
||||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
|
||||||
return [2]Loc{start, end}, true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [2]Loc{}, false
|
return [2]Loc{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) findAll(r *regexp.Regexp, start, end Loc) [][2]Loc {
|
||||||
|
var matches [][2]Loc
|
||||||
|
loc := start
|
||||||
|
for {
|
||||||
|
match, found := b.findDown(r, loc, end)
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
matches = append(matches, match)
|
||||||
|
if match[0] != match[1] {
|
||||||
|
loc = match[1]
|
||||||
|
} else if match[1] != end {
|
||||||
|
loc = match[1].Move(1, b)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
// FindNext finds the next occurrence of a given string in the buffer
|
// FindNext finds the next occurrence of a given string in the buffer
|
||||||
// It returns the start and end location of the match (if found) and
|
// It returns the start and end location of the match (if found) and
|
||||||
// a boolean indicating if it was found
|
// a boolean indicating if it was found
|
||||||
@ -146,49 +181,58 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
||||||
// and returns the number of replacements made and the number of runes
|
// and returns the number of replacements made and the number of characters
|
||||||
// added or removed on the last line of the range
|
// added or removed on the last line of the range
|
||||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) {
|
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) {
|
||||||
if start.GreaterThan(end) {
|
if start.GreaterThan(end) {
|
||||||
start, end = end, start
|
start, end = end, start
|
||||||
}
|
}
|
||||||
|
|
||||||
netrunes := 0
|
charsEnd := util.CharacterCount(b.LineBytes(end.Y))
|
||||||
|
|
||||||
found := 0
|
found := 0
|
||||||
var deltas []Delta
|
var deltas []Delta
|
||||||
|
|
||||||
for i := start.Y; i <= end.Y; i++ {
|
for i := start.Y; i <= end.Y; i++ {
|
||||||
l := b.lines[i].data
|
l := b.LineBytes(i)
|
||||||
charpos := 0
|
charCount := util.CharacterCount(l)
|
||||||
|
if (i == start.Y && start.X > 0) || (i == end.Y && end.X < charCount) {
|
||||||
|
// This replacement code works in general, but it creates a separate
|
||||||
|
// modification for each match. We only use it for the first and last
|
||||||
|
// lines, which may use padded regexps
|
||||||
|
|
||||||
if start.Y == end.Y && i == start.Y {
|
from := Loc{0, i}.Clamp(start, end)
|
||||||
l = util.SliceStart(l, end.X)
|
to := Loc{charCount, i}.Clamp(start, end)
|
||||||
l = util.SliceEnd(l, start.X)
|
matches := b.findAll(search, from, to)
|
||||||
charpos = start.X
|
found += len(matches)
|
||||||
} else if i == start.Y {
|
|
||||||
l = util.SliceEnd(l, start.X)
|
for j := len(matches) - 1; j >= 0; j-- {
|
||||||
charpos = start.X
|
// if we counted upwards, the different deltas would interfere
|
||||||
} else if i == end.Y {
|
match := matches[j]
|
||||||
l = util.SliceStart(l, end.X)
|
var newText []byte
|
||||||
|
if captureGroups {
|
||||||
|
newText = search.ReplaceAll(b.Substr(match[0], match[1]), replace)
|
||||||
|
} else {
|
||||||
|
newText = replace
|
||||||
|
}
|
||||||
|
deltas = append(deltas, Delta{newText, match[0], match[1]})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newLine := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
||||||
|
found++
|
||||||
|
var result []byte
|
||||||
|
if captureGroups {
|
||||||
|
match := search.FindSubmatchIndex(in)
|
||||||
|
result = search.Expand(result, replace, in, match)
|
||||||
|
} else {
|
||||||
|
result = replace
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
deltas = append(deltas, Delta{newLine, Loc{0, i}, Loc{charCount, i}})
|
||||||
}
|
}
|
||||||
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
|
||||||
result := []byte{}
|
|
||||||
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
|
|
||||||
result = search.Expand(result, replace, in, submatches)
|
|
||||||
}
|
|
||||||
found++
|
|
||||||
if i == end.Y {
|
|
||||||
netrunes += util.CharacterCount(result) - util.CharacterCount(in)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
from := Loc{charpos, i}
|
|
||||||
to := Loc{charpos + util.CharacterCount(l), i}
|
|
||||||
|
|
||||||
deltas = append(deltas, Delta{newText, from, to})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.MultipleReplace(deltas)
|
b.MultipleReplace(deltas)
|
||||||
|
|
||||||
return found, netrunes
|
return found, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/text/encoding"
|
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
@ -31,16 +29,18 @@ func (b *Buffer) Serialize() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))
|
var buf bytes.Buffer
|
||||||
|
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
|
||||||
return overwriteFile(name, encoding.Nop, func(file io.Writer) error {
|
b.EventHandler,
|
||||||
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
b.GetActiveCursor().Loc,
|
||||||
b.EventHandler,
|
b.ModTime,
|
||||||
b.GetActiveCursor().Loc,
|
})
|
||||||
b.ModTime,
|
if err != nil {
|
||||||
})
|
|
||||||
return err
|
return err
|
||||||
}, false)
|
}
|
||||||
|
|
||||||
|
name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)
|
||||||
|
return util.SafeWrite(name, buf.Bytes(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unserialize loads the buffer info from config.ConfigDir/buffers
|
// Unserialize loads the buffer info from config.ConfigDir/buffers
|
||||||
@ -50,7 +50,7 @@ func (b *Buffer) Unserialize() error {
|
|||||||
if b.Path == "" {
|
if b.Path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)))
|
file, err := os.Open(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
var buffer SerializedBuffer
|
var buffer SerializedBuffer
|
||||||
|
@ -1,36 +1,89 @@
|
|||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
"golang.org/x/text/encoding/htmlindex"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
luar "layeh.com/gopher-luar"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
func (b *Buffer) ReloadSettings(reloadFiletype bool) {
|
||||||
|
settings := config.ParsedSettings()
|
||||||
|
config.UpdatePathGlobLocals(settings, b.AbsPath)
|
||||||
|
|
||||||
|
oldFiletype := b.Settings["filetype"].(string)
|
||||||
|
|
||||||
|
_, local := b.LocalSettings["filetype"]
|
||||||
|
_, volatile := config.VolatileSettings["filetype"]
|
||||||
|
if reloadFiletype && !local && !volatile {
|
||||||
|
// need to update filetype before updating other settings based on it
|
||||||
|
b.Settings["filetype"] = "unknown"
|
||||||
|
if v, ok := settings["filetype"]; ok {
|
||||||
|
b.Settings["filetype"] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update syntax rules, which will also update filetype if needed
|
||||||
|
b.UpdateRules()
|
||||||
|
|
||||||
|
curFiletype := b.Settings["filetype"].(string)
|
||||||
|
if oldFiletype != curFiletype {
|
||||||
|
b.doCallbacks("filetype", oldFiletype, curFiletype)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.UpdateFileTypeLocals(settings, curFiletype)
|
||||||
|
|
||||||
|
for k, v := range config.DefaultCommonSettings() {
|
||||||
|
if k == "filetype" {
|
||||||
|
// prevent recursion
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := config.VolatileSettings[k]; ok {
|
||||||
|
// reload should not override volatile settings
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := b.LocalSettings[k]; ok {
|
||||||
|
// reload should not override local settings
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := settings[k]; ok {
|
||||||
|
b.DoSetOptionNative(k, settings[k])
|
||||||
|
} else {
|
||||||
|
b.DoSetOptionNative(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
|
||||||
|
oldValue := b.Settings[option]
|
||||||
|
if reflect.DeepEqual(oldValue, nativeValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
b.Settings[option] = nativeValue
|
b.Settings[option] = nativeValue
|
||||||
|
|
||||||
if option == "fastdirty" {
|
if option == "fastdirty" {
|
||||||
if !nativeValue.(bool) {
|
if !nativeValue.(bool) {
|
||||||
if !b.Modified() {
|
if b.Size() > LargeFileThreshold {
|
||||||
e := calcHash(b, &b.origHash)
|
b.Settings["fastdirty"] = true
|
||||||
if e == ErrFileTooLarge {
|
} else {
|
||||||
b.Settings["fastdirty"] = false
|
if !b.isModified {
|
||||||
|
calcHash(b, &b.origHash)
|
||||||
|
} else {
|
||||||
|
// prevent using an old stale origHash value
|
||||||
|
b.origHash = [md5.Size]byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if option == "statusline" {
|
} else if option == "statusline" {
|
||||||
screen.Redraw()
|
screen.Redraw()
|
||||||
} else if option == "filetype" {
|
} else if option == "filetype" {
|
||||||
config.InitRuntimeFiles()
|
b.ReloadSettings(false)
|
||||||
err := config.ReadSettings()
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage(err)
|
|
||||||
}
|
|
||||||
err = config.InitGlobalSettings()
|
|
||||||
if err != nil {
|
|
||||||
screen.TermMessage(err)
|
|
||||||
}
|
|
||||||
config.InitLocalSettings(b.Settings, b.Path)
|
|
||||||
b.UpdateRules()
|
|
||||||
} else if option == "fileformat" {
|
} else if option == "fileformat" {
|
||||||
switch b.Settings["fileformat"].(string) {
|
switch b.Settings["fileformat"].(string) {
|
||||||
case "unix":
|
case "unix":
|
||||||
@ -46,6 +99,12 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
|||||||
b.UpdateRules()
|
b.UpdateRules()
|
||||||
}
|
}
|
||||||
} else if option == "encoding" {
|
} else if option == "encoding" {
|
||||||
|
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||||
|
if err != nil {
|
||||||
|
enc = unicode.UTF8
|
||||||
|
b.Settings["encoding"] = "utf-8"
|
||||||
|
}
|
||||||
|
b.encoding = enc
|
||||||
b.isModified = true
|
b.isModified = true
|
||||||
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
|
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
|
||||||
b.Type.Readonly = nativeValue.(bool)
|
b.Type.Readonly = nativeValue.(bool)
|
||||||
@ -74,12 +133,19 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if b.OptionCallback != nil {
|
|
||||||
b.OptionCallback(option, nativeValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.doCallbacks(option, oldValue, nativeValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
||||||
|
if err := config.OptionIsValid(option, nativeValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.DoSetOptionNative(option, nativeValue)
|
||||||
|
b.LocalSettings[option] = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,3 +162,15 @@ func (b *Buffer) SetOption(option, value string) error {
|
|||||||
|
|
||||||
return b.SetOptionNative(option, nativeValue)
|
return b.SetOptionNative(option, nativeValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) doCallbacks(option string, oldValue interface{}, newValue interface{}) {
|
||||||
|
if b.OptionCallback != nil {
|
||||||
|
b.OptionCallback(option, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.RunPluginFn("onBufferOptionChanged",
|
||||||
|
luar.New(ulua.L, b), luar.New(ulua.L, option),
|
||||||
|
luar.New(ulua.L, oldValue), luar.New(ulua.L, newValue)); err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type terminalClipboard struct{}
|
type terminalClipboard struct{}
|
||||||
|
@ -1,44 +1,49 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Autosave chan bool
|
var Autosave chan bool
|
||||||
var autotime int
|
var autotime chan float64
|
||||||
|
|
||||||
// lock for autosave
|
|
||||||
var autolock sync.Mutex
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Autosave = make(chan bool)
|
Autosave = make(chan bool)
|
||||||
|
autotime = make(chan float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetAutoTime(a int) {
|
func SetAutoTime(a float64) {
|
||||||
autolock.Lock()
|
autotime <- a
|
||||||
autotime = a
|
|
||||||
autolock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAutoTime() int {
|
|
||||||
autolock.Lock()
|
|
||||||
a := autotime
|
|
||||||
autolock.Unlock()
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartAutoSave() {
|
func StartAutoSave() {
|
||||||
go func() {
|
go func() {
|
||||||
|
var a float64
|
||||||
|
var t *time.Timer
|
||||||
|
var elapsed <-chan time.Time
|
||||||
for {
|
for {
|
||||||
autolock.Lock()
|
select {
|
||||||
a := autotime
|
case a = <-autotime:
|
||||||
autolock.Unlock()
|
if t != nil {
|
||||||
if a < 1 {
|
t.Stop()
|
||||||
break
|
for len(elapsed) > 0 {
|
||||||
|
<-elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a > 0 {
|
||||||
|
if t != nil {
|
||||||
|
t.Reset(time.Duration(a * float64(time.Second)))
|
||||||
|
} else {
|
||||||
|
t = time.NewTimer(time.Duration(a * float64(time.Second)))
|
||||||
|
elapsed = t.C
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-elapsed:
|
||||||
|
if a > 0 {
|
||||||
|
t.Reset(time.Duration(a * float64(time.Second)))
|
||||||
|
Autosave <- true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(a) * time.Second)
|
|
||||||
Autosave <- true
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefStyle is Micro's default style
|
// DefStyle is Micro's default style
|
||||||
@ -52,43 +52,55 @@ func InitColorscheme() error {
|
|||||||
Colorscheme = make(map[string]tcell.Style)
|
Colorscheme = make(map[string]tcell.Style)
|
||||||
DefStyle = tcell.StyleDefault
|
DefStyle = tcell.StyleDefault
|
||||||
|
|
||||||
return LoadDefaultColorscheme()
|
c, err := LoadDefaultColorscheme()
|
||||||
|
if err == nil {
|
||||||
|
Colorscheme = c
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
|
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
|
||||||
func LoadDefaultColorscheme() error {
|
func LoadDefaultColorscheme() (map[string]tcell.Style, error) {
|
||||||
return LoadColorscheme(GlobalSettings["colorscheme"].(string))
|
var parsedColorschemes []string
|
||||||
|
return LoadColorscheme(GlobalSettings["colorscheme"].(string), &parsedColorschemes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadColorscheme loads the given colorscheme from a directory
|
// LoadColorscheme loads the given colorscheme from a directory
|
||||||
func LoadColorscheme(colorschemeName string) error {
|
func LoadColorscheme(colorschemeName string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
|
||||||
|
c := make(map[string]tcell.Style)
|
||||||
file := FindRuntimeFile(RTColorscheme, colorschemeName)
|
file := FindRuntimeFile(RTColorscheme, colorschemeName)
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return errors.New(colorschemeName + " is not a valid colorscheme")
|
return c, errors.New(colorschemeName + " is not a valid colorscheme")
|
||||||
}
|
}
|
||||||
if data, err := file.Data(); err != nil {
|
if data, err := file.Data(); err != nil {
|
||||||
return errors.New("Error loading colorscheme: " + err.Error())
|
return c, errors.New("Error loading colorscheme: " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
Colorscheme, err = ParseColorscheme(string(data))
|
var err error
|
||||||
|
c, err = ParseColorscheme(file.Name(), string(data), parsedColorschemes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return c, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
|
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
|
||||||
// Colorschemes are made up of color-link statements linking a color group to a list of colors
|
// Colorschemes are made up of color-link statements linking a color group to a list of colors
|
||||||
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
|
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
|
||||||
// red background
|
// red background
|
||||||
func ParseColorscheme(text string) (map[string]tcell.Style, error) {
|
func ParseColorscheme(name string, text string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
|
||||||
var err error
|
var err error
|
||||||
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
colorParser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||||
|
includeParser := regexp.MustCompile(`include\s+"(.*)"`)
|
||||||
lines := strings.Split(text, "\n")
|
lines := strings.Split(text, "\n")
|
||||||
|
|
||||||
c := make(map[string]tcell.Style)
|
c := make(map[string]tcell.Style)
|
||||||
|
|
||||||
|
if parsedColorschemes != nil {
|
||||||
|
*parsedColorschemes = append(*parsedColorschemes, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
lineLoop:
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.TrimSpace(line) == "" ||
|
if strings.TrimSpace(line) == "" ||
|
||||||
strings.TrimSpace(line)[0] == '#' {
|
strings.TrimSpace(line)[0] == '#' {
|
||||||
@ -96,7 +108,30 @@ func ParseColorscheme(text string) (map[string]tcell.Style, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := parser.FindSubmatch([]byte(line))
|
matches := includeParser.FindSubmatch([]byte(line))
|
||||||
|
if len(matches) == 2 {
|
||||||
|
// support includes only in case parsedColorschemes are given
|
||||||
|
if parsedColorschemes != nil {
|
||||||
|
include := string(matches[1])
|
||||||
|
for _, name := range *parsedColorschemes {
|
||||||
|
// check for circular includes...
|
||||||
|
if name == include {
|
||||||
|
// ...and prevent them
|
||||||
|
continue lineLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
includeScheme, err := LoadColorscheme(include, parsedColorschemes)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
for k, v := range includeScheme {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = colorParser.FindSubmatch([]byte(line))
|
||||||
if len(matches) == 3 {
|
if len(matches) == 3 {
|
||||||
link := string(matches[1])
|
link := string(matches[1])
|
||||||
colors := string(matches[2])
|
colors := string(matches[2])
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSimpleStringToStyle(t *testing.T) {
|
func TestSimpleStringToStyle(t *testing.T) {
|
||||||
@ -65,7 +65,7 @@ color-link constant "#AE81FF,#282828"
|
|||||||
color-link constant.string "#E6DB74,#282828"
|
color-link constant.string "#E6DB74,#282828"
|
||||||
color-link constant.string.char "#BDE6AD,#282828"`
|
color-link constant.string.char "#BDE6AD,#282828"`
|
||||||
|
|
||||||
c, err := ParseColorscheme(testColorscheme)
|
c, err := ParseColorscheme("testColorscheme", testColorscheme, nil)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
fg, bg, _ := c["comment"].Decompose()
|
fg, bg, _ := c["comment"].Decompose()
|
||||||
|
@ -143,15 +143,3 @@ func FindPlugin(name string) *Plugin {
|
|||||||
}
|
}
|
||||||
return pl
|
return pl
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindAnyPlugin does not require the plugin to be enabled
|
|
||||||
func FindAnyPlugin(name string) *Plugin {
|
|
||||||
var pl *Plugin
|
|
||||||
for _, p := range Plugins {
|
|
||||||
if p.Name == name {
|
|
||||||
pl = p
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pl
|
|
||||||
}
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -15,7 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
"github.com/zyedidia/json5"
|
"github.com/micro-editor/json5"
|
||||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
|
|
||||||
"github.com/zyedidia/json5"
|
"github.com/micro-editor/json5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDependencyResolving(t *testing.T) {
|
func TestDependencyResolving(t *testing.T) {
|
||||||
|
@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -40,6 +39,10 @@ var allFiles [][]RuntimeFile
|
|||||||
var realFiles [][]RuntimeFile
|
var realFiles [][]RuntimeFile
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
initRuntimeVars()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initRuntimeVars() {
|
||||||
allFiles = make([][]RuntimeFile, NumTypes)
|
allFiles = make([][]RuntimeFile, NumTypes)
|
||||||
realFiles = make([][]RuntimeFile, NumTypes)
|
realFiles = make([][]RuntimeFile, NumTypes)
|
||||||
}
|
}
|
||||||
@ -58,12 +61,6 @@ type realFile string
|
|||||||
// some asset file
|
// some asset file
|
||||||
type assetFile string
|
type assetFile string
|
||||||
|
|
||||||
// some file on filesystem but with a different name
|
|
||||||
type namedFile struct {
|
|
||||||
realFile
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// a file with the data stored in memory
|
// a file with the data stored in memory
|
||||||
type memoryFile struct {
|
type memoryFile struct {
|
||||||
name string
|
name string
|
||||||
@ -83,7 +80,7 @@ func (rf realFile) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rf realFile) Data() ([]byte, error) {
|
func (rf realFile) Data() ([]byte, error) {
|
||||||
return ioutil.ReadFile(string(rf))
|
return os.ReadFile(string(rf))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (af assetFile) Name() string {
|
func (af assetFile) Name() string {
|
||||||
@ -95,10 +92,6 @@ func (af assetFile) Data() ([]byte, error) {
|
|||||||
return rt.Asset(string(af))
|
return rt.Asset(string(af))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nf namedFile) Name() string {
|
|
||||||
return nf.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRuntimeFile registers a file for the given filetype
|
// AddRuntimeFile registers a file for the given filetype
|
||||||
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
|
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
|
||||||
allFiles[fileType] = append(allFiles[fileType], file)
|
allFiles[fileType] = append(allFiles[fileType], file)
|
||||||
@ -113,7 +106,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
|
|||||||
// AddRuntimeFilesFromDirectory registers each file from the given directory for
|
// AddRuntimeFilesFromDirectory registers each file from the given directory for
|
||||||
// the filetype which matches the file-pattern
|
// the filetype which matches the file-pattern
|
||||||
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
|
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
|
||||||
files, _ := ioutil.ReadDir(directory)
|
files, _ := os.ReadDir(directory)
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
|
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
|
||||||
fullPath := filepath.Join(directory, f.Name())
|
fullPath := filepath.Join(directory, f.Name())
|
||||||
@ -129,9 +122,17 @@ func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assetLoop:
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if ok, _ := path.Match(pattern, f); ok {
|
if ok, _ := path.Match(pattern, f); ok {
|
||||||
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
|
af := assetFile(path.Join(directory, f))
|
||||||
|
for _, rf := range realFiles[fileType] {
|
||||||
|
if af.Name() == rf.Name() {
|
||||||
|
continue assetLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddRuntimeFile(fileType, af)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,19 +159,30 @@ func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
|
|||||||
return realFiles[fileType]
|
return realFiles[fileType]
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitRuntimeFiles initializes all assets file and the config directory
|
// InitRuntimeFiles initializes all assets files and the config directory.
|
||||||
func InitRuntimeFiles() {
|
// If `user` is false, InitRuntimeFiles ignores the config directory and
|
||||||
|
// initializes asset files only.
|
||||||
|
func InitRuntimeFiles(user bool) {
|
||||||
add := func(fileType RTFiletype, dir, pattern string) {
|
add := func(fileType RTFiletype, dir, pattern string) {
|
||||||
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
|
if user {
|
||||||
|
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
|
||||||
|
}
|
||||||
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
|
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initRuntimeVars()
|
||||||
|
|
||||||
add(RTColorscheme, "colorschemes", "*.micro")
|
add(RTColorscheme, "colorschemes", "*.micro")
|
||||||
add(RTSyntax, "syntax", "*.yaml")
|
add(RTSyntax, "syntax", "*.yaml")
|
||||||
add(RTSyntaxHeader, "syntax", "*.hdr")
|
add(RTSyntaxHeader, "syntax", "*.hdr")
|
||||||
add(RTHelp, "help", "*.md")
|
add(RTHelp, "help", "*.md")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitPlugins initializes the plugins
|
||||||
|
func InitPlugins() {
|
||||||
|
Plugins = Plugins[:0]
|
||||||
initlua := filepath.Join(ConfigDir, "init.lua")
|
initlua := filepath.Join(ConfigDir, "init.lua")
|
||||||
|
|
||||||
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
|
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
|
||||||
p := new(Plugin)
|
p := new(Plugin)
|
||||||
p.Name = "initlua"
|
p.Name = "initlua"
|
||||||
@ -181,14 +193,14 @@ func InitRuntimeFiles() {
|
|||||||
|
|
||||||
// Search ConfigDir for plugin-scripts
|
// Search ConfigDir for plugin-scripts
|
||||||
plugdir := filepath.Join(ConfigDir, "plug")
|
plugdir := filepath.Join(ConfigDir, "plug")
|
||||||
files, _ := ioutil.ReadDir(plugdir)
|
files, _ := os.ReadDir(plugdir)
|
||||||
|
|
||||||
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
|
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
|
||||||
|
|
||||||
for _, d := range files {
|
for _, d := range files {
|
||||||
plugpath := filepath.Join(plugdir, d.Name())
|
plugpath := filepath.Join(plugdir, d.Name())
|
||||||
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
|
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
|
||||||
srcs, _ := ioutil.ReadDir(plugpath)
|
srcs, _ := os.ReadDir(plugpath)
|
||||||
p := new(Plugin)
|
p := new(Plugin)
|
||||||
p.Name = d.Name()
|
p.Name = d.Name()
|
||||||
p.DirName = d.Name()
|
p.DirName = d.Name()
|
||||||
@ -196,7 +208,7 @@ func InitRuntimeFiles() {
|
|||||||
if strings.HasSuffix(f.Name(), ".lua") {
|
if strings.HasSuffix(f.Name(), ".lua") {
|
||||||
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
|
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
|
||||||
} else if strings.HasSuffix(f.Name(), ".json") {
|
} else if strings.HasSuffix(f.Name(), ".json") {
|
||||||
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
|
data, err := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -218,7 +230,15 @@ func InitRuntimeFiles() {
|
|||||||
|
|
||||||
plugdir = filepath.Join("runtime", "plugins")
|
plugdir = filepath.Join("runtime", "plugins")
|
||||||
if files, err := rt.AssetDir(plugdir); err == nil {
|
if files, err := rt.AssetDir(plugdir); err == nil {
|
||||||
|
outer:
|
||||||
for _, d := range files {
|
for _, d := range files {
|
||||||
|
for _, p := range Plugins {
|
||||||
|
if p.Name == d {
|
||||||
|
log.Println(p.Name, "built-in plugin overridden by user-defined one")
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if srcs, err := rt.AssetDir(filepath.Join(plugdir, d)); err == nil {
|
if srcs, err := rt.AssetDir(filepath.Join(plugdir, d)); err == nil {
|
||||||
p := new(Plugin)
|
p := new(Plugin)
|
||||||
p.Name = d
|
p.Name = d
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
InitRuntimeFiles()
|
InitRuntimeFiles(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddFile(t *testing.T) {
|
func TestAddFile(t *testing.T) {
|
||||||
|
@ -4,340 +4,103 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zyedidia/glob"
|
"github.com/zyedidia/glob"
|
||||||
"github.com/zyedidia/json5"
|
"github.com/micro-editor/json5"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"golang.org/x/text/encoding/htmlindex"
|
"golang.org/x/text/encoding/htmlindex"
|
||||||
)
|
)
|
||||||
|
|
||||||
type optionValidator func(string, interface{}) error
|
type optionValidator func(string, interface{}) error
|
||||||
|
|
||||||
var (
|
// a list of settings that need option validators
|
||||||
ErrInvalidOption = errors.New("Invalid option")
|
|
||||||
ErrInvalidValue = errors.New("Invalid value")
|
|
||||||
|
|
||||||
// The options that the user can set
|
|
||||||
GlobalSettings map[string]interface{}
|
|
||||||
|
|
||||||
// This is the raw parsed json
|
|
||||||
parsedSettings map[string]interface{}
|
|
||||||
settingsParseError bool
|
|
||||||
|
|
||||||
// ModifiedSettings is a map of settings which should be written to disk
|
|
||||||
// because they have been modified by the user in this session
|
|
||||||
ModifiedSettings map[string]bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
ModifiedSettings = make(map[string]bool)
|
|
||||||
parsedSettings = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options with validators
|
|
||||||
var optionValidators = map[string]optionValidator{
|
var optionValidators = map[string]optionValidator{
|
||||||
"autosave": validateNonNegativeValue,
|
"autosave": validateNonNegativeValue,
|
||||||
"clipboard": validateClipboard,
|
"clipboard": validateChoice,
|
||||||
"tabsize": validatePositiveValue,
|
"colorcolumn": validateNonNegativeValue,
|
||||||
"scrollmargin": validateNonNegativeValue,
|
"colorscheme": validateColorscheme,
|
||||||
"scrollspeed": validateNonNegativeValue,
|
"detectlimit": validateNonNegativeValue,
|
||||||
"colorscheme": validateColorscheme,
|
"encoding": validateEncoding,
|
||||||
"colorcolumn": validateNonNegativeValue,
|
"fileformat": validateChoice,
|
||||||
"fileformat": validateLineEnding,
|
"helpsplit": validateChoice,
|
||||||
"encoding": validateEncoding,
|
"matchbracestyle": validateChoice,
|
||||||
"multiopen": validateMultiOpen,
|
"multiopen": validateChoice,
|
||||||
"reload": validateReload,
|
"pageoverlap": validateNonNegativeValue,
|
||||||
|
"reload": validateChoice,
|
||||||
|
"scrollmargin": validateNonNegativeValue,
|
||||||
|
"scrollspeed": validateNonNegativeValue,
|
||||||
|
"tabsize": validatePositiveValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadSettings() error {
|
// a list of settings with pre-defined choices
|
||||||
filename := filepath.Join(ConfigDir, "settings.json")
|
var OptionChoices = map[string][]string{
|
||||||
if _, e := os.Stat(filename); e == nil {
|
"clipboard": {"internal", "external", "terminal"},
|
||||||
input, err := ioutil.ReadFile(filename)
|
"fileformat": {"unix", "dos"},
|
||||||
if err != nil {
|
"helpsplit": {"hsplit", "vsplit"},
|
||||||
settingsParseError = true
|
"matchbracestyle": {"underline", "highlight"},
|
||||||
return errors.New("Error reading settings.json file: " + err.Error())
|
"multiopen": {"tab", "hsplit", "vsplit"},
|
||||||
}
|
"reload": {"prompt", "auto", "disabled"},
|
||||||
if !strings.HasPrefix(string(input), "null") {
|
|
||||||
// Unmarshal the input into the parsed map
|
|
||||||
err = json5.Unmarshal(input, &parsedSettings)
|
|
||||||
if err != nil {
|
|
||||||
settingsParseError = true
|
|
||||||
return errors.New("Error reading settings.json: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if autosave is a boolean and convert it to float if so
|
|
||||||
if v, ok := parsedSettings["autosave"]; ok {
|
|
||||||
s, ok := v.(bool)
|
|
||||||
if ok {
|
|
||||||
if s {
|
|
||||||
parsedSettings["autosave"] = 8.0
|
|
||||||
} else {
|
|
||||||
parsedSettings["autosave"] = 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
|
|
||||||
var interfaceArr []interface{}
|
|
||||||
switch option {
|
|
||||||
case "pluginrepos", "pluginchannels":
|
|
||||||
return value.AssignableTo(reflect.TypeOf(interfaceArr))
|
|
||||||
default:
|
|
||||||
return def.AssignableTo(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitGlobalSettings initializes the options map and sets all options to their default values
|
|
||||||
// Must be called after ReadSettings
|
|
||||||
func InitGlobalSettings() error {
|
|
||||||
var err error
|
|
||||||
GlobalSettings = DefaultGlobalSettings()
|
|
||||||
|
|
||||||
for k, v := range parsedSettings {
|
|
||||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
|
||||||
if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
|
|
||||||
err = fmt.Errorf("Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v), GlobalSettings[k], reflect.TypeOf(GlobalSettings[k]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalSettings[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitLocalSettings scans the json in settings.json and sets the options locally based
|
|
||||||
// on whether the filetype or path matches ft or glob local settings
|
|
||||||
// Must be called after ReadSettings
|
|
||||||
func InitLocalSettings(settings map[string]interface{}, path string) error {
|
|
||||||
var parseError error
|
|
||||||
for k, v := range parsedSettings {
|
|
||||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
|
||||||
if strings.HasPrefix(k, "ft:") {
|
|
||||||
if settings["filetype"].(string) == k[3:] {
|
|
||||||
for k1, v1 := range v.(map[string]interface{}) {
|
|
||||||
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
|
|
||||||
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
settings[k1] = v1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
g, err := glob.Compile(k)
|
|
||||||
if err != nil {
|
|
||||||
parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.MatchString(path) {
|
|
||||||
for k1, v1 := range v.(map[string]interface{}) {
|
|
||||||
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
|
|
||||||
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
settings[k1] = v1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parseError
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSettings writes the settings to the specified filename as JSON
|
|
||||||
func WriteSettings(filename string) error {
|
|
||||||
if settingsParseError {
|
|
||||||
// Don't write settings if there was a parse error
|
|
||||||
// because this will delete the settings.json if it
|
|
||||||
// is invalid. Instead we should allow the user to fix
|
|
||||||
// it manually.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if _, e := os.Stat(ConfigDir); e == nil {
|
|
||||||
defaults := DefaultGlobalSettings()
|
|
||||||
|
|
||||||
// remove any options froms parsedSettings that have since been marked as default
|
|
||||||
for k, v := range parsedSettings {
|
|
||||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
|
||||||
cur, okcur := GlobalSettings[k]
|
|
||||||
if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
|
|
||||||
delete(parsedSettings, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add any options to parsedSettings that have since been marked as non-default
|
|
||||||
for k, v := range GlobalSettings {
|
|
||||||
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
|
||||||
if _, wr := ModifiedSettings[k]; wr {
|
|
||||||
parsedSettings[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
|
||||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// OverwriteSettings writes the current settings to settings.json and
|
|
||||||
// resets any user configuration of local settings present in settings.json
|
|
||||||
func OverwriteSettings(filename string) error {
|
|
||||||
settings := make(map[string]interface{})
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if _, e := os.Stat(ConfigDir); e == nil {
|
|
||||||
defaults := DefaultGlobalSettings()
|
|
||||||
for k, v := range GlobalSettings {
|
|
||||||
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
|
||||||
if _, wr := ModifiedSettings[k]; wr {
|
|
||||||
settings[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(settings, "", " ")
|
|
||||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
|
|
||||||
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
|
||||||
name = pl + "." + name
|
|
||||||
if _, ok := GlobalSettings[name]; !ok {
|
|
||||||
defaultCommonSettings[name] = defaultvalue
|
|
||||||
GlobalSettings[name] = defaultvalue
|
|
||||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Error writing settings.json file: " + err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defaultCommonSettings[name] = defaultvalue
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
|
|
||||||
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
|
||||||
return RegisterGlobalOption(pl+"."+name, defaultvalue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterCommonOption creates a new option
|
|
||||||
func RegisterCommonOption(name string, defaultvalue interface{}) error {
|
|
||||||
if v, ok := GlobalSettings[name]; !ok {
|
|
||||||
defaultCommonSettings[name] = defaultvalue
|
|
||||||
GlobalSettings[name] = defaultvalue
|
|
||||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Error writing settings.json file: " + err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defaultCommonSettings[name] = v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterGlobalOption creates a new global-only option
|
|
||||||
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
|
|
||||||
if v, ok := GlobalSettings[name]; !ok {
|
|
||||||
DefaultGlobalOnlySettings[name] = defaultvalue
|
|
||||||
GlobalSettings[name] = defaultvalue
|
|
||||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Error writing settings.json file: " + err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DefaultGlobalOnlySettings[name] = v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGlobalOption returns the global value of the given option
|
|
||||||
func GetGlobalOption(name string) interface{} {
|
|
||||||
return GlobalSettings[name]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a list of settings that can be globally and locally modified and their
|
||||||
|
// default values
|
||||||
var defaultCommonSettings = map[string]interface{}{
|
var defaultCommonSettings = map[string]interface{}{
|
||||||
"autoindent": true,
|
"autoindent": true,
|
||||||
"autosu": false,
|
"autosu": false,
|
||||||
"backup": true,
|
"backup": true,
|
||||||
"backupdir": "",
|
"backupdir": "",
|
||||||
"basename": false,
|
"basename": false,
|
||||||
"colorcolumn": float64(0),
|
"colorcolumn": float64(0),
|
||||||
"cursorline": true,
|
"cursorline": true,
|
||||||
"diffgutter": false,
|
"detectlimit": float64(100),
|
||||||
"encoding": "utf-8",
|
"diffgutter": false,
|
||||||
"eofnewline": true,
|
"encoding": "utf-8",
|
||||||
"fastdirty": false,
|
"eofnewline": true,
|
||||||
"fileformat": "unix",
|
"fastdirty": false,
|
||||||
"filetype": "unknown",
|
"fileformat": defaultFileFormat(),
|
||||||
"hlsearch": false,
|
"filetype": "unknown",
|
||||||
"incsearch": true,
|
"hlsearch": false,
|
||||||
"ignorecase": true,
|
"hltaberrors": false,
|
||||||
"indentchar": " ",
|
"hltrailingws": false,
|
||||||
"keepautoindent": false,
|
"ignorecase": true,
|
||||||
"matchbrace": true,
|
"incsearch": true,
|
||||||
"mkparents": false,
|
"indentchar": " ",
|
||||||
"permbackup": false,
|
"keepautoindent": false,
|
||||||
"readonly": false,
|
"matchbrace": true,
|
||||||
"reload": "prompt",
|
"matchbraceleft": true,
|
||||||
"rmtrailingws": false,
|
"matchbracestyle": "underline",
|
||||||
"ruler": true,
|
"mkparents": false,
|
||||||
"relativeruler": false,
|
"pageoverlap": float64(2),
|
||||||
"savecursor": false,
|
"permbackup": false,
|
||||||
"saveundo": false,
|
"readonly": false,
|
||||||
"scrollbar": false,
|
"relativeruler": false,
|
||||||
"scrollmargin": float64(3),
|
"reload": "prompt",
|
||||||
"scrollspeed": float64(2),
|
"rmtrailingws": false,
|
||||||
"smartpaste": true,
|
"ruler": true,
|
||||||
"softwrap": false,
|
"savecursor": false,
|
||||||
"splitbottom": true,
|
"saveundo": false,
|
||||||
"splitright": true,
|
"scrollbar": false,
|
||||||
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
|
"scrollmargin": float64(3),
|
||||||
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
|
"scrollspeed": float64(2),
|
||||||
"statusline": true,
|
"smartpaste": true,
|
||||||
"syntax": true,
|
"softwrap": false,
|
||||||
"tabmovement": false,
|
"splitbottom": true,
|
||||||
"tabsize": float64(4),
|
"splitright": true,
|
||||||
"tabstospaces": false,
|
"statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
|
||||||
"useprimary": true,
|
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
|
||||||
"wordwrap": false,
|
"statusline": true,
|
||||||
}
|
"syntax": true,
|
||||||
|
"tabmovement": false,
|
||||||
func GetInfoBarOffset() int {
|
"tabsize": float64(4),
|
||||||
offset := 0
|
"tabstospaces": false,
|
||||||
if GetGlobalOption("infobar").(bool) {
|
"useprimary": true,
|
||||||
offset++
|
"wordwrap": false,
|
||||||
}
|
|
||||||
if GetGlobalOption("keymenu").(bool) {
|
|
||||||
offset += 2
|
|
||||||
}
|
|
||||||
return offset
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultCommonSettings returns the default global settings for micro
|
|
||||||
// Note that colorscheme is a global only option
|
|
||||||
func DefaultCommonSettings() map[string]interface{} {
|
|
||||||
commonsettings := make(map[string]interface{})
|
|
||||||
for k, v := range defaultCommonSettings {
|
|
||||||
commonsettings[k] = v
|
|
||||||
}
|
|
||||||
return commonsettings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// a list of settings that should only be globally modified and their
|
// a list of settings that should only be globally modified and their
|
||||||
@ -349,6 +112,7 @@ var DefaultGlobalOnlySettings = map[string]interface{}{
|
|||||||
"divchars": "|-",
|
"divchars": "|-",
|
||||||
"divreverse": true,
|
"divreverse": true,
|
||||||
"fakecursor": false,
|
"fakecursor": false,
|
||||||
|
"helpsplit": "hsplit",
|
||||||
"infobar": true,
|
"infobar": true,
|
||||||
"keymenu": false,
|
"keymenu": false,
|
||||||
"mouse": true,
|
"mouse": true,
|
||||||
@ -371,21 +135,322 @@ var LocalSettings = []string{
|
|||||||
"readonly",
|
"readonly",
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultGlobalSettings returns the default global settings for micro
|
var (
|
||||||
// Note that colorscheme is a global only option
|
ErrInvalidOption = errors.New("Invalid option")
|
||||||
func DefaultGlobalSettings() map[string]interface{} {
|
ErrInvalidValue = errors.New("Invalid value")
|
||||||
globalsettings := make(map[string]interface{})
|
|
||||||
for k, v := range defaultCommonSettings {
|
// The options that the user can set
|
||||||
globalsettings[k] = v
|
GlobalSettings map[string]interface{}
|
||||||
}
|
|
||||||
for k, v := range DefaultGlobalOnlySettings {
|
// This is the raw parsed json
|
||||||
globalsettings[k] = v
|
parsedSettings map[string]interface{}
|
||||||
}
|
settingsParseError bool
|
||||||
return globalsettings
|
|
||||||
|
// ModifiedSettings is a map of settings which should be written to disk
|
||||||
|
// because they have been modified by the user in this session
|
||||||
|
ModifiedSettings map[string]bool
|
||||||
|
|
||||||
|
// VolatileSettings is a map of settings which should not be written to disk
|
||||||
|
// because they have been temporarily set for this session only
|
||||||
|
VolatileSettings map[string]bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(name string, txt []byte) error {
|
||||||
|
return util.SafeWrite(name, txt, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAllSettings returns a map of all settings and their
|
func init() {
|
||||||
// default values (both common and global settings)
|
ModifiedSettings = make(map[string]bool)
|
||||||
|
VolatileSettings = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateParsedSettings() error {
|
||||||
|
var err error
|
||||||
|
defaults := DefaultAllSettings()
|
||||||
|
for k, v := range parsedSettings {
|
||||||
|
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||||
|
if strings.HasPrefix(k, "ft:") {
|
||||||
|
for k1, v1 := range v.(map[string]interface{}) {
|
||||||
|
if _, ok := defaults[k1]; ok {
|
||||||
|
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
|
||||||
|
err = e
|
||||||
|
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, e := glob.Compile(k); e != nil {
|
||||||
|
err = errors.New("Error with glob setting " + k + ": " + e.Error())
|
||||||
|
delete(parsedSettings, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for k1, v1 := range v.(map[string]interface{}) {
|
||||||
|
if _, ok := defaults[k1]; ok {
|
||||||
|
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
|
||||||
|
err = e
|
||||||
|
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if k == "autosave" {
|
||||||
|
// if autosave is a boolean convert it to float
|
||||||
|
s, ok := v.(bool)
|
||||||
|
if ok {
|
||||||
|
if s {
|
||||||
|
parsedSettings["autosave"] = 8.0
|
||||||
|
} else {
|
||||||
|
parsedSettings["autosave"] = 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := defaults[k]; ok {
|
||||||
|
if e := verifySetting(k, v, defaults[k]); e != nil {
|
||||||
|
err = e
|
||||||
|
parsedSettings[k] = defaults[k]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSettings() error {
|
||||||
|
parsedSettings = make(map[string]interface{})
|
||||||
|
filename := filepath.Join(ConfigDir, "settings.json")
|
||||||
|
if _, e := os.Stat(filename); e == nil {
|
||||||
|
input, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
settingsParseError = true
|
||||||
|
return errors.New("Error reading settings.json file: " + err.Error())
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(string(input), "null") {
|
||||||
|
// Unmarshal the input into the parsed map
|
||||||
|
err = json5.Unmarshal(input, &parsedSettings)
|
||||||
|
if err != nil {
|
||||||
|
settingsParseError = true
|
||||||
|
return errors.New("Error reading settings.json: " + err.Error())
|
||||||
|
}
|
||||||
|
err = validateParsedSettings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsedSettings() map[string]interface{} {
|
||||||
|
s := make(map[string]interface{})
|
||||||
|
for k, v := range parsedSettings {
|
||||||
|
s[k] = v
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifySetting(option string, value interface{}, def interface{}) error {
|
||||||
|
var interfaceArr []interface{}
|
||||||
|
valType := reflect.TypeOf(value)
|
||||||
|
defType := reflect.TypeOf(def)
|
||||||
|
assignable := false
|
||||||
|
|
||||||
|
switch option {
|
||||||
|
case "pluginrepos", "pluginchannels":
|
||||||
|
assignable = valType.AssignableTo(reflect.TypeOf(interfaceArr))
|
||||||
|
default:
|
||||||
|
assignable = defType.AssignableTo(valType)
|
||||||
|
}
|
||||||
|
if !assignable {
|
||||||
|
return fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", option, valType, def, defType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := OptionIsValid(option, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitGlobalSettings initializes the options map and sets all options to their default values
|
||||||
|
// Must be called after ReadSettings
|
||||||
|
func InitGlobalSettings() error {
|
||||||
|
var err error
|
||||||
|
GlobalSettings = DefaultAllSettings()
|
||||||
|
|
||||||
|
for k, v := range parsedSettings {
|
||||||
|
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||||
|
GlobalSettings[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePathGlobLocals scans the already parsed settings and sets the options locally
|
||||||
|
// based on whether the path matches a glob
|
||||||
|
// Must be called after ReadSettings
|
||||||
|
func UpdatePathGlobLocals(settings map[string]interface{}, path string) {
|
||||||
|
for k, v := range parsedSettings {
|
||||||
|
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && !strings.HasPrefix(k, "ft:") {
|
||||||
|
g, _ := glob.Compile(k)
|
||||||
|
if g.MatchString(path) {
|
||||||
|
for k1, v1 := range v.(map[string]interface{}) {
|
||||||
|
settings[k1] = v1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFileTypeLocals scans the already parsed settings and sets the options locally
|
||||||
|
// based on whether the filetype matches to "ft:"
|
||||||
|
// Must be called after ReadSettings
|
||||||
|
func UpdateFileTypeLocals(settings map[string]interface{}, filetype string) {
|
||||||
|
for k, v := range parsedSettings {
|
||||||
|
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && strings.HasPrefix(k, "ft:") {
|
||||||
|
if filetype == k[3:] {
|
||||||
|
for k1, v1 := range v.(map[string]interface{}) {
|
||||||
|
if k1 != "filetype" {
|
||||||
|
settings[k1] = v1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteSettings writes the settings to the specified filename as JSON
|
||||||
|
func WriteSettings(filename string) error {
|
||||||
|
if settingsParseError {
|
||||||
|
// Don't write settings if there was a parse error
|
||||||
|
// because this will delete the settings.json if it
|
||||||
|
// is invalid. Instead we should allow the user to fix
|
||||||
|
// it manually.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if _, e := os.Stat(ConfigDir); e == nil {
|
||||||
|
defaults := DefaultAllSettings()
|
||||||
|
|
||||||
|
// remove any options froms parsedSettings that have since been marked as default
|
||||||
|
for k, v := range parsedSettings {
|
||||||
|
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||||
|
cur, okcur := GlobalSettings[k]
|
||||||
|
_, vol := VolatileSettings[k]
|
||||||
|
if def, ok := defaults[k]; ok && okcur && !vol && reflect.DeepEqual(cur, def) {
|
||||||
|
delete(parsedSettings, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add any options to parsedSettings that have since been marked as non-default
|
||||||
|
for k, v := range GlobalSettings {
|
||||||
|
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
||||||
|
if _, wr := ModifiedSettings[k]; wr {
|
||||||
|
parsedSettings[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||||
|
txt = append(txt, '\n')
|
||||||
|
err = writeFile(filename, txt)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OverwriteSettings writes the current settings to settings.json and
|
||||||
|
// resets any user configuration of local settings present in settings.json
|
||||||
|
func OverwriteSettings(filename string) error {
|
||||||
|
settings := make(map[string]interface{})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if _, e := os.Stat(ConfigDir); e == nil {
|
||||||
|
defaults := DefaultAllSettings()
|
||||||
|
for k, v := range GlobalSettings {
|
||||||
|
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
||||||
|
if _, wr := ModifiedSettings[k]; wr {
|
||||||
|
settings[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||||
|
txt = append(txt, '\n')
|
||||||
|
err = writeFile(filename, txt)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
|
||||||
|
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||||
|
return RegisterCommonOption(pl+"."+name, defaultvalue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
|
||||||
|
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||||
|
return RegisterGlobalOption(pl+"."+name, defaultvalue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCommonOption creates a new option
|
||||||
|
func RegisterCommonOption(name string, defaultvalue interface{}) error {
|
||||||
|
if _, ok := GlobalSettings[name]; !ok {
|
||||||
|
GlobalSettings[name] = defaultvalue
|
||||||
|
}
|
||||||
|
defaultCommonSettings[name] = defaultvalue
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterGlobalOption creates a new global-only option
|
||||||
|
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
|
||||||
|
if _, ok := GlobalSettings[name]; !ok {
|
||||||
|
GlobalSettings[name] = defaultvalue
|
||||||
|
}
|
||||||
|
DefaultGlobalOnlySettings[name] = defaultvalue
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalOption returns the global value of the given option
|
||||||
|
func GetGlobalOption(name string) interface{} {
|
||||||
|
return GlobalSettings[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFileFormat() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "dos"
|
||||||
|
}
|
||||||
|
return "unix"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInfoBarOffset() int {
|
||||||
|
offset := 0
|
||||||
|
if GetGlobalOption("infobar").(bool) {
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
if GetGlobalOption("keymenu").(bool) {
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultCommonSettings returns a map of all common buffer settings
|
||||||
|
// and their default values
|
||||||
|
func DefaultCommonSettings() map[string]interface{} {
|
||||||
|
commonsettings := make(map[string]interface{})
|
||||||
|
for k, v := range defaultCommonSettings {
|
||||||
|
commonsettings[k] = v
|
||||||
|
}
|
||||||
|
return commonsettings
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAllSettings returns a map of all common buffer & global-only settings
|
||||||
|
// and their default values
|
||||||
func DefaultAllSettings() map[string]interface{} {
|
func DefaultAllSettings() map[string]interface{} {
|
||||||
allsettings := make(map[string]interface{})
|
allsettings := make(map[string]interface{})
|
||||||
for k, v := range defaultCommonSettings {
|
for k, v := range defaultCommonSettings {
|
||||||
@ -410,18 +475,15 @@ func GetNativeValue(option string, realValue interface{}, value string) (interfa
|
|||||||
} else if kind == reflect.String {
|
} else if kind == reflect.String {
|
||||||
native = value
|
native = value
|
||||||
} else if kind == reflect.Float64 {
|
} else if kind == reflect.Float64 {
|
||||||
i, err := strconv.Atoi(value)
|
f, err := strconv.ParseFloat(value, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidValue
|
return nil, ErrInvalidValue
|
||||||
}
|
}
|
||||||
native = float64(i)
|
native = f
|
||||||
} else {
|
} else {
|
||||||
return nil, ErrInvalidValue
|
return nil, ErrInvalidValue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := OptionIsValid(option, native); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return native, nil
|
return native, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,13 +499,13 @@ func OptionIsValid(option string, value interface{}) error {
|
|||||||
// Option validators
|
// Option validators
|
||||||
|
|
||||||
func validatePositiveValue(option string, value interface{}) error {
|
func validatePositiveValue(option string, value interface{}) error {
|
||||||
tabsize, ok := value.(float64)
|
nativeValue, ok := value.(float64)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("Expected numeric type for " + option)
|
return errors.New("Expected numeric type for " + option)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tabsize < 1 {
|
if nativeValue < 1 {
|
||||||
return errors.New(option + " must be greater than 0")
|
return errors.New(option + " must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,6 +526,26 @@ func validateNonNegativeValue(option string, value interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateChoice(option string, value interface{}) error {
|
||||||
|
if choices, ok := OptionChoices[option]; ok {
|
||||||
|
val, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Expected string type for " + option)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range choices {
|
||||||
|
if val == v {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
choicesStr := strings.Join(choices, ", ")
|
||||||
|
return errors.New(option + " must be one of: " + choicesStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("Option has no pre-defined choices")
|
||||||
|
}
|
||||||
|
|
||||||
func validateColorscheme(option string, value interface{}) error {
|
func validateColorscheme(option string, value interface{}) error {
|
||||||
colorscheme, ok := value.(string)
|
colorscheme, ok := value.(string)
|
||||||
|
|
||||||
@ -478,69 +560,7 @@ func validateColorscheme(option string, value interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateClipboard(option string, value interface{}) error {
|
|
||||||
val, ok := value.(string)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return errors.New("Expected string type for clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val {
|
|
||||||
case "internal", "external", "terminal":
|
|
||||||
default:
|
|
||||||
return errors.New(option + " must be 'internal', 'external', or 'terminal'")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateLineEnding(option string, value interface{}) error {
|
|
||||||
endingType, ok := value.(string)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return errors.New("Expected string type for file format")
|
|
||||||
}
|
|
||||||
|
|
||||||
if endingType != "unix" && endingType != "dos" {
|
|
||||||
return errors.New("File format must be either 'unix' or 'dos'")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateEncoding(option string, value interface{}) error {
|
func validateEncoding(option string, value interface{}) error {
|
||||||
_, err := htmlindex.Get(value.(string))
|
_, err := htmlindex.Get(value.(string))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateMultiOpen(option string, value interface{}) error {
|
|
||||||
val, ok := value.(string)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return errors.New("Expected string type for multiopen")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val {
|
|
||||||
case "tab", "hsplit", "vsplit":
|
|
||||||
default:
|
|
||||||
return errors.New(option + " must be 'tab', 'hsplit', or 'vsplit'")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateReload(option string, value interface{}) error {
|
|
||||||
val, ok := value.(string)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return errors.New("Expected string type for reload")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val {
|
|
||||||
case "prompt", "auto", "disabled":
|
|
||||||
default:
|
|
||||||
return errors.New(option + " must be 'prompt', 'auto' or 'disabled'")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The BufWindow provides a way of displaying a certain section of a buffer.
|
// The BufWindow provides a way of displaying a certain section of a buffer.
|
||||||
@ -58,7 +58,7 @@ func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
|
|||||||
if option == "softwrap" || option == "wordwrap" {
|
if option == "softwrap" || option == "wordwrap" {
|
||||||
w.Relocate()
|
w.Relocate()
|
||||||
for _, c := range w.Buf.GetCursors() {
|
for _, c := range w.Buf.GetCursors() {
|
||||||
c.LastVisualX = c.GetVisualX()
|
c.LastWrappedVisualX = c.GetVisualX(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,6 +129,11 @@ func (w *BufWindow) updateDisplayInfo() {
|
|||||||
w.bufHeight--
|
w.bufHeight--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollbarWidth := 0
|
||||||
|
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height && w.Width > 0 {
|
||||||
|
scrollbarWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
w.hasMessage = len(b.Messages) > 0
|
w.hasMessage = len(b.Messages) > 0
|
||||||
|
|
||||||
// We need to know the string length of the largest line number
|
// We need to know the string length of the largest line number
|
||||||
@ -146,16 +151,16 @@ func (w *BufWindow) updateDisplayInfo() {
|
|||||||
w.gutterOffset += w.maxLineNumLength + 1
|
w.gutterOffset += w.maxLineNumLength + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
prevBufWidth := w.bufWidth
|
if w.gutterOffset > w.Width-scrollbarWidth {
|
||||||
|
w.gutterOffset = w.Width - scrollbarWidth
|
||||||
w.bufWidth = w.Width - w.gutterOffset
|
|
||||||
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
|
|
||||||
w.bufWidth--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevBufWidth := w.bufWidth
|
||||||
|
w.bufWidth = w.Width - w.gutterOffset - scrollbarWidth
|
||||||
|
|
||||||
if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) {
|
if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) {
|
||||||
for _, c := range w.Buf.GetCursors() {
|
for _, c := range w.Buf.GetCursors() {
|
||||||
c.LastVisualX = c.GetVisualX()
|
c.LastWrappedVisualX = c.GetVisualX(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,7 +238,7 @@ func (w *BufWindow) Relocate() bool {
|
|||||||
|
|
||||||
// horizontal relocation (scrolling)
|
// horizontal relocation (scrolling)
|
||||||
if !b.Settings["softwrap"].(bool) {
|
if !b.Settings["softwrap"].(bool) {
|
||||||
cx := activeC.GetVisualX()
|
cx := activeC.GetVisualX(false)
|
||||||
rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
|
rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
|
||||||
if rw == 0 {
|
if rw == 0 {
|
||||||
rw = 1 // tab or newline
|
rw = 1 // tab or newline
|
||||||
@ -277,13 +282,17 @@ func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
for i := 0; i < 2 && vloc.X < w.gutterOffset; i++ {
|
||||||
vloc.X++
|
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
||||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
vloc.X++
|
||||||
vloc.X++
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
|
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
|
||||||
|
if vloc.X >= w.gutterOffset {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
symbol := ' '
|
symbol := ' '
|
||||||
styleName := ""
|
styleName := ""
|
||||||
|
|
||||||
@ -319,26 +328,28 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc
|
|||||||
} else {
|
} else {
|
||||||
lineInt = bloc.Y - cursorLine
|
lineInt = bloc.Y - cursorLine
|
||||||
}
|
}
|
||||||
lineNum := strconv.Itoa(util.Abs(lineInt))
|
lineNum := []rune(strconv.Itoa(util.Abs(lineInt)))
|
||||||
|
|
||||||
// Write the spaces before the line number if necessary
|
// Write the spaces before the line number if necessary
|
||||||
for i := 0; i < w.maxLineNumLength-len(lineNum); i++ {
|
for i := 0; i < w.maxLineNumLength-len(lineNum) && vloc.X < w.gutterOffset; i++ {
|
||||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||||
vloc.X++
|
vloc.X++
|
||||||
}
|
}
|
||||||
// Write the actual line number
|
// Write the actual line number
|
||||||
for _, ch := range lineNum {
|
for i := 0; i < len(lineNum) && vloc.X < w.gutterOffset; i++ {
|
||||||
if softwrapped {
|
if softwrapped || (w.bufWidth == 0 && w.Buf.Settings["softwrap"] == true) {
|
||||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||||
} else {
|
} else {
|
||||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
|
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, lineNum[i], nil, lineNumStyle)
|
||||||
}
|
}
|
||||||
vloc.X++
|
vloc.X++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the extra space
|
// Write the extra space
|
||||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
if vloc.X < w.gutterOffset {
|
||||||
vloc.X++
|
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||||
|
vloc.X++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getStyle returns the highlight style for the given character position
|
// getStyle returns the highlight style for the given character position
|
||||||
@ -373,18 +384,7 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
|
|
||||||
if b.ModifiedThisFrame {
|
if b.ModifiedThisFrame {
|
||||||
if b.Settings["diffgutter"].(bool) {
|
if b.Settings["diffgutter"].(bool) {
|
||||||
b.UpdateDiff(func(synchronous bool) {
|
b.UpdateDiff()
|
||||||
// If the diff was updated asynchronously, the outer call to
|
|
||||||
// displayBuffer might already be completed and we need to
|
|
||||||
// schedule a redraw in order to display the new diff.
|
|
||||||
// Note that this cannot lead to an infinite recursion
|
|
||||||
// because the modifications were cleared above so there won't
|
|
||||||
// be another call to UpdateDiff when displayBuffer is called
|
|
||||||
// during the redraw.
|
|
||||||
if !synchronous {
|
|
||||||
screen.Redraw()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
b.ModifiedThisFrame = false
|
b.ModifiedThisFrame = false
|
||||||
}
|
}
|
||||||
@ -392,26 +392,20 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
var matchingBraces []buffer.Loc
|
var matchingBraces []buffer.Loc
|
||||||
// bracePairs is defined in buffer.go
|
// bracePairs is defined in buffer.go
|
||||||
if b.Settings["matchbrace"].(bool) {
|
if b.Settings["matchbrace"].(bool) {
|
||||||
for _, bp := range buffer.BracePairs {
|
for _, c := range b.GetCursors() {
|
||||||
for _, c := range b.GetCursors() {
|
if c.HasSelection() {
|
||||||
if c.HasSelection() {
|
continue
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
curX := c.X
|
|
||||||
curLoc := c.Loc
|
|
||||||
|
|
||||||
r := c.RuneUnder(curX)
|
mb, left, found := b.FindMatchingBrace(c.Loc)
|
||||||
rl := c.RuneUnder(curX - 1)
|
if found {
|
||||||
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
|
matchingBraces = append(matchingBraces, mb)
|
||||||
mb, left, found := b.FindMatchingBrace(bp, curLoc)
|
if !left {
|
||||||
if found {
|
if b.Settings["matchbracestyle"].(string) != "highlight" {
|
||||||
matchingBraces = append(matchingBraces, mb)
|
matchingBraces = append(matchingBraces, c.Loc)
|
||||||
if !left {
|
|
||||||
matchingBraces = append(matchingBraces, curLoc)
|
|
||||||
} else {
|
|
||||||
matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
matchingBraces = append(matchingBraces, c.Loc.Move(-1, b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -455,7 +449,7 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
|
|
||||||
currentLine := false
|
currentLine := false
|
||||||
for _, c := range cursors {
|
for _, c := range cursors {
|
||||||
if bloc.Y == c.Y && w.active {
|
if !c.HasSelection() && bloc.Y == c.Y && w.active {
|
||||||
currentLine = true
|
currentLine = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -482,6 +476,12 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
vloc.X = w.gutterOffset
|
vloc.X = w.gutterOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bline := b.LineBytes(bloc.Y)
|
||||||
|
blineLen := util.CharacterCount(bline)
|
||||||
|
|
||||||
|
leadingwsEnd := len(util.GetLeadingWhitespace(bline))
|
||||||
|
trailingwsStart := blineLen - util.CharacterCount(util.GetTrailingWhitespace(bline))
|
||||||
|
|
||||||
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
|
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
|
||||||
if startStyle != nil {
|
if startStyle != nil {
|
||||||
curStyle = *startStyle
|
curStyle = *startStyle
|
||||||
@ -505,6 +505,37 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
// over cursor-line and color-column
|
// over cursor-line and color-column
|
||||||
dontOverrideBackground := origBg != defBg
|
dontOverrideBackground := origBg != defBg
|
||||||
|
|
||||||
|
if b.Settings["hltaberrors"].(bool) {
|
||||||
|
if s, ok := config.Colorscheme["tab-error"]; ok {
|
||||||
|
isTab := (r == '\t') || (r == ' ' && !showcursor)
|
||||||
|
if (b.Settings["tabstospaces"].(bool) && isTab) ||
|
||||||
|
(!b.Settings["tabstospaces"].(bool) && bloc.X < leadingwsEnd && r == ' ' && !isTab) {
|
||||||
|
fg, _, _ := s.Decompose()
|
||||||
|
style = style.Background(fg)
|
||||||
|
dontOverrideBackground = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.Settings["hltrailingws"].(bool) {
|
||||||
|
if s, ok := config.Colorscheme["trailingws"]; ok {
|
||||||
|
if bloc.X >= trailingwsStart && bloc.X < blineLen {
|
||||||
|
hl := true
|
||||||
|
for _, c := range cursors {
|
||||||
|
if c.NewTrailingWsY == bloc.Y {
|
||||||
|
hl = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hl {
|
||||||
|
fg, _, _ := s.Decompose()
|
||||||
|
style = style.Background(fg)
|
||||||
|
dontOverrideBackground = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, c := range cursors {
|
for _, c := range cursors {
|
||||||
if c.HasSelection() &&
|
if c.HasSelection() &&
|
||||||
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
|
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
|
||||||
@ -557,7 +588,15 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
|
|
||||||
for _, mb := range matchingBraces {
|
for _, mb := range matchingBraces {
|
||||||
if mb.X == bloc.X && mb.Y == bloc.Y {
|
if mb.X == bloc.X && mb.Y == bloc.Y {
|
||||||
style = style.Underline(true)
|
if b.Settings["matchbracestyle"].(string) == "highlight" {
|
||||||
|
if s, ok := config.Colorscheme["match-brace"]; ok {
|
||||||
|
style = s
|
||||||
|
} else {
|
||||||
|
style = style.Reverse(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style = style.Underline(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -609,7 +648,7 @@ func (w *BufWindow) displayBuffer() {
|
|||||||
wordwidth := 0
|
wordwidth := 0
|
||||||
|
|
||||||
totalwidth := w.StartCol - nColsBeforeStart
|
totalwidth := w.StartCol - nColsBeforeStart
|
||||||
for len(line) > 0 {
|
for len(line) > 0 && vloc.X < maxWidth {
|
||||||
r, combc, size := util.DecodeCharacter(line)
|
r, combc, size := util.DecodeCharacter(line)
|
||||||
line = line[size:]
|
line = line[size:]
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/info"
|
"github.com/zyedidia/micro/v2/internal/info"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InfoWindow struct {
|
type InfoWindow struct {
|
||||||
|
@ -291,13 +291,7 @@ func (w *BufWindow) diff(s1, s2 SLoc) int {
|
|||||||
// within the buffer boundaries.
|
// within the buffer boundaries.
|
||||||
func (w *BufWindow) Scroll(s SLoc, n int) SLoc {
|
func (w *BufWindow) Scroll(s SLoc, n int) SLoc {
|
||||||
if !w.Buf.Settings["softwrap"].(bool) {
|
if !w.Buf.Settings["softwrap"].(bool) {
|
||||||
s.Line += n
|
s.Line = util.Clamp(s.Line + n, 0, w.Buf.LinesNum()-1)
|
||||||
if s.Line < 0 {
|
|
||||||
s.Line = 0
|
|
||||||
}
|
|
||||||
if s.Line > w.Buf.LinesNum()-1 {
|
|
||||||
s.Line = w.Buf.LinesNum() - 1
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return w.scroll(s, n)
|
return w.scroll(s, n)
|
||||||
|
@ -47,6 +47,12 @@ var statusInfo = map[string]func(*buffer.Buffer) string{
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
|
"overwrite": func(b *buffer.Buffer) string {
|
||||||
|
if b.OverwriteMode && !b.Type.Readonly {
|
||||||
|
return "[ovwr] "
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
"lines": func(b *buffer.Buffer) string {
|
"lines": func(b *buffer.Buffer) string {
|
||||||
return strconv.Itoa(b.LinesNum())
|
return strconv.Itoa(b.LinesNum())
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ package display
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
runewidth "github.com/mattn/go-runewidth"
|
runewidth "github.com/mattn/go-runewidth"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
@ -6,8 +6,8 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/shell"
|
"github.com/zyedidia/micro/v2/internal/shell"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
"github.com/zyedidia/terminal"
|
"github.com/micro-editor/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TermWindow struct {
|
type TermWindow struct {
|
||||||
@ -110,6 +110,8 @@ func (w *TermWindow) Display() {
|
|||||||
}
|
}
|
||||||
if w.State.CursorVisible() && w.active {
|
if w.State.CursorVisible() && w.active {
|
||||||
curx, cury := w.State.Cursor()
|
curx, cury := w.State.Cursor()
|
||||||
screen.ShowCursor(curx+w.X, cury+w.Y)
|
if curx < w.Width && cury < w.Height {
|
||||||
|
screen.ShowCursor(curx+w.X, cury+w.Y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package info
|
package info
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,24 +21,23 @@ func (i *InfoBuf) LoadHistory() {
|
|||||||
if config.GetGlobalOption("savehistory").(bool) {
|
if config.GetGlobalOption("savehistory").(bool) {
|
||||||
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
|
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
|
||||||
var decodedMap map[string][]string
|
var decodedMap map[string][]string
|
||||||
if err == nil {
|
if err != nil {
|
||||||
defer file.Close()
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
decoder := gob.NewDecoder(file)
|
i.Error("Error loading history: ", err)
|
||||||
err = decoder.Decode(&decodedMap)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
i.Error("Error loading history:", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
err = gob.NewDecoder(file).Decode(&decodedMap)
|
||||||
|
if err != nil {
|
||||||
|
i.Error("Error decoding history: ", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if decodedMap != nil {
|
if decodedMap != nil {
|
||||||
i.History = decodedMap
|
i.History = decodedMap
|
||||||
} else {
|
|
||||||
i.History = make(map[string][]string)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
i.History = make(map[string][]string)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,16 +52,18 @@ func (i *InfoBuf) SaveHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history"))
|
var buf bytes.Buffer
|
||||||
if err == nil {
|
err := gob.NewEncoder(&buf).Encode(i.History)
|
||||||
defer file.Close()
|
if err != nil {
|
||||||
encoder := gob.NewEncoder(file)
|
screen.TermMessage("Error encoding history: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = encoder.Encode(i.History)
|
filename := filepath.Join(config.ConfigDir, "buffers", "history")
|
||||||
if err != nil {
|
err = util.SafeWrite(filename, buf.Bytes(), true)
|
||||||
i.Error("Error saving history:", err)
|
if err != nil {
|
||||||
return
|
screen.TermMessage("Error saving history: ", err)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// The InfoBuf displays messages and other info at the bottom of the screen.
|
// The InfoBuf displays messages and other info at the bottom of the screen.
|
||||||
// It is respresented as a buffer and a message with a style.
|
// It is represented as a buffer and a message with a style.
|
||||||
type InfoBuf struct {
|
type InfoBuf struct {
|
||||||
*buffer.Buffer
|
*buffer.Buffer
|
||||||
|
|
||||||
@ -143,13 +143,12 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
|
|||||||
if i.PromptCallback != nil {
|
if i.PromptCallback != nil {
|
||||||
if canceled {
|
if canceled {
|
||||||
i.Replace(i.Start(), i.End(), "")
|
i.Replace(i.Start(), i.End(), "")
|
||||||
i.PromptCallback("", true)
|
|
||||||
h := i.History[i.PromptType]
|
h := i.History[i.PromptType]
|
||||||
i.History[i.PromptType] = h[:len(h)-1]
|
i.History[i.PromptType] = h[:len(h)-1]
|
||||||
|
i.PromptCallback("", true)
|
||||||
} else {
|
} else {
|
||||||
resp := string(i.LineBytes(0))
|
resp := string(i.LineBytes(0))
|
||||||
i.Replace(i.Start(), i.End(), "")
|
i.Replace(i.Start(), i.End(), "")
|
||||||
i.PromptCallback(resp, false)
|
|
||||||
h := i.History[i.PromptType]
|
h := i.History[i.PromptType]
|
||||||
h[len(h)-1] = resp
|
h[len(h)-1] = resp
|
||||||
|
|
||||||
@ -160,6 +159,8 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.PromptCallback(resp, false)
|
||||||
}
|
}
|
||||||
// i.PromptCallback = nil
|
// i.PromptCallback = nil
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@ -27,7 +26,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var L *lua.LState
|
var L *lua.LState
|
||||||
var Lock sync.Mutex
|
|
||||||
|
|
||||||
// LoadFile loads a lua file
|
// LoadFile loads a lua file
|
||||||
func LoadFile(module string, file string, data []byte) error {
|
func LoadFile(module string, file string, data []byte) error {
|
||||||
@ -127,6 +125,7 @@ func importIo() *lua.LTable {
|
|||||||
L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter))
|
L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter))
|
||||||
L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader))
|
L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader))
|
||||||
L.SetField(pkg, "Pipe", luar.New(L, io.Pipe))
|
L.SetField(pkg, "Pipe", luar.New(L, io.Pipe))
|
||||||
|
L.SetField(pkg, "ReadAll", luar.New(L, io.ReadAll))
|
||||||
L.SetField(pkg, "ReadAtLeast", luar.New(L, io.ReadAtLeast))
|
L.SetField(pkg, "ReadAtLeast", luar.New(L, io.ReadAtLeast))
|
||||||
L.SetField(pkg, "ReadFull", luar.New(L, io.ReadFull))
|
L.SetField(pkg, "ReadFull", luar.New(L, io.ReadFull))
|
||||||
L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader))
|
L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader))
|
||||||
@ -372,6 +371,8 @@ func importOs() *lua.LTable {
|
|||||||
L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator))
|
L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator))
|
||||||
L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator))
|
L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator))
|
||||||
L.SetField(pkg, "Pipe", luar.New(L, os.Pipe))
|
L.SetField(pkg, "Pipe", luar.New(L, os.Pipe))
|
||||||
|
L.SetField(pkg, "ReadDir", luar.New(L, os.ReadDir))
|
||||||
|
L.SetField(pkg, "ReadFile", luar.New(L, os.ReadFile))
|
||||||
L.SetField(pkg, "Readlink", luar.New(L, os.Readlink))
|
L.SetField(pkg, "Readlink", luar.New(L, os.Readlink))
|
||||||
L.SetField(pkg, "Remove", luar.New(L, os.Remove))
|
L.SetField(pkg, "Remove", luar.New(L, os.Remove))
|
||||||
L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll))
|
L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll))
|
||||||
@ -390,6 +391,7 @@ func importOs() *lua.LTable {
|
|||||||
L.SetField(pkg, "TempDir", luar.New(L, os.TempDir))
|
L.SetField(pkg, "TempDir", luar.New(L, os.TempDir))
|
||||||
L.SetField(pkg, "Truncate", luar.New(L, os.Truncate))
|
L.SetField(pkg, "Truncate", luar.New(L, os.Truncate))
|
||||||
L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir))
|
L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir))
|
||||||
|
L.SetField(pkg, "WriteFile", luar.New(L, os.WriteFile))
|
||||||
|
|
||||||
return pkg
|
return pkg
|
||||||
}
|
}
|
||||||
@ -523,21 +525,16 @@ func importErrors() *lua.LTable {
|
|||||||
func importTime() *lua.LTable {
|
func importTime() *lua.LTable {
|
||||||
pkg := L.NewTable()
|
pkg := L.NewTable()
|
||||||
|
|
||||||
L.SetField(pkg, "After", luar.New(L, time.After))
|
|
||||||
L.SetField(pkg, "Sleep", luar.New(L, time.Sleep))
|
L.SetField(pkg, "Sleep", luar.New(L, time.Sleep))
|
||||||
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
|
|
||||||
L.SetField(pkg, "Since", luar.New(L, time.Since))
|
L.SetField(pkg, "Since", luar.New(L, time.Since))
|
||||||
L.SetField(pkg, "FixedZone", luar.New(L, time.FixedZone))
|
L.SetField(pkg, "FixedZone", luar.New(L, time.FixedZone))
|
||||||
L.SetField(pkg, "LoadLocation", luar.New(L, time.LoadLocation))
|
L.SetField(pkg, "LoadLocation", luar.New(L, time.LoadLocation))
|
||||||
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
|
|
||||||
L.SetField(pkg, "Date", luar.New(L, time.Date))
|
L.SetField(pkg, "Date", luar.New(L, time.Date))
|
||||||
L.SetField(pkg, "Now", luar.New(L, time.Now))
|
L.SetField(pkg, "Now", luar.New(L, time.Now))
|
||||||
L.SetField(pkg, "Parse", luar.New(L, time.Parse))
|
L.SetField(pkg, "Parse", luar.New(L, time.Parse))
|
||||||
L.SetField(pkg, "ParseDuration", luar.New(L, time.ParseDuration))
|
L.SetField(pkg, "ParseDuration", luar.New(L, time.ParseDuration))
|
||||||
L.SetField(pkg, "ParseInLocation", luar.New(L, time.ParseInLocation))
|
L.SetField(pkg, "ParseInLocation", luar.New(L, time.ParseInLocation))
|
||||||
L.SetField(pkg, "Unix", luar.New(L, time.Unix))
|
L.SetField(pkg, "Unix", luar.New(L, time.Unix))
|
||||||
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
|
|
||||||
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
|
|
||||||
L.SetField(pkg, "Nanosecond", luar.New(L, time.Nanosecond))
|
L.SetField(pkg, "Nanosecond", luar.New(L, time.Nanosecond))
|
||||||
L.SetField(pkg, "Microsecond", luar.New(L, time.Microsecond))
|
L.SetField(pkg, "Microsecond", luar.New(L, time.Microsecond))
|
||||||
L.SetField(pkg, "Millisecond", luar.New(L, time.Millisecond))
|
L.SetField(pkg, "Millisecond", luar.New(L, time.Millisecond))
|
||||||
@ -545,6 +542,15 @@ func importTime() *lua.LTable {
|
|||||||
L.SetField(pkg, "Minute", luar.New(L, time.Minute))
|
L.SetField(pkg, "Minute", luar.New(L, time.Minute))
|
||||||
L.SetField(pkg, "Hour", luar.New(L, time.Hour))
|
L.SetField(pkg, "Hour", luar.New(L, time.Hour))
|
||||||
|
|
||||||
|
// TODO: these raw Go timer APIs don't provide any synchronization
|
||||||
|
// with micro. Stop exposing them to lua once plugins switch to using
|
||||||
|
// the safer micro.After() interface instead. See issue #3209
|
||||||
|
L.SetField(pkg, "After", luar.New(L, time.After))
|
||||||
|
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
|
||||||
|
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
|
||||||
|
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
|
||||||
|
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
|
||||||
|
|
||||||
return pkg
|
return pkg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Screen is the tcell screen we use to draw to the terminal
|
// Screen is the tcell screen we use to draw to the terminal
|
||||||
@ -22,6 +22,10 @@ var Screen tcell.Screen
|
|||||||
// Events is the channel of tcell events
|
// Events is the channel of tcell events
|
||||||
var Events chan (tcell.Event)
|
var Events chan (tcell.Event)
|
||||||
|
|
||||||
|
// RestartCallback is called when the screen is restarted after it was
|
||||||
|
// temporarily shut down
|
||||||
|
var RestartCallback func()
|
||||||
|
|
||||||
// The lock is necessary since the screen is polled on a separate thread
|
// The lock is necessary since the screen is polled on a separate thread
|
||||||
var lock sync.Mutex
|
var lock sync.Mutex
|
||||||
|
|
||||||
@ -29,6 +33,12 @@ var lock sync.Mutex
|
|||||||
// written to even if no event user event has occurred
|
// written to even if no event user event has occurred
|
||||||
var drawChan chan bool
|
var drawChan chan bool
|
||||||
|
|
||||||
|
// rawSeq is the list of raw escape sequences that are bound to some actions
|
||||||
|
// via keybindings and thus should be parsed by tcell. We need to register
|
||||||
|
// them in tcell every time we reinitialize the screen, so we need to remember
|
||||||
|
// them in a list
|
||||||
|
var rawSeq = make([]string, 0)
|
||||||
|
|
||||||
// Lock locks the screen lock
|
// Lock locks the screen lock
|
||||||
func Lock() {
|
func Lock() {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
@ -117,6 +127,34 @@ func SetContent(x, y int, mainc rune, combc []rune, style tcell.Style) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterRawSeq registers a raw escape sequence that should be parsed by tcell
|
||||||
|
func RegisterRawSeq(r string) {
|
||||||
|
for _, seq := range rawSeq {
|
||||||
|
if seq == r {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rawSeq = append(rawSeq, r)
|
||||||
|
|
||||||
|
if Screen != nil {
|
||||||
|
Screen.RegisterRawSeq(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterRawSeq unregisters a raw escape sequence that should be parsed by tcell
|
||||||
|
func UnregisterRawSeq(r string) {
|
||||||
|
for i, seq := range rawSeq {
|
||||||
|
if seq == r {
|
||||||
|
rawSeq[i] = rawSeq[len(rawSeq)-1]
|
||||||
|
rawSeq = rawSeq[:len(rawSeq)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Screen != nil {
|
||||||
|
Screen.UnregisterRawSeq(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TempFini shuts the screen down temporarily
|
// TempFini shuts the screen down temporarily
|
||||||
func TempFini() bool {
|
func TempFini() bool {
|
||||||
screenWasNil := Screen == nil
|
screenWasNil := Screen == nil
|
||||||
@ -134,6 +172,10 @@ func TempStart(screenWasNil bool) {
|
|||||||
if !screenWasNil {
|
if !screenWasNil {
|
||||||
Init()
|
Init()
|
||||||
Unlock()
|
Unlock()
|
||||||
|
|
||||||
|
if RestartCallback != nil {
|
||||||
|
RestartCallback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +229,10 @@ func Init() error {
|
|||||||
Screen.EnableMouse()
|
Screen.EnableMouse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, r := range rawSeq {
|
||||||
|
Screen.RegisterRawSeq(r)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,8 +78,10 @@ func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit func(
|
|||||||
go func() {
|
go func() {
|
||||||
// Run the process in the background and create the onExit callback
|
// Run the process in the background and create the onExit callback
|
||||||
proc.Run()
|
proc.Run()
|
||||||
jobFunc := JobFunction{onExit, outbuf.String(), userargs}
|
if onExit != nil {
|
||||||
Jobs <- jobFunc
|
jobFunc := JobFunction{onExit, outbuf.String(), userargs}
|
||||||
|
Jobs <- jobFunc
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return &Job{proc, stdin}
|
return &Job{proc, stdin}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
shellquote "github.com/kballard/go-shellquote"
|
shellquote "github.com/kballard/go-shellquote"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecCommand executes a command using exec
|
// ExecCommand executes a command using exec
|
||||||
@ -95,28 +96,30 @@ func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error
|
|||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
// This is a trap for Ctrl-C so that it doesn't kill micro
|
// This is a trap for Ctrl-C so that it doesn't kill micro
|
||||||
// Instead we trap Ctrl-C to kill the program we're running
|
// micro is killed if the signal is ignored only on Windows, so it is
|
||||||
|
// received
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Reset(os.Interrupt)
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(c, os.Interrupt)
|
||||||
go func() {
|
err = cmd.Start()
|
||||||
for range c {
|
if err == nil {
|
||||||
cmd.Process.Kill()
|
err = cmd.Wait()
|
||||||
|
if wait {
|
||||||
|
// This is just so we don't return right away and let the user press enter to return
|
||||||
|
screen.TermMessage("")
|
||||||
}
|
}
|
||||||
}()
|
} else {
|
||||||
|
screen.TermMessage(err)
|
||||||
cmd.Start()
|
}
|
||||||
err = cmd.Wait()
|
|
||||||
|
|
||||||
output := outputBytes.String()
|
output := outputBytes.String()
|
||||||
|
|
||||||
if wait {
|
|
||||||
// This is just so we don't return right away and let the user press enter to return
|
|
||||||
screen.TermMessage("")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the screen back up
|
// Start the screen back up
|
||||||
screen.TempStart(screenb)
|
screen.TempStart(screenb)
|
||||||
|
|
||||||
|
signal.Notify(util.Sigterm, os.Interrupt)
|
||||||
|
signal.Stop(c)
|
||||||
|
|
||||||
return output, err
|
return output, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/terminal"
|
"github.com/micro-editor/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TermType int
|
type TermType int
|
||||||
|
@ -6,7 +6,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -16,6 +18,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
runewidth "github.com/mattn/go-runewidth"
|
runewidth "github.com/mattn/go-runewidth"
|
||||||
@ -40,8 +43,46 @@ var (
|
|||||||
|
|
||||||
// Stdout is a buffer that is written to stdout when micro closes
|
// Stdout is a buffer that is written to stdout when micro closes
|
||||||
Stdout *bytes.Buffer
|
Stdout *bytes.Buffer
|
||||||
|
// Sigterm is a channel where micro exits when written
|
||||||
|
Sigterm chan os.Signal
|
||||||
|
|
||||||
|
// To be used for fails on (over-)write with safe writes
|
||||||
|
ErrOverwrite = OverwriteError{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// To be used for file writes before umask is applied
|
||||||
|
const FileMode os.FileMode = 0666
|
||||||
|
|
||||||
|
const OverwriteFailMsg = `An error occurred while writing to the file:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
The file may be corrupted now. The good news is that it has been
|
||||||
|
successfully backed up. Next time you open this file with Micro,
|
||||||
|
Micro will ask if you want to recover it from the backup.
|
||||||
|
|
||||||
|
The backup path is:
|
||||||
|
|
||||||
|
%s`
|
||||||
|
|
||||||
|
// OverwriteError is a custom error to add additional information
|
||||||
|
type OverwriteError struct {
|
||||||
|
What error
|
||||||
|
BackupName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e OverwriteError) Error() string {
|
||||||
|
return fmt.Sprintf(OverwriteFailMsg, e.What, e.BackupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e OverwriteError) Is(target error) bool {
|
||||||
|
return target == ErrOverwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e OverwriteError) Unwrap() error {
|
||||||
|
return e.What
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
SemVersion, err = semver.Make(Version)
|
SemVersion, err = semver.Make(Version)
|
||||||
@ -217,10 +258,56 @@ func FSize(f *os.File) int64 {
|
|||||||
return fi.Size()
|
return fi.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWordChar returns whether or not the string is a 'word character'
|
// IsWordChar returns whether or not a rune is a 'word character'
|
||||||
// Word characters are defined as numbers, letters, or '_'
|
// Word characters are defined as numbers, letters or sub-word delimiters
|
||||||
func IsWordChar(r rune) bool {
|
func IsWordChar(r rune) bool {
|
||||||
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
|
return IsAlphanumeric(r) || IsSubwordDelimiter(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNonWordChar returns whether or not a rune is not a 'word character'
|
||||||
|
// Non word characters are defined as all characters not being numbers, letters or sub-word delimiters
|
||||||
|
// See IsWordChar()
|
||||||
|
func IsNonWordChar(r rune) bool {
|
||||||
|
return !IsWordChar(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSubwordDelimiter returns whether or not a rune is a 'sub-word delimiter character'
|
||||||
|
// i.e. is considered a part of the word and is used as a delimiter between sub-words of the word.
|
||||||
|
// For now the only sub-word delimiter character is '_'.
|
||||||
|
func IsSubwordDelimiter(r rune) bool {
|
||||||
|
return r == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAlphanumeric returns whether or not a rune is an 'alphanumeric character'
|
||||||
|
// Alphanumeric characters are defined as numbers or letters
|
||||||
|
func IsAlphanumeric(r rune) bool {
|
||||||
|
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUpperAlphanumeric returns whether or not a rune is an 'upper alphanumeric character'
|
||||||
|
// Upper alphanumeric characters are defined as numbers or upper-case letters
|
||||||
|
func IsUpperAlphanumeric(r rune) bool {
|
||||||
|
return IsUpperLetter(r) || unicode.IsNumber(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLowerAlphanumeric returns whether or not a rune is a 'lower alphanumeric character'
|
||||||
|
// Lower alphanumeric characters are defined as numbers or lower-case letters
|
||||||
|
func IsLowerAlphanumeric(r rune) bool {
|
||||||
|
return IsLowerLetter(r) || unicode.IsNumber(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUpperLetter returns whether or not a rune is an 'upper letter character'
|
||||||
|
// Upper letter characters are defined as upper-case letters
|
||||||
|
func IsUpperLetter(r rune) bool {
|
||||||
|
// unicode.IsUpper() returns true for letters only
|
||||||
|
return unicode.IsUpper(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLowerLetter returns whether or not a rune is a 'lower letter character'
|
||||||
|
// Lower letter characters are defined as lower-case letters
|
||||||
|
func IsLowerLetter(r rune) bool {
|
||||||
|
// unicode.IsLower() returns true for letters only
|
||||||
|
return unicode.IsLower(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spaces returns a string with n spaces
|
// Spaces returns a string with n spaces
|
||||||
@ -271,6 +358,28 @@ func RunePos(b []byte, i int) int {
|
|||||||
return CharacterCount(b[:i])
|
return CharacterCount(b[:i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IndexAnyUnquoted returns the first position in s of a character from chars.
|
||||||
|
// Escaped (with backslash) and quoted (with single or double quotes) characters
|
||||||
|
// are ignored. Returns -1 if not successful
|
||||||
|
func IndexAnyUnquoted(s, chars string) int {
|
||||||
|
var e bool
|
||||||
|
var q rune
|
||||||
|
for i, r := range s {
|
||||||
|
if e {
|
||||||
|
e = false
|
||||||
|
} else if (q == 0 || q == '"') && r == '\\' {
|
||||||
|
e = true
|
||||||
|
} else if r == q {
|
||||||
|
q = 0
|
||||||
|
} else if q == 0 && (r == '\'' || r == '"') {
|
||||||
|
q = r
|
||||||
|
} else if q == 0 && strings.IndexRune(chars, r) >= 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
// MakeRelative will attempt to make a relative path between path and base
|
// MakeRelative will attempt to make a relative path between path and base
|
||||||
func MakeRelative(path, base string) (string, error) {
|
func MakeRelative(path, base string) (string, error) {
|
||||||
if len(path) > 0 {
|
if len(path) > 0 {
|
||||||
@ -315,7 +424,7 @@ func ReplaceHome(path string) (string, error) {
|
|||||||
// This is used for opening files like util.go:10:5 to specify a line and column
|
// This is used for opening files like util.go:10:5 to specify a line and column
|
||||||
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
|
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
|
||||||
func GetPathAndCursorPosition(path string) (string, []string) {
|
func GetPathAndCursorPosition(path string) (string, []string) {
|
||||||
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
|
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?$`)
|
||||||
match := re.FindStringSubmatch(path)
|
match := re.FindStringSubmatch(path)
|
||||||
// no lines/columns were specified in the path, return just the path with no cursor location
|
// no lines/columns were specified in the path, return just the path with no cursor location
|
||||||
if len(match) == 0 {
|
if len(match) == 0 {
|
||||||
@ -337,8 +446,17 @@ func GetModTime(path string) (time.Time, error) {
|
|||||||
return info.ModTime(), nil
|
return info.ModTime(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EscapePath replaces every path separator in a given path with a %
|
func AppendBackupSuffix(path string) string {
|
||||||
func EscapePath(path string) string {
|
return path + ".micro-backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapePathUrl encodes the path in URL query form
|
||||||
|
func EscapePathUrl(path string) string {
|
||||||
|
return url.QueryEscape(filepath.ToSlash(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapePathLegacy replaces every path separator in a given path with a %
|
||||||
|
func EscapePathLegacy(path string) string {
|
||||||
path = filepath.ToSlash(path)
|
path = filepath.ToSlash(path)
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// ':' is not valid in a path name on Windows but is ok on Unix
|
// ':' is not valid in a path name on Windows but is ok on Unix
|
||||||
@ -347,6 +465,24 @@ func EscapePath(path string) string {
|
|||||||
return strings.ReplaceAll(path, "/", "%")
|
return strings.ReplaceAll(path, "/", "%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetermineEscapePath escapes a path, determining whether it should be escaped
|
||||||
|
// using URL encoding (preferred, since it encodes unambiguously) or
|
||||||
|
// legacy encoding with '%' (for backward compatibility, if the legacy-escaped
|
||||||
|
// path exists in the given directory).
|
||||||
|
func DetermineEscapePath(dir string, path string) string {
|
||||||
|
url := filepath.Join(dir, EscapePathUrl(path))
|
||||||
|
if _, err := os.Stat(url); err == nil {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
legacy := filepath.Join(dir, EscapePathLegacy(path))
|
||||||
|
if _, err := os.Stat(legacy); err == nil {
|
||||||
|
return legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
// GetLeadingWhitespace returns the leading whitespace of the given byte array
|
// GetLeadingWhitespace returns the leading whitespace of the given byte array
|
||||||
func GetLeadingWhitespace(b []byte) []byte {
|
func GetLeadingWhitespace(b []byte) []byte {
|
||||||
ws := []byte{}
|
ws := []byte{}
|
||||||
@ -363,6 +499,28 @@ func GetLeadingWhitespace(b []byte) []byte {
|
|||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTrailingWhitespace returns the trailing whitespace of the given byte array
|
||||||
|
func GetTrailingWhitespace(b []byte) []byte {
|
||||||
|
ws := []byte{}
|
||||||
|
for len(b) > 0 {
|
||||||
|
r, size := utf8.DecodeLastRune(b)
|
||||||
|
if IsWhitespace(r) {
|
||||||
|
ws = append([]byte(string(r)), ws...)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
b = b[:len(b)-size]
|
||||||
|
}
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTrailingWhitespace returns true if the given byte array ends with a whitespace
|
||||||
|
func HasTrailingWhitespace(b []byte) bool {
|
||||||
|
r, _ := utf8.DecodeLastRune(b)
|
||||||
|
return IsWhitespace(r)
|
||||||
|
}
|
||||||
|
|
||||||
// IntOpt turns a float64 setting to an int
|
// IntOpt turns a float64 setting to an int
|
||||||
func IntOpt(opt interface{}) int {
|
func IntOpt(opt interface{}) int {
|
||||||
return int(opt.(float64))
|
return int(opt.(float64))
|
||||||
@ -422,19 +580,9 @@ func Clamp(val, min, max int) int {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
|
|
||||||
func IsNonAlphaNumeric(c rune) bool {
|
|
||||||
return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAutocomplete returns whether a character should begin an autocompletion.
|
// IsAutocomplete returns whether a character should begin an autocompletion.
|
||||||
func IsAutocomplete(c rune) bool {
|
func IsAutocomplete(c rune) bool {
|
||||||
return c == '.' || !IsNonAlphaNumeric(c)
|
return c == '.' || IsWordChar(c)
|
||||||
}
|
|
||||||
|
|
||||||
// ParseSpecial replaces escaped ts with '\t'.
|
|
||||||
func ParseSpecial(s string) string {
|
|
||||||
return strings.ReplaceAll(s, "\\t", "\t")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String converts a byte array to a string (for lua plugins)
|
// String converts a byte array to a string (for lua plugins)
|
||||||
@ -507,3 +655,77 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon
|
|||||||
}
|
}
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SafeWrite writes bytes to a file in a "safe" way, preventing loss of the
|
||||||
|
// contents of the file if it fails to write the new contents.
|
||||||
|
// This means that the file is not overwritten directly but by writing to a
|
||||||
|
// temporary file first.
|
||||||
|
//
|
||||||
|
// If rename is true, write is performed atomically, by renaming the temporary
|
||||||
|
// file to the target file after the data is successfully written to the
|
||||||
|
// temporary file. This guarantees that the file will not remain in a corrupted
|
||||||
|
// state, but it also has limitations, e.g. the file should not be a symlink
|
||||||
|
// (otherwise SafeWrite silently replaces this symlink with a regular file),
|
||||||
|
// the file creation date in Linux is not preserved (since the file inode
|
||||||
|
// changes) etc. Use SafeWrite with rename=true for files that are only created
|
||||||
|
// and used by micro for its own needs and are not supposed to be used directly
|
||||||
|
// by the user.
|
||||||
|
//
|
||||||
|
// If rename is false, write is performed by overwriting the target file after
|
||||||
|
// the data is successfully written to the temporary file.
|
||||||
|
// This means that the target file may remain corrupted if overwrite fails,
|
||||||
|
// but in such case the temporary file is preserved as a backup so the file
|
||||||
|
// can be recovered later. So it is less convenient than atomic write but more
|
||||||
|
// universal. Use SafeWrite with rename=false for files that may be managed
|
||||||
|
// directly by the user, like settings.json and bindings.json.
|
||||||
|
func SafeWrite(path string, bytes []byte, rename bool) error {
|
||||||
|
var err error
|
||||||
|
if _, err = os.Stat(path); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Force rename for new files!
|
||||||
|
rename = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var file *os.File
|
||||||
|
if !rename {
|
||||||
|
file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, FileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := AppendBackupSuffix(path)
|
||||||
|
err = os.WriteFile(tmp, bytes, FileMode)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rename {
|
||||||
|
err = os.Rename(tmp, path)
|
||||||
|
} else {
|
||||||
|
err = file.Truncate(0)
|
||||||
|
if err == nil {
|
||||||
|
_, err = file.Write(bytes)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = file.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if rename {
|
||||||
|
os.Remove(tmp)
|
||||||
|
} else {
|
||||||
|
err = OverwriteError{err, tmp}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rename {
|
||||||
|
os.Remove(tmp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -185,6 +185,10 @@ func (n *Node) hResizeSplit(i int, size int) bool {
|
|||||||
|
|
||||||
// ResizeSplit resizes a certain split to a given size
|
// ResizeSplit resizes a certain split to a given size
|
||||||
func (n *Node) ResizeSplit(size int) bool {
|
func (n *Node) ResizeSplit(size int) bool {
|
||||||
|
// TODO: `size < 0` does not work for some reason
|
||||||
|
if size <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if len(n.parent.children) <= 1 {
|
if len(n.parent.children) <= 1 {
|
||||||
// cannot resize a lone node
|
// cannot resize a lone node
|
||||||
return false
|
return false
|
||||||
@ -201,7 +205,7 @@ func (n *Node) ResizeSplit(size int) bool {
|
|||||||
return n.parent.hResizeSplit(ind, size)
|
return n.parent.hResizeSplit(ind, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize sets this node's size and resizes all children accordlingly
|
// Resize sets this node's size and resizes all children accordingly
|
||||||
func (n *Node) Resize(w, h int) {
|
func (n *Node) Resize(w, h int) {
|
||||||
n.W, n.H = w, h
|
n.W, n.H = w, h
|
||||||
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package highlight
|
|
||||||
|
|
||||||
import "regexp"
|
|
||||||
|
|
||||||
// MatchFiletype will use the list of syntax definitions provided and the filename and first line of the file
|
|
||||||
// to determine the filetype of the file
|
|
||||||
// It will return the corresponding syntax definition for the filetype
|
|
||||||
func MatchFiletype(ftdetect [2]*regexp.Regexp, filename string, firstLine []byte) bool {
|
|
||||||
if ftdetect[0] != nil && ftdetect[0].MatchString(filename) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ftdetect[1] != nil {
|
|
||||||
return ftdetect[1].Match(firstLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
@ -51,25 +51,9 @@ func runePos(p int, str []byte) int {
|
|||||||
return CharacterCount(str[:p])
|
return CharacterCount(str[:p])
|
||||||
}
|
}
|
||||||
|
|
||||||
func combineLineMatch(src, dst LineMatch) LineMatch {
|
|
||||||
for k, v := range src {
|
|
||||||
if g, ok := dst[k]; ok {
|
|
||||||
if g == 0 {
|
|
||||||
dst[k] = v
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dst[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// A State represents the region at the end of a line
|
// A State represents the region at the end of a line
|
||||||
type State *region
|
type State *region
|
||||||
|
|
||||||
// EmptyDef is an empty definition.
|
|
||||||
var EmptyDef = Def{nil, &rules{}}
|
|
||||||
|
|
||||||
// LineStates is an interface for a buffer-like object which can also store the states and matches for every line
|
// LineStates is an interface for a buffer-like object which can also store the states and matches for every line
|
||||||
type LineStates interface {
|
type LineStates interface {
|
||||||
LineBytes(n int) []byte
|
LineBytes(n int) []byte
|
||||||
@ -77,6 +61,8 @@ type LineStates interface {
|
|||||||
State(lineN int) State
|
State(lineN int) State
|
||||||
SetState(lineN int, s State)
|
SetState(lineN int, s State)
|
||||||
SetMatch(lineN int, m LineMatch)
|
SetMatch(lineN int, m LineMatch)
|
||||||
|
Lock()
|
||||||
|
Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Highlighter contains the information needed to highlight a string
|
// A Highlighter contains the information needed to highlight a string
|
||||||
@ -303,7 +289,13 @@ func (h *Highlighter) HighlightString(input string) []LineMatch {
|
|||||||
|
|
||||||
// HighlightStates correctly sets all states for the buffer
|
// HighlightStates correctly sets all states for the buffer
|
||||||
func (h *Highlighter) HighlightStates(input LineStates) {
|
func (h *Highlighter) HighlightStates(input LineStates) {
|
||||||
for i := 0; i < input.LinesNum(); i++ {
|
for i := 0; ; i++ {
|
||||||
|
input.Lock()
|
||||||
|
if i >= input.LinesNum() {
|
||||||
|
input.Unlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
line := input.LineBytes(i)
|
line := input.LineBytes(i)
|
||||||
// highlights := make(LineMatch)
|
// highlights := make(LineMatch)
|
||||||
|
|
||||||
@ -316,6 +308,7 @@ func (h *Highlighter) HighlightStates(input LineStates) {
|
|||||||
curState := h.lastRegion
|
curState := h.lastRegion
|
||||||
|
|
||||||
input.SetState(i, curState)
|
input.SetState(i, curState)
|
||||||
|
input.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +317,9 @@ func (h *Highlighter) HighlightStates(input LineStates) {
|
|||||||
// This assumes that all the states are set correctly
|
// This assumes that all the states are set correctly
|
||||||
func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
|
func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
|
||||||
for i := startline; i <= endline; i++ {
|
for i := startline; i <= endline; i++ {
|
||||||
|
input.Lock()
|
||||||
if i >= input.LinesNum() {
|
if i >= input.LinesNum() {
|
||||||
|
input.Unlock()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,6 +334,7 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.SetMatch(i, match)
|
input.SetMatch(i, match)
|
||||||
|
input.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,9 +346,19 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
|
|||||||
|
|
||||||
h.lastRegion = nil
|
h.lastRegion = nil
|
||||||
if startline > 0 {
|
if startline > 0 {
|
||||||
h.lastRegion = input.State(startline - 1)
|
input.Lock()
|
||||||
|
if startline-1 < input.LinesNum() {
|
||||||
|
h.lastRegion = input.State(startline - 1)
|
||||||
|
}
|
||||||
|
input.Unlock()
|
||||||
}
|
}
|
||||||
for i := startline; i < input.LinesNum(); i++ {
|
for i := startline; ; i++ {
|
||||||
|
input.Lock()
|
||||||
|
if i >= input.LinesNum() {
|
||||||
|
input.Unlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
line := input.LineBytes(i)
|
line := input.LineBytes(i)
|
||||||
// highlights := make(LineMatch)
|
// highlights := make(LineMatch)
|
||||||
|
|
||||||
@ -366,6 +372,7 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
|
|||||||
lastState := input.State(i)
|
lastState := input.State(i)
|
||||||
|
|
||||||
input.SetState(i, curState)
|
input.SetState(i, curState)
|
||||||
|
input.Unlock()
|
||||||
|
|
||||||
if curState == lastState {
|
if curState == lastState {
|
||||||
return i
|
return i
|
||||||
@ -377,6 +384,9 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
|
|||||||
|
|
||||||
// ReHighlightLine will rehighlight the state and match for a single line
|
// ReHighlightLine will rehighlight the state and match for a single line
|
||||||
func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
|
func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
|
||||||
|
input.Lock()
|
||||||
|
defer input.Unlock()
|
||||||
|
|
||||||
line := input.LineBytes(lineN)
|
line := input.LineBytes(lineN)
|
||||||
highlights := make(LineMatch)
|
highlights := make(LineMatch)
|
||||||
|
|
||||||
|
@ -33,27 +33,28 @@ func (g Group) String() string {
|
|||||||
// Then it has the rules which define how to highlight the file
|
// Then it has the rules which define how to highlight the file
|
||||||
type Def struct {
|
type Def struct {
|
||||||
*Header
|
*Header
|
||||||
|
|
||||||
rules *rules
|
rules *rules
|
||||||
}
|
}
|
||||||
|
|
||||||
type Header struct {
|
type Header struct {
|
||||||
FileType string
|
FileType string
|
||||||
FtDetect [2]*regexp.Regexp
|
FileNameRegex *regexp.Regexp
|
||||||
|
HeaderRegex *regexp.Regexp
|
||||||
|
SignatureRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeaderYaml struct {
|
type HeaderYaml struct {
|
||||||
FileType string `yaml:"filetype"`
|
FileType string `yaml:"filetype"`
|
||||||
Detect struct {
|
Detect struct {
|
||||||
FNameRgx string `yaml:"filename"`
|
FNameRegexStr string `yaml:"filename"`
|
||||||
HeaderRgx string `yaml:"header"`
|
HeaderRegexStr string `yaml:"header"`
|
||||||
|
SignatureRegexStr string `yaml:"signature"`
|
||||||
} `yaml:"detect"`
|
} `yaml:"detect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
FileType string
|
FileType string
|
||||||
|
yamlSrc map[interface{}]interface{}
|
||||||
yamlSrc map[interface{}]interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Pattern is one simple syntax rule
|
// A Pattern is one simple syntax rule
|
||||||
@ -97,20 +98,24 @@ func init() {
|
|||||||
// A yaml file might take ~400us to parse while a header file only takes ~20us
|
// A yaml file might take ~400us to parse while a header file only takes ~20us
|
||||||
func MakeHeader(data []byte) (*Header, error) {
|
func MakeHeader(data []byte) (*Header, error) {
|
||||||
lines := bytes.Split(data, []byte{'\n'})
|
lines := bytes.Split(data, []byte{'\n'})
|
||||||
if len(lines) < 3 {
|
if len(lines) < 4 {
|
||||||
return nil, errors.New("Header file has incorrect format")
|
return nil, errors.New("Header file has incorrect format")
|
||||||
}
|
}
|
||||||
header := new(Header)
|
header := new(Header)
|
||||||
var err error
|
var err error
|
||||||
header.FileType = string(lines[0])
|
header.FileType = string(lines[0])
|
||||||
fnameRgx := string(lines[1])
|
fnameRegexStr := string(lines[1])
|
||||||
headerRgx := string(lines[2])
|
headerRegexStr := string(lines[2])
|
||||||
|
signatureRegexStr := string(lines[3])
|
||||||
|
|
||||||
if fnameRgx != "" {
|
if fnameRegexStr != "" {
|
||||||
header.FtDetect[0], err = regexp.Compile(fnameRgx)
|
header.FileNameRegex, err = regexp.Compile(fnameRegexStr)
|
||||||
}
|
}
|
||||||
if err == nil && headerRgx != "" {
|
if err == nil && headerRegexStr != "" {
|
||||||
header.FtDetect[1], err = regexp.Compile(headerRgx)
|
header.HeaderRegex, err = regexp.Compile(headerRegexStr)
|
||||||
|
}
|
||||||
|
if err == nil && signatureRegexStr != "" {
|
||||||
|
header.SignatureRegex, err = regexp.Compile(signatureRegexStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -132,11 +137,14 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
|
|||||||
header := new(Header)
|
header := new(Header)
|
||||||
header.FileType = hdrYaml.FileType
|
header.FileType = hdrYaml.FileType
|
||||||
|
|
||||||
if hdrYaml.Detect.FNameRgx != "" {
|
if hdrYaml.Detect.FNameRegexStr != "" {
|
||||||
header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx)
|
header.FileNameRegex, err = regexp.Compile(hdrYaml.Detect.FNameRegexStr)
|
||||||
}
|
}
|
||||||
if err == nil && hdrYaml.Detect.HeaderRgx != "" {
|
if err == nil && hdrYaml.Detect.HeaderRegexStr != "" {
|
||||||
header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx)
|
header.HeaderRegex, err = regexp.Compile(hdrYaml.Detect.HeaderRegexStr)
|
||||||
|
}
|
||||||
|
if err == nil && hdrYaml.Detect.SignatureRegexStr != "" {
|
||||||
|
header.SignatureRegex, err = regexp.Compile(hdrYaml.Detect.SignatureRegexStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -146,6 +154,37 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
|
|||||||
return header, nil
|
return header, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchFileName will check the given file name with the stored regex
|
||||||
|
func (header *Header) MatchFileName(filename string) bool {
|
||||||
|
if header.FileNameRegex != nil {
|
||||||
|
return header.FileNameRegex.MatchString(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (header *Header) MatchFileHeader(firstLine []byte) bool {
|
||||||
|
if header.HeaderRegex != nil {
|
||||||
|
return header.HeaderRegex.Match(firstLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFileSignature checks the presence of a stored signature
|
||||||
|
func (header *Header) HasFileSignature() bool {
|
||||||
|
return header.SignatureRegex != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchFileSignature will check the given line with the stored regex
|
||||||
|
func (header *Header) MatchFileSignature(line []byte) bool {
|
||||||
|
if header.SignatureRegex != nil {
|
||||||
|
return header.SignatureRegex.Match(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func ParseFile(input []byte) (f *File, err error) {
|
func ParseFile(input []byte) (f *File, err error) {
|
||||||
// This is just so if we have an error, we can exit cleanly and return the parse error to the user
|
// This is just so if we have an error, we can exit cleanly and return the parse error to the user
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -170,11 +209,19 @@ func ParseFile(input []byte) (f *File, err error) {
|
|||||||
if k == "filetype" {
|
if k == "filetype" {
|
||||||
filetype := v.(string)
|
filetype := v.(string)
|
||||||
|
|
||||||
|
if filetype == "" {
|
||||||
|
return nil, errors.New("empty filetype")
|
||||||
|
}
|
||||||
|
|
||||||
f.FileType = filetype
|
f.FileType = filetype
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.FileType == "" {
|
||||||
|
return nil, errors.New("missing filetype")
|
||||||
|
}
|
||||||
|
|
||||||
return f, err
|
return f, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,12 +238,12 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
rules := f.yamlSrc
|
src := f.yamlSrc
|
||||||
|
|
||||||
s = new(Def)
|
s = new(Def)
|
||||||
s.Header = header
|
s.Header = header
|
||||||
|
|
||||||
for k, v := range rules {
|
for k, v := range src {
|
||||||
if k == "rules" {
|
if k == "rules" {
|
||||||
inputRules := v.([]interface{})
|
inputRules := v.([]interface{})
|
||||||
|
|
||||||
@ -209,6 +256,11 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.rules == nil {
|
||||||
|
// allow empty rules
|
||||||
|
s.rules = &rules{}
|
||||||
|
}
|
||||||
|
|
||||||
return s, err
|
return s, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +355,10 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
|
|||||||
|
|
||||||
switch object := val.(type) {
|
switch object := val.(type) {
|
||||||
case string:
|
case string:
|
||||||
|
if object == "" {
|
||||||
|
return nil, fmt.Errorf("Empty rule %s", k)
|
||||||
|
}
|
||||||
|
|
||||||
if k == "include" {
|
if k == "include" {
|
||||||
ru.includes = append(ru.includes, object)
|
ru.includes = append(ru.includes, object)
|
||||||
} else {
|
} else {
|
||||||
@ -356,30 +412,56 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
|
|||||||
r.group = groupNum
|
r.group = groupNum
|
||||||
r.parent = prevRegion
|
r.parent = prevRegion
|
||||||
|
|
||||||
r.start, err = regexp.Compile(regionInfo["start"].(string))
|
// start is mandatory
|
||||||
|
if start, ok := regionInfo["start"]; ok {
|
||||||
|
start := start.(string)
|
||||||
|
if start == "" {
|
||||||
|
return nil, fmt.Errorf("Empty start in %s", group)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
r.start, err = regexp.Compile(start)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Missing start in %s", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.end, err = regexp.Compile(regionInfo["end"].(string))
|
// end is mandatory
|
||||||
|
if end, ok := regionInfo["end"]; ok {
|
||||||
|
end := end.(string)
|
||||||
|
if end == "" {
|
||||||
|
return nil, fmt.Errorf("Empty end in %s", group)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
r.end, err = regexp.Compile(end)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("Missing end in %s", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip is optional
|
// skip is optional
|
||||||
if _, ok := regionInfo["skip"]; ok {
|
if skip, ok := regionInfo["skip"]; ok {
|
||||||
r.skip, err = regexp.Compile(regionInfo["skip"].(string))
|
skip := skip.(string)
|
||||||
|
if skip == "" {
|
||||||
|
return nil, fmt.Errorf("Empty skip in %s", group)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.skip, err = regexp.Compile(skip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// limit-color is optional
|
// limit-color is optional
|
||||||
if _, ok := regionInfo["limit-group"]; ok {
|
if groupStr, ok := regionInfo["limit-group"]; ok {
|
||||||
groupStr := regionInfo["limit-group"].(string)
|
groupStr := groupStr.(string)
|
||||||
|
if groupStr == "" {
|
||||||
|
return nil, fmt.Errorf("Empty limit-group in %s", group)
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := Groups[groupStr]; !ok {
|
if _, ok := Groups[groupStr]; !ok {
|
||||||
numGroups++
|
numGroups++
|
||||||
Groups[groupStr] = numGroups
|
Groups[groupStr] = numGroups
|
||||||
@ -394,10 +476,17 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
|
|||||||
r.limitGroup = r.group
|
r.limitGroup = r.group
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rules, err = parseRules(regionInfo["rules"].([]interface{}), r)
|
// rules are optional
|
||||||
|
if rules, ok := regionInfo["rules"]; ok {
|
||||||
|
r.rules, err = parseRules(rules.([]interface{}), r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if r.rules == nil {
|
||||||
return nil, err
|
// allow empty rules
|
||||||
|
r.rules = &rules{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
|
@ -28,3 +28,6 @@ color-link color-column "#2D2F31"
|
|||||||
#No extended types (bool in C, etc.)
|
#No extended types (bool in C, etc.)
|
||||||
#color-link type.extended "default"
|
#color-link type.extended "default"
|
||||||
#Plain brackets
|
#Plain brackets
|
||||||
|
color-link match-brace "#1D1F21,#62B1FE"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -26,3 +26,6 @@ color-link color-column "254"
|
|||||||
#No extended types (bool in C, &c.) and plain brackets
|
#No extended types (bool in C, &c.) and plain brackets
|
||||||
color-link type.extended "241,231"
|
color-link type.extended "241,231"
|
||||||
color-link symbol.brackets "241,231"
|
color-link symbol.brackets "241,231"
|
||||||
|
color-link match-brace "231,28"
|
||||||
|
color-link tab-error "210"
|
||||||
|
color-link trailingws "210"
|
||||||
|
@ -42,3 +42,6 @@ color-link gutter-warning "red"
|
|||||||
color-link color-column "cyan"
|
color-link color-column "cyan"
|
||||||
color-link underlined.url "underline blue, white"
|
color-link underlined.url "underline blue, white"
|
||||||
color-link divider "blue"
|
color-link divider "blue"
|
||||||
|
color-link match-brace "black,cyan"
|
||||||
|
color-link tab-error "brightred"
|
||||||
|
color-link trailingws "brightred"
|
||||||
|
@ -38,3 +38,6 @@ color-link color-column "#f26522"
|
|||||||
color-link constant.bool "bold #55ffff"
|
color-link constant.bool "bold #55ffff"
|
||||||
color-link constant.bool.true "bold #85ff85"
|
color-link constant.bool.true "bold #85ff85"
|
||||||
color-link constant.bool.false "bold #ff8585"
|
color-link constant.bool.false "bold #ff8585"
|
||||||
|
color-link match-brace "#1e2124,#55ffff"
|
||||||
|
color-link tab-error "#d75f5f"
|
||||||
|
color-link trailingws "#d75f5f"
|
||||||
|
@ -29,3 +29,6 @@ color-link color-column "#2C2C2C"
|
|||||||
color-link type.extended "default"
|
color-link type.extended "default"
|
||||||
#color-link symbol.brackets "default"
|
#color-link symbol.brackets "default"
|
||||||
color-link symbol.tag "#AE81FF,#242424"
|
color-link symbol.tag "#AE81FF,#242424"
|
||||||
|
color-link match-brace "#242424,#7A9EC2"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -1,31 +1 @@
|
|||||||
color-link default "#F8F8F2,#282828"
|
include "monokai"
|
||||||
color-link comment "#75715E,#282828"
|
|
||||||
color-link identifier "#66D9EF,#282828"
|
|
||||||
color-link constant "#AE81FF,#282828"
|
|
||||||
color-link constant.string "#E6DB74,#282828"
|
|
||||||
color-link constant.string.char "#BDE6AD,#282828"
|
|
||||||
color-link statement "#F92672,#282828"
|
|
||||||
color-link symbol.operator "#F92671,#282828"
|
|
||||||
color-link preproc "#CB4B16,#282828"
|
|
||||||
color-link type "#66D9EF,#282828"
|
|
||||||
color-link special "#A6E22E,#282828"
|
|
||||||
color-link underlined "#D33682,#282828"
|
|
||||||
color-link error "bold #CB4B16,#282828"
|
|
||||||
color-link todo "bold #D33682,#282828"
|
|
||||||
color-link hlsearch "#282828,#E6DB74"
|
|
||||||
color-link statusline "#282828,#F8F8F2"
|
|
||||||
color-link tabbar "#282828,#F8F8F2"
|
|
||||||
color-link indent-char "#505050,#282828"
|
|
||||||
color-link line-number "#AAAAAA,#323232"
|
|
||||||
color-link current-line-number "#AAAAAA,#282828"
|
|
||||||
color-link diff-added "#00AF00"
|
|
||||||
color-link diff-modified "#FFAF00"
|
|
||||||
color-link diff-deleted "#D70000"
|
|
||||||
color-link gutter-error "#CB4B16,#282828"
|
|
||||||
color-link gutter-warning "#E6DB74,#282828"
|
|
||||||
color-link cursor-line "#323232"
|
|
||||||
color-link color-column "#323232"
|
|
||||||
#No extended types; Plain brackets.
|
|
||||||
color-link type.extended "default"
|
|
||||||
#color-link symbol.brackets "default"
|
|
||||||
color-link symbol.tag "#AE81FF,#282828"
|
|
||||||
|
@ -43,3 +43,7 @@ color-link cursor-line "#44475A,#F8F8F2"
|
|||||||
color-link color-column "#44475A"
|
color-link color-column "#44475A"
|
||||||
color-link type.extended "default"
|
color-link type.extended "default"
|
||||||
|
|
||||||
|
color-link match-brace "#282A36,#FF79C6"
|
||||||
|
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#001e28"
|
|||||||
color-link type.keyword "bold #5aaae6,#001e28"
|
color-link type.keyword "bold #5aaae6,#001e28"
|
||||||
color-link type.extended "#ffffff,#001e28"
|
color-link type.extended "#ffffff,#001e28"
|
||||||
color-link underlined "#608b4e,#001e28"
|
color-link underlined "#608b4e,#001e28"
|
||||||
|
color-link match-brace "#001e28,#5aaae6"
|
||||||
|
color-link tab-error "#d75f5f"
|
||||||
|
color-link trailingws "#d75f5f"
|
||||||
|
@ -33,3 +33,6 @@ color-link type "bold #004080,#f0f0f0"
|
|||||||
color-link type.keyword "bold #780050,#f0f0f0"
|
color-link type.keyword "bold #780050,#f0f0f0"
|
||||||
color-link type.extended "#000000,#f0f0f0"
|
color-link type.extended "#000000,#f0f0f0"
|
||||||
color-link underlined "#3f7f5f,#f0f0f0"
|
color-link underlined "#3f7f5f,#f0f0f0"
|
||||||
|
color-link match-brace "#f0f0f0,#780050"
|
||||||
|
color-link tab-error "#ff8787"
|
||||||
|
color-link trailingws "#ff8787"
|
||||||
|
@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#2d0023"
|
|||||||
color-link type.keyword "bold #5aaae6,#2d0023"
|
color-link type.keyword "bold #5aaae6,#2d0023"
|
||||||
color-link type.extended "#ffffff,#2d0023"
|
color-link type.extended "#ffffff,#2d0023"
|
||||||
color-link underlined "#886484,#2d0023"
|
color-link underlined "#886484,#2d0023"
|
||||||
|
color-link match-brace "#2d0023,#5aaae6"
|
||||||
|
color-link tab-error "#d75f5f"
|
||||||
|
color-link trailingws "#d75f5f"
|
||||||
|
@ -24,3 +24,6 @@ color-link diff-modified "yellow"
|
|||||||
color-link diff-deleted "red"
|
color-link diff-deleted "red"
|
||||||
color-link gutter-error ",red"
|
color-link gutter-error ",red"
|
||||||
color-link gutter-warning "red"
|
color-link gutter-warning "red"
|
||||||
|
color-link match-brace "black,cyan"
|
||||||
|
color-link tab-error "brightred"
|
||||||
|
color-link trailingws "brightred"
|
||||||
|
@ -24,3 +24,6 @@ color-link gutter-warning "#EDB443,#11151C"
|
|||||||
color-link cursor-line "#091F2E"
|
color-link cursor-line "#091F2E"
|
||||||
color-link color-column "#11151C"
|
color-link color-column "#11151C"
|
||||||
color-link symbol "#99D1CE,#0C1014"
|
color-link symbol "#99D1CE,#0C1014"
|
||||||
|
color-link match-brace "#0C1014,#D26937"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -24,3 +24,6 @@ color-link cursor-line "#3c3836"
|
|||||||
color-link color-column "#79740e"
|
color-link color-column "#79740e"
|
||||||
color-link statusline "#ebdbb2,#665c54"
|
color-link statusline "#ebdbb2,#665c54"
|
||||||
color-link tabbar "#ebdbb2,#665c54"
|
color-link tabbar "#ebdbb2,#665c54"
|
||||||
|
color-link match-brace "#282828,#d3869b"
|
||||||
|
color-link tab-error "#d75f5f"
|
||||||
|
color-link trailingws "#d75f5f"
|
||||||
|
@ -21,3 +21,6 @@ color-link cursor-line "237"
|
|||||||
color-link color-column "237"
|
color-link color-column "237"
|
||||||
color-link statusline "223,237"
|
color-link statusline "223,237"
|
||||||
color-link tabbar "223,237"
|
color-link tabbar "223,237"
|
||||||
|
color-link match-brace "235,72"
|
||||||
|
color-link tab-error "167"
|
||||||
|
color-link trailingws "167"
|
||||||
|
@ -20,6 +20,7 @@ color-link identifier.macro "#FFCB6B,#263238"
|
|||||||
color-link indent-char "#505050,#263238"
|
color-link indent-char "#505050,#263238"
|
||||||
color-link line-number "#656866,#283942"
|
color-link line-number "#656866,#283942"
|
||||||
color-link preproc "#C792EA,#263238"
|
color-link preproc "#C792EA,#263238"
|
||||||
|
color-link scrollbar "#80DEEA,#283942"
|
||||||
color-link special "#C792EA,#263238"
|
color-link special "#C792EA,#263238"
|
||||||
color-link statement "#C792EA,#263238"
|
color-link statement "#C792EA,#263238"
|
||||||
color-link statusline "#80DEEA,#3b4d56"
|
color-link statusline "#80DEEA,#3b4d56"
|
||||||
@ -30,3 +31,6 @@ color-link tabbar "#80DEEA,#3b4d56"
|
|||||||
color-link todo "bold #C792EA,#263238"
|
color-link todo "bold #C792EA,#263238"
|
||||||
color-link type "#FFCB6B,#263238"
|
color-link type "#FFCB6B,#263238"
|
||||||
color-link underlined "underline #EEFFFF,#263238"
|
color-link underlined "underline #EEFFFF,#263238"
|
||||||
|
color-link match-brace "#263238,#C792EA"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -23,3 +23,6 @@ color-link gutter-error "#CB4B16"
|
|||||||
color-link gutter-warning "#E6DB74"
|
color-link gutter-warning "#E6DB74"
|
||||||
color-link cursor-line "#323232"
|
color-link cursor-line "#323232"
|
||||||
color-link color-column "#323232"
|
color-link color-column "#323232"
|
||||||
|
color-link match-brace "#1D0000,#AE81FF"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -29,3 +29,6 @@ color-link color-column "#323232"
|
|||||||
color-link type.extended "default"
|
color-link type.extended "default"
|
||||||
#color-link symbol.brackets "default"
|
#color-link symbol.brackets "default"
|
||||||
color-link symbol.tag "#AE81FF,#282828"
|
color-link symbol.tag "#AE81FF,#282828"
|
||||||
|
color-link match-brace "#282828,#AE81FF"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -34,3 +34,6 @@ color-link todo "#8B98AB"
|
|||||||
color-link type "#66D9EF"
|
color-link type "#66D9EF"
|
||||||
color-link type.keyword "#C678DD"
|
color-link type.keyword "#C678DD"
|
||||||
color-link underlined "#8996A8"
|
color-link underlined "#8996A8"
|
||||||
|
color-link match-brace "#21252C,#C678DD"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -28,6 +28,10 @@ color-link tabbar "bold #b1b1b1,#232323"
|
|||||||
color-link cursor-line "#353535"
|
color-link cursor-line "#353535"
|
||||||
color-link color-column "#353535"
|
color-link color-column "#353535"
|
||||||
color-link space "underline #e6e1dc,#2b2b2b"
|
color-link space "underline #e6e1dc,#2b2b2b"
|
||||||
|
color-link tab-error "#d75f5f"
|
||||||
|
color-link trailingws "#d75f5f"
|
||||||
|
|
||||||
#the Python syntax definition are wrong. This is not how you should do decorators!
|
#the Python syntax definition are wrong. This is not how you should do decorators!
|
||||||
color-link brightgreen "#edb753,#2b2b2b"
|
color-link brightgreen "#edb753,#2b2b2b"
|
||||||
|
|
||||||
|
color-link match-brace "#2b2b2b,#a5c261"
|
||||||
|
@ -10,6 +10,7 @@ color-link ignore "default"
|
|||||||
color-link error ",brightred"
|
color-link error ",brightred"
|
||||||
color-link todo ",brightyellow"
|
color-link todo ",brightyellow"
|
||||||
color-link hlsearch "black,yellow"
|
color-link hlsearch "black,yellow"
|
||||||
|
color-link statusline "black,white"
|
||||||
color-link indent-char "black"
|
color-link indent-char "black"
|
||||||
color-link line-number "yellow"
|
color-link line-number "yellow"
|
||||||
color-link current-line-number "red"
|
color-link current-line-number "red"
|
||||||
@ -27,3 +28,6 @@ color-link type.extended "default"
|
|||||||
color-link symbol.brackets "default"
|
color-link symbol.brackets "default"
|
||||||
#Color shebangs the comment color
|
#Color shebangs the comment color
|
||||||
color-link preproc.shebang "comment"
|
color-link preproc.shebang "comment"
|
||||||
|
color-link match-brace ",magenta"
|
||||||
|
color-link tab-error "brightred"
|
||||||
|
color-link trailingws "brightred"
|
||||||
|
@ -26,3 +26,6 @@ color-link cursor-line "#003541"
|
|||||||
color-link color-column "#003541"
|
color-link color-column "#003541"
|
||||||
color-link type.extended "#839496,#002833"
|
color-link type.extended "#839496,#002833"
|
||||||
color-link symbol.brackets "#839496,#002833"
|
color-link symbol.brackets "#839496,#002833"
|
||||||
|
color-link match-brace "#002833,#268BD2"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -25,3 +25,6 @@ color-link cursor-line "black"
|
|||||||
color-link color-column "black"
|
color-link color-column "black"
|
||||||
color-link type.extended "default"
|
color-link type.extended "default"
|
||||||
color-link symbol.brackets "default"
|
color-link symbol.brackets "default"
|
||||||
|
color-link match-brace ",blue"
|
||||||
|
color-link tab-error "brightred"
|
||||||
|
color-link trailingws "brightred"
|
||||||
|
@ -24,3 +24,6 @@ color-link gutter-warning "88"
|
|||||||
color-link cursor-line "229"
|
color-link cursor-line "229"
|
||||||
#color-link color-column "196"
|
#color-link color-column "196"
|
||||||
color-link current-line-number "246"
|
color-link current-line-number "246"
|
||||||
|
color-line match-brace "230,22"
|
||||||
|
color-link tab-error "210"
|
||||||
|
color-link trailingws "210"
|
||||||
|
@ -35,3 +35,6 @@ color-link todo "#8B98AB"
|
|||||||
color-link type "#F9EE98"
|
color-link type "#F9EE98"
|
||||||
color-link type.keyword "#CDA869"
|
color-link type.keyword "#CDA869"
|
||||||
color-link underlined "#8996A8"
|
color-link underlined "#8996A8"
|
||||||
|
color-link match-brace "#141414,#E0C589"
|
||||||
|
color-link tab-error "#D75F5F"
|
||||||
|
color-link trailingws "#D75F5F"
|
||||||
|
@ -25,3 +25,6 @@ color-link gutter-warning "174,237"
|
|||||||
color-link cursor-line "238"
|
color-link cursor-line "238"
|
||||||
color-link color-column "238"
|
color-link color-column "238"
|
||||||
color-link current-line-number "188,237"
|
color-link current-line-number "188,237"
|
||||||
|
color-link match-brace "237,223"
|
||||||
|
color-link tab-error "167"
|
||||||
|
color-link trailingws "167"
|
||||||
|
@ -8,7 +8,7 @@ This help page aims to cover two aspects of micro's syntax highlighting engine:
|
|||||||
|
|
||||||
## Colorschemes
|
## Colorschemes
|
||||||
|
|
||||||
To change your colorscheme, press Ctrl-e in micro to bring up the command
|
To change your colorscheme, press `Ctrl-e` in micro to bring up the command
|
||||||
prompt, and type:
|
prompt, and type:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -93,7 +93,7 @@ and set this variable yourself.
|
|||||||
* `solarized-tc`: this is the solarized colorscheme for true color.
|
* `solarized-tc`: this is the solarized colorscheme for true color.
|
||||||
* `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme.
|
* `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme.
|
||||||
* `cmc-tc`: A true colour variant of the cmc theme. It requires true color to
|
* `cmc-tc`: A true colour variant of the cmc theme. It requires true color to
|
||||||
look its best. Use cmc-16 if your terminal doesn't support true color.
|
look its best. Use cmc-16 if your terminal doesn't support true color.
|
||||||
* `gruvbox-tc`: The true color version of the gruvbox colorscheme
|
* `gruvbox-tc`: The true color version of the gruvbox colorscheme
|
||||||
* `material-tc`: Colorscheme based off of Google's Material Design palette
|
* `material-tc`: Colorscheme based off of Google's Material Design palette
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ be found
|
|||||||
Custom colorschemes should be placed in the `~/.config/micro/colorschemes`
|
Custom colorschemes should be placed in the `~/.config/micro/colorschemes`
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
A number of custom directives are placed in a `.micro` file. Colorschemes are
|
A number of custom directives are placed in a `.micro` file. Colorschemes are
|
||||||
typically only 18-30 lines in total.
|
typically only 18-30 lines in total.
|
||||||
|
|
||||||
To create the colorscheme you need to link highlight groups with
|
To create the colorscheme you need to link highlight groups with
|
||||||
@ -152,7 +152,7 @@ Then you can use the terminals 256 colors by using their numbers 1-256 (numbers
|
|||||||
|
|
||||||
If the user's terminal supports true color, then you can also specify colors
|
If the user's terminal supports true color, then you can also specify colors
|
||||||
exactly using their hex codes. If the terminal is not true color but micro is
|
exactly using their hex codes. If the terminal is not true color but micro is
|
||||||
told to use a true color colorscheme it will attempt to map the colors to the
|
told to use a true color colorscheme it will attempt to map the colors to the
|
||||||
available 256 colors.
|
available 256 colors.
|
||||||
|
|
||||||
Generally colorschemes which require true color terminals to look good are
|
Generally colorschemes which require true color terminals to look good are
|
||||||
@ -177,10 +177,14 @@ Here is a list of the colorscheme groups that you can use:
|
|||||||
* todo
|
* todo
|
||||||
* selection (Color of the text selection)
|
* selection (Color of the text selection)
|
||||||
* statusline (Color of the statusline)
|
* statusline (Color of the statusline)
|
||||||
|
* statusline.inactive (Color of the statusline of inactive split panes)
|
||||||
|
* statusline.suggestions (Color of the autocomplete suggestions menu)
|
||||||
* tabbar (Color of the tabbar that lists open files)
|
* tabbar (Color of the tabbar that lists open files)
|
||||||
|
* tabbar.active (Color of the active tab in the tabbar)
|
||||||
* indent-char (Color of the character which indicates tabs if the option is
|
* indent-char (Color of the character which indicates tabs if the option is
|
||||||
enabled)
|
enabled)
|
||||||
* line-number
|
* line-number
|
||||||
|
* gutter-info
|
||||||
* gutter-error
|
* gutter-error
|
||||||
* gutter-warning
|
* gutter-warning
|
||||||
* diff-added
|
* diff-added
|
||||||
@ -194,6 +198,10 @@ Here is a list of the colorscheme groups that you can use:
|
|||||||
* divider (Color of the divider between vertical splits)
|
* divider (Color of the divider between vertical splits)
|
||||||
* message (Color of messages in the bottom line of the screen)
|
* message (Color of messages in the bottom line of the screen)
|
||||||
* error-message (Color of error messages in the bottom line of the screen)
|
* error-message (Color of error messages in the bottom line of the screen)
|
||||||
|
* match-brace (Color of matching brackets when `matchbracestyle` is set to `highlight`)
|
||||||
|
* hlsearch (Color of highlighted search results when `hlsearch` is enabled)
|
||||||
|
* tab-error (Color of tab vs space errors when `hltaberrors` is enabled)
|
||||||
|
* trailingws (Color of trailing whitespaces when `hltrailingws` is enabled)
|
||||||
|
|
||||||
Colorschemes must be placed in the `~/.config/micro/colorschemes` directory to
|
Colorschemes must be placed in the `~/.config/micro/colorschemes` directory to
|
||||||
be used.
|
be used.
|
||||||
@ -210,9 +218,9 @@ safe and recommended to use subgroups in your custom syntax files.
|
|||||||
For example if `constant.string` is found in your colorscheme, micro will us
|
For example if `constant.string` is found in your colorscheme, micro will us
|
||||||
that for highlighting strings. If it's not found, it will use constant instead.
|
that for highlighting strings. If it's not found, it will use constant instead.
|
||||||
Micro tries to match the largest set of groups it can find in the colorscheme
|
Micro tries to match the largest set of groups it can find in the colorscheme
|
||||||
definitions, so if, for examle `constant.bool.true` is found then micro will
|
definitions, so if, for example `constant.bool.true` is found then micro will
|
||||||
use that. If `constant.bool.true` is not found but `constant.bool` is found
|
use that. If `constant.bool.true` is not found but `constant.bool` is found
|
||||||
micro will use `constant.bool`. If not, it uses `constant`.
|
micro will use `constant.bool`. If not, it uses `constant`.
|
||||||
|
|
||||||
Here's a list of subgroups used in micro's built-in syntax files.
|
Here's a list of subgroups used in micro's built-in syntax files.
|
||||||
|
|
||||||
@ -220,10 +228,10 @@ Here's a list of subgroups used in micro's built-in syntax files.
|
|||||||
* constant.bool
|
* constant.bool
|
||||||
* constant.bool.true
|
* constant.bool.true
|
||||||
* constant.bool.false
|
* constant.bool.false
|
||||||
* constant.number
|
* constant.number
|
||||||
* constant.specialChar
|
* constant.specialChar
|
||||||
* constant.string
|
* constant.string
|
||||||
* constant.string.url
|
* constant.string.url
|
||||||
* identifier.class (Also used for functions)
|
* identifier.class (Also used for functions)
|
||||||
* identifier.macro
|
* identifier.macro
|
||||||
* identifier.var
|
* identifier.var
|
||||||
@ -236,6 +244,12 @@ Here's a list of subgroups used in micro's built-in syntax files.
|
|||||||
|
|
||||||
In the future, plugins may also be able to use color groups for styling.
|
In the future, plugins may also be able to use color groups for styling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Last but not least it's even possible to use `include` followed by the
|
||||||
|
colorscheme name as string to include a different colorscheme within a new one.
|
||||||
|
Additionally the groups can then be extended or overwritten. The `default.micro`
|
||||||
|
theme can be seen as an example, which links to the chosen default colorscheme.
|
||||||
|
|
||||||
## Syntax files
|
## Syntax files
|
||||||
|
|
||||||
@ -244,7 +258,7 @@ languages.
|
|||||||
|
|
||||||
Micro's builtin syntax highlighting tries very hard to be sane, sensible and
|
Micro's builtin syntax highlighting tries very hard to be sane, sensible and
|
||||||
provide ample coverage of the meaningful elements of a language. Micro has
|
provide ample coverage of the meaningful elements of a language. Micro has
|
||||||
syntax files built in for over 100 languages now! However, there may be
|
syntax files built in for over 100 languages now! However, there may be
|
||||||
situations where you find Micro's highlighting to be insufficient or not to
|
situations where you find Micro's highlighting to be insufficient or not to
|
||||||
your liking. The good news is that you can create your own syntax files, and
|
your liking. The good news is that you can create your own syntax files, and
|
||||||
place them in `~/.config/micro/syntax` and Micro will use those instead.
|
place them in `~/.config/micro/syntax` and Micro will use those instead.
|
||||||
@ -267,8 +281,9 @@ detect:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Micro will match this regex against a given filename to detect the filetype.
|
Micro will match this regex against a given filename to detect the filetype.
|
||||||
You may also provide an optional `header` regex that will check the first line
|
|
||||||
of the file. For example:
|
In addition to the `filename` regex (or even instead of it) you can provide
|
||||||
|
a `header` regex that will check the first line of the file. For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
detect:
|
detect:
|
||||||
@ -276,6 +291,32 @@ detect:
|
|||||||
header: "%YAML"
|
header: "%YAML"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This is useful in cases when the given file name is not sufficient to determine
|
||||||
|
the filetype, e.g. with the above example, if a YAML file has no `.yaml`
|
||||||
|
extension but may contain a `%YAML` directive in its first line.
|
||||||
|
|
||||||
|
`filename` takes precedence over `header`, i.e. if there is a syntax file that
|
||||||
|
matches the file with a filetype by the `filename` and another syntax file that
|
||||||
|
matches the same file with another filetype by the `header`, the first filetype
|
||||||
|
will be used.
|
||||||
|
|
||||||
|
Finally, in addition to `filename` and/or `header` (but not instead of them)
|
||||||
|
you may also provide an optional `signature` regex which is useful for resolving
|
||||||
|
ambiguities when there are multiple syntax files matching the same file with
|
||||||
|
different filetypes. If a `signature` regex is given, micro will match a certain
|
||||||
|
amount of first lines in the file (this amount is determined by the `detectlimit`
|
||||||
|
option) against this regex, and if any of the lines match, this syntax file's
|
||||||
|
filetype will be preferred over other matching filetypes.
|
||||||
|
|
||||||
|
For example, to distinguish C++ header files from C and Objective-C header files
|
||||||
|
that have the same `.h` extension:
|
||||||
|
|
||||||
|
```
|
||||||
|
detect:
|
||||||
|
filename: "\\.c(c|pp|xx)$|\\.h(h|pp|xx)?$"
|
||||||
|
signature: "namespace|template|public|protected|private"
|
||||||
|
```
|
||||||
|
|
||||||
### Syntax rules
|
### Syntax rules
|
||||||
|
|
||||||
Next you must provide the syntax highlighting rules. There are two types of
|
Next you must provide the syntax highlighting rules. There are two types of
|
||||||
@ -334,7 +375,6 @@ highlighted. For example:
|
|||||||
start: "\""
|
start: "\""
|
||||||
end: "\""
|
end: "\""
|
||||||
skip: "\\."
|
skip: "\\."
|
||||||
rules: []
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Includes
|
#### Includes
|
||||||
@ -356,15 +396,28 @@ example, the following is possible for html:
|
|||||||
- include: "css"
|
- include: "css"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Syntax file headers
|
Note that nested include (i.e. including syntax files that include other syntax
|
||||||
|
files) is not supported yet.
|
||||||
|
|
||||||
Syntax file headers are an optimization and it is likely you do not need to
|
### Default syntax highlighting
|
||||||
worry about them.
|
|
||||||
|
|
||||||
Syntax file headers are files that contain only the filetype and the detection
|
If micro cannot detect the filetype of the file, it falls back to using the
|
||||||
regular expressions for a given syntax file. They have a `.hdr` suffix and are
|
default syntax highlighting for it, which highlights just the bare minimum:
|
||||||
used by default only for the pre-installed syntax files. Header files allow
|
email addresses, URLs etc.
|
||||||
micro to parse the syntax files much faster when checking the filetype of a
|
|
||||||
certain file. Custom syntax files may provide header files in
|
Just like in other cases, you can override the default highlighting by adding
|
||||||
`~/.config/micro/syntax` as well but it is not necessary (only do this if you
|
your own custom `default.yaml` file to `~/.config/micro/syntax`.
|
||||||
have many (100+) custom syntax files and want to improve performance).
|
|
||||||
|
For example, if you work with various config files that use the `#` sign to mark
|
||||||
|
the beginning of a comment, you can use the following custom `default.yaml` to
|
||||||
|
highlight those comments by default:
|
||||||
|
|
||||||
|
```
|
||||||
|
filetype: unknown
|
||||||
|
|
||||||
|
detect:
|
||||||
|
filename: ""
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- comment: "(^|\\s)#.*$"
|
||||||
|
```
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Command bar
|
# Command bar
|
||||||
|
|
||||||
The command bar is opened by pressing Ctrl-e. It is a single-line buffer,
|
The command bar is opened by pressing `Ctrl-e`. It is a single-line buffer,
|
||||||
meaning that all keybindings from a normal buffer are supported (as well
|
meaning that all keybindings from a normal buffer are supported (as well
|
||||||
as mouse and selection).
|
as mouse and selection).
|
||||||
|
|
||||||
@ -21,32 +21,49 @@ quotes here but these are not necessary when entering the command in micro.
|
|||||||
This command will modify `bindings.json` and overwrite any bindings to
|
This command will modify `bindings.json` and overwrite any bindings to
|
||||||
`key` that already exist.
|
`key` that already exist.
|
||||||
|
|
||||||
* `help 'topic'?`: opens the corresponding help topic. If no topic is provided
|
* `help ['topic'] ['flags']`: opens the corresponding help topics.
|
||||||
opens the default help screen. Help topics are stored as `.md` files in the
|
If no topic is provided opens the default help screen. If multiple topics are
|
||||||
`runtime/help` directory of the source tree, which is embedded in the final
|
provided (separated via ` `) they are opened all as splits.
|
||||||
binary.
|
Help topics are stored as `.md` files in the `runtime/help` directory of
|
||||||
|
the source tree, which is embedded in the final binary.
|
||||||
|
The `flags` are optional.
|
||||||
|
* `-hsplit`: Opens the help topic in a horizontal split
|
||||||
|
* `-vsplit`: Opens the help topic in a vertical split
|
||||||
|
|
||||||
* `save 'filename'?`: saves the current buffer. If the file is provided it
|
The default split type is defined by the global `helpsplit` option.
|
||||||
|
|
||||||
|
* `save ['filename']`: saves the current buffer. If the file is provided it
|
||||||
will 'save as' the filename.
|
will 'save as' the filename.
|
||||||
|
|
||||||
* `quit`: quits micro.
|
* `quit`: quits micro.
|
||||||
|
|
||||||
* `goto 'line'`: jumps to the given line number. A negative number can be
|
* `goto 'line[:col]'`: goes to the given absolute line (and optional column)
|
||||||
passed to jump inward from the end of the file; for example, -5 jumps
|
number.
|
||||||
to the 5th-last line in the file.
|
A negative number can be passed to go inward from the end of the file.
|
||||||
|
Example: -5 goes to the 5th-last line in the file.
|
||||||
|
|
||||||
* `replace 'search' 'value' 'flags'?`: This will replace `search` with `value`.
|
* `jump 'line[:col]'`: goes to the given relative number from the current
|
||||||
|
line (and optional absolute column) number.
|
||||||
|
Example: -5 jumps 5 lines up in the file, while (+)3 jumps 3 lines down.
|
||||||
|
|
||||||
|
* `replace 'search' 'value' ['flags']`: This will replace `search` with `value`.
|
||||||
The `flags` are optional. Possible flags are:
|
The `flags` are optional. Possible flags are:
|
||||||
* `-a`: Replace all occurrences at once
|
* `-a`: Replace all occurrences at once
|
||||||
* `-l`: Do a literal search instead of a regex search
|
* `-l`: Do a literal search instead of a regex search
|
||||||
|
|
||||||
Note that `search` must be a valid regex (unless `-l` is passed). If one
|
Note that `search` must be a valid regex (unless `-l` is passed). If one
|
||||||
of the arguments does not have any spaces in it, you may omit the quotes.
|
of the arguments does not have any spaces in it, you may omit the quotes.
|
||||||
|
|
||||||
|
In case the search is done non-literal (without `-l`), the 'value'
|
||||||
|
is interpreted as a template:
|
||||||
|
* `$3` or `${3}` substitutes the submatch of the 3rd (capturing group)
|
||||||
|
* `$foo` or `${foo}` substitutes the submatch of the (?P<foo>named group)
|
||||||
|
* You have to write `$$` to substitute a literal dollar.
|
||||||
|
|
||||||
* `replaceall 'search' 'value'`: this will replace all occurrences of `search`
|
* `replaceall 'search' 'value'`: this will replace all occurrences of `search`
|
||||||
with `value` without user confirmation.
|
with `value` without user confirmation.
|
||||||
|
|
||||||
See `replace` command for more information.
|
See `replace` command for more information.
|
||||||
|
|
||||||
* `set 'option' 'value'`: sets the option to value. See the `options` help
|
* `set 'option' 'value'`: sets the option to value. See the `options` help
|
||||||
topic for a list of options you can set. This will modify your
|
topic for a list of options you can set. This will modify your
|
||||||
@ -57,18 +74,21 @@ quotes here but these are not necessary when entering the command in micro.
|
|||||||
|
|
||||||
* `show 'option'`: shows the current value of the given option.
|
* `show 'option'`: shows the current value of the given option.
|
||||||
|
|
||||||
* `run 'sh-command'`: runs the given shell command in the background. The
|
* `run 'sh-command'`: runs the given shell command in the background. The
|
||||||
command's output will be displayed in one line when it finishes running.
|
command's output will be displayed in one line when it finishes running.
|
||||||
|
|
||||||
* `vsplit 'filename'`: opens a vertical split with `filename`. If no filename
|
* `vsplit ['filename']`: opens a vertical split with `filename`. If no filename
|
||||||
is provided, a vertical split is opened with an empty buffer.
|
is provided, a vertical split is opened with an empty buffer. If multiple
|
||||||
|
files are provided (separated via ` `) they are opened all as splits.
|
||||||
|
|
||||||
* `hsplit 'filename'`: same as `vsplit` but opens a horizontal split instead
|
* `hsplit ['filename']`: same as `vsplit` but opens a horizontal split instead
|
||||||
of a vertical split.
|
of a vertical split.
|
||||||
|
|
||||||
* `tab 'filename'`: opens the given file in a new tab.
|
* `tab ['filename']`: opens the given file in a new tab. If no filename
|
||||||
|
is provided, a tab is opened with an empty buffer. If multiple files are
|
||||||
|
provided (separated via ` `) they are opened all as tabs.
|
||||||
|
|
||||||
* `tabmove '[-+]?n'`: Moves the active tab to another slot. `n` is an integer.
|
* `tabmove '[-+]n'`: Moves the active tab to another slot. `n` is an integer.
|
||||||
If `n` is prefixed with `-` or `+`, then it represents a relative position
|
If `n` is prefixed with `-` or `+`, then it represents a relative position
|
||||||
(e.g. `tabmove +2` moves the tab to the right by `2`). If `n` has no prefix,
|
(e.g. `tabmove +2` moves the tab to the right by `2`). If `n` has no prefix,
|
||||||
it represents an absolute position (e.g. `tabmove 2` moves the tab to slot `2`).
|
it represents an absolute position (e.g. `tabmove 2` moves the tab to slot `2`).
|
||||||
@ -89,14 +109,17 @@ quotes here but these are not necessary when entering the command in micro.
|
|||||||
|
|
||||||
* `plugin remove 'pl'`: remove a plugin.
|
* `plugin remove 'pl'`: remove a plugin.
|
||||||
|
|
||||||
* `plugin update 'pl'`: update a plugin (if no arguments are provided
|
* `plugin update ['pl']`: update a plugin (if no arguments are provided
|
||||||
updates all plugins).
|
updates all plugins).
|
||||||
|
|
||||||
* `plugin search 'pl'`: search available plugins for a keyword.
|
* `plugin search 'pl'`: search available plugins for a keyword.
|
||||||
|
|
||||||
* `plugin available`: show available plugins that can be installed.
|
* `plugin available`: show available plugins that can be installed.
|
||||||
|
|
||||||
* `reload`: reloads all runtime files.
|
* `reload`: reloads all runtime files (settings, keybindings, syntax files,
|
||||||
|
colorschemes, plugins). All plugins will be unloaded by running their
|
||||||
|
`deinit()` function (if it exists), and then loaded again by calling the
|
||||||
|
`preinit()`, `init()` and `postinit()` functions (if they exist).
|
||||||
|
|
||||||
* `cd 'path'`: Change the working directory to the given `path`.
|
* `cd 'path'`: Change the working directory to the given `path`.
|
||||||
|
|
||||||
@ -104,6 +127,8 @@ quotes here but these are not necessary when entering the command in micro.
|
|||||||
|
|
||||||
* `open 'filename'`: Open a file in the current buffer.
|
* `open 'filename'`: Open a file in the current buffer.
|
||||||
|
|
||||||
|
* `reopen`: Reopens the current file from disk.
|
||||||
|
|
||||||
* `reset 'option'`: resets the given option to its default value
|
* `reset 'option'`: resets the given option to its default value
|
||||||
|
|
||||||
* `retab`: Replaces all leading tabs with spaces or leading spaces with tabs
|
* `retab`: Replaces all leading tabs with spaces or leading spaces with tabs
|
||||||
@ -114,10 +139,10 @@ quotes here but these are not necessary when entering the command in micro.
|
|||||||
the terminal and helps you see which bindings aren't possible and why. This
|
the terminal and helps you see which bindings aren't possible and why. This
|
||||||
is most useful for debugging keybindings.
|
is most useful for debugging keybindings.
|
||||||
|
|
||||||
* `showkey`: Show the action(s) bound to a given key. For example
|
* `showkey 'key'`: Show the action(s) bound to a given key. For example
|
||||||
running `> showkey Ctrl-c` will display `Copy`.
|
running `> showkey Ctrl-c` will display `Copy`.
|
||||||
|
|
||||||
* `term exec?`: Open a terminal emulator running the given executable. If no
|
* `term ['exec']`: Open a terminal emulator running the given executable. If no
|
||||||
executable is given, this will open the default shell in the terminal
|
executable is given, this will open the default shell in the terminal
|
||||||
emulator.
|
emulator.
|
||||||
|
|
||||||
|
@ -12,13 +12,13 @@ is limited support among terminal emulators for the terminal clipboard
|
|||||||
(which uses the OSC 52 protocol to communicate clipboard contents).
|
(which uses the OSC 52 protocol to communicate clipboard contents).
|
||||||
Here is a list of terminal emulators and their status:
|
Here is a list of terminal emulators and their status:
|
||||||
|
|
||||||
* Kitty: supported, but only writing is enabled by default. To enable
|
* `Kitty`: supported, but only writing is enabled by default. To enable
|
||||||
reading, add `read-primary` and `read-clipboard` to the
|
reading, add `read-primary` and `read-clipboard` to the
|
||||||
`clipboard_control` option.
|
`clipboard_control` option.
|
||||||
|
|
||||||
* iTerm2: only copying (writing to clipboard) is supported. Must be enabled in
|
* `iTerm2`: only copying (writing to clipboard) is supported. Must be enabled in
|
||||||
`Preferences->General-> Selection->Applications in terminal may access clipboard`.
|
`Preferences->General-> Selection->Applications in terminal may access clipboard`.
|
||||||
You can use Command-v to paste.
|
You can use `Command-v` to paste.
|
||||||
|
|
||||||
* `st`: supported.
|
* `st`: supported.
|
||||||
|
|
||||||
@ -31,7 +31,8 @@ Here is a list of terminal emulators and their status:
|
|||||||
|
|
||||||
* `gnome-terminal`: does not support OSC 52.
|
* `gnome-terminal`: does not support OSC 52.
|
||||||
|
|
||||||
* `alacritty`: supported.
|
* `alacritty`: supported. Since 0.13.0, reading has been disabled by default.
|
||||||
|
To reenable it, set the `terminal.osc52` option to `CopyPaste`.
|
||||||
|
|
||||||
* `foot`: supported.
|
* `foot`: supported.
|
||||||
|
|
||||||
@ -48,12 +49,12 @@ supports OSC 52.
|
|||||||
|
|
||||||
The recommended method of pasting is the following:
|
The recommended method of pasting is the following:
|
||||||
|
|
||||||
* If you are not working over SSH, use the micro keybinding (Ctrl-v
|
* If you are not working over SSH, use the micro keybinding (`Ctrl-v`
|
||||||
by default) to perform pastes. If on Linux, install `xclip` or
|
by default) to perform pastes. If on Linux, install `xclip` or
|
||||||
`xsel` beforehand.
|
`xsel` beforehand.
|
||||||
|
|
||||||
* If you are working over SSH, use the terminal keybinding
|
* If you are working over SSH, use the terminal keybinding
|
||||||
(Ctrl-Shift-v or Command-v) to perform pastes. If your terminal
|
(`Ctrl-Shift-v` or `Command-v`) to perform pastes. If your terminal
|
||||||
does not support bracketed paste, when performing a paste first
|
does not support bracketed paste, when performing a paste first
|
||||||
enable the `paste` option, and when finished disable the option.
|
enable the `paste` option, and when finished disable the option.
|
||||||
|
|
||||||
@ -62,8 +63,8 @@ The recommended method of pasting is the following:
|
|||||||
Micro is an application that runs within the terminal. This means
|
Micro is an application that runs within the terminal. This means
|
||||||
that the terminal sends micro events, such as key events, mouse
|
that the terminal sends micro events, such as key events, mouse
|
||||||
events, resize events, and paste events. Micro's default keybinding
|
events, resize events, and paste events. Micro's default keybinding
|
||||||
for paste is Ctrl-v. This means that when micro receives the key
|
for paste is `Ctrl-v`. This means that when micro receives the key
|
||||||
event saying Ctrl-v has been pressed from the terminal, it will
|
event saying `Ctrl-v` has been pressed from the terminal, it will
|
||||||
attempt to access the system clipboard and effect a paste. The
|
attempt to access the system clipboard and effect a paste. The
|
||||||
system clipboard will be accessed through `pbpaste` on MacOS
|
system clipboard will be accessed through `pbpaste` on MacOS
|
||||||
(installed by default), `xclip` or `xsel` on Linux (these
|
(installed by default), `xclip` or `xsel` on Linux (these
|
||||||
@ -76,8 +77,8 @@ For certain keypresses, the terminal will not send an event to
|
|||||||
micro and will instead do something itself. In this document,
|
micro and will instead do something itself. In this document,
|
||||||
such keypresses will be called "terminal keybindings." Often
|
such keypresses will be called "terminal keybindings." Often
|
||||||
there will be a terminal keybinding for pasting and copying. On
|
there will be a terminal keybinding for pasting and copying. On
|
||||||
MacOS these are Command-v and Command-c and on Linux Ctrl-Shift-v
|
MacOS these are Command-v and Command-c and on Linux `Ctrl-Shift-v`
|
||||||
and Ctrl-Shift-c. When the terminal keybinding for paste is
|
and `Ctrl-Shift-c`. When the terminal keybinding for paste is
|
||||||
executed, your terminal will access the system clipboard, and send
|
executed, your terminal will access the system clipboard, and send
|
||||||
micro either a paste event or a list of key events (one key for each
|
micro either a paste event or a list of key events (one key for each
|
||||||
character in the paste), depending on whether or not your terminal
|
character in the paste), depending on whether or not your terminal
|
||||||
@ -89,7 +90,7 @@ sends a list of key events, this can cause issues because micro
|
|||||||
will think you manually entered each character and may add closing
|
will think you manually entered each character and may add closing
|
||||||
brackets or automatic indentation, which will mess up the pasted
|
brackets or automatic indentation, which will mess up the pasted
|
||||||
text. To avoid this, you can temporarily enable the `paste` option
|
text. To avoid this, you can temporarily enable the `paste` option
|
||||||
while you perform the paste. When paste option is on, micro will
|
while you perform the paste. When paste option is on, micro will
|
||||||
aggregate lists of multiple key events into larger paste events.
|
aggregate lists of multiple key events into larger paste events.
|
||||||
It is a good idea to disable the `paste` option during normal use
|
It is a good idea to disable the `paste` option during normal use
|
||||||
as occasionally if you are typing quickly, the terminal will send
|
as occasionally if you are typing quickly, the terminal will send
|
||||||
@ -100,7 +101,7 @@ entered.
|
|||||||
|
|
||||||
When working over SSH, micro is running on the remote machine and
|
When working over SSH, micro is running on the remote machine and
|
||||||
your terminal is running on your local machine. Therefore if you
|
your terminal is running on your local machine. Therefore if you
|
||||||
would like to paste, using Ctrl-v (micro's keybinding) will not
|
would like to paste, using `Ctrl-v` (micro's keybinding) will not
|
||||||
work because when micro attempts to access the system clipboard,
|
work because when micro attempts to access the system clipboard,
|
||||||
it will access the remote machine's clipboard rather than the local
|
it will access the remote machine's clipboard rather than the local
|
||||||
machine's clipboard. On the other hand, the terminal keybinding
|
machine's clipboard. On the other hand, the terminal keybinding
|
||||||
@ -113,12 +114,12 @@ the network as a paste event, which is what you want.
|
|||||||
|
|
||||||
The recommended method of copying is the following:
|
The recommended method of copying is the following:
|
||||||
|
|
||||||
* If you are not working over SSH, use the micro keybinding (Ctrl-c by
|
* If you are not working over SSH, use the micro keybinding (`Ctrl-c` by
|
||||||
default) to perform copies. If on Linux, install `xclip` or `xsel`
|
default) to perform copies. If on Linux, install `xclip` or `xsel`
|
||||||
beforehand.
|
beforehand.
|
||||||
|
|
||||||
* If you are working over SSH, use the terminal keybinding
|
* If you are working over SSH, use the terminal keybinding
|
||||||
(Ctrl-Shift-c or Command-c) to perform copies. You must first disable
|
(`Ctrl-Shift-c` or `Command-c`) to perform copies. You must first disable
|
||||||
the `mouse` option to perform a terminal selection, and you may wish
|
the `mouse` option to perform a terminal selection, and you may wish
|
||||||
to disable line numbers and diff indicators (`ruler` and `diffgutter`
|
to disable line numbers and diff indicators (`ruler` and `diffgutter`
|
||||||
options) and close other splits. This method will only be able to copy
|
options) and close other splits. This method will only be able to copy
|
||||||
@ -129,14 +130,14 @@ Copying follows a similar discussion to the one above about pasting.
|
|||||||
The primary difference is before performing a copy, the application
|
The primary difference is before performing a copy, the application
|
||||||
doing the copy must be told what text needs to be copied.
|
doing the copy must be told what text needs to be copied.
|
||||||
|
|
||||||
Micro has a keybinding (Ctrl-c) for copying and will access the system
|
Micro has a keybinding (`Ctrl-c`) for copying and will access the system
|
||||||
clipboard to perform the copy. The text that micro will copy into is
|
clipboard to perform the copy. The text that micro will copy into is
|
||||||
the text that is currently selected in micro (usually such text is
|
the text that is currently selected in micro (usually such text is
|
||||||
displayed with a white background). When the `mouse` option is enabled,
|
displayed with a white background). When the `mouse` option is enabled,
|
||||||
the mouse can be used to select text, as well as other keybindings,
|
the mouse can be used to select text, as well as other keybindings,
|
||||||
such as ShiftLeft, etc...
|
such as ShiftLeft, etc...
|
||||||
|
|
||||||
The terminal also has a keybinding (Ctrl-Shift-c or Command-c) to perform
|
The terminal also has a keybinding (`Ctrl-Shift-c` or `Command-c`) to perform
|
||||||
a copy, and the text that it copies is the text selected by the terminal's
|
a copy, and the text that it copies is the text selected by the terminal's
|
||||||
selection (*not* micro's selection). To select text with the terminal
|
selection (*not* micro's selection). To select text with the terminal
|
||||||
selection, micro's mouse support must first be disabled by turning the
|
selection, micro's mouse support must first be disabled by turning the
|
||||||
|
@ -52,9 +52,9 @@ can change it!
|
|||||||
| Ctrl-n | Find next instance of current search |
|
| Ctrl-n | Find next instance of current search |
|
||||||
| Ctrl-p | Find previous instance of current search |
|
| Ctrl-p | Find previous instance of current search |
|
||||||
|
|
||||||
Note: Ctrl-n and Ctrl-p should be used from the main buffer, not from inside
|
Note: `Ctrl-n` and `Ctrl-p` should be used from the main buffer, not from inside
|
||||||
the search prompt. After Ctrl-f, press enter to complete the search and then
|
the search prompt. After `Ctrl-f`, press enter to complete the search and then
|
||||||
you can use Ctrl-n and Ctrl-p to cycle through matches.
|
you can use `Ctrl-n` and `Ctrl-p` to cycle through matches.
|
||||||
|
|
||||||
### File Operations
|
### File Operations
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ you can use Ctrl-n and Ctrl-p to cycle through matches.
|
|||||||
|
|
||||||
### Function keys.
|
### Function keys.
|
||||||
|
|
||||||
Warning! The function keys may not work in all terminals!
|
Warning! The function keys may not work in all terminals!
|
||||||
|
|
||||||
| Key | Description of function |
|
| Key | Description of function |
|
||||||
|------ |-------------------------- |
|
|------ |-------------------------- |
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user