mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-29 22:27:13 +09:00
Compare commits
3 Commits
master
...
terminal-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d03a5fc998 | ||
|
|
ef4a385380 | ||
|
|
a1fcbde863 |
@@ -1,6 +0,0 @@
|
||||
# See https://editorconfig.org
|
||||
|
||||
# In Go files we indent with tabs but still
|
||||
# set indent_size to control the GitHub web viewer.
|
||||
[*.go]
|
||||
indent_size=4
|
||||
9
.github/ISSUE_TEMPLATE
vendored
Normal file
9
.github/ISSUE_TEMPLATE
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
## Description of the problem or steps to reproduce
|
||||
|
||||
## Specifications
|
||||
|
||||
You can use `micro -version` to get the commit hash.
|
||||
|
||||
Commit hash:
|
||||
OS:
|
||||
Terminal:
|
||||
25
.github/ISSUE_TEMPLATE/01-bug.yml
vendored
25
.github/ISSUE_TEMPLATE/01-bug.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "<title>"
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Description of the problem and steps to reproduce.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **Version**: 2.0.15 and/or commit hash ($ micro -version)
|
||||
- **OS**: Debian
|
||||
- **Terminal**: ptyxis
|
||||
value: |
|
||||
- Version:
|
||||
- OS:
|
||||
- Terminal:
|
||||
validations:
|
||||
required: true
|
||||
11
.github/ISSUE_TEMPLATE/02-feature.yml
vendored
11
.github/ISSUE_TEMPLATE/02-feature.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Feature Request
|
||||
description: File a feature request.
|
||||
title: "<title>"
|
||||
labels: ["feature"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Description of the feature.
|
||||
validations:
|
||||
required: true
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
46
.github/workflows/nightly.yaml
vendored
46
.github/workflows/nightly.yaml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Nightly builds
|
||||
on:
|
||||
workflow_dispatch: # Allows manual trigger
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
nightly:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Setup
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Build
|
||||
run: tools/cross-compile.sh nightly
|
||||
|
||||
- name: Tag
|
||||
uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: nightly
|
||||
force_push_tag: true
|
||||
message: "Pre-release nightly"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: nightly
|
||||
tag_name: nightly
|
||||
files: binaries/*
|
||||
prerelease: true
|
||||
|
||||
- name: Cleanup
|
||||
run: rm -rf binaries
|
||||
36
.github/workflows/release.yaml
vendored
36
.github/workflows/release.yaml
vendored
@@ -1,36 +0,0 @@
|
||||
name: Release builds
|
||||
on:
|
||||
workflow_dispatch: # Allows manual trigger
|
||||
# push:
|
||||
# tags:
|
||||
# - 'v*.*.*' # automatically react on semantic versioned tags
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Setup
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Build
|
||||
run: tools/cross-compile.sh
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: binaries/*
|
||||
|
||||
- name: Cleanup
|
||||
run: rm -rf binaries
|
||||
27
.github/workflows/test.yaml
vendored
27
.github/workflows/test.yaml
vendored
@@ -1,27 +0,0 @@
|
||||
on: [push, pull_request]
|
||||
name: Build and Test
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x, 1.23.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
make test
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,20 +1,5 @@
|
||||
.DS_Store
|
||||
|
||||
micro
|
||||
micro.exe
|
||||
./micro
|
||||
!cmd/micro
|
||||
binaries/
|
||||
tmp.sh
|
||||
test/
|
||||
.idea/
|
||||
packages/
|
||||
todo.txt
|
||||
test.txt
|
||||
log.txt
|
||||
*.old
|
||||
benchmark_results*
|
||||
tools/build-version
|
||||
tools/build-date
|
||||
tools/info-plist
|
||||
tools/vscode-tests/
|
||||
*.hdr
|
||||
|
||||
2
.travis.yml
Normal file
2
.travis.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
language: go
|
||||
script: make test
|
||||
4
LICENSE
4
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
Micro is licensed under the MIT "Expat" License:
|
||||
|
||||
Copyright (c) 2016-2020: Zachary Yedidia, et al.
|
||||
Copyright (c) 2016: Zachary Yedidia.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
1306
LICENSE-THIRD-PARTY
1306
LICENSE-THIRD-PARTY
File diff suppressed because it is too large
Load Diff
92
Makefile
92
Makefile
@@ -1,78 +1,48 @@
|
||||
.PHONY: runtime build generate build-quick
|
||||
.PHONY: runtime
|
||||
|
||||
VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/build-version.go)
|
||||
VERSION = $(shell git describe --tags --abbrev=0)
|
||||
HASH = $(shell git rev-parse --short HEAD)
|
||||
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/build-date.go)
|
||||
GOBIN ?= $(shell go env GOPATH)/bin
|
||||
GOVARS = -X github.com/micro-editor/micro/v2/internal/util.Version=$(VERSION) -X github.com/micro-editor/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/micro-editor/micro/v2/internal/util.CompileDate=$(DATE)'
|
||||
DEBUGVAR = -X github.com/micro-editor/micro/v2/internal/util.Debug=ON
|
||||
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)
|
||||
DATE = $(shell go run tools/build-date.go)
|
||||
|
||||
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
|
||||
# Builds micro after checking dependencies but without updating the runtime
|
||||
build: deps tcell
|
||||
go build -ldflags "-X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)'" ./cmd/micro
|
||||
|
||||
build: generate build-quick
|
||||
# Builds micro after building the runtime and checking dependencies
|
||||
build-all: runtime build
|
||||
|
||||
# Builds micro without checking for dependencies
|
||||
build-quick:
|
||||
CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
go build -ldflags "-X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)'" ./cmd/micro
|
||||
|
||||
build-dbg:
|
||||
CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "$(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
|
||||
# Same as 'build' but installs to $GOPATH/bin afterward
|
||||
install: build
|
||||
mv micro $(GOPATH)/bin
|
||||
|
||||
build-tags: fetch-tags build
|
||||
# Same as 'build-all' but installs to $GOPATH/bin afterward
|
||||
install-all: runtime install
|
||||
|
||||
build-all: build
|
||||
# Same as 'build-quick' but installs to $GOPATH/bin afterward
|
||||
install-quick: build-quick
|
||||
mv micro $(GOPATH)/bin
|
||||
|
||||
install: generate
|
||||
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
# Updates tcell
|
||||
tcell:
|
||||
git -C $(GOPATH)/src/github.com/zyedidia/tcell pull
|
||||
|
||||
install-all: install
|
||||
# Checks for dependencies
|
||||
deps:
|
||||
go get -d ./cmd/micro
|
||||
|
||||
fetch-tags:
|
||||
git fetch --tags --force
|
||||
|
||||
generate:
|
||||
GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime
|
||||
|
||||
testgen:
|
||||
mkdir -p tools/vscode-tests
|
||||
cd tools/vscode-tests && \
|
||||
curl --remote-name-all $(VSCODE_TESTS_BASE_URL){editableTextModelAuto,editableTextModel,model.line}.test.ts
|
||||
tsc tools/vscode-tests/*.ts > /dev/null; true
|
||||
go run tools/testgen.go tools/vscode-tests/*.js > buffer_generated_test.go
|
||||
mv buffer_generated_test.go internal/buffer
|
||||
gofmt -w internal/buffer/buffer_generated_test.go
|
||||
# Builds the runtime
|
||||
runtime:
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
$(GOPATH)/bin/go-bindata -nometadata -o runtime.go runtime/...
|
||||
mv runtime.go cmd/micro
|
||||
|
||||
test:
|
||||
go test ./internal/...
|
||||
go test ./cmd/...
|
||||
|
||||
bench:
|
||||
for i in 1 2 3; do \
|
||||
go test -bench=. ./internal/...; \
|
||||
done > benchmark_results
|
||||
benchstat benchmark_results
|
||||
|
||||
bench-baseline:
|
||||
for i in 1 2 3; do \
|
||||
go test -bench=. ./internal/...; \
|
||||
done > benchmark_results_baseline
|
||||
|
||||
bench-compare:
|
||||
for i in 1 2 3; do \
|
||||
go test -bench=. ./internal/...; \
|
||||
done > benchmark_results
|
||||
benchstat -alpha 0.15 benchmark_results_baseline benchmark_results
|
||||
go get -d ./cmd/micro
|
||||
go test ./cmd/micro
|
||||
|
||||
clean:
|
||||
rm -f micro
|
||||
|
||||
297
README.md
297
README.md
@@ -1,253 +1,117 @@
|
||||
<img alt="micro logo" src="./assets/micro-logo-drop.svg" width="500px"/>
|
||||
# 
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/micro-editor/micro/v2)
|
||||
[](https://github.com/micro-editor/micro/releases)
|
||||
[](https://github.com/micro-editor/micro/blob/master/LICENSE)
|
||||
[](https://travis-ci.org/zyedidia/micro)
|
||||
[](https://goreportcard.com/report/github.com/zyedidia/micro)
|
||||
[](https://gitter.im/zyedidia/micro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://snapcraft.io/micro)
|
||||
[](https://github.com/zyedidia/micro/blob/master/LICENSE)
|
||||
|
||||
**micro** is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the capabilities
|
||||
of modern terminals. It comes as a single, batteries-included, static binary with no dependencies; you can download and use it right now!
|
||||
Micro is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the full capabilities
|
||||
of modern terminals. It comes as one single, batteries-included, static binary with no dependencies, and you can download and use it right now.
|
||||
|
||||
As its name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use.
|
||||
It strives to be enjoyable as a full-time editor for people who prefer to work in a terminal, or those who regularly edit files over SSH.
|
||||
As the name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use in a pinch, but micro also aims to be
|
||||
enjoyable to use full time, whether you work in the terminal because you prefer it (like me), or because you need to (over ssh).
|
||||
|
||||
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 all of the default colorschemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
|
||||
|
||||
You can also check out the website for Micro at https://micro-editor.github.io.
|
||||
# Features
|
||||
|
||||
- - -
|
||||
* Easy to use and to install
|
||||
* No dependencies or external files are needed -- just the binary you can download further down the page
|
||||
* Common keybindings (ctrl-s, ctrl-c, ctrl-v, ctrl-z...)
|
||||
* Keybindings can be rebound to your liking
|
||||
* Sane defaults
|
||||
* You shouldn't have to configure much out of the box (and it is extremely easy to configure)
|
||||
* Splits and tabs
|
||||
* Extremely good mouse support
|
||||
* This means mouse dragging to create a selection, double click to select by word, and triple click to select by line
|
||||
* Cross platform (It should work on all the platforms Go runs on)
|
||||
* Note that while Windows is supported, there are still some bugs that need to be worked out
|
||||
* Plugin system (plugins are written in Lua)
|
||||
* Persistent undo
|
||||
* Automatic linting and error notifications
|
||||
* Syntax highlighting (for over [75 languages](runtime/syntax)!)
|
||||
* Colorscheme support
|
||||
* By default, micro comes with 16, 256, and true color themes.
|
||||
* True color support (set the `MICRO_TRUECOLOR` env variable to 1 to enable it)
|
||||
* Copy and paste with the system clipboard
|
||||
* Small and simple
|
||||
* Easily configurable
|
||||
* Macros
|
||||
* Common editor things such as undo/redo, line numbers, unicode support...
|
||||
|
||||
## Features
|
||||
Although not yet implemented, I hope to add more features such as autocompletion ([#174](https://github.com/zyedidia/micro/issues/174)), and multiple cursors ([#5](https://github.com/zyedidia/micro/issues/5)) in the future.
|
||||
|
||||
- Easy to use and install.
|
||||
- No dependencies or external files are needed — just the binary you can download further down the page.
|
||||
- Multiple cursors.
|
||||
- Common keybindings (<kbd>Ctrl-s</kbd>, <kbd>Ctrl-c</kbd>, <kbd>Ctrl-v</kbd>, <kbd>Ctrl-z</kbd>, …).
|
||||
- Keybindings can be rebound to your liking.
|
||||
- Sane defaults.
|
||||
- You shouldn't have to configure much out of the box (and it is extremely easy to configure).
|
||||
- Splits and tabs.
|
||||
- nano-like menu to help you remember the keybindings.
|
||||
- Extremely good mouse support.
|
||||
- This means mouse dragging to create a selection, double click to select by word, and triple click to select by line.
|
||||
- Cross-platform (it should work on all the platforms Go runs on).
|
||||
- Note that while Windows is supported, Mingw/Cygwin is not (see below).
|
||||
- Plugin system (plugins are written in Lua).
|
||||
- micro has a built-in plugin manager to automatically install, remove, and update plugins.
|
||||
- Built-in diff gutter.
|
||||
- Simple autocompletion.
|
||||
- Persistent undo.
|
||||
- Automatic linting and error notifications.
|
||||
- Syntax highlighting for over [130 languages](runtime/syntax).
|
||||
- Color scheme support.
|
||||
- By default, micro comes with 16, 256, and true color themes.
|
||||
- True color support.
|
||||
- Copy and paste with the system clipboard.
|
||||
- Small and simple.
|
||||
- Easily configurable.
|
||||
- Macros.
|
||||
- Smart highlighting of trailing whitespace and tab vs space errors.
|
||||
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
|
||||
# Installation
|
||||
|
||||
## Installation
|
||||
To install micro, you can download a [prebuilt binary](https://github.com/zyedidia/micro/releases), or you can build it from source.
|
||||
|
||||
To install micro, you can download a [prebuilt binary](https://github.com/micro-editor/micro/releases), or you can build it from source.
|
||||
If you want more information about ways to install micro, see this [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro)
|
||||
|
||||
If you want more information about ways to install micro, see this [wiki page](https://github.com/micro-editor/micro/wiki/Installing-Micro).
|
||||
### Prebuilt binaries
|
||||
|
||||
Use `micro -version` to get the version information after installing. It is only guaranteed that you are installing the most recent
|
||||
stable version if you install from the prebuilt binaries, Homebrew, or Snap.
|
||||
All you need to install micro is one file, the binary itself. It's as simple as that!
|
||||
|
||||
A desktop entry file and man page can be found in the [assets/packaging](https://github.com/micro-editor/micro/tree/master/assets/packaging) directory.
|
||||
Download the binary from the [releases](https://github.com/zyedidia/micro/releases) page.
|
||||
|
||||
### Pre-built binaries
|
||||
On that page you'll see the nightly release, which contains binaries for micro which are built every night,
|
||||
and you'll see all the stable releases with the corresponding binaries.
|
||||
|
||||
Pre-built binaries are distributed in [releases](https://github.com/micro-editor/micro/releases).
|
||||
|
||||
To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`.
|
||||
|
||||
#### Third-party quick-install script
|
||||
|
||||
```bash
|
||||
curl https://getmic.ro | bash
|
||||
```
|
||||
|
||||
The script will place the micro binary in the current directory. From there, you can move it to a directory on your path of your choosing (e.g. `sudo mv micro /usr/bin`). See its [GitHub repository](https://github.com/benweissmann/getmic.ro) for more information.
|
||||
|
||||
#### Eget
|
||||
|
||||
With [Eget](https://github.com/zyedidia/eget) installed, you can easily get a pre-built binary:
|
||||
|
||||
```
|
||||
eget micro-editor/micro
|
||||
```
|
||||
|
||||
Use `--tag VERSION` to download a specific tagged version.
|
||||
|
||||
```
|
||||
eget --tag nightly micro-editor/micro # download the nightly version (compiled every day at midnight UTC)
|
||||
eget --tag v2.0.8 micro-editor/micro # download version 2.0.8 rather than the latest release
|
||||
```
|
||||
|
||||
You can install `micro` by adding `--to /usr/local/bin` to the `eget` command, or move the binary manually to a directory on your `$PATH` after the download completes.
|
||||
|
||||
See [Eget](https://github.com/zyedidia/eget) for more information.
|
||||
|
||||
### Package managers
|
||||
|
||||
You can install micro using Homebrew on Mac:
|
||||
|
||||
```
|
||||
brew install micro
|
||||
```
|
||||
|
||||
**Note for Mac:** All micro keybindings use the control or alt (option) key, not the command
|
||||
key. By default, macOS terminals do not forward alt key events. To fix this, please see
|
||||
the section on [macOS terminals](https://github.com/micro-editor/micro#macos-terminal) further below.
|
||||
|
||||
On Linux, you can install micro through [snap](https://snapcraft.io/docs/core/install)
|
||||
|
||||
```
|
||||
snap install micro --classic
|
||||
```
|
||||
|
||||
Micro is also available through other package managers on Linux such as dnf, AUR, Nix, and package managers
|
||||
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. -->
|
||||
|
||||
* Linux:
|
||||
* distro-specific package managers:
|
||||
* `dnf install micro` (Fedora).
|
||||
* `apt install micro` (Ubuntu and Debian).
|
||||
* `pacman -S micro` (Arch Linux).
|
||||
* `emerge app-editors/micro` (Gentoo).
|
||||
* `zypper install micro-editor` (SUSE)
|
||||
* `eopkg install micro` (Solus).
|
||||
* `pacstall -I micro` (Pacstall).
|
||||
* `apt-get install micro` (ALT Linux)
|
||||
* See [wiki](https://github.com/micro-editor/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`.
|
||||
* `scoop install micro`.
|
||||
* `winget install zyedidia.micro`
|
||||
* OpenBSD: Available in the ports tree and also available as a binary package.
|
||||
* `pkg_add -v micro`.
|
||||
* NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](https://www.pkgsrc.org/)-current:
|
||||
* `pkg_add micro`
|
||||
* macOS: Available in package managers.
|
||||
* `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:**
|
||||
|
||||
For interfacing with the local system clipboard, the following tools need to be installed:
|
||||
* For X11, `xclip` or `xsel`
|
||||
* For [Wayland](https://wayland.freedesktop.org/), `wl-clipboard`
|
||||
|
||||
Without these tools installed, micro will use an internal clipboard for copy and paste, but it won't be accessible to external applications.
|
||||
If you'd like to see more information after installing micro, run `micro -version`.
|
||||
|
||||
### Building from source
|
||||
|
||||
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 binary, but does run Go, you can build from source.
|
||||
|
||||
Make sure that you have Go version 1.19 or greater and Go modules are enabled.
|
||||
Make sure that you have Go version 1.5 or greater (Go 1.4 will work if your version supports CGO).
|
||||
|
||||
```
|
||||
git clone https://github.com/micro-editor/micro
|
||||
cd micro
|
||||
make build
|
||||
sudo mv micro /usr/local/bin # optional
|
||||
```sh
|
||||
go get -u github.com/zyedidia/micro/...
|
||||
```
|
||||
|
||||
The binary will be placed in the current directory and can be moved to
|
||||
anywhere you like (for example `/usr/local/bin`).
|
||||
### Linux clipboard support
|
||||
|
||||
The command `make install` will install the binary to `$GOPATH/bin` or `$GOBIN`.
|
||||
On Linux, clipboard support requires 'xclip' or 'xsel' command to be installed.
|
||||
|
||||
You can install directly with `go get` (`go get github.com/micro-editor/micro/cmd/micro`) but this isn't
|
||||
recommended because it doesn't build micro with version information (necessary for the plugin manager),
|
||||
and doesn't disable debug mode.
|
||||
For Ubuntu:
|
||||
|
||||
### Fully static or dynamically linked binary
|
||||
|
||||
By default, the micro binary is linked statically to increase the portability of the prebuilt binaries.
|
||||
This behavior can simply be overriden by providing `CGO_ENABLED=1` to the build target.
|
||||
|
||||
```
|
||||
CGO_ENABLED=1 make build
|
||||
```sh
|
||||
sudo apt-get install xclip
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
If you are using macOS, you should consider using [iTerm2](https://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 still insist on using the default Mac terminal, be sure to set `Use Option key as Meta key` under
|
||||
`Preferences->Profiles->Keyboard` to use <kbd>option</kbd> as <kbd>alt</kbd>.
|
||||
|
||||
### WSL and Windows Console
|
||||
|
||||
If you use micro within WSL, it is highly recommended that you use the [Windows
|
||||
Terminal](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701?hl=en-us&gl=us)
|
||||
instead of the default Windows Console.
|
||||
|
||||
If you must use Windows Console for some reason, note that there is a bug in
|
||||
Windows Console WSL that causes a font change whenever micro tries to access
|
||||
the external clipboard via powershell. To fix this, use an internal clipboard
|
||||
with `set clipboard internal` (though your system clipboard will no longer be
|
||||
available in micro).
|
||||
If you don't have xclip or xsel, micro will use an internal clipboard for copy and paste, but it won't work with external applications.
|
||||
|
||||
### Colors and syntax highlighting
|
||||
|
||||
If you open micro and it doesn't seem like syntax highlighting is working, this is probably because
|
||||
you are using a terminal which does not support 256 color mode. Try changing the color scheme to `simple`
|
||||
by pressing <kbd>Ctrl-e</kbd> in micro and typing `set colorscheme simple`.
|
||||
you are using a terminal which does not support 256 color. Try changing the colorscheme to `simple`
|
||||
by running `> set colorscheme simple`.
|
||||
|
||||
If you are using the default Ubuntu terminal, to enable 256 color mode make sure your `TERM` variable is set
|
||||
If you are using the default ubuntu terminal, to enable 256 make sure your `TERM` variable is set
|
||||
to `xterm-256color`.
|
||||
|
||||
Many of the Windows terminals don't support more than 16 colors, which means
|
||||
that micro's default color scheme won't look very good. You can either set
|
||||
the color scheme to `simple`, or download and configure a better terminal emulator
|
||||
than the Windows default.
|
||||
that micro's default colorscheme won't look very good. You can either set
|
||||
the colorscheme to `simple`, or download a better terminal emulator, like
|
||||
mintty.
|
||||
|
||||
### Cygwin, Mingw, Plan9
|
||||
### Plan9, NaCl, Cygwin
|
||||
|
||||
Cygwin, Mingw, and Plan9 are unfortunately not officially supported. In Cygwin and Mingw, micro will often work when run using
|
||||
the `winpty` utility:
|
||||
|
||||
```
|
||||
winpty micro.exe ...
|
||||
```
|
||||
|
||||
Micro uses the amazing [tcell library](https://github.com/gdamore/tcell), but this
|
||||
Please note that micro uses the amazing [tcell library](https://github.com/gdamore/tcell), but this
|
||||
means that micro is restricted to the platforms tcell supports. As a result, micro does not support
|
||||
Plan9 or Cygwin (although this may change in the future). Micro also doesn't support NaCl (which is deprecated anyway).
|
||||
Plan9, NaCl, and Cygwin (although this may change in the future).
|
||||
|
||||
## Usage
|
||||
# Usage
|
||||
|
||||
Once you have built the editor, start it by running `micro path/to/file.txt` or `micro` to open an empty buffer.
|
||||
Once you have built the editor, simply start it by running `micro path/to/file.txt` or simply `micro` to open an empty buffer.
|
||||
|
||||
micro also supports creating buffers from `stdin`:
|
||||
Micro also supports creating buffers from `stdin`:
|
||||
|
||||
```sh
|
||||
ip a | micro
|
||||
ifconfig | micro
|
||||
```
|
||||
|
||||
You can move the cursor around with the arrow keys and mouse.
|
||||
@@ -256,30 +120,25 @@ You can also use the mouse to manipulate the text. Simply clicking and dragging
|
||||
will select text. You can also double click to enable word selection, and triple
|
||||
click to enable line selection.
|
||||
|
||||
## Documentation and Help
|
||||
# Documentation and Help
|
||||
|
||||
micro has a built-in help system which you can access by pressing <kbd>Ctrl-e</kbd> and typing `help`. Additionally, you can
|
||||
Micro has a built-in help system which you can access by pressing `CtrlE` and typing `help`. Additionally, you can
|
||||
view the help files here:
|
||||
|
||||
- [main help](https://github.com/micro-editor/micro/tree/master/runtime/help/help.md)
|
||||
- [keybindings](https://github.com/micro-editor/micro/tree/master/runtime/help/keybindings.md)
|
||||
- [commands](https://github.com/micro-editor/micro/tree/master/runtime/help/commands.md)
|
||||
- [colors](https://github.com/micro-editor/micro/tree/master/runtime/help/colors.md)
|
||||
- [options](https://github.com/micro-editor/micro/tree/master/runtime/help/options.md)
|
||||
- [plugins](https://github.com/micro-editor/micro/tree/master/runtime/help/plugins.md)
|
||||
* [main help](https://github.com/zyedidia/micro/tree/master/runtime/help/help.md)
|
||||
* [keybindings](https://github.com/zyedidia/micro/tree/master/runtime/help/keybindings.md)
|
||||
* [commands](https://github.com/zyedidia/micro/tree/master/runtime/help/commands.md)
|
||||
* [colors](https://github.com/zyedidia/micro/tree/master/runtime/help/colors.md)
|
||||
* [options](https://github.com/zyedidia/micro/tree/master/runtime/help/options.md)
|
||||
* [plugins](https://github.com/zyedidia/micro/tree/master/runtime/help/plugins.md)
|
||||
|
||||
I also recommend reading the [tutorial](https://github.com/micro-editor/micro/tree/master/runtime/help/tutorial.md) for
|
||||
I also recommend reading the [tutorial](https://github.com/zyedidia/micro/tree/master/runtime/help/tutorial.md) for
|
||||
a brief introduction to the more powerful configuration features micro offers.
|
||||
|
||||
There is also an unofficial Discord, which you can join at https://discord.gg/nhWR6armnR.
|
||||
|
||||
## Contributing
|
||||
# Contributing
|
||||
|
||||
If you find any bugs, please report them! I am also happy to accept pull requests from anyone.
|
||||
|
||||
You can use the [GitHub issue tracker](https://github.com/micro-editor/micro/issues)
|
||||
to report bugs, ask questions, or suggest new features.
|
||||
You can use the Github issue tracker to report bugs, ask questions, or suggest new features.
|
||||
|
||||
For a more informal setting to discuss the editor, you can join the [Gitter chat](https://gitter.im/zyedidia/micro) or the [Discord](https://discord.gg/nhWR6armnR). You can also use the [Discussions](https://github.com/micro-editor/micro/discussions) section on Github for a forum-like setting or for Q&A.
|
||||
|
||||
Sometimes I am unresponsive, and I apologize! If that happens, please ping me.
|
||||
For a more informal setting to discuss the editor, you can join the [Gitter chat](https://gitter.im/zyedidia/micro).
|
||||
|
||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 304.70001 103.2"
|
||||
enable-background="new 0 0 960 560"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="micro-logo-drop.svg"
|
||||
width="304.70001"
|
||||
height="103.2"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||
id="metadata21"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs19"><filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Blur"
|
||||
id="filter1040"
|
||||
x="-0.028037383"
|
||||
y="-0.10549451"
|
||||
width="1.0560748"
|
||||
height="1.210989"><feGaussianBlur
|
||||
stdDeviation="2 2"
|
||||
result="blur"
|
||||
id="feGaussianBlur1038" /></filter></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1080"
|
||||
id="namedview17"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="13.204388"
|
||||
inkscape:cx="71.832181"
|
||||
inkscape:cy="63.956011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" /><g
|
||||
id="g838"
|
||||
transform="translate(-178,-172.8)"
|
||||
style="fill:#ffffff;fill-opacity:1;filter:url(#filter1040)"><path
|
||||
d="m 306.8,213.8 v -2.6 c 1.6,-0.1 2.9,-0.4 4.1,-0.8 1.2,-0.4 2.5,-1 4,-1.8 h 2.3 v 5.2 c 2.4,-1.9 4.2,-3.1 5.5,-3.8 2,-1 4,-1.5 5.8,-1.5 1.3,0 2.5,0.2 3.7,0.7 1.2,0.5 2.2,1 2.9,1.7 0.7,0.7 1.4,1.6 1.9,2.8 2.2,-1.9 4.2,-3.3 6,-4 1.9,-0.8 3.7,-1.2 5.6,-1.2 1.8,0 3.4,0.4 4.8,1.1 1.4,0.8 2.4,1.7 3,2.8 0.6,1.1 0.9,2.8 0.9,5 v 14.4 c 0,1.5 0,2.4 0.1,2.6 0.1,0.4 0.3,0.8 0.7,1.1 0.3,0.4 0.7,0.6 1.2,0.7 0.4,0.1 1.2,0.2 2.4,0.2 h 1 v 2.6 h -15.5 v -2.6 c 1.8,0 2.9,-0.1 3.5,-0.4 0.5,-0.2 0.9,-0.6 1.2,-1.2 0.3,-0.6 0.4,-1.6 0.4,-3.2 v -13.7 c 0,-1.7 -0.2,-2.9 -0.5,-3.6 -0.3,-0.7 -0.9,-1.2 -1.7,-1.7 -0.8,-0.4 -1.8,-0.7 -3,-0.7 -1.5,0 -3,0.4 -4.6,1.2 -2.2,1.1 -3.9,2.3 -5.1,3.6 v 14.8 c 0,1.4 0.1,2.4 0.2,2.8 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.5 1.1,0.6 0.4,0.1 1.5,0.2 3.1,0.2 v 2.6 h -15.3 v -2.6 h 0.9 c 1.2,0 2.1,-0.1 2.6,-0.4 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.2,-0.5 0.3,-1.5 0.3,-2.9 v -13.2 c 0,-1.9 -0.2,-3.3 -0.5,-3.9 -0.3,-0.7 -0.9,-1.3 -1.7,-1.7 -0.8,-0.5 -1.8,-0.7 -3,-0.7 -1.3,0 -2.7,0.3 -4.1,1 -2,1 -3.9,2.2 -5.6,3.8 v 15.9 c 0,1 0.1,1.6 0.4,2.1 0.3,0.4 0.7,0.8 1.2,1.1 0.6,0.3 1.3,0.4 2.3,0.4 h 1.1 v 2.6 h -15.6 v -2.6 h 0.8 c 1.4,0 2.4,-0.1 2.8,-0.3 0.7,-0.3 1.1,-0.8 1.4,-1.5 0.2,-0.4 0.2,-1.3 0.2,-2.9 v -18.1 h -5.1 z"
|
||||
id="path828"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
d="m 366.4,213.7 v -2.6 c 1.7,-0.2 3.2,-0.5 4.3,-0.9 1.2,-0.4 2.5,-1 4,-1.7 h 2.3 v 24.9 c 0,0.9 0.1,1.5 0.4,2 0.2,0.4 0.6,0.8 1,0.9 0.4,0.2 1.3,0.3 2.4,0.3 h 1.5 v 2.6 h -15.9 v -2.6 h 1.3 c 1.4,0 2.3,-0.1 2.8,-0.4 0.5,-0.2 0.8,-0.6 1,-1.1 0.2,-0.5 0.3,-1.5 0.3,-3.2 v -18.3 h -5.4 z m 7.9,-19.2 c 1,0 1.8,0.3 2.5,1 0.7,0.7 1.1,1.5 1.1,2.5 0,1 -0.4,1.8 -1.1,2.5 -0.7,0.7 -1.6,1.1 -2.5,1.1 -1,0 -1.8,-0.4 -2.5,-1.1 -0.7,-0.7 -1.1,-1.6 -1.1,-2.5 0,-1 0.4,-1.8 1.1,-2.5 0.6,-0.6 1.5,-1 2.5,-1 z"
|
||||
id="path830"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
d="m 413.1,230.6 2,1.6 c -3.9,5.2 -8.6,7.8 -14,7.8 -4.2,0 -7.8,-1.5 -10.7,-4.5 -2.9,-3 -4.4,-6.8 -4.4,-11.3 0,-3 0.7,-5.7 2,-8.1 1.3,-2.4 3.2,-4.2 5.6,-5.6 2.4,-1.3 5.2,-2 8.3,-2 3.6,0 6.5,0.9 8.9,2.6 2.4,1.7 3.6,3.5 3.6,5.3 0,1 -0.3,1.7 -0.8,2.2 -0.5,0.5 -1.2,0.8 -1.9,0.8 -0.4,0 -0.7,-0.1 -1.1,-0.3 -0.4,-0.2 -0.7,-0.5 -1.1,-0.9 -0.2,-0.2 -0.5,-0.8 -0.9,-1.7 -0.6,-1.2 -1,-2 -1.3,-2.4 -0.6,-0.8 -1.4,-1.5 -2.4,-2 -0.9,-0.5 -2,-0.7 -3.1,-0.7 -1.8,0 -3.4,0.5 -4.9,1.5 -1.5,1 -2.7,2.4 -3.6,4.3 -0.9,1.9 -1.3,4.2 -1.3,6.8 0,4.1 1.1,7.3 3.3,9.7 1.9,2.1 4.1,3.1 6.7,3.1 1.2,0 2.4,-0.2 3.6,-0.6 1.2,-0.4 2.4,-1 3.5,-1.8 0.9,-0.6 2.2,-1.8 4,-3.8 z"
|
||||
id="path832"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
d="m 418.7,213.7 v -2.6 c 1.5,-0.1 2.8,-0.4 4,-0.8 1.2,-0.4 2.5,-1 4,-1.9 h 2.3 v 5.9 c 1.5,-1.8 3.2,-3.2 5.1,-4.3 1.9,-1.1 3.7,-1.6 5.2,-1.6 1.5,0 2.7,0.4 3.6,1.1 0.9,0.7 1.3,1.6 1.3,2.6 0,0.7 -0.3,1.4 -0.9,2 -0.6,0.6 -1.3,0.9 -2.1,0.9 -0.4,0 -0.7,-0.1 -1,-0.2 -0.3,-0.1 -0.7,-0.3 -1.2,-0.7 -1.1,-0.7 -2.1,-1.1 -2.9,-1.1 -1,0 -2.2,0.4 -3.4,1.3 -1.6,1.1 -2.8,2.2 -3.7,3.3 V 232 c 0,1.2 0.1,2.1 0.2,2.5 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.6 1.1,0.7 0.5,0.1 1.3,0.2 2.4,0.2 h 1 v 2.6 h -16 v -2.6 h 1.3 c 1.3,0 2.1,-0.1 2.5,-0.3 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.3,-0.5 0.4,-1.5 0.4,-3 v -18.3 h -5.1 z"
|
||||
id="path834"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1" /><path
|
||||
d="m 462.8,208.5 c 3,0 5.7,0.6 7.9,1.9 2.2,1.3 4,3.1 5.3,5.5 1.3,2.4 1.9,5.2 1.9,8.3 0,3.1 -0.7,5.9 -2,8.3 -1.3,2.4 -3.1,4.3 -5.4,5.5 -2.3,1.3 -5,1.9 -8.1,1.9 -5,0 -8.8,-1.6 -11.3,-4.7 -2.5,-3.1 -3.8,-6.8 -3.8,-11 0,-3.1 0.7,-5.8 2,-8.2 1.3,-2.4 3.1,-4.2 5.5,-5.6 2.4,-1.2 5.1,-1.9 8,-1.9 z m -0.2,3 c -2.4,0 -4.4,0.9 -6,2.8 -2.1,2.3 -3.1,5.7 -3.1,10.1 0,4.3 0.9,7.5 2.6,9.7 1.6,2 3.8,3 6.5,3 1.8,0 3.3,-0.5 4.7,-1.4 1.4,-0.9 2.5,-2.4 3.3,-4.5 0.8,-2 1.3,-4.4 1.3,-7.2 0,-2.7 -0.5,-5.1 -1.4,-7.2 -0.7,-1.7 -1.8,-3 -3.2,-4 -1.3,-0.8 -2.9,-1.3 -4.7,-1.3 z"
|
||||
id="path836"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#ffffff;fill-opacity:1" /></g><g
|
||||
id="g3"
|
||||
transform="translate(-178,-172.8)"><path
|
||||
d="m 306.8,213.8 v -2.6 c 1.6,-0.1 2.9,-0.4 4.1,-0.8 1.2,-0.4 2.5,-1 4,-1.8 h 2.3 v 5.2 c 2.4,-1.9 4.2,-3.1 5.5,-3.8 2,-1 4,-1.5 5.8,-1.5 1.3,0 2.5,0.2 3.7,0.7 1.2,0.5 2.2,1 2.9,1.7 0.7,0.7 1.4,1.6 1.9,2.8 2.2,-1.9 4.2,-3.3 6,-4 1.9,-0.8 3.7,-1.2 5.6,-1.2 1.8,0 3.4,0.4 4.8,1.1 1.4,0.8 2.4,1.7 3,2.8 0.6,1.1 0.9,2.8 0.9,5 v 14.4 c 0,1.5 0,2.4 0.1,2.6 0.1,0.4 0.3,0.8 0.7,1.1 0.3,0.4 0.7,0.6 1.2,0.7 0.4,0.1 1.2,0.2 2.4,0.2 h 1 v 2.6 h -15.5 v -2.6 c 1.8,0 2.9,-0.1 3.5,-0.4 0.5,-0.2 0.9,-0.6 1.2,-1.2 0.3,-0.6 0.4,-1.6 0.4,-3.2 v -13.7 c 0,-1.7 -0.2,-2.9 -0.5,-3.6 -0.3,-0.7 -0.9,-1.2 -1.7,-1.7 -0.8,-0.4 -1.8,-0.7 -3,-0.7 -1.5,0 -3,0.4 -4.6,1.2 -2.2,1.1 -3.9,2.3 -5.1,3.6 v 14.8 c 0,1.4 0.1,2.4 0.2,2.8 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.5 1.1,0.6 0.4,0.1 1.5,0.2 3.1,0.2 v 2.6 h -15.3 v -2.6 h 0.9 c 1.2,0 2.1,-0.1 2.6,-0.4 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.2,-0.5 0.3,-1.5 0.3,-2.9 v -13.2 c 0,-1.9 -0.2,-3.3 -0.5,-3.9 -0.3,-0.7 -0.9,-1.3 -1.7,-1.7 -0.8,-0.5 -1.8,-0.7 -3,-0.7 -1.3,0 -2.7,0.3 -4.1,1 -2,1 -3.9,2.2 -5.6,3.8 v 15.9 c 0,1 0.1,1.6 0.4,2.1 0.3,0.4 0.7,0.8 1.2,1.1 0.6,0.3 1.3,0.4 2.3,0.4 h 1.1 v 2.6 h -15.6 v -2.6 h 0.8 c 1.4,0 2.4,-0.1 2.8,-0.3 0.7,-0.3 1.1,-0.8 1.4,-1.5 0.2,-0.4 0.2,-1.3 0.2,-2.9 v -18.1 h -5.1 z"
|
||||
id="path5"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 366.4,213.7 v -2.6 c 1.7,-0.2 3.2,-0.5 4.3,-0.9 1.2,-0.4 2.5,-1 4,-1.7 h 2.3 v 24.9 c 0,0.9 0.1,1.5 0.4,2 0.2,0.4 0.6,0.8 1,0.9 0.4,0.2 1.3,0.3 2.4,0.3 h 1.5 v 2.6 h -15.9 v -2.6 h 1.3 c 1.4,0 2.3,-0.1 2.8,-0.4 0.5,-0.2 0.8,-0.6 1,-1.1 0.2,-0.5 0.3,-1.5 0.3,-3.2 v -18.3 h -5.4 z m 7.9,-19.2 c 1,0 1.8,0.3 2.5,1 0.7,0.7 1.1,1.5 1.1,2.5 0,1 -0.4,1.8 -1.1,2.5 -0.7,0.7 -1.6,1.1 -2.5,1.1 -1,0 -1.8,-0.4 -2.5,-1.1 -0.7,-0.7 -1.1,-1.6 -1.1,-2.5 0,-1 0.4,-1.8 1.1,-2.5 0.6,-0.6 1.5,-1 2.5,-1 z"
|
||||
id="path7"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 413.1,230.6 2,1.6 c -3.9,5.2 -8.6,7.8 -14,7.8 -4.2,0 -7.8,-1.5 -10.7,-4.5 -2.9,-3 -4.4,-6.8 -4.4,-11.3 0,-3 0.7,-5.7 2,-8.1 1.3,-2.4 3.2,-4.2 5.6,-5.6 2.4,-1.3 5.2,-2 8.3,-2 3.6,0 6.5,0.9 8.9,2.6 2.4,1.7 3.6,3.5 3.6,5.3 0,1 -0.3,1.7 -0.8,2.2 -0.5,0.5 -1.2,0.8 -1.9,0.8 -0.4,0 -0.7,-0.1 -1.1,-0.3 -0.4,-0.2 -0.7,-0.5 -1.1,-0.9 -0.2,-0.2 -0.5,-0.8 -0.9,-1.7 -0.6,-1.2 -1,-2 -1.3,-2.4 -0.6,-0.8 -1.4,-1.5 -2.4,-2 -0.9,-0.5 -2,-0.7 -3.1,-0.7 -1.8,0 -3.4,0.5 -4.9,1.5 -1.5,1 -2.7,2.4 -3.6,4.3 -0.9,1.9 -1.3,4.2 -1.3,6.8 0,4.1 1.1,7.3 3.3,9.7 1.9,2.1 4.1,3.1 6.7,3.1 1.2,0 2.4,-0.2 3.6,-0.6 1.2,-0.4 2.4,-1 3.5,-1.8 0.9,-0.6 2.2,-1.8 4,-3.8 z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 418.7,213.7 v -2.6 c 1.5,-0.1 2.8,-0.4 4,-0.8 1.2,-0.4 2.5,-1 4,-1.9 h 2.3 v 5.9 c 1.5,-1.8 3.2,-3.2 5.1,-4.3 1.9,-1.1 3.7,-1.6 5.2,-1.6 1.5,0 2.7,0.4 3.6,1.1 0.9,0.7 1.3,1.6 1.3,2.6 0,0.7 -0.3,1.4 -0.9,2 -0.6,0.6 -1.3,0.9 -2.1,0.9 -0.4,0 -0.7,-0.1 -1,-0.2 -0.3,-0.1 -0.7,-0.3 -1.2,-0.7 -1.1,-0.7 -2.1,-1.1 -2.9,-1.1 -1,0 -2.2,0.4 -3.4,1.3 -1.6,1.1 -2.8,2.2 -3.7,3.3 V 232 c 0,1.2 0.1,2.1 0.2,2.5 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.6 1.1,0.7 0.5,0.1 1.3,0.2 2.4,0.2 h 1 v 2.6 h -16 v -2.6 h 1.3 c 1.3,0 2.1,-0.1 2.5,-0.3 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.3,-0.5 0.4,-1.5 0.4,-3 v -18.3 h -5.1 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 462.8,208.5 c 3,0 5.7,0.6 7.9,1.9 2.2,1.3 4,3.1 5.3,5.5 1.3,2.4 1.9,5.2 1.9,8.3 0,3.1 -0.7,5.9 -2,8.3 -1.3,2.4 -3.1,4.3 -5.4,5.5 -2.3,1.3 -5,1.9 -8.1,1.9 -5,0 -8.8,-1.6 -11.3,-4.7 -2.5,-3.1 -3.8,-6.8 -3.8,-11 0,-3.1 0.7,-5.8 2,-8.2 1.3,-2.4 3.1,-4.2 5.5,-5.6 2.4,-1.2 5.1,-1.9 8,-1.9 z m -0.2,3 c -2.4,0 -4.4,0.9 -6,2.8 -2.1,2.3 -3.1,5.7 -3.1,10.1 0,4.3 0.9,7.5 2.6,9.7 1.6,2 3.8,3 6.5,3 1.8,0 3.3,-0.5 4.7,-1.4 1.4,-0.9 2.5,-2.4 3.3,-4.5 0.8,-2 1.3,-4.4 1.3,-7.2 0,-2.7 -0.5,-5.1 -1.4,-7.2 -0.7,-1.7 -1.8,-3 -3.2,-4 -1.3,-0.8 -2.9,-1.3 -4.7,-1.3 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0" /></g><path
|
||||
d="M 51.6,0 C 23.1,0 0,23.1 0,51.6 c 0,28.5 23.1,51.6 51.6,51.6 28.5,0 51.6,-23.1 51.6,-51.6 C 103.2,23.1 80.1,0 51.6,0 Z m 24.5,58.6 c -0.5,2 -1.3,3.6 -2.4,4.9 -1,1.3 -2,2.1 -3.1,2.5 -1.1,0.4 -2.2,0.6 -3.4,0.6 -1.2,0 -2.2,-0.2 -3,-0.7 C 63.4,65.5 62.8,64.8 62.3,64 61.8,63.2 61.5,62.2 61.3,61.1 61.1,60 61,58.8 61,57.5 c 0,-0.5 0,-1 0.1,-1.7 0.1,-0.7 0.2,-1.6 0.3,-1.6 h -0.2 c -1.6,4 -3.8,6.9 -6.6,9.2 -2.8,2.3 -5.9,3.4 -9.3,3.4 -2.3,0 -4.2,-0.9 -5.5,-2.6 -1.4,-1.7 -2.1,-4.3 -2.1,-7.7 0,-0.5 0,-1 0.1,-1.6 0.1,-0.5 0.1,-0.7 0.2,-1.7 h -0.7 c -0.9,2 -1.7,4.8 -2.3,7.3 -0.6,2.5 -1.1,4.8 -1.4,6.9 -0.4,2.1 -0.6,4 -0.8,5.6 -0.2,1.6 -0.3,2.7 -0.4,3.3 0.1,0.5 0.2,1 0.3,1.6 0.2,0.6 0.3,1.2 0.5,1.7 0.2,0.5 0.3,1.1 0.4,1.6 0.1,0.5 0.2,0.9 0.2,1.2 0,1.4 -0.3,2.5 -0.9,3.2 -0.6,0.7 -1.3,1.1 -2,1.1 -0.9,0 -1.7,-0.3 -2.3,-0.8 -0.7,-0.6 -1,-1.5 -1,-2.7 0,-1.7 0.3,-3.9 0.9,-6.5 0.6,-2.6 1.5,-5.9 2.6,-9.8 0.6,-1.8 1.1,-3.6 1.7,-5.4 0.6,-1.8 1.1,-3.5 1.6,-5 0.5,-1.5 0.9,-2.9 1.3,-4.1 0.4,-1.2 0.6,-2.1 0.7,-2.8 0.1,-0.3 0.2,-1 0.3,-2 0.1,-1 0.2,-2.1 0.4,-3.4 0.2,-1.3 0.3,-2.7 0.5,-4.1 0.2,-1.5 0.4,-2.8 0.5,-4 0.2,-0.9 0.3,-1.9 0.5,-3 0.2,-1.1 0.5,-2.2 0.9,-3.1 0.4,-1 1,-1.8 1.7,-2.5 0.7,-0.7 1.6,-1 2.7,-1 1.2,0 2,0.4 2.4,1.1 0.4,0.7 0.6,1.6 0.5,2.6 -0.1,1 -0.2,2.1 -0.5,3.2 -0.3,1.1 -0.6,2.1 -0.9,2.9 -0.8,2.5 -1.6,4.8 -2.5,6.7 -0.9,1.9 -1.7,4 -2.4,6.2 -0.6,1.5 -0.8,2.9 -0.8,4.1 0,2.2 0.7,3.8 2,5 1.4,1.2 3,1.7 4.9,1.7 1.5,0 3,-0.5 4.4,-1.6 1.4,-1.1 2.7,-2.4 3.9,-3.9 1.2,-1.5 2.2,-3.1 3,-4.9 0.8,-1.7 1.4,-3.3 1.8,-4.6 0.1,-0.2 0.2,-0.6 0.3,-1.4 0.2,-0.8 0.3,-1.7 0.5,-2.7 0.2,-1 0.4,-2 0.6,-3.1 0.2,-1.1 0.4,-2 0.5,-2.7 0.2,-0.8 0.3,-1.6 0.5,-2.6 0.2,-1 0.5,-1.9 0.9,-2.8 0.4,-0.9 1,-1.6 1.6,-2.2 0.7,-0.6 1.5,-0.9 2.6,-0.9 1.3,0 2.1,0.4 2.6,1.1 0.4,0.7 0.6,1.6 0.6,2.6 -0.1,1 -0.2,2 -0.5,3 -0.3,1 -0.5,1.8 -0.7,2.4 -0.8,2.5 -1.6,4.7 -2.4,6.7 -0.8,2 -1.5,3.8 -2.2,5.2 -0.6,1.5 -1.1,2.6 -1.5,3.5 -0.4,0.9 -0.6,1.5 -0.6,1.8 0,2.6 0.6,4.5 1.7,5.6 1.1,1.1 2.3,1.7 3.6,1.7 2.2,0 3.9,-0.7 5.2,-2 1.3,-1.4 2.3,-3.9 2.9,-6.9 h 0.8 c 0.2,2.9 -0.1,5.3 -0.6,7.3 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2e3192" /><path
|
||||
style="fill:#ffffff;stroke-width:0.0757324"
|
||||
d="m 30.026506,86.559353 c -1.017302,-0.241662 -1.787869,-0.887419 -2.143612,-1.796406 -0.545654,-1.394246 -0.158934,-4.812615 1.126179,-9.954732 1.255925,-5.025324 2.459082,-9.096362 5.109736,-17.289458 0.344312,-1.064257 1.654133,-5.2136 1.888607,-5.982859 0.296596,-0.97307 0.598551,-2.708021 0.79743,-4.581811 0.108312,-1.020494 0.246431,-2.186451 0.306932,-2.591018 0.0605,-0.404565 0.178758,-1.341754 0.262796,-2.082641 0.224837,-1.982189 0.649291,-5.218012 0.916787,-6.98913 0.444542,-2.943359 0.753682,-4.198397 1.354756,-5.499991 0.686842,-1.487323 1.771061,-2.655188 2.805126,-3.021538 0.542395,-0.19216 1.381388,-0.270583 1.982594,-0.185316 1.252526,0.17764 1.883508,0.754167 2.211742,2.020866 0.313761,1.21084 -0.05565,3.930951 -0.877141,6.458782 -1.290698,3.971623 -2.036395,5.990995 -2.986916,8.088674 -1.185138,2.61545 -2.712212,6.873258 -2.939609,8.196258 -0.49042,2.853282 0.04972,5.146283 1.578225,6.6999 0.913915,0.928929 2.023939,1.521458 3.413442,1.82209 0.903748,0.195534 2.608483,0.179674 3.407958,-0.03171 1.383427,-0.365777 2.763884,-1.250325 4.377299,-2.804821 3.163126,-3.047616 5.113532,-6.222841 6.797438,-11.066108 0.353971,-1.018094 0.493359,-1.574562 0.749316,-2.991429 0.271014,-1.500218 1.040858,-5.574621 1.51657,-8.026458 0.08082,-0.416528 0.218253,-1.149239 0.305416,-1.628246 0.472088,-2.594388 1.148516,-4.178722 2.330295,-5.458032 0.763841,-0.826879 1.674493,-1.206419 2.894632,-1.206419 1.24359,0 2.138991,0.401576 2.574266,1.154526 0.974305,1.685378 0.683954,4.053139 -1.163626,9.489195 -0.954432,2.808181 -2.572717,6.998752 -3.493593,9.046702 -0.971745,2.161077 -2.201912,5.041664 -2.441809,5.717796 l -0.268706,0.757324 0.09021,1.120423 c 0.212423,2.638199 0.889316,4.086035 2.469149,5.281365 0.932959,0.705895 1.786459,0.982601 3.026274,0.981126 2.426542,-0.0029 4.480731,-1.028876 5.685658,-2.839769 0.811784,-1.220036 1.58443,-3.158397 2.044887,-5.130071 l 0.207813,-0.889855 h 0.356374 0.356373 l 0.04799,0.892492 c 0.0554,1.030319 -0.04881,3.015268 -0.219241,4.175846 -0.345822,2.354993 -1.040859,4.427262 -1.983165,5.91286 -0.701565,1.106055 -1.958204,2.491062 -2.717404,2.994989 -1.555814,1.032691 -4.187858,1.499135 -6.161832,1.091984 -0.603718,-0.124523 -1.72865,-0.689523 -2.178956,-1.094387 -1.477985,-1.328835 -2.187139,-3.341642 -2.360358,-6.699454 -0.08196,-1.588814 0.0522,-3.504923 0.298559,-4.263967 0.05681,-0.175039 0.04587,-0.208265 -0.06857,-0.208265 -0.09667,0 -0.197671,0.148268 -0.348229,0.511194 -0.711765,1.715746 -1.965261,3.867832 -3.142896,5.395934 -0.680786,0.883388 -2.612844,2.822501 -3.483678,3.496397 -2.517073,1.947843 -5.073167,2.951502 -8.060525,3.164993 -1.592379,0.1138 -2.868371,-0.07567 -4.016971,-0.596469 -1.69649,-0.769225 -3.109446,-2.469115 -3.819014,-4.594555 -0.614034,-1.839276 -0.863382,-4.754214 -0.580679,-6.788275 0.05951,-0.428202 0.126068,-0.957467 0.147897,-1.176145 l 0.03969,-0.397595 H 37.651633 37.254872 L 36.96284,53.90253 c -0.705326,1.783387 -1.458627,4.293583 -2.085205,6.948448 -1.027173,4.352223 -1.56307,7.486558 -2.197428,12.852248 -0.310323,2.624858 -0.310577,2.629265 -0.189513,3.294359 0.13956,0.766706 0.417018,1.85334 0.68249,2.672894 0.306093,0.944956 0.565598,2.296449 0.565598,2.945615 0,1.819491 -0.751236,3.258298 -2.006909,3.84374 -0.402074,0.187462 -1.15114,0.231172 -1.705369,0.09951 z"
|
||||
id="path218" /></svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 103.2 103.2"
|
||||
enable-background="new 0 0 960 560"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="micro-logo-mark.svg"
|
||||
width="103.2"
|
||||
height="103.2"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||
id="metadata9"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs7" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1080"
|
||||
id="namedview5"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="5.405335"
|
||||
inkscape:cx="75.573484"
|
||||
inkscape:cy="51.153166"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" /><path
|
||||
d="M 51.6,0 C 23.1,0 0,23.1 0,51.6 c 0,28.5 23.1,51.6 51.6,51.6 28.5,0 51.6,-23.1 51.6,-51.6 C 103.2,23.1 80.1,0 51.6,0 Z m 24.5,58.6 c -0.5,2 -1.3,3.6 -2.4,4.9 -1,1.3 -2,2.1 -3.1,2.5 -1.1,0.4 -2.2,0.6 -3.4,0.6 -1.2,0 -2.2,-0.2 -3,-0.7 C 63.4,65.5 62.8,64.8 62.3,64 61.8,63.2 61.5,62.2 61.3,61.1 61.1,60 61,58.8 61,57.5 c 0,-0.5 0,-1 0.1,-1.7 0.1,-0.7 0.2,-1.6 0.3,-1.6 l -0.2,0 c -1.6,4 -3.8,6.9 -6.6,9.2 -2.8,2.3 -5.9,3.4 -9.3,3.4 -2.3,0 -4.2,-0.9 -5.5,-2.6 -1.4,-1.7 -2.1,-4.3 -2.1,-7.7 0,-0.5 0,-1 0.1,-1.6 0.1,-0.5 0.1,-0.7 0.2,-1.7 l -0.7,0 c -0.9,2 -1.7,4.8 -2.3,7.3 -0.6,2.5 -1.1,4.8 -1.4,6.9 -0.4,2.1 -0.6,4 -0.8,5.6 -0.2,1.6 -0.3,2.7 -0.4,3.3 0.1,0.5 0.2,1 0.3,1.6 0.2,0.6 0.3,1.2 0.5,1.7 0.2,0.5 0.3,1.1 0.4,1.6 0.1,0.5 0.2,0.9 0.2,1.2 0,1.4 -0.3,2.5 -0.9,3.2 -0.6,0.7 -1.3,1.1 -2,1.1 -0.9,0 -1.7,-0.3 -2.3,-0.8 -0.7,-0.6 -1,-1.5 -1,-2.7 0,-1.7 0.3,-3.9 0.9,-6.5 0.6,-2.6 1.5,-5.9 2.6,-9.8 0.6,-1.8 1.1,-3.6 1.7,-5.4 0.6,-1.8 1.1,-3.5 1.6,-5 0.5,-1.5 0.9,-2.9 1.3,-4.1 0.4,-1.2 0.6,-2.1 0.7,-2.8 0.1,-0.3 0.2,-1 0.3,-2 0.1,-1 0.2,-2.1 0.4,-3.4 0.2,-1.3 0.3,-2.7 0.5,-4.1 0.2,-1.5 0.4,-2.8 0.5,-4 0.2,-0.9 0.3,-1.9 0.5,-3 0.2,-1.1 0.5,-2.2 0.9,-3.1 0.4,-1 1,-1.8 1.7,-2.5 0.7,-0.7 1.6,-1 2.7,-1 1.2,0 2,0.4 2.4,1.1 0.4,0.7 0.6,1.6 0.5,2.6 -0.1,1 -0.2,2.1 -0.5,3.2 -0.3,1.1 -0.6,2.1 -0.9,2.9 -0.8,2.5 -1.6,4.8 -2.5,6.7 -0.9,1.9 -1.7,4 -2.4,6.2 -0.6,1.5 -0.8,2.9 -0.8,4.1 0,2.2 0.7,3.8 2,5 1.4,1.2 3,1.7 4.9,1.7 1.5,0 3,-0.5 4.4,-1.6 1.4,-1.1 2.7,-2.4 3.9,-3.9 1.2,-1.5 2.2,-3.1 3,-4.9 0.8,-1.7 1.4,-3.3 1.8,-4.6 0.1,-0.2 0.2,-0.6 0.3,-1.4 0.2,-0.8 0.3,-1.7 0.5,-2.7 0.2,-1 0.4,-2 0.6,-3.1 0.2,-1.1 0.4,-2 0.5,-2.7 0.2,-0.8 0.3,-1.6 0.5,-2.6 0.2,-1 0.5,-1.9 0.9,-2.8 0.4,-0.9 1,-1.6 1.6,-2.2 0.7,-0.6 1.5,-0.9 2.6,-0.9 1.3,0 2.1,0.4 2.6,1.1 0.4,0.7 0.6,1.6 0.6,2.6 -0.1,1 -0.2,2 -0.5,3 -0.3,1 -0.5,1.8 -0.7,2.4 -0.8,2.5 -1.6,4.7 -2.4,6.7 -0.8,2 -1.5,3.8 -2.2,5.2 -0.6,1.5 -1.1,2.6 -1.5,3.5 -0.4,0.9 -0.6,1.5 -0.6,1.8 0,2.6 0.6,4.5 1.7,5.6 1.1,1.1 2.3,1.7 3.6,1.7 2.2,0 3.9,-0.7 5.2,-2 1.3,-1.4 2.3,-3.9 2.9,-6.9 l 0.8,0 c 0.2,2.9 -0.1,5.3 -0.6,7.3 z"
|
||||
id="path3"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2e3192" /><path
|
||||
style="fill:#ffffff;stroke-width:0.185002"
|
||||
d="m 29.320064,86.164872 c -1.277771,-0.647664 -1.573829,-1.327981 -1.549788,-3.561297 0.04016,-3.730697 1.622887,-10.030031 5.903272,-23.495306 2.770635,-8.715885 2.799071,-8.822813 3.148729,-11.840154 0.585284,-5.050637 1.565844,-12.45598 1.8369,-13.872547 0.43516,-2.274196 0.976755,-3.690519 1.880879,-4.918684 0.974445,-1.323691 1.896478,-1.826405 3.360953,-1.832474 3.009215,-0.01247 3.55713,2.574946 1.786201,8.434969 -0.742771,2.45784 -2.2493,6.487571 -3.407575,9.114735 -0.420971,0.954834 -1.151241,2.827983 -1.622823,4.162554 -0.839682,2.376289 -0.857669,2.47434 -0.869358,4.739023 -0.01095,2.122185 0.02796,2.3976 0.472736,3.346042 0.91751,1.956495 2.602228,3.131322 5.078862,3.541714 2.587757,0.428804 4.551892,-0.347899 7.187533,-2.842264 2.232774,-2.113092 3.746907,-4.117682 4.998184,-6.617188 1.816108,-3.627792 2.213624,-4.978174 3.527565,-11.983266 0.66466,-3.543546 1.376157,-6.951356 1.581104,-7.57291 0.970636,-2.943689 2.922262,-4.567831 5.096985,-4.241711 1.740397,0.260989 2.500104,1.361773 2.494406,3.614287 -0.0068,2.696563 -2.48184,9.966491 -6.424307,18.870246 l -1.269708,2.867537 0.02005,1.757523 c 0.01504,1.318294 0.119434,2.015481 0.417735,2.789716 1.028756,2.67011 3.517063,4.054736 6.342356,3.529224 3.19144,-0.593617 4.98902,-2.612828 6.217715,-6.984325 0.403553,-1.435775 0.552101,-1.739647 0.850428,-1.739647 0.34646,0 0.356492,0.101757 0.241656,2.451282 -0.238951,4.888854 -1.330826,7.853563 -3.80789,10.339358 -1.255532,1.259957 -1.547319,1.456015 -2.694109,1.81022 -1.395674,0.431082 -3.784736,0.537505 -4.865716,0.216749 -1.759682,-0.522141 -3.031085,-2.027386 -3.686869,-4.364972 -0.336042,-1.197843 -0.516218,-5.455318 -0.283812,-6.706338 0.266094,-1.432359 -0.105859,-1.235144 -0.879069,0.466093 -1.724383,3.794037 -4.750586,7.236231 -8.063683,9.172148 -2.368072,1.383716 -5.903865,2.143782 -8.230062,1.769159 -2.672688,-0.430424 -4.588062,-2.213422 -5.66376,-5.272324 -0.491128,-1.396592 -0.514658,-1.618704 -0.512739,-4.840059 0.0018,-3.093063 -0.02515,-3.376294 -0.321772,-3.376294 -0.414677,0 -0.706335,0.582138 -1.434591,2.863386 -1.443227,4.52088 -2.73082,10.895957 -3.516703,17.411762 l -0.381426,3.162426 0.469219,1.740138 c 0.927877,3.441104 1.066474,4.326417 0.841521,5.375336 -0.537458,2.506081 -2.272098,3.528416 -4.269226,2.516133 z"
|
||||
id="path210" /></svg>
|
||||
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -1,76 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 299.89999 103.2"
|
||||
enable-background="new 0 0 960 560"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="micro-logo.svg"
|
||||
width="299.89999"
|
||||
height="103.2"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||
id="metadata21"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs19" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1080"
|
||||
id="namedview17"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="16.645603"
|
||||
inkscape:cx="65.092264"
|
||||
inkscape:cy="49.051992"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" /><g
|
||||
id="g3"
|
||||
transform="translate(-178,-172.8)"><path
|
||||
d="m 306.8,213.8 0,-2.6 c 1.6,-0.1 2.9,-0.4 4.1,-0.8 1.2,-0.4 2.5,-1 4,-1.8 l 2.3,0 0,5.2 c 2.4,-1.9 4.2,-3.1 5.5,-3.8 2,-1 4,-1.5 5.8,-1.5 1.3,0 2.5,0.2 3.7,0.7 1.2,0.5 2.2,1 2.9,1.7 0.7,0.7 1.4,1.6 1.9,2.8 2.2,-1.9 4.2,-3.3 6,-4 1.9,-0.8 3.7,-1.2 5.6,-1.2 1.8,0 3.4,0.4 4.8,1.1 1.4,0.8 2.4,1.7 3,2.8 0.6,1.1 0.9,2.8 0.9,5 l 0,14.4 c 0,1.5 0,2.4 0.1,2.6 0.1,0.4 0.3,0.8 0.7,1.1 0.3,0.4 0.7,0.6 1.2,0.7 0.4,0.1 1.2,0.2 2.4,0.2 l 1,0 0,2.6 -15.5,0 0,-2.6 c 1.8,0 2.9,-0.1 3.5,-0.4 0.5,-0.2 0.9,-0.6 1.2,-1.2 0.3,-0.6 0.4,-1.6 0.4,-3.2 l 0,-13.7 c 0,-1.7 -0.2,-2.9 -0.5,-3.6 -0.3,-0.7 -0.9,-1.2 -1.7,-1.7 -0.8,-0.4 -1.8,-0.7 -3,-0.7 -1.5,0 -3,0.4 -4.6,1.2 -2.2,1.1 -3.9,2.3 -5.1,3.6 l 0,14.8 c 0,1.4 0.1,2.4 0.2,2.8 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.5 1.1,0.6 0.4,0.1 1.5,0.2 3.1,0.2 l 0,2.6 -15.3,0 0,-2.6 0.9,0 c 1.2,0 2.1,-0.1 2.6,-0.4 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.2,-0.5 0.3,-1.5 0.3,-2.9 l 0,-13.2 c 0,-1.9 -0.2,-3.3 -0.5,-3.9 -0.3,-0.7 -0.9,-1.3 -1.7,-1.7 -0.8,-0.5 -1.8,-0.7 -3,-0.7 -1.3,0 -2.7,0.3 -4.1,1 -2,1 -3.9,2.2 -5.6,3.8 l 0,15.9 c 0,1 0.1,1.6 0.4,2.1 0.3,0.4 0.7,0.8 1.2,1.1 0.6,0.3 1.3,0.4 2.3,0.4 l 1.1,0 0,2.6 -15.6,0 0,-2.6 0.8,0 c 1.4,0 2.4,-0.1 2.8,-0.3 0.7,-0.3 1.1,-0.8 1.4,-1.5 0.2,-0.4 0.2,-1.3 0.2,-2.9 l 0,-18.1 -5.1,0 z"
|
||||
id="path5"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 366.4,213.7 0,-2.6 c 1.7,-0.2 3.2,-0.5 4.3,-0.9 1.2,-0.4 2.5,-1 4,-1.7 l 2.3,0 0,24.9 c 0,0.9 0.1,1.5 0.4,2 0.2,0.4 0.6,0.8 1,0.9 0.4,0.2 1.3,0.3 2.4,0.3 l 1.5,0 0,2.6 -15.9,0 0,-2.6 1.3,0 c 1.4,0 2.3,-0.1 2.8,-0.4 0.5,-0.2 0.8,-0.6 1,-1.1 0.2,-0.5 0.3,-1.5 0.3,-3.2 l 0,-18.3 -5.4,0 z m 7.9,-19.2 c 1,0 1.8,0.3 2.5,1 0.7,0.7 1.1,1.5 1.1,2.5 0,1 -0.4,1.8 -1.1,2.5 -0.7,0.7 -1.6,1.1 -2.5,1.1 -1,0 -1.8,-0.4 -2.5,-1.1 -0.7,-0.7 -1.1,-1.6 -1.1,-2.5 0,-1 0.4,-1.8 1.1,-2.5 0.6,-0.6 1.5,-1 2.5,-1 z"
|
||||
id="path7"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 413.1,230.6 2,1.6 c -3.9,5.2 -8.6,7.8 -14,7.8 -4.2,0 -7.8,-1.5 -10.7,-4.5 -2.9,-3 -4.4,-6.8 -4.4,-11.3 0,-3 0.7,-5.7 2,-8.1 1.3,-2.4 3.2,-4.2 5.6,-5.6 2.4,-1.3 5.2,-2 8.3,-2 3.6,0 6.5,0.9 8.9,2.6 2.4,1.7 3.6,3.5 3.6,5.3 0,1 -0.3,1.7 -0.8,2.2 -0.5,0.5 -1.2,0.8 -1.9,0.8 -0.4,0 -0.7,-0.1 -1.1,-0.3 -0.4,-0.2 -0.7,-0.5 -1.1,-0.9 -0.2,-0.2 -0.5,-0.8 -0.9,-1.7 -0.6,-1.2 -1,-2 -1.3,-2.4 -0.6,-0.8 -1.4,-1.5 -2.4,-2 -0.9,-0.5 -2,-0.7 -3.1,-0.7 -1.8,0 -3.4,0.5 -4.9,1.5 -1.5,1 -2.7,2.4 -3.6,4.3 -0.9,1.9 -1.3,4.2 -1.3,6.8 0,4.1 1.1,7.3 3.3,9.7 1.9,2.1 4.1,3.1 6.7,3.1 1.2,0 2.4,-0.2 3.6,-0.6 1.2,-0.4 2.4,-1 3.5,-1.8 0.9,-0.6 2.2,-1.8 4,-3.8 z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 418.7,213.7 0,-2.6 c 1.5,-0.1 2.8,-0.4 4,-0.8 1.2,-0.4 2.5,-1 4,-1.9 l 2.3,0 0,5.9 c 1.5,-1.8 3.2,-3.2 5.1,-4.3 1.9,-1.1 3.7,-1.6 5.2,-1.6 1.5,0 2.7,0.4 3.6,1.1 0.9,0.7 1.3,1.6 1.3,2.6 0,0.7 -0.3,1.4 -0.9,2 -0.6,0.6 -1.3,0.9 -2.1,0.9 -0.4,0 -0.7,-0.1 -1,-0.2 -0.3,-0.1 -0.7,-0.3 -1.2,-0.7 -1.1,-0.7 -2.1,-1.1 -2.9,-1.1 -1,0 -2.2,0.4 -3.4,1.3 -1.6,1.1 -2.8,2.2 -3.7,3.3 l 0,14.4 c 0,1.2 0.1,2.1 0.2,2.5 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.6 1.1,0.7 0.5,0.1 1.3,0.2 2.4,0.2 l 1,0 0,2.6 -16,0 0,-2.6 1.3,0 c 1.3,0 2.1,-0.1 2.5,-0.3 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.3,-0.5 0.4,-1.5 0.4,-3 l 0,-18.3 -5.1,0 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 462.8,208.5 c 3,0 5.7,0.6 7.9,1.9 2.2,1.3 4,3.1 5.3,5.5 1.3,2.4 1.9,5.2 1.9,8.3 0,3.1 -0.7,5.9 -2,8.3 -1.3,2.4 -3.1,4.3 -5.4,5.5 -2.3,1.3 -5,1.9 -8.1,1.9 -5,0 -8.8,-1.6 -11.3,-4.7 -2.5,-3.1 -3.8,-6.8 -3.8,-11 0,-3.1 0.7,-5.8 2,-8.2 1.3,-2.4 3.1,-4.2 5.5,-5.6 2.4,-1.2 5.1,-1.9 8,-1.9 z m -0.2,3 c -2.4,0 -4.4,0.9 -6,2.8 -2.1,2.3 -3.1,5.7 -3.1,10.1 0,4.3 0.9,7.5 2.6,9.7 1.6,2 3.8,3 6.5,3 1.8,0 3.3,-0.5 4.7,-1.4 1.4,-0.9 2.5,-2.4 3.3,-4.5 0.8,-2 1.3,-4.4 1.3,-7.2 0,-2.7 -0.5,-5.1 -1.4,-7.2 -0.7,-1.7 -1.8,-3 -3.2,-4 -1.3,-0.8 -2.9,-1.3 -4.7,-1.3 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0" /></g><path
|
||||
d="M 51.6,0 C 23.1,0 0,23.1 0,51.6 c 0,28.5 23.1,51.6 51.6,51.6 28.5,0 51.6,-23.1 51.6,-51.6 C 103.2,23.1 80.1,0 51.6,0 Z m 24.5,58.6 c -0.5,2 -1.3,3.6 -2.4,4.9 -1,1.3 -2,2.1 -3.1,2.5 -1.1,0.4 -2.2,0.6 -3.4,0.6 -1.2,0 -2.2,-0.2 -3,-0.7 C 63.4,65.5 62.8,64.8 62.3,64 61.8,63.2 61.5,62.2 61.3,61.1 61.1,60 61,58.8 61,57.5 c 0,-0.5 0,-1 0.1,-1.7 0.1,-0.7 0.2,-1.6 0.3,-1.6 l -0.2,0 c -1.6,4 -3.8,6.9 -6.6,9.2 -2.8,2.3 -5.9,3.4 -9.3,3.4 -2.3,0 -4.2,-0.9 -5.5,-2.6 -1.4,-1.7 -2.1,-4.3 -2.1,-7.7 0,-0.5 0,-1 0.1,-1.6 0.1,-0.5 0.1,-0.7 0.2,-1.7 l -0.7,0 c -0.9,2 -1.7,4.8 -2.3,7.3 -0.6,2.5 -1.1,4.8 -1.4,6.9 -0.4,2.1 -0.6,4 -0.8,5.6 -0.2,1.6 -0.3,2.7 -0.4,3.3 0.1,0.5 0.2,1 0.3,1.6 0.2,0.6 0.3,1.2 0.5,1.7 0.2,0.5 0.3,1.1 0.4,1.6 0.1,0.5 0.2,0.9 0.2,1.2 0,1.4 -0.3,2.5 -0.9,3.2 -0.6,0.7 -1.3,1.1 -2,1.1 -0.9,0 -1.7,-0.3 -2.3,-0.8 -0.7,-0.6 -1,-1.5 -1,-2.7 0,-1.7 0.3,-3.9 0.9,-6.5 0.6,-2.6 1.5,-5.9 2.6,-9.8 0.6,-1.8 1.1,-3.6 1.7,-5.4 0.6,-1.8 1.1,-3.5 1.6,-5 0.5,-1.5 0.9,-2.9 1.3,-4.1 0.4,-1.2 0.6,-2.1 0.7,-2.8 0.1,-0.3 0.2,-1 0.3,-2 0.1,-1 0.2,-2.1 0.4,-3.4 0.2,-1.3 0.3,-2.7 0.5,-4.1 0.2,-1.5 0.4,-2.8 0.5,-4 0.2,-0.9 0.3,-1.9 0.5,-3 0.2,-1.1 0.5,-2.2 0.9,-3.1 0.4,-1 1,-1.8 1.7,-2.5 0.7,-0.7 1.6,-1 2.7,-1 1.2,0 2,0.4 2.4,1.1 0.4,0.7 0.6,1.6 0.5,2.6 -0.1,1 -0.2,2.1 -0.5,3.2 -0.3,1.1 -0.6,2.1 -0.9,2.9 -0.8,2.5 -1.6,4.8 -2.5,6.7 -0.9,1.9 -1.7,4 -2.4,6.2 -0.6,1.5 -0.8,2.9 -0.8,4.1 0,2.2 0.7,3.8 2,5 1.4,1.2 3,1.7 4.9,1.7 1.5,0 3,-0.5 4.4,-1.6 1.4,-1.1 2.7,-2.4 3.9,-3.9 1.2,-1.5 2.2,-3.1 3,-4.9 0.8,-1.7 1.4,-3.3 1.8,-4.6 0.1,-0.2 0.2,-0.6 0.3,-1.4 0.2,-0.8 0.3,-1.7 0.5,-2.7 0.2,-1 0.4,-2 0.6,-3.1 0.2,-1.1 0.4,-2 0.5,-2.7 0.2,-0.8 0.3,-1.6 0.5,-2.6 0.2,-1 0.5,-1.9 0.9,-2.8 0.4,-0.9 1,-1.6 1.6,-2.2 0.7,-0.6 1.5,-0.9 2.6,-0.9 1.3,0 2.1,0.4 2.6,1.1 0.4,0.7 0.6,1.6 0.6,2.6 -0.1,1 -0.2,2 -0.5,3 -0.3,1 -0.5,1.8 -0.7,2.4 -0.8,2.5 -1.6,4.7 -2.4,6.7 -0.8,2 -1.5,3.8 -2.2,5.2 -0.6,1.5 -1.1,2.6 -1.5,3.5 -0.4,0.9 -0.6,1.5 -0.6,1.8 0,2.6 0.6,4.5 1.7,5.6 1.1,1.1 2.3,1.7 3.6,1.7 2.2,0 3.9,-0.7 5.2,-2 1.3,-1.4 2.3,-3.9 2.9,-6.9 l 0.8,0 c 0.2,2.9 -0.1,5.3 -0.6,7.3 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2e3192" /><path
|
||||
style="fill:#ffffff;stroke-width:0.0600759"
|
||||
d="m 30.192709,86.597991 c -0.530828,-0.09608 -1.19875,-0.411872 -1.578921,-0.746511 -0.792953,-0.697985 -1.054327,-1.680313 -0.947823,-3.562219 0.16271,-2.875042 0.852662,-6.034057 2.963728,-13.569713 0.66017,-2.356543 0.955814,-3.307037 3.762987,-12.097989 1.219825,-3.820007 1.435496,-4.505244 1.616654,-5.136492 0.306236,-1.067081 0.590331,-2.663175 0.753866,-4.235353 0.08592,-0.826044 0.236455,-2.096649 0.334514,-2.823568 0.09806,-0.726919 0.246246,-1.916422 0.329306,-2.643341 0.08306,-0.726918 0.231698,-1.902905 0.330307,-2.613302 0.09861,-0.710398 0.231242,-1.724179 0.294741,-2.252848 0.19473,-1.621264 0.604712,-4.037809 0.845956,-4.986301 0.495326,-1.947452 1.158621,-3.216325 2.26111,-4.325467 0.731983,-0.736399 1.547763,-1.051329 2.723316,-1.051329 1.344787,0 2.103359,0.409522 2.539237,1.370828 0.373167,0.823003 0.432731,1.702332 0.227502,3.358553 -0.206897,1.669687 -0.429401,2.498899 -1.62432,6.053417 -0.891865,2.653022 -1.418886,4.025585 -2.237847,5.828196 -0.890733,1.960586 -1.401439,3.281416 -2.291175,5.925621 -0.696894,2.071095 -0.858755,3.003396 -0.79649,4.587665 0.05016,1.276299 0.270881,2.168068 0.761945,3.078469 1.114561,2.066325 3.341124,3.259541 6.082361,3.259541 0.831865,0 1.52957,-0.113832 2.245267,-0.366322 1.037155,-0.365895 1.69838,-0.767468 2.829986,-1.718697 2.058613,-1.730473 4.031033,-4.098263 5.356083,-6.429706 1.132231,-1.992175 2.742129,-5.986041 2.978686,-7.389579 0.126006,-0.747618 0.37151,-2.073261 0.753923,-4.070941 0.459374,-2.399719 0.965049,-5.073707 1.26106,-6.668427 0.439666,-2.368642 0.948255,-3.731056 1.831386,-4.905927 1.000947,-1.33161 1.919678,-1.818989 3.424905,-1.816884 1.371199,0.0019 2.259901,0.453797 2.692584,1.369104 0.199937,0.42295 0.37898,1.160518 0.431897,1.779189 0.0423,0.494585 -0.08313,1.707742 -0.270194,2.613303 -0.520247,2.51845 -2.995194,9.527499 -4.836622,13.697311 -0.189691,0.429543 -0.709117,1.619046 -1.154281,2.64334 -0.445164,1.024295 -0.903857,2.078627 -1.019317,2.342962 -0.593057,1.357747 -0.644155,1.607255 -0.563046,2.7493 0.142046,2.000035 0.604952,3.420811 1.436759,4.409774 0.719848,0.85585 1.902762,1.62255 2.859809,1.853569 0.533147,0.128695 1.669602,0.128252 2.472607,-9.67e-4 1.437635,-0.231339 2.769133,-0.900566 3.72751,-1.873493 1.098243,-1.114915 2.227996,-3.662559 2.785802,-6.282105 l 0.13752,-0.645816 h 0.37414 0.37414 l 0.04419,0.94284 c 0.124949,2.666054 -0.382363,6.016009 -1.237138,8.16926 -0.848692,2.137927 -2.617365,4.354096 -4.156972,5.208738 -1.58257,0.878493 -4.420415,1.19721 -6.111929,0.68643 -0.649563,-0.196146 -1.47209,-0.685817 -1.961392,-1.167665 -1.354216,-1.333585 -1.999054,-3.254244 -2.18916,-6.52045 -0.03525,-0.60571 -0.04689,-1.38515 -0.02584,-1.732089 0.04435,-0.731258 0.257009,-2.357205 0.335205,-2.562875 0.04613,-0.121335 0.03516,-0.140427 -0.08025,-0.139702 -0.11259,7.09e-4 -0.171074,0.09313 -0.370649,0.58574 -0.571777,1.411317 -1.625409,3.288777 -2.58713,4.609988 -2.555402,3.510606 -5.935984,6.014779 -9.311242,6.897323 -1.386313,0.362485 -1.927076,0.42829 -3.514441,0.427668 -1.398071,-5.41e-4 -1.500695,-0.0084 -2.047014,-0.157216 -1.248806,-0.340101 -2.244463,-0.904197 -3.05944,-1.733346 -1.343156,-1.366511 -2.129105,-3.116872 -2.494126,-5.554581 -0.150028,-1.001927 -0.191427,-3.616227 -0.06949,-4.388291 0.05195,-0.328906 0.113311,-0.84947 0.136367,-1.156809 l 0.04192,-0.558799 -0.380315,0.01812 -0.380315,0.01812 -0.231805,0.570721 c -1.478913,3.641182 -3.072314,10.383891 -3.918324,16.580955 -0.190557,1.395837 -0.701916,5.676121 -0.706953,5.917479 -0.0093,0.446744 0.454257,2.427922 0.818884,3.499628 0.121802,0.358001 0.382754,1.549663 0.538684,2.459961 0.04595,0.268246 -0.06655,1.468043 -0.178759,1.906478 -0.165253,0.645686 -0.477741,1.20884 -0.915337,1.649588 -0.463951,0.467293 -0.819805,0.689321 -1.309045,0.816755 -0.410787,0.106995 -0.564727,0.106887 -1.159735,-7.81e-4 z"
|
||||
id="path240" /></svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 289 KiB |
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ]; then
|
||||
update-alternatives --install /usr/bin/editor editor /usr/bin/micro 40 \
|
||||
--slave /usr/share/man/man1/editor.1 editor.1 \
|
||||
/usr/share/man/man1/micro.1
|
||||
fi
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$1" != "upgrade" ]; then
|
||||
update-alternatives --remove editor /usr/bin/micro
|
||||
fi
|
||||
@@ -1,140 +0,0 @@
|
||||
.TH micro 1 "2025-09-03"
|
||||
.SH NAME
|
||||
micro \- A modern and intuitive terminal-based text editor
|
||||
.SH SYNOPSIS
|
||||
.B micro
|
||||
.RI [ OPTION ]...\&
|
||||
.RI [ FILE ]...\&
|
||||
.RI [+ LINE [: COL ]]\&
|
||||
.RI [+/ REGEX ]
|
||||
.br
|
||||
.B micro
|
||||
.RI [ OPTION ]...\&
|
||||
.RI [ FILE [: LINE [: COL ]]]...\&
|
||||
\& (only if the `parsecursor` option is enabled)
|
||||
.SH DESCRIPTION
|
||||
Micro is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the full capabilities
|
||||
of modern terminals. It comes as one single, batteries-included, static binary with no dependencies.
|
||||
|
||||
As the name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use in a pinch, but micro also aims to be
|
||||
enjoyable to use full time, whether you work in the terminal because you prefer it (like me), or because you need to (over ssh).
|
||||
|
||||
Use Ctrl-q to quit, Ctrl-s to save, and Ctrl-g to open the in-editor help menu.
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
.B \-clean
|
||||
.RS 4
|
||||
Clean the configuration directory and exit
|
||||
.RE
|
||||
.PP
|
||||
.B \-config-dir
|
||||
.I dir
|
||||
.RS 4
|
||||
Specify a custom location for the configuration directory
|
||||
.RE
|
||||
.PP
|
||||
.IR FILE : LINE [: COL ]
|
||||
(only if the `parsecursor` option is enabled)
|
||||
.br
|
||||
.IR FILE \ + LINE [: COL ]
|
||||
.RS 4
|
||||
Specify a line and column to start the cursor at when opening a buffer
|
||||
.RE
|
||||
.PP
|
||||
.RI +/ REGEX
|
||||
.RS 4
|
||||
Specify a regex to search for when opening a buffer
|
||||
.RE
|
||||
.PP
|
||||
.B \-options
|
||||
.RS 4
|
||||
Show all options help and exit
|
||||
.RE
|
||||
.PP
|
||||
.B \-debug
|
||||
.RS 4
|
||||
Enable debug mode (enables logging to ./log.txt)
|
||||
.RE
|
||||
.PP
|
||||
.B \-profile
|
||||
.RS 4
|
||||
Enable CPU profiling (writes profile info to ./micro.prof so it can be analyzed later with "go tool pprof micro.prof")
|
||||
.RE
|
||||
.PP
|
||||
.B \-version
|
||||
.RS 4
|
||||
Show the version number and information and exit
|
||||
.RE
|
||||
|
||||
Micro's plugins can be managed at the command line with the following commands.
|
||||
.RS 4
|
||||
.PP
|
||||
.B \-plugin install
|
||||
.RI [ PLUGIN ]...
|
||||
.RS 4
|
||||
Install plugin(s)
|
||||
.RE
|
||||
.PP
|
||||
.B \-plugin remove
|
||||
.RI [ PLUGIN ]...
|
||||
.RS 4
|
||||
Remove plugin(s)
|
||||
.RE
|
||||
.PP
|
||||
.B \-plugin update
|
||||
.RI [ PLUGIN ]...
|
||||
.RS 4
|
||||
Update plugin(s) (if no argument is given, updates all plugins)
|
||||
.RE
|
||||
.PP
|
||||
.B \-plugin search
|
||||
.RI [ PLUGIN ]...
|
||||
.RS 4
|
||||
Search for a plugin
|
||||
.RE
|
||||
.PP
|
||||
.B \-plugin list
|
||||
.RS 4
|
||||
List installed plugins
|
||||
.RE
|
||||
.PP
|
||||
.B \-plugin available
|
||||
.RS 4
|
||||
List available plugins
|
||||
.RE
|
||||
.RE
|
||||
|
||||
Micro's options can also be set via command line arguments for quick
|
||||
adjustments. For real configuration, please use the settings.json
|
||||
file (see 'help options').
|
||||
.RS 4
|
||||
.PP
|
||||
.BI \-< option >
|
||||
.I value
|
||||
.RS 4
|
||||
Set `option` to `value` for this session.
|
||||
.br
|
||||
For example: `micro -syntax off file.c`
|
||||
.RE
|
||||
.RE
|
||||
.PP
|
||||
Use `micro -options` to see the full list of configuration options.
|
||||
.SH CONFIGURATION
|
||||
Micro uses $MICRO_CONFIG_HOME as the configuration directory.
|
||||
If this environment variable is not set, it uses $XDG_CONFIG_HOME/micro instead.
|
||||
If that environment variable is not set, it uses ~/.config/micro as the configuration directory.
|
||||
In the documentation, we use ~/.config/micro to refer to the configuration directory
|
||||
(even if it may in fact be somewhere else if you have set either of the above environment variables).
|
||||
.SH NOTICE
|
||||
This manpage is intended only to serve as a quick guide to the invocation of
|
||||
micro and is not intended to replace the full documentation included with micro
|
||||
which can be accessed from within micro. Micro tells you what key combination to
|
||||
press to get help in the lower right.
|
||||
.SH BUGS
|
||||
A comprehensive list of bugs will not be listed in this manpage. See the Github
|
||||
page at \fBhttps://github.com/micro-editor/micro/issues\fP for a list of known bugs
|
||||
and to report any newly encountered bugs you may find. We strive to correct
|
||||
bugs as swiftly as possible.
|
||||
.SH COPYRIGHT
|
||||
Copyright \(co 2020 Zachary Yedidia, et al. MIT license.
|
||||
See \fBhttps://github.com/micro-editor/micro\fP for details.
|
||||
@@ -1,15 +0,0 @@
|
||||
[Desktop Entry]
|
||||
|
||||
Name=Micro
|
||||
GenericName=Text Editor
|
||||
Comment=Edit text files in a terminal
|
||||
|
||||
Icon=micro
|
||||
Type=Application
|
||||
Categories=Utility;TextEditor;Development;
|
||||
Keywords=text;editor;syntax;terminal;
|
||||
|
||||
Exec=micro %F
|
||||
StartupNotify=false
|
||||
Terminal=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;
|
||||
1543
cmd/micro/actions.go
Normal file
1543
cmd/micro/actions.go
Normal file
File diff suppressed because it is too large
Load Diff
148
cmd/micro/autocomplete.go
Normal file
148
cmd/micro/autocomplete.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
var pluginCompletions []func(string) []string
|
||||
|
||||
// This file is meant (for now) for autocompletion in command mode, not
|
||||
// while coding. This helps micro autocomplete commands and then filenames
|
||||
// for example with `vsplit filename`.
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(input string) (string, []string) {
|
||||
var sep string = string(os.PathSeparator)
|
||||
dirs := strings.Split(input, sep)
|
||||
var files []os.FileInfo
|
||||
var err error
|
||||
if len(dirs) > 1 {
|
||||
home, _ := homedir.Dir()
|
||||
|
||||
directories := strings.Join(dirs[:len(dirs)-1], sep)
|
||||
if strings.HasPrefix(directories, "~") {
|
||||
directories = strings.Replace(directories, "~", home, 1)
|
||||
}
|
||||
files, err = ioutil.ReadDir(directories)
|
||||
} else {
|
||||
files, err = ioutil.ReadDir(".")
|
||||
}
|
||||
var suggestions []string
|
||||
if err != nil {
|
||||
return "", suggestions
|
||||
}
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() {
|
||||
name += sep
|
||||
}
|
||||
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
if len(dirs) > 1 {
|
||||
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[0]
|
||||
} else {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
} else {
|
||||
if len(dirs) > 1 {
|
||||
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep
|
||||
}
|
||||
}
|
||||
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
// CommandComplete autocompletes commands
|
||||
func CommandComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
// HelpComplete autocompletes help topics
|
||||
func HelpComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
|
||||
for _, topic := range helpFiles {
|
||||
if strings.HasPrefix(topic, input) {
|
||||
|
||||
suggestions = append(suggestions, topic)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OptionComplete autocompletes options
|
||||
func OptionComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
localSettings := DefaultLocalSettings()
|
||||
for option := range globalSettings {
|
||||
if strings.HasPrefix(option, input) {
|
||||
suggestions = append(suggestions, option)
|
||||
}
|
||||
}
|
||||
for option := range localSettings {
|
||||
if strings.HasPrefix(option, input) && !contains(suggestions, option) {
|
||||
suggestions = append(suggestions, option)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
// MakeCompletion registeres a function from a plugin for autocomplete commands
|
||||
func MakeCompletion(function string) Completion {
|
||||
pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
|
||||
return Completion(-len(pluginCompletions))
|
||||
}
|
||||
|
||||
// PluginComplete autocompletes from plugin function
|
||||
func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) {
|
||||
idx := int(-complete) - 1
|
||||
|
||||
if len(pluginCompletions) <= idx {
|
||||
return "", nil
|
||||
}
|
||||
suggestions = pluginCompletions[idx](input)
|
||||
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
424
cmd/micro/bindings.go
Normal file
424
cmd/micro/bindings.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/yosuke-furukawa/json5/encoding/json5"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
var bindings map[Key][]func(*View, bool) bool
|
||||
var helpBinding string
|
||||
|
||||
var bindingActions = map[string]func(*View, bool) bool{
|
||||
"CursorUp": (*View).CursorUp,
|
||||
"CursorDown": (*View).CursorDown,
|
||||
"CursorPageUp": (*View).CursorPageUp,
|
||||
"CursorPageDown": (*View).CursorPageDown,
|
||||
"CursorLeft": (*View).CursorLeft,
|
||||
"CursorRight": (*View).CursorRight,
|
||||
"CursorStart": (*View).CursorStart,
|
||||
"CursorEnd": (*View).CursorEnd,
|
||||
"SelectToStart": (*View).SelectToStart,
|
||||
"SelectToEnd": (*View).SelectToEnd,
|
||||
"SelectUp": (*View).SelectUp,
|
||||
"SelectDown": (*View).SelectDown,
|
||||
"SelectLeft": (*View).SelectLeft,
|
||||
"SelectRight": (*View).SelectRight,
|
||||
"WordRight": (*View).WordRight,
|
||||
"WordLeft": (*View).WordLeft,
|
||||
"SelectWordRight": (*View).SelectWordRight,
|
||||
"SelectWordLeft": (*View).SelectWordLeft,
|
||||
"DeleteWordRight": (*View).DeleteWordRight,
|
||||
"DeleteWordLeft": (*View).DeleteWordLeft,
|
||||
"SelectToStartOfLine": (*View).SelectToStartOfLine,
|
||||
"SelectToEndOfLine": (*View).SelectToEndOfLine,
|
||||
"InsertNewline": (*View).InsertNewline,
|
||||
"InsertSpace": (*View).InsertSpace,
|
||||
"Backspace": (*View).Backspace,
|
||||
"Delete": (*View).Delete,
|
||||
"InsertTab": (*View).InsertTab,
|
||||
"Save": (*View).Save,
|
||||
"Find": (*View).Find,
|
||||
"FindNext": (*View).FindNext,
|
||||
"FindPrevious": (*View).FindPrevious,
|
||||
"Center": (*View).Center,
|
||||
"Undo": (*View).Undo,
|
||||
"Redo": (*View).Redo,
|
||||
"Copy": (*View).Copy,
|
||||
"Cut": (*View).Cut,
|
||||
"CutLine": (*View).CutLine,
|
||||
"DuplicateLine": (*View).DuplicateLine,
|
||||
"DeleteLine": (*View).DeleteLine,
|
||||
"IndentSelection": (*View).IndentSelection,
|
||||
"OutdentSelection": (*View).OutdentSelection,
|
||||
"Paste": (*View).Paste,
|
||||
"PastePrimary": (*View).PastePrimary,
|
||||
"SelectAll": (*View).SelectAll,
|
||||
"OpenFile": (*View).OpenFile,
|
||||
"Start": (*View).Start,
|
||||
"End": (*View).End,
|
||||
"PageUp": (*View).PageUp,
|
||||
"PageDown": (*View).PageDown,
|
||||
"HalfPageUp": (*View).HalfPageUp,
|
||||
"HalfPageDown": (*View).HalfPageDown,
|
||||
"StartOfLine": (*View).StartOfLine,
|
||||
"EndOfLine": (*View).EndOfLine,
|
||||
"ToggleHelp": (*View).ToggleHelp,
|
||||
"ToggleRuler": (*View).ToggleRuler,
|
||||
"JumpLine": (*View).JumpLine,
|
||||
"ClearStatus": (*View).ClearStatus,
|
||||
"ShellMode": (*View).ShellMode,
|
||||
"CommandMode": (*View).CommandMode,
|
||||
"Quit": (*View).Quit,
|
||||
"QuitAll": (*View).QuitAll,
|
||||
"AddTab": (*View).AddTab,
|
||||
"PreviousTab": (*View).PreviousTab,
|
||||
"NextTab": (*View).NextTab,
|
||||
"NextSplit": (*View).NextSplit,
|
||||
"PreviousSplit": (*View).PreviousSplit,
|
||||
"ToggleMacro": (*View).ToggleMacro,
|
||||
"PlayMacro": (*View).PlayMacro,
|
||||
|
||||
// This was changed to InsertNewline but I don't want to break backwards compatibility
|
||||
"InsertEnter": (*View).InsertNewline,
|
||||
}
|
||||
|
||||
var bindingKeys = map[string]tcell.Key{
|
||||
"Up": tcell.KeyUp,
|
||||
"Down": tcell.KeyDown,
|
||||
"Right": tcell.KeyRight,
|
||||
"Left": tcell.KeyLeft,
|
||||
"UpLeft": tcell.KeyUpLeft,
|
||||
"UpRight": tcell.KeyUpRight,
|
||||
"DownLeft": tcell.KeyDownLeft,
|
||||
"DownRight": tcell.KeyDownRight,
|
||||
"Center": tcell.KeyCenter,
|
||||
"PageUp": tcell.KeyPgUp,
|
||||
"PageDown": tcell.KeyPgDn,
|
||||
"Home": tcell.KeyHome,
|
||||
"End": tcell.KeyEnd,
|
||||
"Insert": tcell.KeyInsert,
|
||||
"Delete": tcell.KeyDelete,
|
||||
"Help": tcell.KeyHelp,
|
||||
"Exit": tcell.KeyExit,
|
||||
"Clear": tcell.KeyClear,
|
||||
"Cancel": tcell.KeyCancel,
|
||||
"Print": tcell.KeyPrint,
|
||||
"Pause": tcell.KeyPause,
|
||||
"Backtab": tcell.KeyBacktab,
|
||||
"F1": tcell.KeyF1,
|
||||
"F2": tcell.KeyF2,
|
||||
"F3": tcell.KeyF3,
|
||||
"F4": tcell.KeyF4,
|
||||
"F5": tcell.KeyF5,
|
||||
"F6": tcell.KeyF6,
|
||||
"F7": tcell.KeyF7,
|
||||
"F8": tcell.KeyF8,
|
||||
"F9": tcell.KeyF9,
|
||||
"F10": tcell.KeyF10,
|
||||
"F11": tcell.KeyF11,
|
||||
"F12": tcell.KeyF12,
|
||||
"F13": tcell.KeyF13,
|
||||
"F14": tcell.KeyF14,
|
||||
"F15": tcell.KeyF15,
|
||||
"F16": tcell.KeyF16,
|
||||
"F17": tcell.KeyF17,
|
||||
"F18": tcell.KeyF18,
|
||||
"F19": tcell.KeyF19,
|
||||
"F20": tcell.KeyF20,
|
||||
"F21": tcell.KeyF21,
|
||||
"F22": tcell.KeyF22,
|
||||
"F23": tcell.KeyF23,
|
||||
"F24": tcell.KeyF24,
|
||||
"F25": tcell.KeyF25,
|
||||
"F26": tcell.KeyF26,
|
||||
"F27": tcell.KeyF27,
|
||||
"F28": tcell.KeyF28,
|
||||
"F29": tcell.KeyF29,
|
||||
"F30": tcell.KeyF30,
|
||||
"F31": tcell.KeyF31,
|
||||
"F32": tcell.KeyF32,
|
||||
"F33": tcell.KeyF33,
|
||||
"F34": tcell.KeyF34,
|
||||
"F35": tcell.KeyF35,
|
||||
"F36": tcell.KeyF36,
|
||||
"F37": tcell.KeyF37,
|
||||
"F38": tcell.KeyF38,
|
||||
"F39": tcell.KeyF39,
|
||||
"F40": tcell.KeyF40,
|
||||
"F41": tcell.KeyF41,
|
||||
"F42": tcell.KeyF42,
|
||||
"F43": tcell.KeyF43,
|
||||
"F44": tcell.KeyF44,
|
||||
"F45": tcell.KeyF45,
|
||||
"F46": tcell.KeyF46,
|
||||
"F47": tcell.KeyF47,
|
||||
"F48": tcell.KeyF48,
|
||||
"F49": tcell.KeyF49,
|
||||
"F50": tcell.KeyF50,
|
||||
"F51": tcell.KeyF51,
|
||||
"F52": tcell.KeyF52,
|
||||
"F53": tcell.KeyF53,
|
||||
"F54": tcell.KeyF54,
|
||||
"F55": tcell.KeyF55,
|
||||
"F56": tcell.KeyF56,
|
||||
"F57": tcell.KeyF57,
|
||||
"F58": tcell.KeyF58,
|
||||
"F59": tcell.KeyF59,
|
||||
"F60": tcell.KeyF60,
|
||||
"F61": tcell.KeyF61,
|
||||
"F62": tcell.KeyF62,
|
||||
"F63": tcell.KeyF63,
|
||||
"F64": tcell.KeyF64,
|
||||
"CtrlSpace": tcell.KeyCtrlSpace,
|
||||
"CtrlA": tcell.KeyCtrlA,
|
||||
"CtrlB": tcell.KeyCtrlB,
|
||||
"CtrlC": tcell.KeyCtrlC,
|
||||
"CtrlD": tcell.KeyCtrlD,
|
||||
"CtrlE": tcell.KeyCtrlE,
|
||||
"CtrlF": tcell.KeyCtrlF,
|
||||
"CtrlG": tcell.KeyCtrlG,
|
||||
"CtrlH": tcell.KeyCtrlH,
|
||||
"CtrlI": tcell.KeyCtrlI,
|
||||
"CtrlJ": tcell.KeyCtrlJ,
|
||||
"CtrlK": tcell.KeyCtrlK,
|
||||
"CtrlL": tcell.KeyCtrlL,
|
||||
"CtrlM": tcell.KeyCtrlM,
|
||||
"CtrlN": tcell.KeyCtrlN,
|
||||
"CtrlO": tcell.KeyCtrlO,
|
||||
"CtrlP": tcell.KeyCtrlP,
|
||||
"CtrlQ": tcell.KeyCtrlQ,
|
||||
"CtrlR": tcell.KeyCtrlR,
|
||||
"CtrlS": tcell.KeyCtrlS,
|
||||
"CtrlT": tcell.KeyCtrlT,
|
||||
"CtrlU": tcell.KeyCtrlU,
|
||||
"CtrlV": tcell.KeyCtrlV,
|
||||
"CtrlW": tcell.KeyCtrlW,
|
||||
"CtrlX": tcell.KeyCtrlX,
|
||||
"CtrlY": tcell.KeyCtrlY,
|
||||
"CtrlZ": tcell.KeyCtrlZ,
|
||||
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
|
||||
"CtrlBackslash": tcell.KeyCtrlBackslash,
|
||||
"CtrlRightSq": tcell.KeyCtrlRightSq,
|
||||
"CtrlCarat": tcell.KeyCtrlCarat,
|
||||
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
|
||||
"Backspace": tcell.KeyBackspace,
|
||||
"Tab": tcell.KeyTab,
|
||||
"Esc": tcell.KeyEsc,
|
||||
"Escape": tcell.KeyEscape,
|
||||
"Enter": tcell.KeyEnter,
|
||||
"Backspace2": tcell.KeyBackspace2,
|
||||
|
||||
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
|
||||
"PgUp": tcell.KeyPgUp,
|
||||
"PgDown": tcell.KeyPgDn,
|
||||
}
|
||||
|
||||
// The Key struct holds the data for a keypress (keycode + modifiers)
|
||||
type Key struct {
|
||||
keyCode tcell.Key
|
||||
modifiers tcell.ModMask
|
||||
r rune
|
||||
}
|
||||
|
||||
// InitBindings initializes the keybindings for micro
|
||||
func InitBindings() {
|
||||
bindings = make(map[Key][]func(*View, bool) bool)
|
||||
|
||||
var parsed map[string]string
|
||||
defaults := DefaultBindings()
|
||||
|
||||
filename := configDir + "/bindings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error reading bindings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading bindings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
parseBindings(defaults)
|
||||
parseBindings(parsed)
|
||||
}
|
||||
|
||||
func parseBindings(userBindings map[string]string) {
|
||||
for k, v := range userBindings {
|
||||
BindKey(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// findKey will find binding Key 'b' using string 'k'
|
||||
func findKey(k string) (b Key, ok bool) {
|
||||
modifiers := tcell.ModNone
|
||||
|
||||
// First, we'll strip off all the modifiers in the name and add them to the
|
||||
// ModMask
|
||||
modSearch:
|
||||
for {
|
||||
switch {
|
||||
case strings.HasPrefix(k, "-"):
|
||||
// We optionally support dashes between modifiers
|
||||
k = k[1:]
|
||||
case strings.HasPrefix(k, "Ctrl"):
|
||||
k = k[4:]
|
||||
modifiers |= tcell.ModCtrl
|
||||
case strings.HasPrefix(k, "Alt"):
|
||||
k = k[3:]
|
||||
modifiers |= tcell.ModAlt
|
||||
case strings.HasPrefix(k, "Shift"):
|
||||
k = k[5:]
|
||||
modifiers |= tcell.ModShift
|
||||
default:
|
||||
break modSearch
|
||||
}
|
||||
}
|
||||
|
||||
// Control is handled specially, since some character codes in bindingKeys
|
||||
// are different when Control is depressed. We should check for Control keys
|
||||
// first.
|
||||
if modifiers&tcell.ModCtrl != 0 {
|
||||
// see if the key is in bindingKeys with the Ctrl prefix.
|
||||
if code, ok := bindingKeys["Ctrl"+k]; ok {
|
||||
// It is, we're done.
|
||||
return Key{
|
||||
keyCode: code,
|
||||
modifiers: modifiers,
|
||||
r: 0,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingKeys
|
||||
if code, ok := bindingKeys[k]; ok {
|
||||
return Key{
|
||||
keyCode: code,
|
||||
modifiers: modifiers,
|
||||
r: 0,
|
||||
}, true
|
||||
}
|
||||
|
||||
// If we were given one character, then we've got a rune.
|
||||
if len(k) == 1 {
|
||||
return Key{
|
||||
keyCode: tcell.KeyRune,
|
||||
modifiers: modifiers,
|
||||
r: rune(k[0]),
|
||||
}, true
|
||||
}
|
||||
|
||||
// We don't know what happened.
|
||||
return Key{}, false
|
||||
}
|
||||
|
||||
// findAction will find 'action' using string 'v'
|
||||
func findAction(v string) (action func(*View, bool) bool) {
|
||||
action, ok := bindingActions[v]
|
||||
if !ok {
|
||||
// If the user seems to be binding a function that doesn't exist
|
||||
// We hope that it's a lua function that exists and bind it to that
|
||||
action = LuaFunctionBinding(v)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
// BindKey takes a key and an action and binds the two together
|
||||
func BindKey(k, v string) {
|
||||
key, ok := findKey(k)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v == "ToggleHelp" {
|
||||
helpBinding = k
|
||||
}
|
||||
|
||||
actionNames := strings.Split(v, ",")
|
||||
actions := make([]func(*View, bool) bool, 0, len(actionNames))
|
||||
for _, actionName := range actionNames {
|
||||
actions = append(actions, findAction(actionName))
|
||||
}
|
||||
|
||||
bindings[key] = actions
|
||||
}
|
||||
|
||||
// DefaultBindings returns a map containing micro's default keybindings
|
||||
func DefaultBindings() map[string]string {
|
||||
return map[string]string{
|
||||
"Up": "CursorUp",
|
||||
"Down": "CursorDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"AltLeft": "WordLeft",
|
||||
"AltRight": "WordRight",
|
||||
"AltShiftRight": "SelectWordRight",
|
||||
"AltShiftLeft": "SelectWordLeft",
|
||||
"CtrlLeft": "StartOfLine",
|
||||
"CtrlRight": "EndOfLine",
|
||||
"CtrlShiftLeft": "SelectToStartOfLine",
|
||||
"CtrlShiftRight": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Enter": "InsertNewline",
|
||||
"Space": "InsertSpace",
|
||||
"Backspace": "Backspace",
|
||||
"Backspace2": "Backspace",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Alt-Backspace2": "DeleteWordLeft",
|
||||
"Tab": "IndentSelection,InsertTab",
|
||||
"Backtab": "OutdentSelection",
|
||||
"CtrlO": "OpenFile",
|
||||
"CtrlS": "Save",
|
||||
"CtrlF": "Find",
|
||||
"CtrlN": "FindNext",
|
||||
"CtrlP": "FindPrevious",
|
||||
"CtrlZ": "Undo",
|
||||
"CtrlY": "Redo",
|
||||
"CtrlC": "Copy",
|
||||
"CtrlX": "Cut",
|
||||
"CtrlK": "CutLine",
|
||||
"CtrlD": "DuplicateLine",
|
||||
"CtrlV": "Paste",
|
||||
"CtrlA": "SelectAll",
|
||||
"CtrlT": "AddTab",
|
||||
"CtrlRightSq": "PreviousTab",
|
||||
"CtrlBackslash": "NextTab",
|
||||
"Home": "StartOfLine",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"PageUp": "CursorPageUp",
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlG": "ToggleHelp",
|
||||
"CtrlR": "ToggleRuler",
|
||||
"CtrlL": "JumpLine",
|
||||
"Delete": "Delete",
|
||||
"Esc": "ClearStatus",
|
||||
"CtrlB": "ShellMode",
|
||||
"CtrlQ": "Quit",
|
||||
"CtrlE": "CommandMode",
|
||||
"CtrlW": "NextSplit",
|
||||
"CtrlU": "ToggleMacro",
|
||||
"CtrlJ": "PlayMacro",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfLine",
|
||||
"Alt-e": "EndOfLine",
|
||||
"Alt-p": "CursorUp",
|
||||
"Alt-n": "CursorDown",
|
||||
}
|
||||
}
|
||||
355
cmd/micro/buffer.go
Normal file
355
cmd/micro/buffer.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Buffer stores the text for files that are loaded into the text editor
|
||||
// It uses a rope to efficiently store the string and contains some
|
||||
// simple functions for saving and wrapper functions for modifying the rope
|
||||
type Buffer struct {
|
||||
// The eventhandler for undo/redo
|
||||
*EventHandler
|
||||
// This stores all the text in the buffer as an array of lines
|
||||
*LineArray
|
||||
|
||||
Cursor Cursor
|
||||
|
||||
// Path to the file on disk
|
||||
Path string
|
||||
// Name of the buffer on the status line
|
||||
Name string
|
||||
|
||||
// Whether or not the buffer has been modified since it was opened
|
||||
IsModified bool
|
||||
|
||||
// Stores the last modification time of the file the buffer is pointing to
|
||||
ModTime time.Time
|
||||
|
||||
NumLines int
|
||||
|
||||
// Syntax highlighting rules
|
||||
rules []SyntaxRule
|
||||
|
||||
// Buffer local settings
|
||||
Settings map[string]interface{}
|
||||
}
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Cursor
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer from `txt` with path and name `path`
|
||||
func NewBuffer(txt []byte, path string) *Buffer {
|
||||
b := new(Buffer)
|
||||
b.LineArray = NewLineArray(txt)
|
||||
|
||||
b.Settings = DefaultLocalSettings()
|
||||
for k, v := range globalSettings {
|
||||
if _, ok := b.Settings[k]; ok {
|
||||
b.Settings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = path
|
||||
b.Name = path
|
||||
|
||||
// If the file doesn't have a path to disk then we give it no name
|
||||
if path == "" {
|
||||
b.Name = "No name"
|
||||
}
|
||||
|
||||
// The last time this file was modified
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
|
||||
b.EventHandler = NewEventHandler(b)
|
||||
|
||||
b.Update()
|
||||
b.FindFileType()
|
||||
b.UpdateRules()
|
||||
|
||||
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
|
||||
os.Mkdir(configDir+"/buffers/", os.ModePerm)
|
||||
}
|
||||
|
||||
// Put the cursor at the first spot
|
||||
cursorStartX := 0
|
||||
cursorStartY := 0
|
||||
// If -startpos LINE,COL was passed, use start position LINE,COL
|
||||
if len(*flagStartPos) > 0 {
|
||||
positions := strings.Split(*flagStartPos, ",")
|
||||
if len(positions) == 2 {
|
||||
lineNum, errPos1 := strconv.Atoi(positions[0])
|
||||
colNum, errPos2 := strconv.Atoi(positions[1])
|
||||
if errPos1 == nil && errPos2 == nil {
|
||||
cursorStartX = colNum
|
||||
cursorStartY = lineNum - 1
|
||||
// Check to avoid line overflow
|
||||
if cursorStartY > b.NumLines {
|
||||
cursorStartY = b.NumLines - 1
|
||||
} else if cursorStartY < 0 {
|
||||
cursorStartY = 0
|
||||
}
|
||||
// Check to avoid column overflow
|
||||
if cursorStartX > len(b.Line(cursorStartY)) {
|
||||
cursorStartX = len(b.Line(cursorStartY))
|
||||
} else if cursorStartX < 0 {
|
||||
cursorStartX = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Cursor = Cursor{
|
||||
Loc: Loc{
|
||||
X: cursorStartX,
|
||||
Y: cursorStartY,
|
||||
},
|
||||
buf: b,
|
||||
}
|
||||
|
||||
InitLocalSettings(b)
|
||||
|
||||
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
absPath, _ := filepath.Abs(b.Path)
|
||||
file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
|
||||
if err == nil {
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
gob.Register(TextEvent{})
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.Cursor = buffer.Cursor
|
||||
b.Cursor.buf = b
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
// We should only use last time's eventhandler if the file wasn't by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.buf = b
|
||||
}
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||
// This is called when the colorscheme changes
|
||||
func (b *Buffer) UpdateRules() {
|
||||
b.rules = GetRules(b)
|
||||
}
|
||||
|
||||
// FindFileType identifies this buffer's filetype based on the extension or header
|
||||
func (b *Buffer) FindFileType() {
|
||||
b.Settings["filetype"] = FindFileType(b)
|
||||
}
|
||||
|
||||
// FileType returns the buffer's filetype
|
||||
func (b *Buffer) FileType() string {
|
||||
return b.Settings["filetype"].(string)
|
||||
}
|
||||
|
||||
// CheckModTime makes sure that the file this buffer points to hasn't been updated
|
||||
// by an external program since it was last read
|
||||
// If it has, we ask the user if they would like to reload the file
|
||||
func (b *Buffer) CheckModTime() {
|
||||
modTime, ok := GetModTime(b.Path)
|
||||
if ok {
|
||||
if modTime != b.ModTime {
|
||||
choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
|
||||
messenger.Reset()
|
||||
messenger.Clear()
|
||||
if !choice || canceled {
|
||||
// Don't load new changes -- do nothing
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
} else {
|
||||
// Load new changes
|
||||
b.ReOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer from disk
|
||||
func (b *Buffer) ReOpen() {
|
||||
data, err := ioutil.ReadFile(b.Path)
|
||||
txt := string(data)
|
||||
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
b.EventHandler.ApplyDiff(txt)
|
||||
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
b.IsModified = false
|
||||
b.Update()
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
|
||||
func (b *Buffer) Update() {
|
||||
b.NumLines = len(b.lines)
|
||||
}
|
||||
|
||||
// Save saves the buffer to its default path
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// SaveWithSudo saves the buffer to the default path with sudo
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to configDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
||||
absPath, _ := filepath.Abs(b.Path)
|
||||
file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
|
||||
if err == nil {
|
||||
enc := gob.NewEncoder(file)
|
||||
gob.Register(TextEvent{})
|
||||
err = enc.Encode(SerializedBuffer{
|
||||
b.EventHandler,
|
||||
b.Cursor,
|
||||
b.ModTime,
|
||||
})
|
||||
}
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
b.FindFileType()
|
||||
b.UpdateRules()
|
||||
b.Name = filename
|
||||
b.Path = filename
|
||||
data := []byte(b.String())
|
||||
err := ioutil.WriteFile(filename, data, 0644)
|
||||
if err == nil {
|
||||
b.IsModified = false
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
return b.Serialize()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveAsWithSudo is the same as SaveAs except it uses a neat trick
|
||||
// with tee to use sudo so the user doesn't have to reopen micro with sudo
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
b.FindFileType()
|
||||
b.UpdateRules()
|
||||
b.Name = filename
|
||||
b.Path = filename
|
||||
|
||||
// The user may have already used sudo in which case we won't need the password
|
||||
// It's a bit nicer for them if they don't have to enter the password every time
|
||||
_, err := RunShellCommand("sudo -v")
|
||||
needPassword := err != nil
|
||||
|
||||
// If we need the password, we have to close the screen and ask using the shell
|
||||
if needPassword {
|
||||
// Shut down the screen because we're going to interact directly with the shell
|
||||
screen.Fini()
|
||||
screen = nil
|
||||
}
|
||||
|
||||
// Set up everything for the command
|
||||
cmd := exec.Command("sudo", "tee", filename)
|
||||
cmd.Stdin = bytes.NewBufferString(b.String())
|
||||
|
||||
// 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
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the command
|
||||
cmd.Start()
|
||||
err = cmd.Wait()
|
||||
|
||||
// If we needed the password, we closed the screen, so we have to initialize it again
|
||||
if needPassword {
|
||||
// Start the screen back up
|
||||
InitScreen()
|
||||
}
|
||||
if err == nil {
|
||||
b.IsModified = false
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
b.Serialize()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Buffer) insert(pos Loc, value []byte) {
|
||||
b.IsModified = true
|
||||
b.LineArray.insert(pos, value)
|
||||
b.Update()
|
||||
}
|
||||
func (b *Buffer) remove(start, end Loc) string {
|
||||
b.IsModified = true
|
||||
sub := b.LineArray.remove(start, end)
|
||||
b.Update()
|
||||
return sub
|
||||
}
|
||||
|
||||
// Start returns the location of the first character in the buffer
|
||||
func (b *Buffer) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (b *Buffer) End() Loc {
|
||||
return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
|
||||
}
|
||||
|
||||
// Line returns a single line
|
||||
func (b *Buffer) Line(n int) string {
|
||||
return string(b.lines[n])
|
||||
}
|
||||
|
||||
// Lines returns an array of strings containing the lines from start to end
|
||||
func (b *Buffer) Lines(start, end int) []string {
|
||||
lines := b.lines[start:end]
|
||||
var slice []string
|
||||
for _, line := range lines {
|
||||
slice = append(slice, string(line))
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// Len gives the length of the buffer
|
||||
func (b *Buffer) Len() int {
|
||||
return Count(b.String())
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
func shouldContinue() bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Continue [Y/n]: ")
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
|
||||
text = strings.TrimRight(text, "\r\n")
|
||||
|
||||
return len(text) == 0 || strings.ToLower(text)[0] == 'y'
|
||||
}
|
||||
|
||||
// CleanConfig performs cleanup in the user's configuration directory
|
||||
func CleanConfig() {
|
||||
fmt.Println("Cleaning your configuration directory at", config.ConfigDir)
|
||||
fmt.Printf("Please consider backing up %s before continuing\n", config.ConfigDir)
|
||||
|
||||
if !shouldContinue() {
|
||||
fmt.Println("Stopping early")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Cleaning default settings")
|
||||
|
||||
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
|
||||
var unusedOptions []string
|
||||
defaultSettings := config.DefaultAllSettings()
|
||||
for k := range config.GlobalSettings {
|
||||
if _, ok := defaultSettings[k]; !ok {
|
||||
valid := false
|
||||
for _, p := range config.Plugins {
|
||||
if strings.HasPrefix(k, p.Name+".") || k == p.Name {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
unusedOptions = append(unusedOptions, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(unusedOptions) > 0 {
|
||||
fmt.Println("The following options are unused:")
|
||||
|
||||
sort.Strings(unusedOptions)
|
||||
|
||||
for _, s := range unusedOptions {
|
||||
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
|
||||
}
|
||||
|
||||
fmt.Printf("These options will be removed from %s\n", settingsFile)
|
||||
|
||||
if shouldContinue() {
|
||||
for _, s := range unusedOptions {
|
||||
delete(config.GlobalSettings, s)
|
||||
}
|
||||
|
||||
err := config.OverwriteSettings(settingsFile)
|
||||
if err != nil {
|
||||
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.Print("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// detect incorrectly formatted buffer/ files
|
||||
buffersPath := filepath.Join(config.ConfigDir, "buffers")
|
||||
files, err := os.ReadDir(buffersPath)
|
||||
if err == nil {
|
||||
var badFiles []string
|
||||
var buffer buffer.SerializedBuffer
|
||||
for _, f := range files {
|
||||
fname := filepath.Join(buffersPath, f.Name())
|
||||
file, e := os.Open(fname)
|
||||
|
||||
if e == nil {
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&buffer)
|
||||
|
||||
if err != nil && f.Name() != "history" {
|
||||
badFiles = append(badFiles, fname)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if len(badFiles) > 0 {
|
||||
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.Printf("Removing badly formatted files in %s\n", buffersPath)
|
||||
|
||||
if shouldContinue() {
|
||||
removed := 0
|
||||
for _, f := range badFiles {
|
||||
err := os.Remove(f)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
removed++
|
||||
}
|
||||
|
||||
if removed == 0 {
|
||||
fmt.Println("Failed to remove files")
|
||||
} else {
|
||||
fmt.Printf("Removed %d badly formatted files\n", removed)
|
||||
}
|
||||
fmt.Print("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detect plugins/ directory
|
||||
plugins := filepath.Join(config.ConfigDir, "plugins")
|
||||
if stat, err := os.Stat(plugins); err == nil && stat.IsDir() {
|
||||
fmt.Printf("Found directory %s\n", plugins)
|
||||
fmt.Printf("Plugins should now be stored in %s\n", filepath.Join(config.ConfigDir, "plug"))
|
||||
fmt.Printf("Removing %s\n", plugins)
|
||||
|
||||
if shouldContinue() {
|
||||
os.RemoveAll(plugins)
|
||||
}
|
||||
|
||||
fmt.Print("\n\n")
|
||||
}
|
||||
|
||||
fmt.Println("Done cleaning")
|
||||
}
|
||||
231
cmd/micro/colorscheme.go
Normal file
231
cmd/micro/colorscheme.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// Colorscheme is a map from string to style -- it represents a colorscheme
|
||||
type Colorscheme map[string]tcell.Style
|
||||
|
||||
// The current colorscheme
|
||||
var colorscheme Colorscheme
|
||||
|
||||
var preInstalledColors = []string{"default", "simple", "solarized", "solarized-tc", "atom-dark-tc", "monokai", "gruvbox", "zenburn", "bubblegum"}
|
||||
|
||||
// ColorschemeExists checks if a given colorscheme exists
|
||||
func ColorschemeExists(colorschemeName string) bool {
|
||||
files, _ := ioutil.ReadDir(configDir)
|
||||
for _, f := range files {
|
||||
if f.Name() == colorschemeName+".micro" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range preInstalledColors {
|
||||
if name == colorschemeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// InitColorscheme picks and initializes the colorscheme when micro starts
|
||||
func InitColorscheme() {
|
||||
LoadDefaultColorscheme()
|
||||
}
|
||||
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
|
||||
func LoadDefaultColorscheme() {
|
||||
LoadColorscheme(globalSettings["colorscheme"].(string), configDir+"/colorschemes")
|
||||
}
|
||||
|
||||
// LoadColorscheme loads the given colorscheme from a directory
|
||||
func LoadColorscheme(colorschemeName, dir string) {
|
||||
files, _ := ioutil.ReadDir(dir)
|
||||
found := false
|
||||
for _, f := range files {
|
||||
if f.Name() == colorschemeName+".micro" {
|
||||
text, err := ioutil.ReadFile(dir + "/" + f.Name())
|
||||
if err != nil {
|
||||
fmt.Println("Error loading colorscheme:", err)
|
||||
continue
|
||||
}
|
||||
colorscheme = ParseColorscheme(string(text))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range preInstalledColors {
|
||||
if name == colorschemeName {
|
||||
data, err := Asset("runtime/colorschemes/" + name + ".micro")
|
||||
if err != nil {
|
||||
TermMessage("Unable to load pre-installed colorscheme " + name)
|
||||
continue
|
||||
}
|
||||
colorscheme = ParseColorscheme(string(data))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
TermMessage(colorschemeName, "is not a valid colorscheme")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
|
||||
// red background
|
||||
func ParseColorscheme(text string) Colorscheme {
|
||||
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
c := make(Colorscheme)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
strings.TrimSpace(line)[0] == '#' {
|
||||
// Ignore this line
|
||||
continue
|
||||
}
|
||||
|
||||
matches := parser.FindSubmatch([]byte(line))
|
||||
if len(matches) == 3 {
|
||||
link := string(matches[1])
|
||||
colors := string(matches[2])
|
||||
|
||||
c[link] = StringToStyle(colors)
|
||||
} else {
|
||||
fmt.Println("Color-link statement is not valid:", line)
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// StringToStyle returns a style from a string
|
||||
// The strings must be in the format "extra foregroundcolor,backgroundcolor"
|
||||
// The 'extra' can be bold, reverse, or underline
|
||||
func StringToStyle(str string) tcell.Style {
|
||||
var fg string
|
||||
bg := "default"
|
||||
split := strings.Split(str, ",")
|
||||
if len(split) > 1 {
|
||||
fg, bg = split[0], split[1]
|
||||
} else {
|
||||
fg = split[0]
|
||||
}
|
||||
fg = strings.TrimSpace(fg)
|
||||
bg = strings.TrimSpace(bg)
|
||||
|
||||
style := defStyle.Foreground(StringToColor(fg)).Background(StringToColor(bg))
|
||||
if strings.Contains(str, "bold") {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
if strings.Contains(str, "reverse") {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
if strings.Contains(str, "underline") {
|
||||
style = style.Underline(true)
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// StringToColor returns a tcell color from a string representation of a color
|
||||
// We accept either bright... or light... to mean the brighter version of a color
|
||||
func StringToColor(str string) tcell.Color {
|
||||
switch str {
|
||||
case "black":
|
||||
return tcell.ColorBlack
|
||||
case "red":
|
||||
return tcell.ColorMaroon
|
||||
case "green":
|
||||
return tcell.ColorGreen
|
||||
case "yellow":
|
||||
return tcell.ColorOlive
|
||||
case "blue":
|
||||
return tcell.ColorNavy
|
||||
case "magenta":
|
||||
return tcell.ColorPurple
|
||||
case "cyan":
|
||||
return tcell.ColorTeal
|
||||
case "white":
|
||||
return tcell.ColorSilver
|
||||
case "brightblack", "lightblack":
|
||||
return tcell.ColorGray
|
||||
case "brightred", "lightred":
|
||||
return tcell.ColorRed
|
||||
case "brightgreen", "lightgreen":
|
||||
return tcell.ColorLime
|
||||
case "brightyellow", "lightyellow":
|
||||
return tcell.ColorYellow
|
||||
case "brightblue", "lightblue":
|
||||
return tcell.ColorBlue
|
||||
case "brightmagenta", "lightmagenta":
|
||||
return tcell.ColorFuchsia
|
||||
case "brightcyan", "lightcyan":
|
||||
return tcell.ColorAqua
|
||||
case "brightwhite", "lightwhite":
|
||||
return tcell.ColorWhite
|
||||
case "default":
|
||||
return tcell.ColorDefault
|
||||
default:
|
||||
// Check if this is a 256 color
|
||||
if num, err := strconv.Atoi(str); err == nil {
|
||||
return GetColor256(num)
|
||||
}
|
||||
// Probably a truecolor hex value
|
||||
return tcell.GetColor(str)
|
||||
}
|
||||
}
|
||||
|
||||
// GetColor256 returns the tcell color for a number between 0 and 255
|
||||
func GetColor256(color int) tcell.Color {
|
||||
colors := []tcell.Color{tcell.ColorBlack, tcell.ColorMaroon, tcell.ColorGreen,
|
||||
tcell.ColorOlive, tcell.ColorNavy, tcell.ColorPurple,
|
||||
tcell.ColorTeal, tcell.ColorSilver, tcell.ColorGray,
|
||||
tcell.ColorRed, tcell.ColorLime, tcell.ColorYellow,
|
||||
tcell.ColorBlue, tcell.ColorFuchsia, tcell.ColorAqua,
|
||||
tcell.ColorWhite, tcell.Color16, tcell.Color17, tcell.Color18, tcell.Color19, tcell.Color20,
|
||||
tcell.Color21, tcell.Color22, tcell.Color23, tcell.Color24, tcell.Color25, tcell.Color26, tcell.Color27, tcell.Color28,
|
||||
tcell.Color29, tcell.Color30, tcell.Color31, tcell.Color32, tcell.Color33, tcell.Color34, tcell.Color35, tcell.Color36,
|
||||
tcell.Color37, tcell.Color38, tcell.Color39, tcell.Color40, tcell.Color41, tcell.Color42, tcell.Color43, tcell.Color44,
|
||||
tcell.Color45, tcell.Color46, tcell.Color47, tcell.Color48, tcell.Color49, tcell.Color50, tcell.Color51, tcell.Color52,
|
||||
tcell.Color53, tcell.Color54, tcell.Color55, tcell.Color56, tcell.Color57, tcell.Color58, tcell.Color59, tcell.Color60,
|
||||
tcell.Color61, tcell.Color62, tcell.Color63, tcell.Color64, tcell.Color65, tcell.Color66, tcell.Color67, tcell.Color68,
|
||||
tcell.Color69, tcell.Color70, tcell.Color71, tcell.Color72, tcell.Color73, tcell.Color74, tcell.Color75, tcell.Color76,
|
||||
tcell.Color77, tcell.Color78, tcell.Color79, tcell.Color80, tcell.Color81, tcell.Color82, tcell.Color83, tcell.Color84,
|
||||
tcell.Color85, tcell.Color86, tcell.Color87, tcell.Color88, tcell.Color89, tcell.Color90, tcell.Color91, tcell.Color92,
|
||||
tcell.Color93, tcell.Color94, tcell.Color95, tcell.Color96, tcell.Color97, tcell.Color98, tcell.Color99, tcell.Color100,
|
||||
tcell.Color101, tcell.Color102, tcell.Color103, tcell.Color104, tcell.Color105, tcell.Color106, tcell.Color107, tcell.Color108,
|
||||
tcell.Color109, tcell.Color110, tcell.Color111, tcell.Color112, tcell.Color113, tcell.Color114, tcell.Color115, tcell.Color116,
|
||||
tcell.Color117, tcell.Color118, tcell.Color119, tcell.Color120, tcell.Color121, tcell.Color122, tcell.Color123, tcell.Color124,
|
||||
tcell.Color125, tcell.Color126, tcell.Color127, tcell.Color128, tcell.Color129, tcell.Color130, tcell.Color131, tcell.Color132,
|
||||
tcell.Color133, tcell.Color134, tcell.Color135, tcell.Color136, tcell.Color137, tcell.Color138, tcell.Color139, tcell.Color140,
|
||||
tcell.Color141, tcell.Color142, tcell.Color143, tcell.Color144, tcell.Color145, tcell.Color146, tcell.Color147, tcell.Color148,
|
||||
tcell.Color149, tcell.Color150, tcell.Color151, tcell.Color152, tcell.Color153, tcell.Color154, tcell.Color155, tcell.Color156,
|
||||
tcell.Color157, tcell.Color158, tcell.Color159, tcell.Color160, tcell.Color161, tcell.Color162, tcell.Color163, tcell.Color164,
|
||||
tcell.Color165, tcell.Color166, tcell.Color167, tcell.Color168, tcell.Color169, tcell.Color170, tcell.Color171, tcell.Color172,
|
||||
tcell.Color173, tcell.Color174, tcell.Color175, tcell.Color176, tcell.Color177, tcell.Color178, tcell.Color179, tcell.Color180,
|
||||
tcell.Color181, tcell.Color182, tcell.Color183, tcell.Color184, tcell.Color185, tcell.Color186, tcell.Color187, tcell.Color188,
|
||||
tcell.Color189, tcell.Color190, tcell.Color191, tcell.Color192, tcell.Color193, tcell.Color194, tcell.Color195, tcell.Color196,
|
||||
tcell.Color197, tcell.Color198, tcell.Color199, tcell.Color200, tcell.Color201, tcell.Color202, tcell.Color203, tcell.Color204,
|
||||
tcell.Color205, tcell.Color206, tcell.Color207, tcell.Color208, tcell.Color209, tcell.Color210, tcell.Color211, tcell.Color212,
|
||||
tcell.Color213, tcell.Color214, tcell.Color215, tcell.Color216, tcell.Color217, tcell.Color218, tcell.Color219, tcell.Color220,
|
||||
tcell.Color221, tcell.Color222, tcell.Color223, tcell.Color224, tcell.Color225, tcell.Color226, tcell.Color227, tcell.Color228,
|
||||
tcell.Color229, tcell.Color230, tcell.Color231, tcell.Color232, tcell.Color233, tcell.Color234, tcell.Color235, tcell.Color236,
|
||||
tcell.Color237, tcell.Color238, tcell.Color239, tcell.Color240, tcell.Color241, tcell.Color242, tcell.Color243, tcell.Color244,
|
||||
tcell.Color245, tcell.Color246, tcell.Color247, tcell.Color248, tcell.Color249, tcell.Color250, tcell.Color251, tcell.Color252,
|
||||
tcell.Color253, tcell.Color254, tcell.Color255,
|
||||
}
|
||||
|
||||
return colors[color]
|
||||
}
|
||||
411
cmd/micro/command.go
Normal file
411
cmd/micro/command.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
action func([]string)
|
||||
completions []Completion
|
||||
}
|
||||
|
||||
type StrCommand struct {
|
||||
action string
|
||||
completions []Completion
|
||||
}
|
||||
|
||||
var commands map[string]Command
|
||||
|
||||
var commandActions = map[string]func([]string){
|
||||
"Set": Set,
|
||||
"SetLocal": SetLocal,
|
||||
"Show": Show,
|
||||
"Run": Run,
|
||||
"Bind": Bind,
|
||||
"Quit": Quit,
|
||||
"Save": Save,
|
||||
"Replace": Replace,
|
||||
"VSplit": VSplit,
|
||||
"HSplit": HSplit,
|
||||
"Tab": NewTab,
|
||||
"Help": Help,
|
||||
}
|
||||
|
||||
// InitCommands initializes the default commands
|
||||
func InitCommands() {
|
||||
commands = make(map[string]Command)
|
||||
|
||||
defaults := DefaultCommands()
|
||||
parseCommands(defaults)
|
||||
}
|
||||
|
||||
func parseCommands(userCommands map[string]StrCommand) {
|
||||
for k, v := range userCommands {
|
||||
MakeCommand(k, v.action, v.completions...)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCommand is a function to easily create new commands
|
||||
// This can be called by plugins in Lua so that plugins can define their own commands
|
||||
func MakeCommand(name, function string, completions ...Completion) {
|
||||
action := commandActions[function]
|
||||
if _, ok := commandActions[function]; !ok {
|
||||
// If the user seems to be binding a function that doesn't exist
|
||||
// We hope that it's a lua function that exists and bind it to that
|
||||
action = LuaFunctionCommand(function)
|
||||
}
|
||||
|
||||
commands[name] = Command{action, completions}
|
||||
}
|
||||
|
||||
// DefaultCommands returns a map containing micro's default commands
|
||||
func DefaultCommands() map[string]StrCommand {
|
||||
return map[string]StrCommand{
|
||||
"set": {"Set", []Completion{OptionCompletion, NoCompletion}},
|
||||
"setlocal": {"SetLocal", []Completion{OptionCompletion, NoCompletion}},
|
||||
"show": {"Show", []Completion{OptionCompletion, NoCompletion}},
|
||||
"bind": {"Bind", []Completion{NoCompletion}},
|
||||
"run": {"Run", []Completion{NoCompletion}},
|
||||
"quit": {"Quit", []Completion{NoCompletion}},
|
||||
"save": {"Save", []Completion{NoCompletion}},
|
||||
"replace": {"Replace", []Completion{NoCompletion}},
|
||||
"vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}},
|
||||
"hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}},
|
||||
"tab": {"Tab", []Completion{FileCompletion, NoCompletion}},
|
||||
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
|
||||
}
|
||||
}
|
||||
|
||||
// Help tries to open the given help page in a horizontal split
|
||||
func Help(args []string) {
|
||||
if len(args) < 1 {
|
||||
// Open the default help if the user just typed "> help"
|
||||
CurView().openHelp("help")
|
||||
} else {
|
||||
helpPage := args[0]
|
||||
if _, ok := helpPages[helpPage]; ok {
|
||||
CurView().openHelp(helpPage)
|
||||
} else {
|
||||
messenger.Error("Sorry, no help for ", helpPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VSplit opens a vertical split with file given in the first argument
|
||||
// If no file is given, it opens an empty buffer in a new split
|
||||
func VSplit(args []string) {
|
||||
if len(args) == 0 {
|
||||
CurView().VSplit(NewBuffer([]byte{}, ""))
|
||||
} else {
|
||||
filename := args[0]
|
||||
home, _ := homedir.Dir()
|
||||
filename = strings.Replace(filename, "~", home, 1)
|
||||
file, err := ioutil.ReadFile(filename)
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBuffer([]byte{}, filename)
|
||||
} else {
|
||||
buf = NewBuffer(file, filename)
|
||||
}
|
||||
CurView().VSplit(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// HSplit opens a horizontal split with file given in the first argument
|
||||
// If no file is given, it opens an empty buffer in a new split
|
||||
func HSplit(args []string) {
|
||||
if len(args) == 0 {
|
||||
CurView().HSplit(NewBuffer([]byte{}, ""))
|
||||
} else {
|
||||
filename := args[0]
|
||||
home, _ := homedir.Dir()
|
||||
filename = strings.Replace(filename, "~", home, 1)
|
||||
file, err := ioutil.ReadFile(filename)
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBuffer([]byte{}, filename)
|
||||
} else {
|
||||
buf = NewBuffer(file, filename)
|
||||
}
|
||||
CurView().HSplit(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTab opens the given file in a new tab
|
||||
func NewTab(args []string) {
|
||||
if len(args) == 0 {
|
||||
CurView().AddTab(true)
|
||||
} else {
|
||||
filename := args[0]
|
||||
home, _ := homedir.Dir()
|
||||
filename = strings.Replace(filename, "~", home, 1)
|
||||
file, _ := ioutil.ReadFile(filename)
|
||||
|
||||
tab := NewTabFromView(NewView(NewBuffer(file, filename)))
|
||||
tab.SetNum(len(tabs))
|
||||
tabs = append(tabs, tab)
|
||||
curTab++
|
||||
if len(tabs) == 2 {
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.views {
|
||||
v.ToggleTabbar()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets an option
|
||||
func Set(args []string) {
|
||||
if len(args) < 2 {
|
||||
messenger.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := strings.TrimSpace(args[0])
|
||||
value := strings.TrimSpace(args[1])
|
||||
|
||||
SetOptionAndSettings(option, value)
|
||||
}
|
||||
|
||||
// SetLocal sets an option local to the buffer
|
||||
func SetLocal(args []string) {
|
||||
if len(args) < 2 {
|
||||
messenger.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := strings.TrimSpace(args[0])
|
||||
value := strings.TrimSpace(args[1])
|
||||
|
||||
err := SetLocalOption(option, value, CurView())
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Show shows the value of the given option
|
||||
func Show(args []string) {
|
||||
if len(args) < 1 {
|
||||
messenger.Error("Please provide an option to show")
|
||||
return
|
||||
}
|
||||
|
||||
option := GetOption(args[0])
|
||||
|
||||
if option == nil {
|
||||
messenger.Error(args[0], " is not a valid option")
|
||||
return
|
||||
}
|
||||
|
||||
messenger.Message(option)
|
||||
}
|
||||
|
||||
// Bind creates a new keybinding
|
||||
func Bind(args []string) {
|
||||
if len(args) < 2 {
|
||||
messenger.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
BindKey(args[0], args[1])
|
||||
}
|
||||
|
||||
// Run runs a shell command in the background
|
||||
func Run(args []string) {
|
||||
// Run a shell command in the background (openTerm is false)
|
||||
HandleShellCommand(JoinCommandArgs(args...), false)
|
||||
}
|
||||
|
||||
// Quit closes the main view
|
||||
func Quit(args []string) {
|
||||
// Close the main view
|
||||
CurView().Quit(true)
|
||||
}
|
||||
|
||||
// Save saves the buffer in the main view
|
||||
func Save(args []string) {
|
||||
// Save the main view
|
||||
CurView().Save(true)
|
||||
}
|
||||
|
||||
// Replace runs search and replace
|
||||
func Replace(args []string) {
|
||||
if len(args) < 2 {
|
||||
// We need to find both a search and replace expression
|
||||
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
|
||||
return
|
||||
}
|
||||
|
||||
var flags string
|
||||
if len(args) == 3 {
|
||||
// The user included some flags
|
||||
flags = args[2]
|
||||
}
|
||||
|
||||
search := string(args[0])
|
||||
replace := string(args[1])
|
||||
|
||||
regex, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
// There was an error with the user's regex
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
view := CurView()
|
||||
|
||||
found := 0
|
||||
if strings.Contains(flags, "c") {
|
||||
for {
|
||||
// The 'check' flag was used
|
||||
Search(search, view, true)
|
||||
if !view.Cursor.HasSelection() {
|
||||
break
|
||||
}
|
||||
view.Relocate()
|
||||
if view.Buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
RedrawAll()
|
||||
choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
|
||||
if canceled {
|
||||
if view.Cursor.HasSelection() {
|
||||
view.Cursor.Loc = view.Cursor.CurSelection[0]
|
||||
view.Cursor.ResetSelection()
|
||||
}
|
||||
messenger.Reset()
|
||||
break
|
||||
}
|
||||
if choice {
|
||||
view.Cursor.DeleteSelection()
|
||||
view.Buf.Insert(view.Cursor.Loc, replace)
|
||||
view.Cursor.ResetSelection()
|
||||
messenger.Reset()
|
||||
found++
|
||||
} else {
|
||||
if view.Cursor.HasSelection() {
|
||||
searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
|
||||
} else {
|
||||
searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
match := regex.FindStringIndex(view.Buf.String())
|
||||
if match == nil {
|
||||
break
|
||||
}
|
||||
found++
|
||||
view.Buf.Replace(FromCharPos(match[0], view.Buf), FromCharPos(match[1], view.Buf), replace)
|
||||
}
|
||||
}
|
||||
view.Cursor.Relocate()
|
||||
|
||||
if found > 1 {
|
||||
messenger.Message("Replaced ", found, " occurrences of ", search)
|
||||
} else if found == 1 {
|
||||
messenger.Message("Replaced ", found, " occurrence of ", search)
|
||||
} else {
|
||||
messenger.Message("Nothing matched ", search)
|
||||
}
|
||||
}
|
||||
|
||||
// RunShellCommand executes a shell command and returns the output/error
|
||||
func RunShellCommand(input string) (string, error) {
|
||||
inputCmd := SplitCommandArgs(input)[0]
|
||||
args := SplitCommandArgs(input)[1:]
|
||||
|
||||
cmd := exec.Command(inputCmd, args...)
|
||||
outputBytes := &bytes.Buffer{}
|
||||
cmd.Stdout = outputBytes
|
||||
cmd.Stderr = outputBytes
|
||||
cmd.Start()
|
||||
err := cmd.Wait() // wait for command to finish
|
||||
outstring := outputBytes.String()
|
||||
return outstring, err
|
||||
}
|
||||
|
||||
// HandleShellCommand runs the shell command
|
||||
// The openTerm argument specifies whether a terminal should be opened (for viewing output
|
||||
// or interacting with stdin)
|
||||
func HandleShellCommand(input string, openTerm bool) {
|
||||
inputCmd := SplitCommandArgs(input)[0]
|
||||
if !openTerm {
|
||||
// Simply run the command in the background and notify the user when it's done
|
||||
messenger.Message("Running...")
|
||||
go func() {
|
||||
output, err := RunShellCommand(input)
|
||||
totalLines := strings.Split(output, "\n")
|
||||
|
||||
if len(totalLines) < 3 {
|
||||
if err == nil {
|
||||
messenger.Message(inputCmd, " exited without error")
|
||||
} else {
|
||||
messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
|
||||
}
|
||||
} else {
|
||||
messenger.Message(output)
|
||||
}
|
||||
// We have to make sure to redraw
|
||||
RedrawAll()
|
||||
}()
|
||||
} else {
|
||||
// Shut down the screen because we're going to interact directly with the shell
|
||||
screen.Fini()
|
||||
screen = nil
|
||||
|
||||
args := SplitCommandArgs(input)[1:]
|
||||
|
||||
// Set up everything for the command
|
||||
cmd := exec.Command(inputCmd, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// 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
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the command
|
||||
cmd.Start()
|
||||
cmd.Wait()
|
||||
|
||||
// This is just so we don't return right away and let the user press enter to return
|
||||
TermMessage("")
|
||||
|
||||
// Start the screen back up
|
||||
InitScreen()
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCommand handles input from the user
|
||||
func HandleCommand(input string) {
|
||||
args := SplitCommandArgs(input)
|
||||
inputCmd := args[0]
|
||||
|
||||
if _, ok := commands[inputCmd]; !ok {
|
||||
messenger.Error("Unknown command ", inputCmd)
|
||||
} else {
|
||||
commands[inputCmd].action(args[1:])
|
||||
}
|
||||
}
|
||||
350
cmd/micro/cursor.go
Normal file
350
cmd/micro/cursor.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package main
|
||||
|
||||
import "github.com/zyedidia/clipboard"
|
||||
|
||||
// The Cursor struct stores the location of the cursor in the view
|
||||
// The complicated part about the cursor is storing its location.
|
||||
// The cursor must be displayed at an x, y location, but since the buffer
|
||||
// uses a rope to store text, to insert text we must have an index. It
|
||||
// is also simpler to use character indicies for other tasks such as
|
||||
// selection.
|
||||
type Cursor struct {
|
||||
buf *Buffer
|
||||
Loc
|
||||
|
||||
// Last cursor x position
|
||||
LastVisualX int
|
||||
|
||||
// The current selection as a range of character numbers (inclusive)
|
||||
CurSelection [2]Loc
|
||||
// The original selection as a range of character numbers
|
||||
// This is used for line and word selection where it is necessary
|
||||
// to know what the original selection was
|
||||
OrigSelection [2]Loc
|
||||
}
|
||||
|
||||
// Goto puts the cursor at the given cursor's location and gives the current cursor its selection too
|
||||
func (c *Cursor) Goto(b Cursor) {
|
||||
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
|
||||
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
||||
}
|
||||
|
||||
// ResetSelection resets the user's selection
|
||||
func (c *Cursor) ResetSelection() {
|
||||
c.CurSelection[0] = c.buf.Start()
|
||||
c.CurSelection[1] = c.buf.Start()
|
||||
}
|
||||
|
||||
// SetSelectionStart sets the start of the selection
|
||||
func (c *Cursor) SetSelectionStart(pos Loc) {
|
||||
c.CurSelection[0] = pos
|
||||
// Copy to primary clipboard for linux
|
||||
if c.HasSelection() {
|
||||
clipboard.WriteAll(c.GetSelection(), "primary")
|
||||
}
|
||||
}
|
||||
|
||||
// SetSelectionEnd sets the end of the selection
|
||||
func (c *Cursor) SetSelectionEnd(pos Loc) {
|
||||
c.CurSelection[1] = pos
|
||||
// Copy to primary clipboard for linux
|
||||
if c.HasSelection() {
|
||||
clipboard.WriteAll(c.GetSelection(), "primary")
|
||||
}
|
||||
}
|
||||
|
||||
// HasSelection returns whether or not the user has selected anything
|
||||
func (c *Cursor) HasSelection() bool {
|
||||
return c.CurSelection[0] != c.CurSelection[1]
|
||||
}
|
||||
|
||||
// DeleteSelection deletes the currently selected text
|
||||
func (c *Cursor) DeleteSelection() {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
|
||||
c.Loc = c.CurSelection[1]
|
||||
} else if !c.HasSelection() {
|
||||
return
|
||||
} else {
|
||||
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
|
||||
c.Loc = c.CurSelection[0]
|
||||
}
|
||||
}
|
||||
|
||||
// GetSelection returns the cursor's selection
|
||||
func (c *Cursor) GetSelection() string {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
|
||||
}
|
||||
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
|
||||
}
|
||||
|
||||
// SelectLine selects the current line
|
||||
func (c *Cursor) SelectLine() {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.End()
|
||||
if c.buf.NumLines-1 > c.Y {
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
} else {
|
||||
c.SetSelectionEnd(c.Loc)
|
||||
}
|
||||
|
||||
c.OrigSelection = c.CurSelection
|
||||
}
|
||||
|
||||
// AddLineToSelection adds the current line to the selection
|
||||
func (c *Cursor) AddLineToSelection() {
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
c.End()
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
}
|
||||
}
|
||||
|
||||
// SelectWord selects the word the cursor is currently on
|
||||
func (c *Cursor) SelectWord() {
|
||||
if len(c.buf.Line(c.Y)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !IsWordChar(string(c.RuneUnder(c.X))) {
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.OrigSelection = c.CurSelection
|
||||
return
|
||||
}
|
||||
|
||||
forward, backward := c.X, c.X
|
||||
|
||||
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.OrigSelection[0] = c.CurSelection[0]
|
||||
|
||||
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.OrigSelection[1] = c.CurSelection[1]
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// AddWordToSelection adds the word the cursor is currently on to the selection
|
||||
func (c *Cursor) AddWordToSelection() {
|
||||
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
return
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
backward := c.X
|
||||
|
||||
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
forward := c.X
|
||||
|
||||
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// SelectTo selects from the current cursor location to the given location
|
||||
func (c *Cursor) SelectTo(loc Loc) {
|
||||
if loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
c.SetSelectionEnd(loc)
|
||||
} else {
|
||||
c.SetSelectionStart(loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[0])
|
||||
}
|
||||
}
|
||||
|
||||
// WordRight moves the cursor one word to the right
|
||||
func (c *Cursor) WordRight() {
|
||||
for IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == Count(c.buf.Line(c.Y)) {
|
||||
c.Right()
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
c.Right()
|
||||
for IsWordChar(string(c.RuneUnder(c.X))) {
|
||||
if c.X == Count(c.buf.Line(c.Y)) {
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// WordLeft moves the cursor one word to the left
|
||||
func (c *Cursor) WordLeft() {
|
||||
c.Left()
|
||||
for IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Left()
|
||||
for IsWordChar(string(c.RuneUnder(c.X))) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
|
||||
// RuneUnder returns the rune under the given x position
|
||||
func (c *Cursor) RuneUnder(x int) rune {
|
||||
line := []rune(c.buf.Line(c.Y))
|
||||
if len(line) == 0 {
|
||||
return '\n'
|
||||
}
|
||||
if x >= len(line) {
|
||||
return '\n'
|
||||
} else if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
return line[x]
|
||||
}
|
||||
|
||||
// UpN moves the cursor up N lines (if possible)
|
||||
func (c *Cursor) UpN(amount int) {
|
||||
proposedY := c.Y - amount
|
||||
if proposedY < 0 {
|
||||
proposedY = 0
|
||||
} else if proposedY >= c.buf.NumLines {
|
||||
proposedY = c.buf.NumLines - 1
|
||||
}
|
||||
if proposedY == c.Y {
|
||||
return
|
||||
}
|
||||
|
||||
c.Y = proposedY
|
||||
runes := []rune(c.buf.Line(c.Y))
|
||||
c.X = c.GetCharPosInLine(c.Y, c.LastVisualX)
|
||||
if c.X > len(runes) {
|
||||
c.X = len(runes)
|
||||
}
|
||||
}
|
||||
|
||||
// DownN moves the cursor down N lines (if possible)
|
||||
func (c *Cursor) DownN(amount int) {
|
||||
c.UpN(-amount)
|
||||
}
|
||||
|
||||
// Up moves the cursor up one line (if possible)
|
||||
func (c *Cursor) Up() {
|
||||
c.UpN(1)
|
||||
}
|
||||
|
||||
// Down moves the cursor down one line (if possible)
|
||||
func (c *Cursor) Down() {
|
||||
c.DownN(1)
|
||||
}
|
||||
|
||||
// Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
|
||||
func (c *Cursor) Left() {
|
||||
if c.Loc == c.buf.Start() {
|
||||
return
|
||||
}
|
||||
if c.X > 0 {
|
||||
c.X--
|
||||
} else {
|
||||
c.Up()
|
||||
c.End()
|
||||
}
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// Right moves the cursor right one cell (if possible) or to the next line if it is at the end
|
||||
func (c *Cursor) Right() {
|
||||
if c.Loc == c.buf.End() {
|
||||
return
|
||||
}
|
||||
if c.X < Count(c.buf.Line(c.Y)) {
|
||||
c.X++
|
||||
} else {
|
||||
c.Down()
|
||||
c.Start()
|
||||
}
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// End moves the cursor to the end of the line it is on
|
||||
func (c *Cursor) End() {
|
||||
c.X = Count(c.buf.Line(c.Y))
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// Start moves the cursor to the start of the line it is on
|
||||
func (c *Cursor) Start() {
|
||||
c.X = 0
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
|
||||
func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
|
||||
// Get the tab size
|
||||
tabSize := int(c.buf.Settings["tabsize"].(float64))
|
||||
visualLineLen := StringWidth(c.buf.Line(lineNum), tabSize)
|
||||
if visualPos > visualLineLen {
|
||||
visualPos = visualLineLen
|
||||
}
|
||||
width := WidthOfLargeRunes(c.buf.Line(lineNum), tabSize)
|
||||
if visualPos >= width {
|
||||
return visualPos - width
|
||||
}
|
||||
return visualPos / tabSize
|
||||
}
|
||||
|
||||
// GetVisualX returns the x value of the cursor in visual spaces
|
||||
func (c *Cursor) GetVisualX() int {
|
||||
runes := []rune(c.buf.Line(c.Y))
|
||||
tabSize := int(c.buf.Settings["tabsize"].(float64))
|
||||
return StringWidth(string(runes[:c.X]), tabSize)
|
||||
}
|
||||
|
||||
// Relocate makes sure that the cursor is inside the bounds of the buffer
|
||||
// If it isn't, it moves it to be within the buffer's lines
|
||||
func (c *Cursor) Relocate() {
|
||||
if c.Y < 0 {
|
||||
c.Y = 0
|
||||
} else if c.Y >= c.buf.NumLines {
|
||||
c.Y = c.buf.NumLines - 1
|
||||
}
|
||||
|
||||
if c.X < 0 {
|
||||
c.X = 0
|
||||
} else if c.X > Count(c.buf.Line(c.Y)) {
|
||||
c.X = Count(c.buf.Line(c.Y))
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
// NullWriter simply sends writes into the void
|
||||
type NullWriter struct{}
|
||||
|
||||
// Write is empty
|
||||
func (NullWriter) Write(data []byte) (n int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
||||
func InitLog() {
|
||||
if util.Debug == "ON" {
|
||||
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, util.FileMode)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
}
|
||||
|
||||
log.SetOutput(f)
|
||||
log.Println("Micro started")
|
||||
} else {
|
||||
log.SetOutput(NullWriter{})
|
||||
}
|
||||
}
|
||||
208
cmd/micro/eventhandler.go
Normal file
208
cmd/micro/eventhandler.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
const (
|
||||
// Opposite and undoing events must have opposite values
|
||||
|
||||
// TextEventInsert repreasents an insertion event
|
||||
TextEventInsert = 1
|
||||
// TextEventRemove represents a deletion event
|
||||
TextEventRemove = -1
|
||||
)
|
||||
|
||||
// TextEvent holds data for a manipulation on some text that can be undone
|
||||
type TextEvent struct {
|
||||
C Cursor
|
||||
|
||||
EventType int
|
||||
Text string
|
||||
Start Loc
|
||||
End Loc
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
|
||||
if t.EventType == TextEventInsert {
|
||||
buf.insert(t.Start, []byte(t.Text))
|
||||
} else if t.EventType == TextEventRemove {
|
||||
t.Text = buf.remove(t.Start, t.End)
|
||||
}
|
||||
}
|
||||
|
||||
// UndoTextEvent undoes a text event
|
||||
func UndoTextEvent(t *TextEvent, buf *Buffer) {
|
||||
t.EventType = -t.EventType
|
||||
ExecuteTextEvent(t, buf)
|
||||
}
|
||||
|
||||
// EventHandler executes text manipulations and allows undoing and redoing
|
||||
type EventHandler struct {
|
||||
buf *Buffer
|
||||
UndoStack *Stack
|
||||
RedoStack *Stack
|
||||
}
|
||||
|
||||
// NewEventHandler returns a new EventHandler
|
||||
func NewEventHandler(buf *Buffer) *EventHandler {
|
||||
eh := new(EventHandler)
|
||||
eh.UndoStack = new(Stack)
|
||||
eh.RedoStack = new(Stack)
|
||||
eh.buf = buf
|
||||
return eh
|
||||
}
|
||||
|
||||
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
|
||||
// the buffer equal to that string
|
||||
// This means that we can transform the buffer into any string and still preserve undo/redo
|
||||
// through insert and delete events
|
||||
func (eh *EventHandler) ApplyDiff(new string) {
|
||||
differ := dmp.New()
|
||||
diff := differ.DiffMain(eh.buf.String(), new, false)
|
||||
loc := eh.buf.Start()
|
||||
for _, d := range diff {
|
||||
if d.Type == dmp.DiffDelete {
|
||||
eh.Remove(loc, loc.Move(Count(d.Text), eh.buf))
|
||||
} else {
|
||||
if d.Type == dmp.DiffInsert {
|
||||
eh.Insert(loc, d.Text)
|
||||
}
|
||||
loc = loc.Move(Count(d.Text), eh.buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert creates an insert text event and executes it
|
||||
func (eh *EventHandler) Insert(start Loc, text string) {
|
||||
e := &TextEvent{
|
||||
C: eh.buf.Cursor,
|
||||
EventType: TextEventInsert,
|
||||
Text: text,
|
||||
Start: start,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
e.End = start.Move(Count(text), eh.buf)
|
||||
}
|
||||
|
||||
// Remove creates a remove text event and executes it
|
||||
func (eh *EventHandler) Remove(start, end Loc) {
|
||||
e := &TextEvent{
|
||||
C: eh.buf.Cursor,
|
||||
EventType: TextEventRemove,
|
||||
Start: start,
|
||||
End: end,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
}
|
||||
|
||||
// Replace deletes from start to end and replaces it with the given string
|
||||
func (eh *EventHandler) Replace(start, end Loc, replace string) {
|
||||
eh.Remove(start, end)
|
||||
eh.Insert(start, replace)
|
||||
}
|
||||
|
||||
// Execute a textevent and add it to the undo stack
|
||||
func (eh *EventHandler) Execute(t *TextEvent) {
|
||||
if eh.RedoStack.Len() > 0 {
|
||||
eh.RedoStack = new(Stack)
|
||||
}
|
||||
eh.UndoStack.Push(t)
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
// Undo the first event in the undo stack
|
||||
func (eh *EventHandler) Undo() {
|
||||
t := eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.UndoOneEvent()
|
||||
|
||||
for {
|
||||
t = eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if startTime-(t.Time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
|
||||
return
|
||||
}
|
||||
startTime = t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.UndoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// UndoOneEvent undoes one event
|
||||
func (eh *EventHandler) UndoOneEvent() {
|
||||
// This event should be undone
|
||||
// Pop it off the stack
|
||||
t := eh.UndoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Undo it
|
||||
// Modifies the text event
|
||||
UndoTextEvent(t, eh.buf)
|
||||
|
||||
// Set the cursor in the right place
|
||||
teCursor := t.C
|
||||
t.C = eh.buf.Cursor
|
||||
eh.buf.Cursor.Goto(teCursor)
|
||||
|
||||
// Push it to the redo stack
|
||||
eh.RedoStack.Push(t)
|
||||
}
|
||||
|
||||
// Redo the first event in the redo stack
|
||||
func (eh *EventHandler) Redo() {
|
||||
t := eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.RedoOneEvent()
|
||||
|
||||
for {
|
||||
t = eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if (t.Time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
eh.RedoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// RedoOneEvent redoes one event
|
||||
func (eh *EventHandler) RedoOneEvent() {
|
||||
t := eh.RedoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Modifies the text event
|
||||
UndoTextEvent(t, eh.buf)
|
||||
|
||||
teCursor := t.C
|
||||
t.C = eh.buf.Cursor
|
||||
eh.buf.Cursor.Goto(teCursor)
|
||||
|
||||
eh.UndoStack.Push(t)
|
||||
}
|
||||
25
cmd/micro/help.go
Normal file
25
cmd/micro/help.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
var helpPages map[string]string
|
||||
|
||||
var helpFiles = []string{
|
||||
"help",
|
||||
"keybindings",
|
||||
"plugins",
|
||||
"colors",
|
||||
"options",
|
||||
"commands",
|
||||
"tutorial",
|
||||
}
|
||||
|
||||
// LoadHelp loads the help text from inside the binary
|
||||
func LoadHelp() {
|
||||
helpPages = make(map[string]string)
|
||||
for _, file := range helpFiles {
|
||||
data, err := Asset("runtime/help/" + file + ".md")
|
||||
if err != nil {
|
||||
TermMessage("Unable to load help text", file)
|
||||
}
|
||||
helpPages[file] = string(data)
|
||||
}
|
||||
}
|
||||
499
cmd/micro/highlighter.go
Normal file
499
cmd/micro/highlighter.go
Normal file
@@ -0,0 +1,499 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/tcell"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileTypeRules represents a complete set of syntax rules for a filetype
|
||||
type FileTypeRules struct {
|
||||
filetype string
|
||||
filename string
|
||||
text string
|
||||
}
|
||||
|
||||
// SyntaxRule represents a regex to highlight in a certain style
|
||||
type SyntaxRule struct {
|
||||
// What to highlight
|
||||
regex *regexp.Regexp
|
||||
// Any flags
|
||||
flags string
|
||||
// Whether this regex is a start=... end=... regex
|
||||
startend bool
|
||||
// How to highlight it
|
||||
style tcell.Style
|
||||
}
|
||||
|
||||
var syntaxFiles map[[2]*regexp.Regexp]FileTypeRules
|
||||
|
||||
// These syntax files are pre installed and embedded in the resulting binary by go-bindata
|
||||
var preInstalledSynFiles = []string{
|
||||
"Dockerfile",
|
||||
"apacheconf",
|
||||
"arduino",
|
||||
"asciidoc",
|
||||
"asm",
|
||||
"awk",
|
||||
"c",
|
||||
"caddyfile",
|
||||
"cmake",
|
||||
"coffeescript",
|
||||
"colortest",
|
||||
"conf",
|
||||
"conky",
|
||||
"csharp",
|
||||
"css",
|
||||
"cython",
|
||||
"d",
|
||||
"dart",
|
||||
"dot",
|
||||
"erb",
|
||||
"fish",
|
||||
"fortran",
|
||||
"gdscript",
|
||||
"gentoo-ebuild",
|
||||
"gentoo-etc-portage",
|
||||
"git-commit",
|
||||
"git-config",
|
||||
"git-rebase-todo",
|
||||
"glsl",
|
||||
"go",
|
||||
"golo",
|
||||
"groff",
|
||||
"haml",
|
||||
"haskell",
|
||||
"html",
|
||||
"ini",
|
||||
"inputrc",
|
||||
"java",
|
||||
"javascript",
|
||||
"json",
|
||||
"keymap",
|
||||
"kickstart",
|
||||
"ledger",
|
||||
"lilypond",
|
||||
"lisp",
|
||||
"lua",
|
||||
"makefile",
|
||||
"man",
|
||||
"markdown",
|
||||
"mpdconf",
|
||||
"micro",
|
||||
"nanorc",
|
||||
"nginx",
|
||||
"ocaml",
|
||||
"pascal",
|
||||
"patch",
|
||||
"peg",
|
||||
"perl",
|
||||
"perl6",
|
||||
"php",
|
||||
"pkg-config",
|
||||
"pkgbuild",
|
||||
"po",
|
||||
"pov",
|
||||
"privoxy-action",
|
||||
"privoxy-config",
|
||||
"privoxy-filter",
|
||||
"puppet",
|
||||
"python",
|
||||
"r",
|
||||
"reST",
|
||||
"rpmspec",
|
||||
"ruby",
|
||||
"rust",
|
||||
"scala",
|
||||
"sed",
|
||||
"sh",
|
||||
"sls",
|
||||
"sql",
|
||||
"swift",
|
||||
"systemd",
|
||||
"tcl",
|
||||
"tex",
|
||||
"vala",
|
||||
"vi",
|
||||
"xml",
|
||||
"xresources",
|
||||
"yaml",
|
||||
"yum",
|
||||
"zsh",
|
||||
}
|
||||
|
||||
// LoadSyntaxFiles loads the syntax files from the default directory (configDir)
|
||||
func LoadSyntaxFiles() {
|
||||
// Load the user's custom syntax files, if there are any
|
||||
LoadSyntaxFilesFromDir(configDir + "/syntax")
|
||||
|
||||
// Load the pre-installed syntax files from inside the binary
|
||||
for _, filetype := range preInstalledSynFiles {
|
||||
data, err := Asset("runtime/syntax/" + filetype + ".micro")
|
||||
if err != nil {
|
||||
TermMessage("Unable to load pre-installed syntax file " + filetype)
|
||||
continue
|
||||
}
|
||||
|
||||
LoadSyntaxFile(string(data), filetype+".micro")
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSyntaxFilesFromDir loads the syntax files from a specified directory
|
||||
// To load the syntax files, we must fill the `syntaxFiles` map
|
||||
// This involves finding the regex for syntax and if it exists, the regex
|
||||
// for the header. Then we must get the text for the file and the filetype.
|
||||
func LoadSyntaxFilesFromDir(dir string) {
|
||||
colorscheme = make(Colorscheme)
|
||||
InitColorscheme()
|
||||
|
||||
// Default style
|
||||
defStyle = tcell.StyleDefault.
|
||||
Foreground(tcell.ColorDefault).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
// There may be another default style defined in the colorscheme
|
||||
// In that case we should use that one
|
||||
if style, ok := colorscheme["default"]; ok {
|
||||
defStyle = style
|
||||
}
|
||||
if screen != nil {
|
||||
screen.SetStyle(defStyle)
|
||||
}
|
||||
|
||||
syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
|
||||
files, _ := ioutil.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
if filepath.Ext(f.Name()) == ".micro" {
|
||||
filename := dir + "/" + f.Name()
|
||||
text, err := ioutil.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + filename + ": " + err.Error())
|
||||
return
|
||||
}
|
||||
LoadSyntaxFile(string(text), filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JoinRule takes a syntax rule (which can be multiple regular expressions)
|
||||
// and joins it into one regular expression by ORing everything together
|
||||
func JoinRule(rule string) string {
|
||||
split := strings.Split(rule, `" "`)
|
||||
joined := strings.Join(split, ")|(")
|
||||
joined = "(" + joined + ")"
|
||||
return joined
|
||||
}
|
||||
|
||||
// LoadSyntaxFile simply gets the filetype of a the syntax file and the source for the
|
||||
// file and creates FileTypeRules out of it. If this filetype is the one opened by the user
|
||||
// the rules will be loaded and compiled later
|
||||
// In this function we are only concerned with loading the syntax and header regexes
|
||||
func LoadSyntaxFile(text, filename string) {
|
||||
var err error
|
||||
lines := strings.Split(string(text), "\n")
|
||||
|
||||
// Regex for parsing syntax statements
|
||||
syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
|
||||
// Regex for parsing header statements
|
||||
headerParser := regexp.MustCompile(`header "(.*)"`)
|
||||
|
||||
// Is there a syntax definition in this file?
|
||||
hasSyntax := syntaxParser.MatchString(text)
|
||||
// Is there a header definition in this file?
|
||||
hasHeader := headerParser.MatchString(text)
|
||||
|
||||
var syntaxRegex *regexp.Regexp
|
||||
var headerRegex *regexp.Regexp
|
||||
var filetype string
|
||||
for lineNum, line := range lines {
|
||||
if (hasSyntax == (syntaxRegex != nil)) && (hasHeader == (headerRegex != nil)) {
|
||||
// We found what we we're supposed to find
|
||||
break
|
||||
}
|
||||
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
strings.TrimSpace(line)[0] == '#' {
|
||||
// Ignore this line
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "syntax") {
|
||||
// Syntax statement
|
||||
syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
|
||||
if len(syntaxMatches) == 3 {
|
||||
if syntaxRegex != nil {
|
||||
TermError(filename, lineNum, "Syntax statement redeclaration")
|
||||
}
|
||||
|
||||
filetype = string(syntaxMatches[1])
|
||||
extensions := JoinRule(string(syntaxMatches[2]))
|
||||
|
||||
syntaxRegex, err = regexp.Compile(extensions)
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, err.Error())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
TermError(filename, lineNum, "Syntax statement is not valid: "+line)
|
||||
continue
|
||||
}
|
||||
} else if strings.HasPrefix(line, "header") {
|
||||
// Header statement
|
||||
headerMatches := headerParser.FindSubmatch([]byte(line))
|
||||
if len(headerMatches) == 2 {
|
||||
header := JoinRule(string(headerMatches[1]))
|
||||
|
||||
headerRegex, err = regexp.Compile(header)
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, "Regex error: "+err.Error())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
TermError(filename, lineNum, "Header statement is not valid: "+line)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if syntaxRegex != nil {
|
||||
// Add the current rules to the syntaxFiles variable
|
||||
regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
|
||||
syntaxFiles[regexes] = FileTypeRules{filetype, filename, text}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadRulesFromFile loads just the syntax rules from a given file
|
||||
// Only the necessary rules are loaded when the buffer is opened.
|
||||
// If we load all the rules for every filetype when micro starts, there's a bit of lag
|
||||
// A rule just explains how to color certain regular expressions
|
||||
// Example: color comment "//.*"
|
||||
// This would color all strings that match the regex "//.*" in the comment color defined
|
||||
// by the colorscheme
|
||||
func LoadRulesFromFile(text, filename string) []SyntaxRule {
|
||||
lines := strings.Split(string(text), "\n")
|
||||
|
||||
// Regex for parsing standard syntax rules
|
||||
ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
|
||||
// Regex for parsing syntax rules with start="..." end="..."
|
||||
ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
|
||||
|
||||
var rules []SyntaxRule
|
||||
for lineNum, line := range lines {
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
strings.TrimSpace(line)[0] == '#' ||
|
||||
strings.HasPrefix(line, "syntax") ||
|
||||
strings.HasPrefix(line, "header") {
|
||||
// Ignore this line
|
||||
continue
|
||||
}
|
||||
|
||||
// Syntax rule, but it could be standard or start-end
|
||||
if ruleParser.MatchString(line) {
|
||||
// Standard syntax rule
|
||||
// Parse the line
|
||||
submatch := ruleParser.FindSubmatch([]byte(line))
|
||||
var color string
|
||||
var regexStr string
|
||||
var flags string
|
||||
if len(submatch) == 4 {
|
||||
// If len is 4 then the user specified some additional flags to use
|
||||
color = string(submatch[1])
|
||||
flags = string(submatch[2])
|
||||
regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
|
||||
} else if len(submatch) == 3 {
|
||||
// If len is 3, no additional flags were given
|
||||
color = string(submatch[1])
|
||||
regexStr = JoinRule(string(submatch[2]))
|
||||
} else {
|
||||
// If len is not 3 or 4 there is a problem
|
||||
TermError(filename, lineNum, "Invalid statement: "+line)
|
||||
continue
|
||||
}
|
||||
// Compile the regex
|
||||
regex, err := regexp.Compile(regexStr)
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the style
|
||||
// The user could give us a "color" that is really a part of the colorscheme
|
||||
// in which case we should look that up in the colorscheme
|
||||
// They can also just give us a straight up color
|
||||
st := defStyle
|
||||
groups := strings.Split(color, ".")
|
||||
if len(groups) > 1 {
|
||||
curGroup := ""
|
||||
for i, g := range groups {
|
||||
if i != 0 {
|
||||
curGroup += "."
|
||||
}
|
||||
curGroup += g
|
||||
if style, ok := colorscheme[curGroup]; ok {
|
||||
st = style
|
||||
}
|
||||
}
|
||||
} else if style, ok := colorscheme[color]; ok {
|
||||
st = style
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
}
|
||||
// Add the regex, flags, and style
|
||||
// False because this is not start-end
|
||||
rules = append(rules, SyntaxRule{regex, flags, false, st})
|
||||
} else if ruleStartEndParser.MatchString(line) {
|
||||
// Start-end syntax rule
|
||||
submatch := ruleStartEndParser.FindSubmatch([]byte(line))
|
||||
var color string
|
||||
var start string
|
||||
var end string
|
||||
// Use m and s flags by default
|
||||
flags := "ms"
|
||||
if len(submatch) == 5 {
|
||||
// If len is 5 the user provided some additional flags
|
||||
color = string(submatch[1])
|
||||
flags += string(submatch[2])
|
||||
start = string(submatch[3])
|
||||
end = string(submatch[4])
|
||||
} else if len(submatch) == 4 {
|
||||
// If len is 4 the user did not provide additional flags
|
||||
color = string(submatch[1])
|
||||
start = string(submatch[2])
|
||||
end = string(submatch[3])
|
||||
} else {
|
||||
// If len is not 4 or 5 there is a problem
|
||||
TermError(filename, lineNum, "Invalid statement: "+line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Compile the regex
|
||||
regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the style
|
||||
// The user could give us a "color" that is really a part of the colorscheme
|
||||
// in which case we should look that up in the colorscheme
|
||||
// They can also just give us a straight up color
|
||||
st := defStyle
|
||||
if _, ok := colorscheme[color]; ok {
|
||||
st = colorscheme[color]
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
}
|
||||
// Add the regex, flags, and style
|
||||
// True because this is start-end
|
||||
rules = append(rules, SyntaxRule{regex, flags, true, st})
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// FindFileType finds the filetype for the given buffer
|
||||
func FindFileType(buf *Buffer) string {
|
||||
for r := range syntaxFiles {
|
||||
if r[0] != nil && r[0].MatchString(buf.Path) {
|
||||
// The syntax statement matches the extension
|
||||
return syntaxFiles[r].filetype
|
||||
} else if r[1] != nil && r[1].MatchString(buf.Line(0)) {
|
||||
// The header statement matches the first line
|
||||
return syntaxFiles[r].filetype
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// GetRules finds the syntax rules that should be used for the buffer
|
||||
// and returns them. It also returns the filetype of the file
|
||||
func GetRules(buf *Buffer) []SyntaxRule {
|
||||
for r := range syntaxFiles {
|
||||
if syntaxFiles[r].filetype == buf.FileType() {
|
||||
return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyntaxMatches is an alias to a map from character numbers to styles,
|
||||
// so map[3] represents the style of the third character
|
||||
type SyntaxMatches [][]tcell.Style
|
||||
|
||||
// Match takes a buffer and returns the syntax matches: a 2d array specifying how it should be syntax highlighted
|
||||
// We match the rules from up `synLinesUp` lines and down `synLinesDown` lines
|
||||
func Match(v *View) SyntaxMatches {
|
||||
buf := v.Buf
|
||||
rules := v.Buf.rules
|
||||
|
||||
viewStart := v.Topline
|
||||
viewEnd := v.Topline + v.height
|
||||
if viewEnd > buf.NumLines {
|
||||
viewEnd = buf.NumLines
|
||||
}
|
||||
|
||||
lines := buf.Lines(viewStart, viewEnd)
|
||||
matches := make(SyntaxMatches, len(lines))
|
||||
|
||||
for i, line := range lines {
|
||||
matches[i] = make([]tcell.Style, len(line)+1)
|
||||
for j := range matches[i] {
|
||||
matches[i][j] = defStyle
|
||||
}
|
||||
}
|
||||
|
||||
// We don't actually check the entire buffer, just from synLinesUp to synLinesDown
|
||||
totalStart := v.Topline - synLinesUp
|
||||
totalEnd := v.Topline + v.height + synLinesDown
|
||||
if totalStart < 0 {
|
||||
totalStart = 0
|
||||
}
|
||||
if totalEnd > buf.NumLines {
|
||||
totalEnd = buf.NumLines
|
||||
}
|
||||
|
||||
str := strings.Join(buf.Lines(totalStart, totalEnd), "\n")
|
||||
startNum := ToCharPos(Loc{0, totalStart}, v.Buf)
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.startend {
|
||||
if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
|
||||
for _, value := range indicies {
|
||||
value[0] = runePos(value[0], str) + startNum
|
||||
value[1] = runePos(value[1], str) + startNum
|
||||
startLoc := FromCharPos(value[0], buf)
|
||||
endLoc := FromCharPos(value[1], buf)
|
||||
for curLoc := startLoc; curLoc.LessThan(endLoc); curLoc = curLoc.Move(1, buf) {
|
||||
if curLoc.Y < v.Topline {
|
||||
continue
|
||||
}
|
||||
colNum, lineNum := curLoc.X, curLoc.Y
|
||||
if lineNum == -1 || colNum == -1 {
|
||||
continue
|
||||
}
|
||||
lineNum -= viewStart
|
||||
if lineNum >= 0 && lineNum < v.height {
|
||||
matches[lineNum][colNum] = rule.style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for lineN, line := range lines {
|
||||
if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
|
||||
for _, value := range indicies {
|
||||
start := runePos(value[0], line)
|
||||
end := runePos(value[1], line)
|
||||
for i := start; i < end; i++ {
|
||||
matches[lineN][i] = rule.style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/action"
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
ulua "github.com/micro-editor/micro/v2/internal/lua"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/shell"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ulua.L = lua.NewState()
|
||||
ulua.L.SetGlobal("import", luar.New(ulua.L, LuaImport))
|
||||
}
|
||||
|
||||
// LuaImport is meant to be called from lua by a plugin and will import the given micro package
|
||||
func LuaImport(pkg string) *lua.LTable {
|
||||
switch pkg {
|
||||
case "micro":
|
||||
return luaImportMicro()
|
||||
case "micro/shell":
|
||||
return luaImportMicroShell()
|
||||
case "micro/buffer":
|
||||
return luaImportMicroBuffer()
|
||||
case "micro/config":
|
||||
return luaImportMicroConfig()
|
||||
case "micro/util":
|
||||
return luaImportMicroUtil()
|
||||
default:
|
||||
return ulua.Import(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func luaImportMicro() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "TermMessage", luar.New(ulua.L, screen.TermMessage))
|
||||
ulua.L.SetField(pkg, "TermError", luar.New(ulua.L, screen.TermError))
|
||||
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, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
|
||||
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() *action.BufPane {
|
||||
return action.MainTab().CurPane()
|
||||
}))
|
||||
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab))
|
||||
ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList {
|
||||
return action.Tabs
|
||||
}))
|
||||
ulua.L.SetField(pkg, "After", luar.New(ulua.L, func(t time.Duration, f func()) {
|
||||
time.AfterFunc(t, func() {
|
||||
timerChan <- f
|
||||
})
|
||||
}))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroConfig() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "MakeCommand", luar.New(ulua.L, action.MakeCommand))
|
||||
ulua.L.SetField(pkg, "FileComplete", luar.New(ulua.L, buffer.FileComplete))
|
||||
ulua.L.SetField(pkg, "HelpComplete", luar.New(ulua.L, action.HelpComplete))
|
||||
ulua.L.SetField(pkg, "OptionComplete", luar.New(ulua.L, action.OptionComplete))
|
||||
ulua.L.SetField(pkg, "OptionValueComplete", luar.New(ulua.L, action.OptionValueComplete))
|
||||
ulua.L.SetField(pkg, "NoComplete", luar.New(ulua.L, nil))
|
||||
ulua.L.SetField(pkg, "TryBindKey", luar.New(ulua.L, action.TryBindKeyPlug))
|
||||
ulua.L.SetField(pkg, "Reload", luar.New(ulua.L, action.ReloadConfig))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFileFromMemory", luar.New(ulua.L, config.PluginAddRuntimeFileFromMemory))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFilesFromDirectory", luar.New(ulua.L, config.PluginAddRuntimeFilesFromDirectory))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFile", luar.New(ulua.L, config.PluginAddRuntimeFile))
|
||||
ulua.L.SetField(pkg, "ListRuntimeFiles", luar.New(ulua.L, config.PluginListRuntimeFiles))
|
||||
ulua.L.SetField(pkg, "ReadRuntimeFile", luar.New(ulua.L, config.PluginReadRuntimeFile))
|
||||
ulua.L.SetField(pkg, "NewRTFiletype", luar.New(ulua.L, config.NewRTFiletype))
|
||||
ulua.L.SetField(pkg, "RTColorscheme", luar.New(ulua.L, config.RTColorscheme))
|
||||
ulua.L.SetField(pkg, "RTSyntax", luar.New(ulua.L, config.RTSyntax))
|
||||
ulua.L.SetField(pkg, "RTHelp", luar.New(ulua.L, config.RTHelp))
|
||||
ulua.L.SetField(pkg, "RTPlugin", luar.New(ulua.L, config.RTPlugin))
|
||||
ulua.L.SetField(pkg, "RegisterCommonOption", luar.New(ulua.L, config.RegisterCommonOptionPlug))
|
||||
ulua.L.SetField(pkg, "RegisterGlobalOption", luar.New(ulua.L, config.RegisterGlobalOptionPlug))
|
||||
ulua.L.SetField(pkg, "GetGlobalOption", luar.New(ulua.L, config.GetGlobalOption))
|
||||
ulua.L.SetField(pkg, "SetGlobalOption", luar.New(ulua.L, action.SetGlobalOptionPlug))
|
||||
ulua.L.SetField(pkg, "SetGlobalOptionNative", luar.New(ulua.L, action.SetGlobalOptionNativePlug))
|
||||
ulua.L.SetField(pkg, "ConfigDir", luar.New(ulua.L, config.ConfigDir))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroShell() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "ExecCommand", luar.New(ulua.L, shell.ExecCommand))
|
||||
ulua.L.SetField(pkg, "RunCommand", luar.New(ulua.L, shell.RunCommand))
|
||||
ulua.L.SetField(pkg, "RunBackgroundShell", luar.New(ulua.L, shell.RunBackgroundShell))
|
||||
ulua.L.SetField(pkg, "RunInteractiveShell", luar.New(ulua.L, shell.RunInteractiveShell))
|
||||
ulua.L.SetField(pkg, "JobStart", luar.New(ulua.L, shell.JobStart))
|
||||
ulua.L.SetField(pkg, "JobSpawn", luar.New(ulua.L, shell.JobSpawn))
|
||||
ulua.L.SetField(pkg, "JobStop", luar.New(ulua.L, shell.JobStop))
|
||||
ulua.L.SetField(pkg, "JobSend", luar.New(ulua.L, shell.JobSend))
|
||||
ulua.L.SetField(pkg, "RunTermEmulator", luar.New(ulua.L, action.RunTermEmulator))
|
||||
ulua.L.SetField(pkg, "TermEmuSupported", luar.New(ulua.L, action.TermEmuSupported))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroBuffer() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "NewMessage", luar.New(ulua.L, buffer.NewMessage))
|
||||
ulua.L.SetField(pkg, "NewMessageAtLine", luar.New(ulua.L, buffer.NewMessageAtLine))
|
||||
ulua.L.SetField(pkg, "MTInfo", luar.New(ulua.L, buffer.MTInfo))
|
||||
ulua.L.SetField(pkg, "MTWarning", luar.New(ulua.L, buffer.MTWarning))
|
||||
ulua.L.SetField(pkg, "MTError", luar.New(ulua.L, buffer.MTError))
|
||||
ulua.L.SetField(pkg, "Loc", luar.New(ulua.L, func(x, y int) buffer.Loc {
|
||||
return buffer.Loc{x, y}
|
||||
}))
|
||||
ulua.L.SetField(pkg, "SLoc", luar.New(ulua.L, func(line, row int) display.SLoc {
|
||||
return display.SLoc{line, row}
|
||||
}))
|
||||
ulua.L.SetField(pkg, "BTDefault", luar.New(ulua.L, buffer.BTDefault.Kind))
|
||||
ulua.L.SetField(pkg, "BTHelp", luar.New(ulua.L, buffer.BTHelp.Kind))
|
||||
ulua.L.SetField(pkg, "BTLog", luar.New(ulua.L, buffer.BTLog.Kind))
|
||||
ulua.L.SetField(pkg, "BTScratch", luar.New(ulua.L, buffer.BTScratch.Kind))
|
||||
ulua.L.SetField(pkg, "BTRaw", luar.New(ulua.L, buffer.BTRaw.Kind))
|
||||
ulua.L.SetField(pkg, "BTInfo", luar.New(ulua.L, buffer.BTInfo.Kind))
|
||||
ulua.L.SetField(pkg, "NewBuffer", luar.New(ulua.L, func(text, path string) *buffer.Buffer {
|
||||
return buffer.NewBufferFromString(text, path, buffer.BTDefault)
|
||||
}))
|
||||
ulua.L.SetField(pkg, "NewBufferFromFile", luar.New(ulua.L, func(path string) (*buffer.Buffer, error) {
|
||||
return buffer.NewBufferFromFile(path, buffer.BTDefault)
|
||||
}))
|
||||
ulua.L.SetField(pkg, "ByteOffset", luar.New(ulua.L, buffer.ByteOffset))
|
||||
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, buffer.WriteLog))
|
||||
ulua.L.SetField(pkg, "LogBuf", luar.New(ulua.L, buffer.GetLogBuf))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroUtil() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "RuneAt", luar.New(ulua.L, util.LuaRuneAt))
|
||||
ulua.L.SetField(pkg, "GetLeadingWhitespace", luar.New(ulua.L, util.LuaGetLeadingWhitespace))
|
||||
ulua.L.SetField(pkg, "IsWordChar", luar.New(ulua.L, util.LuaIsWordChar))
|
||||
ulua.L.SetField(pkg, "String", luar.New(ulua.L, util.String))
|
||||
ulua.L.SetField(pkg, "Unzip", luar.New(ulua.L, util.Unzip))
|
||||
ulua.L.SetField(pkg, "Version", luar.New(ulua.L, util.Version))
|
||||
ulua.L.SetField(pkg, "SemVersion", luar.New(ulua.L, util.SemVersion))
|
||||
ulua.L.SetField(pkg, "HttpRequest", luar.New(ulua.L, util.HttpRequest))
|
||||
ulua.L.SetField(pkg, "CharacterCountInString", luar.New(ulua.L, util.CharacterCountInString))
|
||||
ulua.L.SetField(pkg, "RuneStr", luar.New(ulua.L, func(r rune) string {
|
||||
return string(r)
|
||||
}))
|
||||
|
||||
return pkg
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
package shell
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Jobs chan JobFunction
|
||||
|
||||
func init() {
|
||||
Jobs = make(chan JobFunction, 100)
|
||||
}
|
||||
|
||||
// Jobs are the way plugins can run processes in the background
|
||||
// A job is simply a process that gets executed asynchronously
|
||||
// There are callbacks for when the job exits, when the job creates stdout
|
||||
@@ -24,75 +19,69 @@ func init() {
|
||||
// JobFunction is a representation of a job (this data structure is what is loaded
|
||||
// into the jobs channel)
|
||||
type JobFunction struct {
|
||||
Function func(string, []any)
|
||||
Output string
|
||||
Args []any
|
||||
function func(string, ...string)
|
||||
output string
|
||||
args []string
|
||||
}
|
||||
|
||||
// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events
|
||||
type CallbackFile struct {
|
||||
io.Writer
|
||||
|
||||
callback func(string, []any)
|
||||
args []any
|
||||
}
|
||||
|
||||
// Job stores the executing command for the job, and the stdin pipe
|
||||
type Job struct {
|
||||
*exec.Cmd
|
||||
Stdin io.WriteCloser
|
||||
callback func(string, ...string)
|
||||
args []string
|
||||
}
|
||||
|
||||
func (f *CallbackFile) Write(data []byte) (int, error) {
|
||||
// This is either stderr or stdout
|
||||
// In either case we create a new job function callback and put it in the jobs channel
|
||||
jobFunc := JobFunction{f.callback, string(data), f.args}
|
||||
Jobs <- jobFunc
|
||||
jobs <- jobFunc
|
||||
return f.Writer.Write(data)
|
||||
}
|
||||
|
||||
// JobStart starts a shell command in the background with the given callbacks
|
||||
// JobStart starts a process in the background with the given callbacks
|
||||
// It returns an *exec.Cmd as the job id
|
||||
func JobStart(cmd string, onStdout, onStderr, onExit func(string, []any), userargs ...any) *Job {
|
||||
return JobSpawn("sh", []string{"-c", cmd}, onStdout, onStderr, onExit, userargs...)
|
||||
}
|
||||
func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
|
||||
split := strings.Split(cmd, " ")
|
||||
args := split[1:]
|
||||
cmdName := split[0]
|
||||
|
||||
// JobSpawn starts a process with args in the background with the given callbacks
|
||||
// It returns an *exec.Cmd as the job id
|
||||
func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit func(string, []any), userargs ...any) *Job {
|
||||
// Set up everything correctly if the functions have been provided
|
||||
proc := exec.Command(cmdName, cmdArgs...)
|
||||
proc := exec.Command(cmdName, args...)
|
||||
var outbuf bytes.Buffer
|
||||
if onStdout != nil {
|
||||
proc.Stdout = &CallbackFile{&outbuf, onStdout, userargs}
|
||||
if onStdout != "" {
|
||||
proc.Stdout = &CallbackFile{&outbuf, LuaFunctionJob(onStdout), userargs}
|
||||
} else {
|
||||
proc.Stdout = &outbuf
|
||||
}
|
||||
if onStderr != nil {
|
||||
proc.Stderr = &CallbackFile{&outbuf, onStderr, userargs}
|
||||
if onStderr != "" {
|
||||
proc.Stderr = &CallbackFile{&outbuf, LuaFunctionJob(onStderr), userargs}
|
||||
} else {
|
||||
proc.Stderr = &outbuf
|
||||
}
|
||||
stdin, _ := proc.StdinPipe()
|
||||
|
||||
go func() {
|
||||
// Run the process in the background and create the onExit callback
|
||||
proc.Run()
|
||||
if onExit != nil {
|
||||
jobFunc := JobFunction{onExit, outbuf.String(), userargs}
|
||||
Jobs <- jobFunc
|
||||
}
|
||||
jobFunc := JobFunction{LuaFunctionJob(onExit), string(outbuf.Bytes()), userargs}
|
||||
jobs <- jobFunc
|
||||
}()
|
||||
|
||||
return &Job{proc, stdin}
|
||||
return proc
|
||||
}
|
||||
|
||||
// JobStop kills a job
|
||||
func JobStop(j *Job) {
|
||||
j.Process.Kill()
|
||||
func JobStop(cmd *exec.Cmd) {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
|
||||
// JobSend sends the given data into the job's stdin stream
|
||||
func JobSend(j *Job, data string) {
|
||||
j.Stdin.Write([]byte(data))
|
||||
func JobSend(cmd *exec.Cmd, data string) {
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stdin.Write([]byte(data))
|
||||
}
|
||||
149
cmd/micro/lineArray.go
Normal file
149
cmd/micro/lineArray.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func runeToByteIndex(n int, txt []byte) int {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
i := 0
|
||||
for len(txt) > 0 {
|
||||
_, size := utf8.DecodeRune(txt)
|
||||
|
||||
txt = txt[size:]
|
||||
count += size
|
||||
i++
|
||||
|
||||
if i == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// A LineArray simply stores and array of lines and makes it easy to insert
|
||||
// and delete in it
|
||||
type LineArray struct {
|
||||
lines [][]byte
|
||||
}
|
||||
|
||||
// NewLineArray returns a new line array from an array of bytes
|
||||
func NewLineArray(text []byte) *LineArray {
|
||||
la := new(LineArray)
|
||||
// Split the bytes into lines
|
||||
split := bytes.Split(text, []byte("\n"))
|
||||
la.lines = make([][]byte, len(split))
|
||||
for i := range split {
|
||||
la.lines[i] = make([]byte, len(split[i]))
|
||||
copy(la.lines[i], split[i])
|
||||
}
|
||||
|
||||
return la
|
||||
}
|
||||
|
||||
// Returns the String representation of the LineArray
|
||||
func (la *LineArray) String() string {
|
||||
return string(bytes.Join(la.lines, []byte("\n")))
|
||||
}
|
||||
|
||||
// NewlineBelow adds a newline below the given line number
|
||||
func (la *LineArray) NewlineBelow(y int) {
|
||||
la.lines = append(la.lines, []byte(" "))
|
||||
copy(la.lines[y+2:], la.lines[y+1:])
|
||||
la.lines[y+1] = []byte("")
|
||||
}
|
||||
|
||||
// inserts a byte array at a given location
|
||||
func (la *LineArray) insert(pos Loc, value []byte) {
|
||||
x, y := runeToByteIndex(pos.X, la.lines[pos.Y]), pos.Y
|
||||
// x, y := pos.x, pos.y
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] == '\n' {
|
||||
la.Split(Loc{x, y})
|
||||
x = 0
|
||||
y++
|
||||
continue
|
||||
}
|
||||
la.insertByte(Loc{x, y}, value[i])
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// inserts a byte at a given location
|
||||
func (la *LineArray) insertByte(pos Loc, value byte) {
|
||||
la.lines[pos.Y] = append(la.lines[pos.Y], 0)
|
||||
copy(la.lines[pos.Y][pos.X+1:], la.lines[pos.Y][pos.X:])
|
||||
la.lines[pos.Y][pos.X] = value
|
||||
}
|
||||
|
||||
// JoinLines joins the two lines a and b
|
||||
func (la *LineArray) JoinLines(a, b int) {
|
||||
la.insert(Loc{len(la.lines[a]), a}, la.lines[b])
|
||||
la.DeleteLine(b)
|
||||
}
|
||||
|
||||
// Split splits a line at a given position
|
||||
func (la *LineArray) Split(pos Loc) {
|
||||
la.NewlineBelow(pos.Y)
|
||||
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y][pos.X:])
|
||||
la.DeleteToEnd(Loc{pos.X, pos.Y})
|
||||
}
|
||||
|
||||
// removes from start to end
|
||||
func (la *LineArray) remove(start, end Loc) string {
|
||||
sub := la.Substr(start, end)
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y])
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y])
|
||||
if start.Y == end.Y {
|
||||
la.lines[start.Y] = append(la.lines[start.Y][:startX], la.lines[start.Y][endX:]...)
|
||||
} else {
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
la.DeleteLine(start.Y + 1)
|
||||
}
|
||||
la.DeleteToEnd(Loc{startX, start.Y})
|
||||
la.DeleteFromStart(Loc{endX - 1, start.Y + 1})
|
||||
la.JoinLines(start.Y, start.Y+1)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// DeleteToEnd deletes from the end of a line to the position
|
||||
func (la *LineArray) DeleteToEnd(pos Loc) {
|
||||
la.lines[pos.Y] = la.lines[pos.Y][:pos.X]
|
||||
}
|
||||
|
||||
// DeleteFromStart deletes from the start of a line to the position
|
||||
func (la *LineArray) DeleteFromStart(pos Loc) {
|
||||
la.lines[pos.Y] = la.lines[pos.Y][pos.X+1:]
|
||||
}
|
||||
|
||||
// DeleteLine deletes the line number
|
||||
func (la *LineArray) DeleteLine(y int) {
|
||||
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
|
||||
}
|
||||
|
||||
// DeleteByte deletes the byte at a position
|
||||
func (la *LineArray) DeleteByte(pos Loc) {
|
||||
la.lines[pos.Y] = la.lines[pos.Y][:pos.X+copy(la.lines[pos.Y][pos.X:], la.lines[pos.Y][pos.X+1:])]
|
||||
}
|
||||
|
||||
// Substr returns the string representation between two locations
|
||||
func (la *LineArray) Substr(start, end Loc) string {
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y])
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y])
|
||||
if start.Y == end.Y {
|
||||
return string(la.lines[start.Y][startX:endX])
|
||||
}
|
||||
var str string
|
||||
str += string(la.lines[start.Y][startX:]) + "\n"
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
str += string(la.lines[i]) + "\n"
|
||||
}
|
||||
str += string(la.lines[end.Y][:endX])
|
||||
return str
|
||||
}
|
||||
127
cmd/micro/loc.go
Normal file
127
cmd/micro/loc.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
// FromCharPos converts from a character position to an x, y position
|
||||
func FromCharPos(loc int, buf *Buffer) Loc {
|
||||
charNum := 0
|
||||
x, y := 0, 0
|
||||
|
||||
lineLen := Count(buf.Line(y)) + 1
|
||||
for charNum+lineLen <= loc {
|
||||
charNum += lineLen
|
||||
y++
|
||||
lineLen = Count(buf.Line(y)) + 1
|
||||
}
|
||||
x = loc - charNum
|
||||
|
||||
return Loc{x, y}
|
||||
}
|
||||
|
||||
// ToCharPos converts from an x, y position to a character position
|
||||
func ToCharPos(start Loc, buf *Buffer) int {
|
||||
x, y := start.X, start.Y
|
||||
loc := 0
|
||||
for i := 0; i < y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += Count(buf.Line(i)) + 1
|
||||
}
|
||||
loc += x
|
||||
return loc
|
||||
}
|
||||
|
||||
// Loc stores a location
|
||||
type Loc struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// LessThan returns true if b is smaller
|
||||
func (l Loc) LessThan(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GreaterThan returns true if b is bigger
|
||||
func (l Loc) GreaterThan(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GreaterEqual returns true if b is greater than or equal to b
|
||||
func (l Loc) GreaterEqual(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
if l == b {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LessEqual returns true if b is less than or equal to b
|
||||
func (l Loc) LessEqual(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
if l == b {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This moves the location one character to the right
|
||||
func (l Loc) right(buf *Buffer) Loc {
|
||||
if l == buf.End() {
|
||||
return Loc{l.X + 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X < Count(buf.Line(l.Y)) {
|
||||
res = Loc{l.X + 1, l.Y}
|
||||
} else {
|
||||
res = Loc{0, l.Y + 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// This moves the given location one character to the left
|
||||
func (l Loc) left(buf *Buffer) Loc {
|
||||
if l == buf.Start() {
|
||||
return Loc{l.X - 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X > 0 {
|
||||
res = Loc{l.X - 1, l.Y}
|
||||
} else {
|
||||
res = Loc{Count(buf.Line(l.Y - 1)), l.Y - 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Move moves the cursor n characters to the left or right
|
||||
// It moves the cursor left if n is negative
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
l = l.right(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
for i := 0; i < Abs(n); i++ {
|
||||
l = l.left(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
388
cmd/micro/messenger.go
Normal file
388
cmd/micro/messenger.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// TermMessage sends a message to the user in the terminal. This usually occurs before
|
||||
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
|
||||
// regular expressions
|
||||
// The function must be called when the screen is not initialized
|
||||
// This will write the message, and wait for the user
|
||||
// to press and key to continue
|
||||
func TermMessage(msg ...interface{}) {
|
||||
screenWasNil := screen == nil
|
||||
if !screenWasNil {
|
||||
screen.Fini()
|
||||
}
|
||||
|
||||
fmt.Println(msg...)
|
||||
fmt.Print("\nPress enter to continue")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
reader.ReadString('\n')
|
||||
|
||||
if !screenWasNil {
|
||||
InitScreen()
|
||||
}
|
||||
}
|
||||
|
||||
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
|
||||
// as an error
|
||||
func TermError(filename string, lineNum int, err string) {
|
||||
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
|
||||
}
|
||||
|
||||
// Messenger is an object that makes it easy to send messages to the user
|
||||
// and get input from the user
|
||||
type Messenger struct {
|
||||
// Are we currently prompting the user?
|
||||
hasPrompt bool
|
||||
// Is there a message to print
|
||||
hasMessage bool
|
||||
|
||||
// Message to print
|
||||
message string
|
||||
// The user's response to a prompt
|
||||
response string
|
||||
// style to use when drawing the message
|
||||
style tcell.Style
|
||||
|
||||
// We have to keep track of the cursor for prompting
|
||||
cursorx int
|
||||
|
||||
// This map stores the history for all the different kinds of uses Prompt has
|
||||
// It's a map of history type -> history array
|
||||
history map[string][]string
|
||||
historyNum int
|
||||
|
||||
// Is the current message a message from the gutter
|
||||
gutterMessage bool
|
||||
}
|
||||
|
||||
// Message sends a message to the user
|
||||
func (m *Messenger) Message(msg ...interface{}) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprint(buf, msg...)
|
||||
m.message = buf.String()
|
||||
m.style = defStyle
|
||||
|
||||
if _, ok := colorscheme["message"]; ok {
|
||||
m.style = colorscheme["message"]
|
||||
}
|
||||
m.hasMessage = true
|
||||
}
|
||||
|
||||
// Error sends an error message to the user
|
||||
func (m *Messenger) Error(msg ...interface{}) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprint(buf, msg...)
|
||||
m.message = buf.String()
|
||||
m.style = defStyle.
|
||||
Foreground(tcell.ColorBlack).
|
||||
Background(tcell.ColorMaroon)
|
||||
|
||||
if _, ok := colorscheme["error-message"]; ok {
|
||||
m.style = colorscheme["error-message"]
|
||||
}
|
||||
m.hasMessage = true
|
||||
}
|
||||
|
||||
// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
|
||||
func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
|
||||
m.hasPrompt = true
|
||||
m.Message(prompt)
|
||||
|
||||
_, h := screen.Size()
|
||||
for {
|
||||
m.Clear()
|
||||
m.Display()
|
||||
screen.ShowCursor(Count(m.message), h-1)
|
||||
screen.Show()
|
||||
event := <-events
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyRune:
|
||||
if e.Rune() == 'y' {
|
||||
m.hasPrompt = false
|
||||
return true, false
|
||||
} else if e.Rune() == 'n' {
|
||||
m.hasPrompt = false
|
||||
return false, false
|
||||
}
|
||||
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
|
||||
m.hasPrompt = false
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LetterPrompt gives the user a prompt and waits for a one letter response
|
||||
func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
|
||||
m.hasPrompt = true
|
||||
m.Message(prompt)
|
||||
|
||||
_, h := screen.Size()
|
||||
for {
|
||||
m.Clear()
|
||||
m.Display()
|
||||
screen.ShowCursor(Count(m.message), h-1)
|
||||
screen.Show()
|
||||
event := <-events
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyRune:
|
||||
for _, r := range responses {
|
||||
if e.Rune() == r {
|
||||
m.Clear()
|
||||
m.Reset()
|
||||
m.hasPrompt = false
|
||||
return r, false
|
||||
}
|
||||
}
|
||||
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
|
||||
m.Clear()
|
||||
m.Reset()
|
||||
m.hasPrompt = false
|
||||
return ' ', true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Completion int
|
||||
|
||||
const (
|
||||
NoCompletion Completion = iota
|
||||
FileCompletion
|
||||
CommandCompletion
|
||||
HelpCompletion
|
||||
OptionCompletion
|
||||
)
|
||||
|
||||
// Prompt sends the user a message and waits for a response to be typed in
|
||||
// This function blocks the main loop while waiting for input
|
||||
func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) {
|
||||
m.hasPrompt = true
|
||||
m.Message(prompt)
|
||||
if _, ok := m.history[historyType]; !ok {
|
||||
m.history[historyType] = []string{""}
|
||||
} else {
|
||||
m.history[historyType] = append(m.history[historyType], "")
|
||||
}
|
||||
m.historyNum = len(m.history[historyType]) - 1
|
||||
|
||||
response, canceled := "", true
|
||||
|
||||
RedrawAll()
|
||||
for m.hasPrompt {
|
||||
var suggestions []string
|
||||
m.Clear()
|
||||
|
||||
event := <-events
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
|
||||
// Cancel
|
||||
m.hasPrompt = false
|
||||
case tcell.KeyEnter:
|
||||
// User is done entering their response
|
||||
m.hasPrompt = false
|
||||
response, canceled = m.response, false
|
||||
m.history[historyType][len(m.history[historyType])-1] = response
|
||||
case tcell.KeyTab:
|
||||
args := SplitCommandArgs(m.response)
|
||||
currentArgNum := len(args) - 1
|
||||
currentArg := args[currentArgNum]
|
||||
var completionType Completion
|
||||
|
||||
if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
|
||||
if command, ok := commands[args[0]]; ok {
|
||||
completionTypes = append([]Completion{CommandCompletion}, command.completions...)
|
||||
}
|
||||
}
|
||||
|
||||
if currentArgNum >= len(completionTypes) {
|
||||
completionType = completionTypes[len(completionTypes)-1]
|
||||
} else {
|
||||
completionType = completionTypes[currentArgNum]
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if completionType == FileCompletion {
|
||||
chosen, suggestions = FileComplete(currentArg)
|
||||
} else if completionType == CommandCompletion {
|
||||
chosen, suggestions = CommandComplete(currentArg)
|
||||
} else if completionType == HelpCompletion {
|
||||
chosen, suggestions = HelpComplete(currentArg)
|
||||
} else if completionType == OptionCompletion {
|
||||
chosen, suggestions = OptionComplete(currentArg)
|
||||
} else if completionType < NoCompletion {
|
||||
chosen, suggestions = PluginComplete(completionType, currentArg)
|
||||
}
|
||||
|
||||
if len(suggestions) > 1 {
|
||||
chosen = chosen + CommonSubstring(suggestions...)
|
||||
}
|
||||
|
||||
if chosen != "" {
|
||||
m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...)
|
||||
m.cursorx = Count(m.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.HandleEvent(event, m.history[historyType])
|
||||
|
||||
m.Clear()
|
||||
for _, v := range tabs[curTab].views {
|
||||
v.Display()
|
||||
}
|
||||
DisplayTabs()
|
||||
m.Display()
|
||||
if len(suggestions) > 1 {
|
||||
m.DisplaySuggestions(suggestions)
|
||||
}
|
||||
screen.Show()
|
||||
}
|
||||
|
||||
m.Clear()
|
||||
m.Reset()
|
||||
return response, canceled
|
||||
}
|
||||
|
||||
// HandleEvent handles an event for the prompter
|
||||
func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyUp:
|
||||
if m.historyNum > 0 {
|
||||
m.historyNum--
|
||||
m.response = history[m.historyNum]
|
||||
m.cursorx = Count(m.response)
|
||||
}
|
||||
case tcell.KeyDown:
|
||||
if m.historyNum < len(history)-1 {
|
||||
m.historyNum++
|
||||
m.response = history[m.historyNum]
|
||||
m.cursorx = Count(m.response)
|
||||
}
|
||||
case tcell.KeyLeft:
|
||||
if m.cursorx > 0 {
|
||||
m.cursorx--
|
||||
}
|
||||
case tcell.KeyRight:
|
||||
if m.cursorx < Count(m.response) {
|
||||
m.cursorx++
|
||||
}
|
||||
case tcell.KeyBackspace2, tcell.KeyBackspace:
|
||||
if m.cursorx > 0 {
|
||||
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
|
||||
m.cursorx--
|
||||
}
|
||||
case tcell.KeyCtrlV:
|
||||
clip, _ := clipboard.ReadAll("clipboard")
|
||||
m.response = Insert(m.response, m.cursorx, clip)
|
||||
m.cursorx += Count(clip)
|
||||
case tcell.KeyRune:
|
||||
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
|
||||
m.cursorx++
|
||||
}
|
||||
history[m.historyNum] = m.response
|
||||
|
||||
case *tcell.EventPaste:
|
||||
clip := e.Text()
|
||||
m.response = Insert(m.response, m.cursorx, clip)
|
||||
m.cursorx += Count(clip)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the messenger's cursor, message and response
|
||||
func (m *Messenger) Reset() {
|
||||
m.cursorx = 0
|
||||
m.message = ""
|
||||
m.response = ""
|
||||
}
|
||||
|
||||
// Clear clears the line at the bottom of the editor
|
||||
func (m *Messenger) Clear() {
|
||||
w, h := screen.Size()
|
||||
for x := 0; x < w; x++ {
|
||||
screen.SetContent(x, h-1, ' ', nil, defStyle)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Messenger) DisplaySuggestions(suggestions []string) {
|
||||
w, screenH := screen.Size()
|
||||
|
||||
y := screenH - 2
|
||||
|
||||
statusLineStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
for x := 0; x < w; x++ {
|
||||
screen.SetContent(x, y, ' ', nil, statusLineStyle)
|
||||
}
|
||||
|
||||
x := 0
|
||||
for _, suggestion := range suggestions {
|
||||
for _, c := range suggestion {
|
||||
screen.SetContent(x, y, c, nil, statusLineStyle)
|
||||
x++
|
||||
}
|
||||
screen.SetContent(x, y, ' ', nil, statusLineStyle)
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// Display displays messages or prompts
|
||||
func (m *Messenger) Display() {
|
||||
_, h := screen.Size()
|
||||
if m.hasMessage {
|
||||
if m.hasPrompt || globalSettings["infobar"].(bool) {
|
||||
runes := []rune(m.message + m.response)
|
||||
for x := 0; x < len(runes); x++ {
|
||||
screen.SetContent(x, h-1, runes[x], nil, m.style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.hasPrompt {
|
||||
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
|
||||
screen.Show()
|
||||
}
|
||||
}
|
||||
|
||||
// A GutterMessage is a message displayed on the side of the editor
|
||||
type GutterMessage struct {
|
||||
lineNum int
|
||||
msg string
|
||||
kind int
|
||||
}
|
||||
|
||||
// These are the different types of messages
|
||||
const (
|
||||
// GutterInfo represents a simple info message
|
||||
GutterInfo = iota
|
||||
// GutterWarning represents a compiler warning
|
||||
GutterWarning
|
||||
// GutterError represents a compiler error
|
||||
GutterError
|
||||
)
|
||||
@@ -3,150 +3,69 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"sort"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
"github.com/micro-editor/micro/v2/internal/action"
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/clipboard"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/shell"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/layeh/gopher-luar"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/tcell"
|
||||
"github.com/zyedidia/tcell/encoding"
|
||||
)
|
||||
|
||||
const (
|
||||
synLinesUp = 75 // How many lines up to look to do syntax highlighting
|
||||
synLinesDown = 75 // How many lines down to look to do syntax highlighting
|
||||
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
|
||||
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
|
||||
)
|
||||
|
||||
var (
|
||||
// Command line flags
|
||||
flagVersion = flag.Bool("version", false, "Show the version number and information")
|
||||
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
|
||||
flagOptions = flag.Bool("options", false, "Show all option help")
|
||||
flagDebug = flag.Bool("debug", false, "Enable debug mode (prints debug info to ./log.txt)")
|
||||
flagProfile = flag.Bool("profile", false, "Enable CPU profiling (writes profile info to ./micro.prof)")
|
||||
flagPlugin = flag.String("plugin", "", "Plugin command")
|
||||
flagClean = flag.Bool("clean", false, "Clean configuration directory")
|
||||
optionFlags map[string]*string
|
||||
// The main screen
|
||||
screen tcell.Screen
|
||||
|
||||
sighup chan os.Signal
|
||||
// Object to send messages and prompts to the user
|
||||
messenger *Messenger
|
||||
|
||||
timerChan chan func()
|
||||
// The default highlighting style
|
||||
// This simply defines the default foreground and background colors
|
||||
defStyle tcell.Style
|
||||
|
||||
// Where the user's configuration is
|
||||
// This should be $XDG_CONFIG_HOME/micro
|
||||
// If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
|
||||
configDir string
|
||||
|
||||
// Version is the version number or commit hash
|
||||
// These variables should be set by the linker when compiling
|
||||
Version = "Unknown"
|
||||
CommitHash = "Unknown"
|
||||
CompileDate = "Unknown"
|
||||
|
||||
// L is the lua state
|
||||
// This is the VM that runs the plugins
|
||||
L *lua.LState
|
||||
|
||||
// The list of views
|
||||
tabs []*Tab
|
||||
// This is the currently open tab
|
||||
// It's just an index to the tab in the tabs array
|
||||
curTab int
|
||||
|
||||
// Channel of jobs running in the background
|
||||
jobs chan JobFunction
|
||||
// Event channel
|
||||
events chan tcell.Event
|
||||
)
|
||||
|
||||
func InitFlags() {
|
||||
// Note: keep this in sync with the man page in assets/packaging/micro.1
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: micro [OPTION]... [FILE]... [+LINE[:COL]] [+/REGEX]")
|
||||
fmt.Println(" micro [OPTION]... [FILE[:LINE[:COL]]]... (only if the `parsecursor` option is enabled)")
|
||||
fmt.Println("-clean")
|
||||
fmt.Println(" \tClean the configuration directory and exit")
|
||||
fmt.Println("-config-dir dir")
|
||||
fmt.Println(" \tSpecify a custom location for the configuration directory")
|
||||
fmt.Println("FILE:LINE[:COL] (only if the `parsecursor` option is enabled)")
|
||||
fmt.Println("FILE +LINE[:COL]")
|
||||
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
|
||||
fmt.Println("+/REGEX")
|
||||
fmt.Println(" \tSpecify a regex to search for when opening a buffer")
|
||||
fmt.Println("-options")
|
||||
fmt.Println(" \tShow all options help and exit")
|
||||
fmt.Println("-debug")
|
||||
fmt.Println(" \tEnable debug mode (enables logging to ./log.txt)")
|
||||
fmt.Println("-profile")
|
||||
fmt.Println(" \tEnable CPU profiling (writes profile info to ./micro.prof")
|
||||
fmt.Println(" \tso it can be analyzed later with \"go tool pprof micro.prof\")")
|
||||
fmt.Println("-version")
|
||||
fmt.Println(" \tShow the version number and information and exit")
|
||||
|
||||
fmt.Print("\nMicro's plugins can be managed at the command line with the following commands.\n")
|
||||
fmt.Println("-plugin install [PLUGIN]...")
|
||||
fmt.Println(" \tInstall plugin(s)")
|
||||
fmt.Println("-plugin remove [PLUGIN]...")
|
||||
fmt.Println(" \tRemove plugin(s)")
|
||||
fmt.Println("-plugin update [PLUGIN]...")
|
||||
fmt.Println(" \tUpdate plugin(s) (if no argument is given, updates all plugins)")
|
||||
fmt.Println("-plugin search [PLUGIN]...")
|
||||
fmt.Println(" \tSearch for a plugin")
|
||||
fmt.Println("-plugin list")
|
||||
fmt.Println(" \tList installed plugins")
|
||||
fmt.Println("-plugin available")
|
||||
fmt.Println(" \tList available plugins")
|
||||
|
||||
fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
|
||||
fmt.Println("-<option> value")
|
||||
fmt.Println(" \tSet `option` to `value` for this session")
|
||||
fmt.Println(" \tFor example: `micro -syntax off file.c`")
|
||||
fmt.Println("\nUse `micro -options` to see the full list of configuration options")
|
||||
}
|
||||
|
||||
optionFlags = make(map[string]*string)
|
||||
|
||||
for k, v := range config.DefaultAllSettings() {
|
||||
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v))
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *flagVersion {
|
||||
// If -version was passed
|
||||
fmt.Println("Version:", util.Version)
|
||||
fmt.Println("Commit hash:", util.CommitHash)
|
||||
fmt.Println("Compiled on", util.CompileDate)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
if *flagOptions {
|
||||
// If -options was passed
|
||||
var keys []string
|
||||
m := config.DefaultAllSettings()
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
v := m[k]
|
||||
fmt.Printf("-%s value\n", k)
|
||||
fmt.Printf(" \tDefault value: '%v'\n", v)
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
|
||||
if util.Debug == "OFF" && *flagDebug {
|
||||
util.Debug = "ON"
|
||||
}
|
||||
}
|
||||
|
||||
// DoPluginFlags parses and executes any flags that require LoadAllPlugins (-plugin and -clean)
|
||||
func DoPluginFlags() {
|
||||
if *flagClean || *flagPlugin != "" {
|
||||
config.LoadAllPlugins()
|
||||
|
||||
if *flagPlugin != "" {
|
||||
args := flag.Args()
|
||||
|
||||
config.PluginCommand(os.Stdout, *flagPlugin, args)
|
||||
} else if *flagClean {
|
||||
CleanConfig()
|
||||
}
|
||||
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadInput determines which files should be loaded into buffers
|
||||
// based on the input stored in flag.Args()
|
||||
func LoadInput(args []string) []*buffer.Buffer {
|
||||
func LoadInput() []*Buffer {
|
||||
// There are a number of ways micro should start given its input
|
||||
|
||||
// 1. If it is given a files in flag.Args(), it should open those
|
||||
@@ -158,394 +77,345 @@ func LoadInput(args []string) []*buffer.Buffer {
|
||||
// 3. If there is no input file and the input is a terminal, an empty buffer
|
||||
// should be opened
|
||||
|
||||
buffers := make([]*buffer.Buffer, 0, len(args))
|
||||
var filename string
|
||||
var input []byte
|
||||
var err error
|
||||
var buffers []*Buffer
|
||||
|
||||
files := make([]string, 0, len(args))
|
||||
|
||||
flagStartPos := buffer.Loc{-1, -1}
|
||||
posFlagr := regexp.MustCompile(`^\+(\d+)(?::(\d+))?$`)
|
||||
posIndex := -1
|
||||
|
||||
searchText := ""
|
||||
searchFlagr := regexp.MustCompile(`^\+\/(.+)$`)
|
||||
searchIndex := -1
|
||||
|
||||
for i, a := range args {
|
||||
posMatch := posFlagr.FindStringSubmatch(a)
|
||||
if len(posMatch) == 3 && posMatch[2] != "" {
|
||||
line, err := strconv.Atoi(posMatch[1])
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
col, err := strconv.Atoi(posMatch[2])
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
flagStartPos = buffer.Loc{col - 1, line - 1}
|
||||
posIndex = i
|
||||
} else if len(posMatch) == 3 && posMatch[2] == "" {
|
||||
line, err := strconv.Atoi(posMatch[1])
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
flagStartPos = buffer.Loc{0, line - 1}
|
||||
posIndex = i
|
||||
} else {
|
||||
searchMatch := searchFlagr.FindStringSubmatch(a)
|
||||
if len(searchMatch) == 2 {
|
||||
searchText = searchMatch[1]
|
||||
searchIndex = i
|
||||
} else {
|
||||
files = append(files, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command := buffer.Command{
|
||||
StartCursor: flagStartPos,
|
||||
SearchRegex: searchText,
|
||||
SearchAfterStart: searchIndex > posIndex,
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
if len(flag.Args()) > 0 {
|
||||
// Option 1
|
||||
// We go through each file and load it
|
||||
for i := 0; i < len(files); i++ {
|
||||
buf, err := buffer.NewBufferFromFileWithCommand(files[i], buffer.BTDefault, command)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
for i := 0; i < len(flag.Args()); i++ {
|
||||
filename = flag.Args()[i]
|
||||
|
||||
// Check that the file exists
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
// If it exists we load it into a buffer
|
||||
input, err = ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
input = []byte{}
|
||||
filename = ""
|
||||
}
|
||||
}
|
||||
// If the file didn't exist, input will be empty, and we'll open an empty buffer
|
||||
buffers = append(buffers, buf)
|
||||
buffers = append(buffers, NewBuffer(input, filename))
|
||||
}
|
||||
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
|
||||
// Option 2
|
||||
// The input is not a terminal, so something is being piped in
|
||||
// and we should read from stdin
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
TermMessage("Error reading from stdin: ", err)
|
||||
input = []byte{}
|
||||
}
|
||||
buffers = append(buffers, NewBuffer(input, filename))
|
||||
} else {
|
||||
btype := buffer.BTDefault
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
btype = buffer.BTStdout
|
||||
}
|
||||
|
||||
if !isatty.IsTerminal(os.Stdin.Fd()) {
|
||||
// Option 2
|
||||
// The input is not a terminal, so something is being piped in
|
||||
// and we should read from stdin
|
||||
input, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading from stdin: ", err)
|
||||
input = []byte{}
|
||||
}
|
||||
buffers = append(buffers, buffer.NewBufferFromStringWithCommand(string(input), "", btype, command))
|
||||
} else {
|
||||
// Option 3, just open an empty buffer
|
||||
buffers = append(buffers, buffer.NewBufferFromStringWithCommand("", "", btype, command))
|
||||
}
|
||||
// Option 3, just open an empty buffer
|
||||
buffers = append(buffers, NewBuffer(input, filename))
|
||||
}
|
||||
|
||||
return buffers
|
||||
}
|
||||
|
||||
func checkBackup(name string) error {
|
||||
target := filepath.Join(config.ConfigDir, name)
|
||||
backup := target + util.BackupSuffix
|
||||
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() {
|
||||
defer func() {
|
||||
if util.Stdout.Len() > 0 {
|
||||
fmt.Fprint(os.Stdout, util.Stdout.String())
|
||||
}
|
||||
exit(0)
|
||||
}()
|
||||
|
||||
var err error
|
||||
|
||||
InitFlags()
|
||||
|
||||
if *flagProfile {
|
||||
f, err := os.Create("micro.prof")
|
||||
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
|
||||
// If no directory is found, it creates one.
|
||||
func InitConfigDir() {
|
||||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgHome == "" {
|
||||
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatal("error creating CPU profile: ", err)
|
||||
TermMessage("Error finding your home directory\nCan't load config files")
|
||||
return
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("error starting CPU profile: ", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
xdgHome = home + "/.config"
|
||||
}
|
||||
configDir = xdgHome + "/micro"
|
||||
|
||||
InitLog()
|
||||
|
||||
err = config.InitConfigDir(*flagConfigDir)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
config.InitRuntimeFiles(true)
|
||||
config.InitPlugins()
|
||||
|
||||
err = checkBackup("settings.json")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
err = config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
err = config.InitGlobalSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
// flag options
|
||||
for k, v := range optionFlags {
|
||||
if *v != "" {
|
||||
nativeValue, err := config.GetNativeValue(k, *v)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
if err = config.OptionIsValid(k, nativeValue); err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
config.GlobalSettings[k] = nativeValue
|
||||
config.VolatileSettings[k] = true
|
||||
if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
|
||||
// If the xdgHome doesn't exist we should create it
|
||||
err = os.Mkdir(xdgHome, os.ModePerm)
|
||||
if err != nil {
|
||||
TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
DoPluginFlags()
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// If the micro specific config directory doesn't exist we should create that too
|
||||
err = os.Mkdir(configDir, os.ModePerm)
|
||||
if err != nil {
|
||||
TermMessage("Error creating configuration directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitScreen creates and initializes the tcell screen
|
||||
func InitScreen() {
|
||||
// Should we enable true color?
|
||||
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
|
||||
|
||||
// In order to enable true color, we have to set the TERM to `xterm-truecolor` when
|
||||
// initializing tcell, but after that, we can set the TERM back to whatever it was
|
||||
oldTerm := os.Getenv("TERM")
|
||||
if truecolor {
|
||||
os.Setenv("TERM", "xterm-truecolor")
|
||||
}
|
||||
|
||||
os.Setenv("TCELLDB", configDir+"/.tcelldb")
|
||||
|
||||
// Initilize tcell
|
||||
var err error
|
||||
screen, err = tcell.NewScreen()
|
||||
|
||||
if err != nil && err.Error() == "terminal entry not found" {
|
||||
var termDB []byte
|
||||
termDB, err = MkInfo()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Terminal entry not found")
|
||||
fmt.Println("Error when trying to read terminfo: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, e := os.Stat(configDir); e == nil {
|
||||
ioutil.WriteFile(configDir+"/.tcelldb", termDB, 0644)
|
||||
}
|
||||
|
||||
screen, err = tcell.NewScreen()
|
||||
}
|
||||
|
||||
err = screen.Init()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Fatal: Micro could not initialize a Screen.")
|
||||
exit(1)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = screen.Init(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
util.Sigterm = make(chan os.Signal, 1)
|
||||
sighup = make(chan os.Signal, 1)
|
||||
signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
|
||||
signal.Notify(sighup, syscall.SIGHUP)
|
||||
// Now we can put the TERM back to what it was before
|
||||
if truecolor {
|
||||
os.Setenv("TERM", oldTerm)
|
||||
}
|
||||
|
||||
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
|
||||
clipErr := clipboard.Initialize(m)
|
||||
screen.SetStyle(defStyle)
|
||||
screen.EnableMouse()
|
||||
}
|
||||
|
||||
// RedrawAll redraws everything -- all the views and the messenger
|
||||
func RedrawAll() {
|
||||
messenger.Clear()
|
||||
for _, v := range tabs[curTab].views {
|
||||
v.Display()
|
||||
}
|
||||
DisplayTabs()
|
||||
messenger.Display()
|
||||
screen.Show()
|
||||
}
|
||||
|
||||
// Passing -version as a flag will have micro print out the version number
|
||||
var flagVersion = flag.Bool("version", false, "Show the version number and information")
|
||||
var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
|
||||
fmt.Println("Micro's options can be set via command line arguments for quick adjustments. For real configuration, please use the bindings.json file (see 'help options').\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
optionFlags := make(map[string]*string)
|
||||
|
||||
for k, v := range DefaultGlobalSettings() {
|
||||
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *flagVersion {
|
||||
// If -version was passed
|
||||
fmt.Println("Version:", Version)
|
||||
fmt.Println("Commit hash:", CommitHash)
|
||||
fmt.Println("Compiled on", CompileDate)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Start the Lua VM for running plugins
|
||||
L = lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
// Some encoding stuff in case the user isn't using UTF-8
|
||||
encoding.Register()
|
||||
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
|
||||
|
||||
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
|
||||
InitConfigDir()
|
||||
|
||||
// Load the user's settings
|
||||
InitGlobalSettings()
|
||||
|
||||
InitCommands()
|
||||
InitBindings()
|
||||
|
||||
// Load the syntax files, including the colorscheme
|
||||
LoadSyntaxFiles()
|
||||
|
||||
// Load the help files
|
||||
LoadHelp()
|
||||
|
||||
// Start the screen
|
||||
InitScreen()
|
||||
|
||||
// This is just so if we have an error, we can exit cleanly and not completely
|
||||
// mess up the terminal being worked in
|
||||
// In other words we need to shut down tcell before the program crashes
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if screen.Screen != nil {
|
||||
screen.Screen.Fini()
|
||||
}
|
||||
if e, ok := err.(*lua.ApiError); ok {
|
||||
fmt.Println("Lua API error:", e)
|
||||
} else {
|
||||
fmt.Println("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/micro-editor/micro/issues")
|
||||
}
|
||||
// immediately backup all buffers with unsaved changes
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if b.Modified() {
|
||||
b.Backup()
|
||||
}
|
||||
}
|
||||
exit(1)
|
||||
screen.Fini()
|
||||
fmt.Println("Micro encountered an error:", err)
|
||||
// Print the stack trace too
|
||||
fmt.Print(errors.Wrap(err, 2).ErrorStack())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
err = config.LoadAllPlugins()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
// Create a new messenger
|
||||
// This is used for sending the user messages in the bottom of the editor
|
||||
messenger = new(Messenger)
|
||||
messenger.history = make(map[string][]string)
|
||||
|
||||
// Now we load the input
|
||||
buffers := LoadInput()
|
||||
for _, buf := range buffers {
|
||||
// For each buffer we create a new tab and place the view in that tab
|
||||
tab := NewTabFromView(NewView(buf))
|
||||
tab.SetNum(len(tabs))
|
||||
tabs = append(tabs, tab)
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.views {
|
||||
v.Center(false)
|
||||
if globalSettings["syntax"].(bool) {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = checkBackup("bindings.json")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
exit(1)
|
||||
for k, v := range optionFlags {
|
||||
if *v != "" {
|
||||
SetOption(k, *v)
|
||||
}
|
||||
}
|
||||
|
||||
action.InitBindings()
|
||||
action.InitCommands()
|
||||
// Load all the plugin stuff
|
||||
// We give plugins access to a bunch of variables here which could be useful to them
|
||||
L.SetGlobal("OS", luar.New(L, runtime.GOOS))
|
||||
L.SetGlobal("tabs", luar.New(L, tabs))
|
||||
L.SetGlobal("curTab", luar.New(L, curTab))
|
||||
L.SetGlobal("messenger", luar.New(L, messenger))
|
||||
L.SetGlobal("GetOption", luar.New(L, GetOption))
|
||||
L.SetGlobal("AddOption", luar.New(L, AddOption))
|
||||
L.SetGlobal("SetOption", luar.New(L, SetOption))
|
||||
L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
|
||||
L.SetGlobal("BindKey", luar.New(L, BindKey))
|
||||
L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
|
||||
L.SetGlobal("CurView", luar.New(L, CurView))
|
||||
L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
|
||||
L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
|
||||
L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
|
||||
L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
|
||||
L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
|
||||
|
||||
timerChan = make(chan func())
|
||||
// Used for asynchronous jobs
|
||||
L.SetGlobal("JobStart", luar.New(L, JobStart))
|
||||
L.SetGlobal("JobSend", luar.New(L, JobSend))
|
||||
L.SetGlobal("JobStop", luar.New(L, JobStop))
|
||||
|
||||
err = config.RunPluginFn("preinit")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
LoadPlugins()
|
||||
|
||||
jobs = make(chan JobFunction, 100)
|
||||
events = make(chan tcell.Event)
|
||||
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.views {
|
||||
for _, pl := range loadedPlugins {
|
||||
_, err := Call(pl+".onViewOpen", v)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if v.Buf.Settings["syntax"].(bool) {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action.InitGlobals()
|
||||
buffer.SetMessager(action.InfoBar)
|
||||
args := flag.Args()
|
||||
b := LoadInput(args)
|
||||
|
||||
if len(b) == 0 {
|
||||
// No buffers to open
|
||||
screen.Screen.Fini()
|
||||
runtime.Goexit()
|
||||
}
|
||||
|
||||
action.InitTabs(b)
|
||||
|
||||
err = config.RunPluginFn("init")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
err = config.RunPluginFn("postinit")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
if clipErr != nil {
|
||||
log.Println(clipErr, " or change 'clipboard' option")
|
||||
}
|
||||
|
||||
config.StartAutoSave()
|
||||
if a := config.GetGlobalOption("autosave").(float64); a > 0 {
|
||||
config.SetAutoTime(a)
|
||||
}
|
||||
|
||||
screen.Events = make(chan tcell.Event)
|
||||
|
||||
// Here is the event loop which runs in a separate thread
|
||||
go func() {
|
||||
for {
|
||||
screen.Lock()
|
||||
e := screen.Screen.PollEvent()
|
||||
screen.Unlock()
|
||||
if e != nil {
|
||||
screen.Events <- e
|
||||
}
|
||||
events <- screen.PollEvent()
|
||||
}
|
||||
}()
|
||||
|
||||
// clear the drawchan so we don't redraw excessively
|
||||
// if someone requested a redraw before we started displaying
|
||||
for len(screen.DrawChan()) > 0 {
|
||||
<-screen.DrawChan()
|
||||
}
|
||||
|
||||
// wait for initial resize event
|
||||
select {
|
||||
case event := <-screen.Events:
|
||||
action.Tabs.HandleEvent(event)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
// time out after 10ms
|
||||
}
|
||||
|
||||
for {
|
||||
DoEvent()
|
||||
}
|
||||
}
|
||||
// Display everything
|
||||
RedrawAll()
|
||||
|
||||
// DoEvent runs the main action loop of the editor
|
||||
func DoEvent() {
|
||||
var event tcell.Event
|
||||
var event tcell.Event
|
||||
|
||||
// Display everything
|
||||
screen.Screen.Fill(' ', config.DefStyle)
|
||||
screen.Screen.HideCursor()
|
||||
action.Tabs.Display()
|
||||
for _, ep := range action.MainTab().Panes {
|
||||
ep.Display()
|
||||
}
|
||||
action.MainTab().Display()
|
||||
action.InfoBar.Display()
|
||||
screen.Screen.Show()
|
||||
|
||||
// Check for new events
|
||||
select {
|
||||
case f := <-shell.Jobs:
|
||||
// If a new job has finished while running in the background we should execute the callback
|
||||
f.Function(f.Output, f.Args)
|
||||
case <-config.Autosave:
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.AutoSave()
|
||||
// Check for new events
|
||||
select {
|
||||
case f := <-jobs:
|
||||
// If a new job has finished while running in the background we should execute the callback
|
||||
f.function(f.output, f.args...)
|
||||
continue
|
||||
case event = <-events:
|
||||
}
|
||||
case <-shell.CloseTerms:
|
||||
action.Tabs.CloseTerms()
|
||||
case event = <-screen.Events:
|
||||
case <-screen.DrawChan():
|
||||
for len(screen.DrawChan()) > 0 {
|
||||
<-screen.DrawChan()
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
if e.Buttons() == tcell.Button1 {
|
||||
// If the user left clicked we check a couple things
|
||||
_, h := screen.Size()
|
||||
x, y := e.Position()
|
||||
if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
|
||||
// If the user clicked in the bottom bar, and there is a message down there
|
||||
// we copy it to the clipboard.
|
||||
// Often error messages are displayed down there so it can be useful to easily
|
||||
// copy the message
|
||||
clipboard.WriteAll(messenger.message, "primary")
|
||||
continue
|
||||
}
|
||||
|
||||
if CurView().mouseReleased {
|
||||
// We loop through each view in the current tab and make sure the current view
|
||||
// is the one being clicked in
|
||||
for _, v := range tabs[curTab].views {
|
||||
if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
|
||||
tabs[curTab].curView = v.Num
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case f := <-timerChan:
|
||||
f()
|
||||
case <-sighup:
|
||||
exit(0)
|
||||
case <-util.Sigterm:
|
||||
exit(0)
|
||||
}
|
||||
|
||||
if e, ok := event.(*tcell.EventError); ok {
|
||||
log.Println("tcell event error: ", e.Error())
|
||||
|
||||
if e.Err() == io.EOF {
|
||||
// shutdown due to terminal closing/becoming inaccessible
|
||||
exit(0)
|
||||
// This function checks the mouse event for the possibility of changing the current tab
|
||||
// If the tab was changed it returns true
|
||||
if TabbarHandleMouseEvent(event) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if event != nil {
|
||||
_, resize := event.(*tcell.EventResize)
|
||||
if resize {
|
||||
action.InfoBar.HandleEvent(event)
|
||||
action.Tabs.HandleEvent(event)
|
||||
} else if action.InfoBar.HasPrompt {
|
||||
action.InfoBar.HandleEvent(event)
|
||||
if searching {
|
||||
// Since searching is done in real time, we need to redraw every time
|
||||
// there is a new event in the search bar so we need a special function
|
||||
// to run instead of the standard HandleEvent.
|
||||
HandleSearchEvent(event, CurView())
|
||||
} else {
|
||||
action.Tabs.HandleEvent(event)
|
||||
// Send it to the view
|
||||
CurView().HandleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
err := config.RunPluginFn("onAnyEvent")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/micro-editor/micro/v2/internal/action"
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var tempDir string
|
||||
var sim tcell.SimulationScreen
|
||||
|
||||
func init() {
|
||||
screen.Events = make(chan tcell.Event, 8)
|
||||
}
|
||||
|
||||
func startup(args []string) (tcell.SimulationScreen, error) {
|
||||
var err error
|
||||
|
||||
tempDir, err = os.MkdirTemp("", "micro_test")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = config.InitConfigDir(tempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.InitRuntimeFiles(true)
|
||||
config.InitPlugins()
|
||||
|
||||
err = config.ReadSettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = config.InitGlobalSettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := screen.InitSimScreen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
screen.Screen.Fini()
|
||||
fmt.Println("Micro encountered an error:", err)
|
||||
// immediately backup all buffers with unsaved changes
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if b.Modified() {
|
||||
b.Backup()
|
||||
}
|
||||
}
|
||||
// Print the stack trace too
|
||||
log.Fatalf(errors.Wrap(err, 2).ErrorStack())
|
||||
}
|
||||
}()
|
||||
|
||||
err = config.LoadAllPlugins()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
action.InitBindings()
|
||||
action.InitCommands()
|
||||
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := LoadInput(args)
|
||||
|
||||
if len(b) == 0 {
|
||||
return nil, errors.New("No buffers opened")
|
||||
}
|
||||
|
||||
action.InitTabs(b)
|
||||
action.InitGlobals()
|
||||
|
||||
err = config.RunPluginFn("init")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.InjectResize()
|
||||
handleEvent()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
func handleEvent() {
|
||||
screen.Lock()
|
||||
e := screen.Screen.PollEvent()
|
||||
screen.Unlock()
|
||||
if e != nil {
|
||||
screen.Events <- e
|
||||
}
|
||||
|
||||
for len(screen.DrawChan()) > 0 || len(screen.Events) > 0 {
|
||||
DoEvent()
|
||||
}
|
||||
}
|
||||
|
||||
func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
|
||||
sim.InjectKey(key, r, mod)
|
||||
handleEvent()
|
||||
}
|
||||
|
||||
func injectMouse(x, y int, buttons tcell.ButtonMask, mod tcell.ModMask) {
|
||||
sim.InjectMouse(x, y, buttons, mod)
|
||||
handleEvent()
|
||||
}
|
||||
|
||||
func injectString(str string) {
|
||||
// the tcell simulation screen event channel can only handle
|
||||
// 10 events at once, so we need to divide up the key events
|
||||
// into chunks of 10 and handle the 10 events before sending
|
||||
// another chunk of events
|
||||
iters := len(str) / 10
|
||||
extra := len(str) % 10
|
||||
|
||||
for i := 0; i < iters; i++ {
|
||||
s := i * 10
|
||||
e := i*10 + 10
|
||||
sim.InjectKeyBytes([]byte(str[s:e]))
|
||||
for i := 0; i < 10; i++ {
|
||||
handleEvent()
|
||||
}
|
||||
}
|
||||
|
||||
sim.InjectKeyBytes([]byte(str[len(str)-extra:]))
|
||||
for i := 0; i < extra; i++ {
|
||||
handleEvent()
|
||||
}
|
||||
}
|
||||
|
||||
func openFile(file string) {
|
||||
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
|
||||
injectString(fmt.Sprintf("open %s", file))
|
||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||
}
|
||||
|
||||
func findBuffer(file string) *buffer.Buffer {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
sim, err = startup([]string{})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
retval := m.Run()
|
||||
cleanup()
|
||||
|
||||
os.Exit(retval)
|
||||
}
|
||||
|
||||
func TestSimpleEdit(t *testing.T) {
|
||||
file := createTestFile(t, "base content")
|
||||
|
||||
openFile(file)
|
||||
|
||||
if findBuffer(file) == nil {
|
||||
t.Fatalf("Could not find buffer %s", file)
|
||||
}
|
||||
|
||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||
injectKey(tcell.KeyUp, 0, tcell.ModNone)
|
||||
injectString("first line")
|
||||
|
||||
// test both kinds of backspace
|
||||
for i := 0; i < len("ne"); i++ {
|
||||
injectKey(tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone)
|
||||
}
|
||||
for i := 0; i < len(" li"); i++ {
|
||||
injectKey(tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone)
|
||||
}
|
||||
injectString("foobar")
|
||||
|
||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "firstfoobar\nbase content\n", string(data))
|
||||
}
|
||||
|
||||
func TestMouse(t *testing.T) {
|
||||
file := createTestFile(t, "base content")
|
||||
|
||||
openFile(file)
|
||||
|
||||
if findBuffer(file) == nil {
|
||||
t.Fatalf("Could not find buffer %s", file)
|
||||
}
|
||||
|
||||
// buffer:
|
||||
// base content
|
||||
// the selections need to happen at different locations to avoid a double click
|
||||
injectMouse(3, 0, tcell.Button1, tcell.ModNone)
|
||||
injectKey(tcell.KeyLeft, 0, tcell.ModNone)
|
||||
injectMouse(0, 0, tcell.ButtonNone, tcell.ModNone)
|
||||
injectString("secondline")
|
||||
// buffer:
|
||||
// secondlinebase content
|
||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||
// buffer:
|
||||
// secondline
|
||||
// base content
|
||||
injectMouse(2, 0, tcell.Button1, tcell.ModNone)
|
||||
injectMouse(0, 0, tcell.ButtonNone, tcell.ModNone)
|
||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||
// buffer:
|
||||
//
|
||||
// secondline
|
||||
// base content
|
||||
injectKey(tcell.KeyUp, 0, tcell.ModNone)
|
||||
injectString("firstline")
|
||||
// buffer:
|
||||
// firstline
|
||||
// secondline
|
||||
// base content
|
||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
|
||||
}
|
||||
|
||||
var srTestStart = `foo
|
||||
foo
|
||||
foofoofoo
|
||||
Ernleȝe foo æðelen
|
||||
`
|
||||
var srTest2 = `test_string
|
||||
test_string
|
||||
test_stringtest_stringtest_string
|
||||
Ernleȝe test_string æðelen
|
||||
`
|
||||
var srTest3 = `test_foo
|
||||
test_string
|
||||
test_footest_stringtest_foo
|
||||
Ernleȝe test_string æðelen
|
||||
`
|
||||
|
||||
func TestSearchAndReplace(t *testing.T) {
|
||||
file := createTestFile(t, srTestStart)
|
||||
|
||||
openFile(file)
|
||||
|
||||
if findBuffer(file) == nil {
|
||||
t.Fatalf("Could not find buffer %s", file)
|
||||
}
|
||||
|
||||
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
|
||||
injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string"))
|
||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||
|
||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, srTest2, string(data))
|
||||
|
||||
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
|
||||
injectString(fmt.Sprintf("replace %s %s", "string", "foo"))
|
||||
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
|
||||
injectString("ynyny")
|
||||
injectKey(tcell.KeyEscape, 0, tcell.ModNone)
|
||||
|
||||
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
|
||||
|
||||
data, err = os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, srTest3, string(data))
|
||||
}
|
||||
|
||||
func TestMultiCursor(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func TestSettingsPersistence(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// more tests (rendering, tabs, plugins)?
|
||||
669
cmd/micro/mkinfo.go
Normal file
669
cmd/micro/mkinfo.go
Normal file
@@ -0,0 +1,669 @@
|
||||
// Copyright 2016 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This command is used to generate suitable configuration files in either
|
||||
// go syntax or in JSON. It defaults to JSON output on stdout. If no
|
||||
// term values are specified on the command line, then $TERM is used.
|
||||
//
|
||||
// Usage is like this:
|
||||
//
|
||||
// mkinfo [-go file.go] [-json file.json] [-quiet] [-nofatal] [<term>...]
|
||||
//
|
||||
// -go specifiles Go output into the named file. Use - for stdout.
|
||||
// -json specifies JSON output in the named file. Use - for stdout
|
||||
// -nofatal indicates that errors loading definitions should not be fatal
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// #include <curses.h>
|
||||
// #include <term.h>
|
||||
// #cgo LDFLAGS: -lcurses
|
||||
//
|
||||
// void noenv() {
|
||||
// use_env(FALSE);
|
||||
// }
|
||||
//
|
||||
// char *tigetstr_good(char *name) {
|
||||
// char *r;
|
||||
// r = tigetstr(name);
|
||||
// if (r == (char *)-1) {
|
||||
// r = NULL;
|
||||
// }
|
||||
// return (r);
|
||||
// }
|
||||
import "C"
|
||||
|
||||
func tigetnum(s string) int {
|
||||
n := C.tigetnum(C.CString(s))
|
||||
return int(n)
|
||||
}
|
||||
|
||||
func tigetflag(s string) bool {
|
||||
n := C.tigetflag(C.CString(s))
|
||||
return n != 0
|
||||
}
|
||||
|
||||
func tigetstr(s string) string {
|
||||
// NB: If the string is invalid, we'll get back -1, which causes
|
||||
// no end of grief. So make sure your capability strings are correct!
|
||||
cs := C.tigetstr_good(C.CString(s))
|
||||
if cs == nil {
|
||||
return ""
|
||||
}
|
||||
return C.GoString(cs)
|
||||
}
|
||||
|
||||
// This program is used to collect data from the system's terminfo library,
|
||||
// and write it into Go source code. That is, we maintain our terminfo
|
||||
// capabilities encoded in the program. It should never need to be run by
|
||||
// an end user, but developers can use this to add codes for additional
|
||||
// terminal types.
|
||||
//
|
||||
// If a terminal name ending with -truecolor is given, and we cannot find
|
||||
// one, we will try to fabricte one from either the -256color (if present)
|
||||
// or the unadorned base name, adding the XTerm specific 24-bit color
|
||||
// escapes. We believe that all 24-bit capable terminals use the same
|
||||
// escape sequences, and terminfo has yet to evolve to support this.
|
||||
func getinfo(name string) (*tcell.Terminfo, error) {
|
||||
addTrueColor := false
|
||||
rsn := C.int(0)
|
||||
C.noenv()
|
||||
rv, _ := C.setupterm(C.CString(name), 1, &rsn)
|
||||
if rv == C.ERR {
|
||||
if strings.HasSuffix(name, "-truecolor") {
|
||||
base := name[:len(name)-len("-truecolor")]
|
||||
// Probably -256color is closest to what we want
|
||||
rv, _ = C.setupterm(C.CString(base+"-256color"), 1,
|
||||
&rsn)
|
||||
// Otherwise try the base
|
||||
if rv == C.ERR {
|
||||
rv, _ = C.setupterm(C.CString(base), 1, &rsn)
|
||||
}
|
||||
if rv != C.ERR {
|
||||
addTrueColor = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if rv == C.ERR {
|
||||
switch rsn {
|
||||
case 1:
|
||||
return nil, errors.New("hardcopy terminal")
|
||||
case 0:
|
||||
return nil, errors.New("terminal definition not found")
|
||||
case -1:
|
||||
return nil, errors.New("terminfo database missing")
|
||||
default:
|
||||
return nil, errors.New("setupterm failed (other)")
|
||||
}
|
||||
}
|
||||
t := &tcell.Terminfo{}
|
||||
t.Name = name
|
||||
t.Colors = tigetnum("colors")
|
||||
t.Columns = tigetnum("cols")
|
||||
t.Lines = tigetnum("lines")
|
||||
t.Bell = tigetstr("bel")
|
||||
t.Clear = tigetstr("clear")
|
||||
t.EnterCA = tigetstr("smcup")
|
||||
t.ExitCA = tigetstr("rmcup")
|
||||
t.ShowCursor = tigetstr("cnorm")
|
||||
t.HideCursor = tigetstr("civis")
|
||||
t.AttrOff = tigetstr("sgr0")
|
||||
t.Underline = tigetstr("smul")
|
||||
t.Bold = tigetstr("bold")
|
||||
t.Blink = tigetstr("blink")
|
||||
t.Dim = tigetstr("dim")
|
||||
t.Reverse = tigetstr("rev")
|
||||
t.EnterKeypad = tigetstr("smkx")
|
||||
t.ExitKeypad = tigetstr("rmkx")
|
||||
t.SetFg = tigetstr("setaf")
|
||||
t.SetBg = tigetstr("setab")
|
||||
t.SetCursor = tigetstr("cup")
|
||||
t.CursorBack1 = tigetstr("cub1")
|
||||
t.CursorUp1 = tigetstr("cuu1")
|
||||
t.KeyF1 = tigetstr("kf1")
|
||||
t.KeyF2 = tigetstr("kf2")
|
||||
t.KeyF3 = tigetstr("kf3")
|
||||
t.KeyF4 = tigetstr("kf4")
|
||||
t.KeyF5 = tigetstr("kf5")
|
||||
t.KeyF6 = tigetstr("kf6")
|
||||
t.KeyF7 = tigetstr("kf7")
|
||||
t.KeyF8 = tigetstr("kf8")
|
||||
t.KeyF9 = tigetstr("kf9")
|
||||
t.KeyF10 = tigetstr("kf10")
|
||||
t.KeyF11 = tigetstr("kf11")
|
||||
t.KeyF12 = tigetstr("kf12")
|
||||
t.KeyF13 = tigetstr("kf13")
|
||||
t.KeyF14 = tigetstr("kf14")
|
||||
t.KeyF15 = tigetstr("kf15")
|
||||
t.KeyF16 = tigetstr("kf16")
|
||||
t.KeyF17 = tigetstr("kf17")
|
||||
t.KeyF18 = tigetstr("kf18")
|
||||
t.KeyF19 = tigetstr("kf19")
|
||||
t.KeyF20 = tigetstr("kf20")
|
||||
t.KeyF21 = tigetstr("kf21")
|
||||
t.KeyF22 = tigetstr("kf22")
|
||||
t.KeyF23 = tigetstr("kf23")
|
||||
t.KeyF24 = tigetstr("kf24")
|
||||
t.KeyF25 = tigetstr("kf25")
|
||||
t.KeyF26 = tigetstr("kf26")
|
||||
t.KeyF27 = tigetstr("kf27")
|
||||
t.KeyF28 = tigetstr("kf28")
|
||||
t.KeyF29 = tigetstr("kf29")
|
||||
t.KeyF30 = tigetstr("kf30")
|
||||
t.KeyF31 = tigetstr("kf31")
|
||||
t.KeyF32 = tigetstr("kf32")
|
||||
t.KeyF33 = tigetstr("kf33")
|
||||
t.KeyF34 = tigetstr("kf34")
|
||||
t.KeyF35 = tigetstr("kf35")
|
||||
t.KeyF36 = tigetstr("kf36")
|
||||
t.KeyF37 = tigetstr("kf37")
|
||||
t.KeyF38 = tigetstr("kf38")
|
||||
t.KeyF39 = tigetstr("kf39")
|
||||
t.KeyF40 = tigetstr("kf40")
|
||||
t.KeyF41 = tigetstr("kf41")
|
||||
t.KeyF42 = tigetstr("kf42")
|
||||
t.KeyF43 = tigetstr("kf43")
|
||||
t.KeyF44 = tigetstr("kf44")
|
||||
t.KeyF45 = tigetstr("kf45")
|
||||
t.KeyF46 = tigetstr("kf46")
|
||||
t.KeyF47 = tigetstr("kf47")
|
||||
t.KeyF48 = tigetstr("kf48")
|
||||
t.KeyF49 = tigetstr("kf49")
|
||||
t.KeyF50 = tigetstr("kf50")
|
||||
t.KeyF51 = tigetstr("kf51")
|
||||
t.KeyF52 = tigetstr("kf52")
|
||||
t.KeyF53 = tigetstr("kf53")
|
||||
t.KeyF54 = tigetstr("kf54")
|
||||
t.KeyF55 = tigetstr("kf55")
|
||||
t.KeyF56 = tigetstr("kf56")
|
||||
t.KeyF57 = tigetstr("kf57")
|
||||
t.KeyF58 = tigetstr("kf58")
|
||||
t.KeyF59 = tigetstr("kf59")
|
||||
t.KeyF60 = tigetstr("kf60")
|
||||
t.KeyF61 = tigetstr("kf61")
|
||||
t.KeyF62 = tigetstr("kf62")
|
||||
t.KeyF63 = tigetstr("kf63")
|
||||
t.KeyF64 = tigetstr("kf64")
|
||||
t.KeyInsert = tigetstr("kich1")
|
||||
t.KeyDelete = tigetstr("kdch1")
|
||||
t.KeyBackspace = tigetstr("kbs")
|
||||
t.KeyHome = tigetstr("khome")
|
||||
t.KeyEnd = tigetstr("kend")
|
||||
t.KeyUp = tigetstr("kcuu1")
|
||||
t.KeyDown = tigetstr("kcud1")
|
||||
t.KeyRight = tigetstr("kcuf1")
|
||||
t.KeyLeft = tigetstr("kcub1")
|
||||
t.KeyPgDn = tigetstr("knp")
|
||||
t.KeyPgUp = tigetstr("kpp")
|
||||
t.KeyBacktab = tigetstr("kcbt")
|
||||
t.KeyExit = tigetstr("kext")
|
||||
t.KeyCancel = tigetstr("kcan")
|
||||
t.KeyPrint = tigetstr("kprt")
|
||||
t.KeyHelp = tigetstr("khlp")
|
||||
t.KeyClear = tigetstr("kclr")
|
||||
t.AltChars = tigetstr("acsc")
|
||||
t.EnterAcs = tigetstr("smacs")
|
||||
t.ExitAcs = tigetstr("rmacs")
|
||||
t.EnableAcs = tigetstr("enacs")
|
||||
t.Mouse = tigetstr("kmous")
|
||||
t.KeyShfRight = tigetstr("kRIT")
|
||||
t.KeyShfLeft = tigetstr("kLFT")
|
||||
t.KeyShfHome = tigetstr("kHOM")
|
||||
t.KeyShfEnd = tigetstr("kEND")
|
||||
|
||||
// Terminfo lacks descriptions for a bunch of modified keys,
|
||||
// but modern XTerm and emulators often have them. Let's add them,
|
||||
// if the shifted right and left arrows are defined.
|
||||
if t.KeyShfRight == "\x1b[1;2C" && t.KeyShfLeft == "\x1b[1;2D" {
|
||||
t.KeyShfUp = "\x1b[1;2A"
|
||||
t.KeyShfDown = "\x1b[1;2B"
|
||||
t.KeyMetaUp = "\x1b[1;9A"
|
||||
t.KeyMetaDown = "\x1b[1;9B"
|
||||
t.KeyMetaRight = "\x1b[1;9C"
|
||||
t.KeyMetaLeft = "\x1b[1;9D"
|
||||
t.KeyAltUp = "\x1b[1;3A"
|
||||
t.KeyAltDown = "\x1b[1;3B"
|
||||
t.KeyAltRight = "\x1b[1;3C"
|
||||
t.KeyAltLeft = "\x1b[1;3D"
|
||||
t.KeyCtrlUp = "\x1b[1;5A"
|
||||
t.KeyCtrlDown = "\x1b[1;5B"
|
||||
t.KeyCtrlRight = "\x1b[1;5C"
|
||||
t.KeyCtrlLeft = "\x1b[1;5D"
|
||||
t.KeyAltShfUp = "\x1b[1;4A"
|
||||
t.KeyAltShfDown = "\x1b[1;4B"
|
||||
t.KeyAltShfRight = "\x1b[1;4C"
|
||||
t.KeyAltShfLeft = "\x1b[1;4D"
|
||||
|
||||
t.KeyMetaShfUp = "\x1b[1;10A"
|
||||
t.KeyMetaShfDown = "\x1b[1;10B"
|
||||
t.KeyMetaShfRight = "\x1b[1;10C"
|
||||
t.KeyMetaShfLeft = "\x1b[1;10D"
|
||||
|
||||
t.KeyCtrlShfUp = "\x1b[1;6A"
|
||||
t.KeyCtrlShfDown = "\x1b[1;6B"
|
||||
t.KeyCtrlShfRight = "\x1b[1;6C"
|
||||
t.KeyCtrlShfLeft = "\x1b[1;6D"
|
||||
}
|
||||
// And also for Home and End
|
||||
if t.KeyShfHome == "\x1b[1;2H" && t.KeyShfEnd == "\x1b[1;2F" {
|
||||
t.KeyCtrlHome = "\x1b[1;5H"
|
||||
t.KeyCtrlEnd = "\x1b[1;5F"
|
||||
t.KeyAltHome = "\x1b[1;9H"
|
||||
t.KeyAltEnd = "\x1b[1;9F"
|
||||
t.KeyCtrlShfHome = "\x1b[1;6H"
|
||||
t.KeyCtrlShfEnd = "\x1b[1;6F"
|
||||
t.KeyAltShfHome = "\x1b[1;4H"
|
||||
t.KeyAltShfEnd = "\x1b[1;4F"
|
||||
t.KeyMetaShfHome = "\x1b[1;10H"
|
||||
t.KeyMetaShfEnd = "\x1b[1;10F"
|
||||
}
|
||||
|
||||
// And the same thing for rxvt and workalikes (Eterm, aterm, etc.)
|
||||
// It seems that urxvt at least send ESC as ALT prefix for these,
|
||||
// although some places seem to indicate a separate ALT key sesquence.
|
||||
if t.KeyShfRight == "\x1b[c" && t.KeyShfLeft == "\x1b[d" {
|
||||
t.KeyShfUp = "\x1b[a"
|
||||
t.KeyShfDown = "\x1b[b"
|
||||
t.KeyCtrlUp = "\x1b[Oa"
|
||||
t.KeyCtrlDown = "\x1b[Ob"
|
||||
t.KeyCtrlRight = "\x1b[Oc"
|
||||
t.KeyCtrlLeft = "\x1b[Od"
|
||||
}
|
||||
if t.KeyShfHome == "\x1b[7$" && t.KeyShfEnd == "\x1b[8$" {
|
||||
t.KeyCtrlHome = "\x1b[7^"
|
||||
t.KeyCtrlEnd = "\x1b[8^"
|
||||
}
|
||||
|
||||
// If the kmous entry is present, then we need to record the
|
||||
// the codes to enter and exit mouse mode. Sadly, this is not
|
||||
// part of the terminfo databases anywhere that I've found, but
|
||||
// is an extension. The escape codes are documented in the XTerm
|
||||
// manual, and all terminals that have kmous are expected to
|
||||
// use these same codes, unless explicitly configured otherwise
|
||||
// vi XM. Note that in any event, we only known how to parse either
|
||||
// x11 or SGR mouse events -- if your terminal doesn't support one
|
||||
// of these two forms, you maybe out of luck.
|
||||
t.MouseMode = tigetstr("XM")
|
||||
if t.Mouse != "" && t.MouseMode == "" {
|
||||
// we anticipate that all xterm mouse tracking compatible
|
||||
// terminals understand mouse tracking (1000), but we hope
|
||||
// that those that don't understand any-event tracking (1003)
|
||||
// will at least ignore it. Likewise we hope that terminals
|
||||
// that don't understand SGR reporting (1006) just ignore it.
|
||||
t.MouseMode = "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;" +
|
||||
"\x1b[?1000%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c"
|
||||
}
|
||||
|
||||
// We only support colors in ANSI 8 or 256 color mode.
|
||||
if t.Colors < 8 || t.SetFg == "" {
|
||||
t.Colors = 0
|
||||
}
|
||||
if t.SetCursor == "" {
|
||||
return nil, errors.New("terminal not cursor addressable")
|
||||
}
|
||||
|
||||
// For padding, we lookup the pad char. If that isn't present,
|
||||
// and npc is *not* set, then we assume a null byte.
|
||||
t.PadChar = tigetstr("pad")
|
||||
if t.PadChar == "" {
|
||||
if !tigetflag("npc") {
|
||||
t.PadChar = "\u0000"
|
||||
}
|
||||
}
|
||||
|
||||
// For some terminals we fabricate a -truecolor entry, that may
|
||||
// not exist in terminfo.
|
||||
if addTrueColor {
|
||||
t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm"
|
||||
t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm"
|
||||
t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" +
|
||||
"48;2;%p4%d;%p5%d;%p6%dm"
|
||||
}
|
||||
|
||||
// For terminals that use "standard" SGR sequences, lets combine the
|
||||
// foreground and background together.
|
||||
if strings.HasPrefix(t.SetFg, "\x1b[") &&
|
||||
strings.HasPrefix(t.SetBg, "\x1b[") &&
|
||||
strings.HasSuffix(t.SetFg, "m") &&
|
||||
strings.HasSuffix(t.SetBg, "m") {
|
||||
fg := t.SetFg[:len(t.SetFg)-1]
|
||||
r := regexp.MustCompile("%p1")
|
||||
bg := r.ReplaceAllString(t.SetBg[2:], "%p2")
|
||||
t.SetFgBg = fg + ";" + bg
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func dotGoAddInt(w io.Writer, n string, i int) {
|
||||
if i == 0 {
|
||||
// initialized to 0, ignore
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, " %-13s %d,\n", n+":", i)
|
||||
}
|
||||
func dotGoAddStr(w io.Writer, n string, s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, " %-13s %q,\n", n+":", s)
|
||||
}
|
||||
|
||||
func dotGoAddArr(w io.Writer, n string, a []string) {
|
||||
if len(a) == 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, " %-13s []string{", n+":")
|
||||
did := false
|
||||
for _, b := range a {
|
||||
if did {
|
||||
fmt.Fprint(w, ", ")
|
||||
}
|
||||
did = true
|
||||
fmt.Fprintf(w, "%q", b)
|
||||
}
|
||||
fmt.Fprintln(w, "},")
|
||||
}
|
||||
|
||||
func dotGoHeader(w io.Writer) {
|
||||
fmt.Fprintf(w, "// Generated by %s (%s/%s) on %s.\n",
|
||||
os.Args[0],
|
||||
runtime.GOOS, runtime.GOARCH,
|
||||
time.Now().Format(time.UnixDate))
|
||||
fmt.Fprintln(w, "// DO NOT HAND-EDIT")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, "package tcell")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, "func init() {")
|
||||
}
|
||||
|
||||
func dotGoTrailer(w io.Writer) {
|
||||
fmt.Fprintln(w, "}")
|
||||
}
|
||||
|
||||
func dotGoInfo(w io.Writer, t *tcell.Terminfo) {
|
||||
fmt.Fprintln(w, " AddTerminfo(&Terminfo{")
|
||||
dotGoAddStr(w, "Name", t.Name)
|
||||
dotGoAddArr(w, "Aliases", t.Aliases)
|
||||
dotGoAddInt(w, "Columns", t.Columns)
|
||||
dotGoAddInt(w, "Lines", t.Lines)
|
||||
dotGoAddInt(w, "Colors", t.Colors)
|
||||
dotGoAddStr(w, "Bell", t.Bell)
|
||||
dotGoAddStr(w, "Clear", t.Clear)
|
||||
dotGoAddStr(w, "EnterCA", t.EnterCA)
|
||||
dotGoAddStr(w, "ExitCA", t.ExitCA)
|
||||
dotGoAddStr(w, "ShowCursor", t.ShowCursor)
|
||||
dotGoAddStr(w, "HideCursor", t.HideCursor)
|
||||
dotGoAddStr(w, "AttrOff", t.AttrOff)
|
||||
dotGoAddStr(w, "Underline", t.Underline)
|
||||
dotGoAddStr(w, "Bold", t.Bold)
|
||||
dotGoAddStr(w, "Dim", t.Dim)
|
||||
dotGoAddStr(w, "Blink", t.Blink)
|
||||
dotGoAddStr(w, "Reverse", t.Reverse)
|
||||
dotGoAddStr(w, "EnterKeypad", t.EnterKeypad)
|
||||
dotGoAddStr(w, "ExitKeypad", t.ExitKeypad)
|
||||
dotGoAddStr(w, "SetFg", t.SetFg)
|
||||
dotGoAddStr(w, "SetBg", t.SetBg)
|
||||
dotGoAddStr(w, "SetFgBg", t.SetFgBg)
|
||||
dotGoAddStr(w, "PadChar", t.PadChar)
|
||||
dotGoAddStr(w, "AltChars", t.AltChars)
|
||||
dotGoAddStr(w, "EnterAcs", t.EnterAcs)
|
||||
dotGoAddStr(w, "ExitAcs", t.ExitAcs)
|
||||
dotGoAddStr(w, "EnableAcs", t.EnableAcs)
|
||||
dotGoAddStr(w, "SetFgRGB", t.SetFgRGB)
|
||||
dotGoAddStr(w, "SetBgRGB", t.SetBgRGB)
|
||||
dotGoAddStr(w, "SetFgBgRGB", t.SetFgBgRGB)
|
||||
dotGoAddStr(w, "Mouse", t.Mouse)
|
||||
dotGoAddStr(w, "MouseMode", t.MouseMode)
|
||||
dotGoAddStr(w, "SetCursor", t.SetCursor)
|
||||
dotGoAddStr(w, "CursorBack1", t.CursorBack1)
|
||||
dotGoAddStr(w, "CursorUp1", t.CursorUp1)
|
||||
dotGoAddStr(w, "KeyUp", t.KeyUp)
|
||||
dotGoAddStr(w, "KeyDown", t.KeyDown)
|
||||
dotGoAddStr(w, "KeyRight", t.KeyRight)
|
||||
dotGoAddStr(w, "KeyLeft", t.KeyLeft)
|
||||
dotGoAddStr(w, "KeyInsert", t.KeyInsert)
|
||||
dotGoAddStr(w, "KeyDelete", t.KeyDelete)
|
||||
dotGoAddStr(w, "KeyBackspace", t.KeyBackspace)
|
||||
dotGoAddStr(w, "KeyHome", t.KeyHome)
|
||||
dotGoAddStr(w, "KeyEnd", t.KeyEnd)
|
||||
dotGoAddStr(w, "KeyPgUp", t.KeyPgUp)
|
||||
dotGoAddStr(w, "KeyPgDn", t.KeyPgDn)
|
||||
dotGoAddStr(w, "KeyF1", t.KeyF1)
|
||||
dotGoAddStr(w, "KeyF2", t.KeyF2)
|
||||
dotGoAddStr(w, "KeyF3", t.KeyF3)
|
||||
dotGoAddStr(w, "KeyF4", t.KeyF4)
|
||||
dotGoAddStr(w, "KeyF5", t.KeyF5)
|
||||
dotGoAddStr(w, "KeyF6", t.KeyF6)
|
||||
dotGoAddStr(w, "KeyF7", t.KeyF7)
|
||||
dotGoAddStr(w, "KeyF8", t.KeyF8)
|
||||
dotGoAddStr(w, "KeyF9", t.KeyF9)
|
||||
dotGoAddStr(w, "KeyF10", t.KeyF10)
|
||||
dotGoAddStr(w, "KeyF11", t.KeyF11)
|
||||
dotGoAddStr(w, "KeyF12", t.KeyF12)
|
||||
dotGoAddStr(w, "KeyF13", t.KeyF13)
|
||||
dotGoAddStr(w, "KeyF14", t.KeyF14)
|
||||
dotGoAddStr(w, "KeyF15", t.KeyF15)
|
||||
dotGoAddStr(w, "KeyF16", t.KeyF16)
|
||||
dotGoAddStr(w, "KeyF17", t.KeyF17)
|
||||
dotGoAddStr(w, "KeyF18", t.KeyF18)
|
||||
dotGoAddStr(w, "KeyF19", t.KeyF19)
|
||||
dotGoAddStr(w, "KeyF20", t.KeyF20)
|
||||
dotGoAddStr(w, "KeyF21", t.KeyF21)
|
||||
dotGoAddStr(w, "KeyF22", t.KeyF22)
|
||||
dotGoAddStr(w, "KeyF23", t.KeyF23)
|
||||
dotGoAddStr(w, "KeyF24", t.KeyF24)
|
||||
dotGoAddStr(w, "KeyF25", t.KeyF25)
|
||||
dotGoAddStr(w, "KeyF26", t.KeyF26)
|
||||
dotGoAddStr(w, "KeyF27", t.KeyF27)
|
||||
dotGoAddStr(w, "KeyF28", t.KeyF28)
|
||||
dotGoAddStr(w, "KeyF29", t.KeyF29)
|
||||
dotGoAddStr(w, "KeyF30", t.KeyF30)
|
||||
dotGoAddStr(w, "KeyF31", t.KeyF31)
|
||||
dotGoAddStr(w, "KeyF32", t.KeyF32)
|
||||
dotGoAddStr(w, "KeyF33", t.KeyF33)
|
||||
dotGoAddStr(w, "KeyF34", t.KeyF34)
|
||||
dotGoAddStr(w, "KeyF35", t.KeyF35)
|
||||
dotGoAddStr(w, "KeyF36", t.KeyF36)
|
||||
dotGoAddStr(w, "KeyF37", t.KeyF37)
|
||||
dotGoAddStr(w, "KeyF38", t.KeyF38)
|
||||
dotGoAddStr(w, "KeyF39", t.KeyF39)
|
||||
dotGoAddStr(w, "KeyF40", t.KeyF40)
|
||||
dotGoAddStr(w, "KeyF41", t.KeyF41)
|
||||
dotGoAddStr(w, "KeyF42", t.KeyF42)
|
||||
dotGoAddStr(w, "KeyF43", t.KeyF43)
|
||||
dotGoAddStr(w, "KeyF44", t.KeyF44)
|
||||
dotGoAddStr(w, "KeyF45", t.KeyF45)
|
||||
dotGoAddStr(w, "KeyF46", t.KeyF46)
|
||||
dotGoAddStr(w, "KeyF47", t.KeyF47)
|
||||
dotGoAddStr(w, "KeyF48", t.KeyF48)
|
||||
dotGoAddStr(w, "KeyF49", t.KeyF49)
|
||||
dotGoAddStr(w, "KeyF50", t.KeyF50)
|
||||
dotGoAddStr(w, "KeyF51", t.KeyF51)
|
||||
dotGoAddStr(w, "KeyF52", t.KeyF52)
|
||||
dotGoAddStr(w, "KeyF53", t.KeyF53)
|
||||
dotGoAddStr(w, "KeyF54", t.KeyF54)
|
||||
dotGoAddStr(w, "KeyF55", t.KeyF55)
|
||||
dotGoAddStr(w, "KeyF56", t.KeyF56)
|
||||
dotGoAddStr(w, "KeyF57", t.KeyF57)
|
||||
dotGoAddStr(w, "KeyF58", t.KeyF58)
|
||||
dotGoAddStr(w, "KeyF59", t.KeyF59)
|
||||
dotGoAddStr(w, "KeyF60", t.KeyF60)
|
||||
dotGoAddStr(w, "KeyF61", t.KeyF61)
|
||||
dotGoAddStr(w, "KeyF62", t.KeyF62)
|
||||
dotGoAddStr(w, "KeyF63", t.KeyF63)
|
||||
dotGoAddStr(w, "KeyF64", t.KeyF64)
|
||||
dotGoAddStr(w, "KeyCancel", t.KeyCancel)
|
||||
dotGoAddStr(w, "KeyPrint", t.KeyPrint)
|
||||
dotGoAddStr(w, "KeyExit", t.KeyExit)
|
||||
dotGoAddStr(w, "KeyHelp", t.KeyHelp)
|
||||
dotGoAddStr(w, "KeyClear", t.KeyClear)
|
||||
dotGoAddStr(w, "KeyBacktab", t.KeyBacktab)
|
||||
dotGoAddStr(w, "KeyShfLeft", t.KeyShfLeft)
|
||||
dotGoAddStr(w, "KeyShfRight", t.KeyShfRight)
|
||||
dotGoAddStr(w, "KeyShfUp", t.KeyShfUp)
|
||||
dotGoAddStr(w, "KeyShfDown", t.KeyShfDown)
|
||||
dotGoAddStr(w, "KeyCtrlLeft", t.KeyCtrlLeft)
|
||||
dotGoAddStr(w, "KeyCtrlRight", t.KeyCtrlRight)
|
||||
dotGoAddStr(w, "KeyCtrlUp", t.KeyCtrlUp)
|
||||
dotGoAddStr(w, "KeyCtrlDown", t.KeyCtrlDown)
|
||||
dotGoAddStr(w, "KeyMetaLeft", t.KeyMetaLeft)
|
||||
dotGoAddStr(w, "KeyMetaRight", t.KeyMetaRight)
|
||||
dotGoAddStr(w, "KeyMetaUp", t.KeyMetaUp)
|
||||
dotGoAddStr(w, "KeyMetaDown", t.KeyMetaDown)
|
||||
dotGoAddStr(w, "KeyAltLeft", t.KeyAltLeft)
|
||||
dotGoAddStr(w, "KeyAltRight", t.KeyAltRight)
|
||||
dotGoAddStr(w, "KeyAltUp", t.KeyAltUp)
|
||||
dotGoAddStr(w, "KeyAltDown", t.KeyAltDown)
|
||||
dotGoAddStr(w, "KeyAltShfLeft", t.KeyAltShfLeft)
|
||||
dotGoAddStr(w, "KeyAltShfRight", t.KeyAltShfRight)
|
||||
dotGoAddStr(w, "KeyAltShfUp", t.KeyAltShfUp)
|
||||
dotGoAddStr(w, "KeyAltShfDown", t.KeyAltShfDown)
|
||||
dotGoAddStr(w, "KeyMetaShfLeft", t.KeyMetaShfLeft)
|
||||
dotGoAddStr(w, "KeyMetaShfRight", t.KeyMetaShfRight)
|
||||
dotGoAddStr(w, "KeyMetaShfUp", t.KeyMetaShfUp)
|
||||
dotGoAddStr(w, "KeyMetaShfDown", t.KeyMetaShfDown)
|
||||
dotGoAddStr(w, "KeyCtrlShfLeft", t.KeyCtrlShfLeft)
|
||||
dotGoAddStr(w, "KeyCtrlShfRight", t.KeyCtrlShfRight)
|
||||
dotGoAddStr(w, "KeyCtrlShfUp", t.KeyCtrlShfUp)
|
||||
dotGoAddStr(w, "KeyCtrlShfDown", t.KeyCtrlShfDown)
|
||||
dotGoAddStr(w, "KeyShfHome", t.KeyShfHome)
|
||||
dotGoAddStr(w, "KeyShfEnd", t.KeyShfEnd)
|
||||
dotGoAddStr(w, "KeyCtrlHome", t.KeyCtrlHome)
|
||||
dotGoAddStr(w, "KeyCtrlEnd", t.KeyCtrlEnd)
|
||||
dotGoAddStr(w, "KeyMetaHome", t.KeyMetaHome)
|
||||
dotGoAddStr(w, "KeyMetaEnd", t.KeyMetaEnd)
|
||||
dotGoAddStr(w, "KeyAltHome", t.KeyAltHome)
|
||||
dotGoAddStr(w, "KeyAltEnd", t.KeyAltEnd)
|
||||
dotGoAddStr(w, "KeyCtrlShfHome", t.KeyCtrlShfHome)
|
||||
dotGoAddStr(w, "KeyCtrlShfEnd", t.KeyCtrlShfEnd)
|
||||
dotGoAddStr(w, "KeyMetaShfHome", t.KeyMetaShfHome)
|
||||
dotGoAddStr(w, "KeyMetaShfEnd", t.KeyMetaShfEnd)
|
||||
dotGoAddStr(w, "KeyAltShfHome", t.KeyAltShfHome)
|
||||
dotGoAddStr(w, "KeyAltShfEnd", t.KeyAltShfEnd)
|
||||
fmt.Fprintln(w, " })")
|
||||
}
|
||||
|
||||
func MkInfo() ([]byte, error) {
|
||||
jsonfile := ""
|
||||
gofile := ""
|
||||
nofatal := true
|
||||
quiet := true
|
||||
|
||||
var e error
|
||||
js := []byte{}
|
||||
|
||||
args := []string{os.Getenv("TERM")}
|
||||
|
||||
tdata := make(map[string]*tcell.Terminfo)
|
||||
adata := make(map[string]string)
|
||||
for _, term := range args {
|
||||
if arr := strings.SplitN(term, "=", 2); len(arr) == 2 {
|
||||
adata[arr[0]] = arr[1]
|
||||
} else if t, e := getinfo(term); e != nil {
|
||||
if !quiet {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"Failed loading %s: %v\n", term, e)
|
||||
}
|
||||
if !nofatal {
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
tdata[t.Name] = t
|
||||
}
|
||||
}
|
||||
for alias, canon := range adata {
|
||||
if t, ok := tdata[canon]; ok {
|
||||
t.Aliases = append(t.Aliases, alias)
|
||||
// sort aliases to avoid extra diffs
|
||||
sort.Strings(t.Aliases)
|
||||
} else {
|
||||
if !quiet {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"Alias %s missing canonical %s\n",
|
||||
alias, canon)
|
||||
}
|
||||
if !nofatal {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gofile != "" {
|
||||
w := os.Stdout
|
||||
if gofile != "-" {
|
||||
if w, e = os.Create(gofile); e != nil {
|
||||
return []byte{}, fmt.Errorf("Failed: %v", e)
|
||||
}
|
||||
}
|
||||
dotGoHeader(w)
|
||||
for _, term := range args {
|
||||
if t := tdata[term]; t != nil {
|
||||
dotGoInfo(w, t)
|
||||
}
|
||||
}
|
||||
dotGoTrailer(w)
|
||||
if w != os.Stdout {
|
||||
w.Close()
|
||||
}
|
||||
} else if jsonfile != "" {
|
||||
w := os.Stdout
|
||||
if jsonfile != "-" {
|
||||
if w, e = os.Create(jsonfile); e != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed: %v", e)
|
||||
}
|
||||
}
|
||||
for _, term := range args {
|
||||
if t := tdata[term]; t != nil {
|
||||
js, e = json.Marshal(t)
|
||||
fmt.Fprintln(w, string(js))
|
||||
}
|
||||
// arguably if there is more than one term, this
|
||||
// should be a javascript array, but that's not how
|
||||
// we load it. We marshal objects one at a time from
|
||||
// the file.
|
||||
}
|
||||
if e != nil {
|
||||
return []byte{}, fmt.Errorf("Failed: %v", e)
|
||||
}
|
||||
} else {
|
||||
for _, term := range args {
|
||||
if t := tdata[term]; t != nil {
|
||||
js, e := json.Marshal(tdata[term])
|
||||
if e != nil {
|
||||
return []byte{}, fmt.Errorf("Failed ", e)
|
||||
}
|
||||
return js, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []byte{}, nil
|
||||
}
|
||||
176
cmd/micro/plugin.go
Normal file
176
cmd/micro/plugin.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/layeh/gopher-luar"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var loadedPlugins []string
|
||||
|
||||
var preInstalledPlugins = []string{
|
||||
"go",
|
||||
"linter",
|
||||
"autoclose",
|
||||
}
|
||||
|
||||
// Call calls the lua function 'function'
|
||||
// If it does not exist nothing happens, if there is an error,
|
||||
// the error is returned
|
||||
func Call(function string, args ...interface{}) (lua.LValue, error) {
|
||||
var luaFunc lua.LValue
|
||||
if strings.Contains(function, ".") {
|
||||
plugin := L.GetGlobal(strings.Split(function, ".")[0])
|
||||
if plugin.String() == "nil" {
|
||||
return nil, errors.New("function does not exist: " + function)
|
||||
}
|
||||
luaFunc = L.GetField(plugin, strings.Split(function, ".")[1])
|
||||
} else {
|
||||
luaFunc = L.GetGlobal(function)
|
||||
}
|
||||
|
||||
if luaFunc.String() == "nil" {
|
||||
return nil, errors.New("function does not exist: " + function)
|
||||
}
|
||||
var luaArgs []lua.LValue
|
||||
for _, v := range args {
|
||||
luaArgs = append(luaArgs, luar.New(L, v))
|
||||
}
|
||||
err := L.CallByParam(lua.P{
|
||||
Fn: luaFunc,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luaArgs...)
|
||||
ret := L.Get(-1) // returned value
|
||||
if ret.String() != "nil" {
|
||||
L.Pop(1) // remove received value
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// LuaFunctionBinding is a function generator which takes the name of a lua function
|
||||
// and creates a function that will call that lua function
|
||||
// Specifically it creates a function that can be called as a binding because this is used
|
||||
// to bind keys to lua functions
|
||||
func LuaFunctionBinding(function string) func(*View, bool) bool {
|
||||
return func(v *View, _ bool) bool {
|
||||
_, err := Call(function, nil)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func unpack(old []string) []interface{} {
|
||||
new := make([]interface{}, len(old))
|
||||
for i, v := range old {
|
||||
new[i] = v
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
// LuaFunctionCommand is the same as LuaFunctionBinding except it returns a normal function
|
||||
// so that a command can be bound to a lua function
|
||||
func LuaFunctionCommand(function string) func([]string) {
|
||||
return func(args []string) {
|
||||
_, err := Call(function, unpack(args)...)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LuaFunctionComplete returns a function which can be used for autocomplete in plugins
|
||||
func LuaFunctionComplete(function string) func(string) []string {
|
||||
return func(input string) (result []string) {
|
||||
|
||||
res, err := Call(function, input)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
if tbl, ok := res.(*lua.LTable); !ok {
|
||||
TermMessage(function, "should return a table of strings")
|
||||
} else {
|
||||
for i := 1; i <= tbl.Len(); i++ {
|
||||
val := tbl.RawGetInt(i)
|
||||
if v, ok := val.(lua.LString); !ok {
|
||||
TermMessage(function, "should return a table of strings")
|
||||
} else {
|
||||
result = append(result, string(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func LuaFunctionJob(function string) func(string, ...string) {
|
||||
return func(output string, args ...string) {
|
||||
_, err := Call(function, unpack(append([]string{output}, args...))...)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins
|
||||
func LoadPlugins() {
|
||||
files, _ := ioutil.ReadDir(configDir + "/plugins")
|
||||
for _, plugin := range files {
|
||||
if plugin.IsDir() {
|
||||
pluginName := plugin.Name()
|
||||
files, _ := ioutil.ReadDir(configDir + "/plugins/" + pluginName)
|
||||
for _, f := range files {
|
||||
if f.Name() == pluginName+".lua" {
|
||||
data, _ := ioutil.ReadFile(configDir + "/plugins/" + pluginName + "/" + f.Name())
|
||||
pluginDef := "\nlocal P = {}\n" + pluginName + " = P\nsetmetatable(" + pluginName + ", {__index = _G})\nsetfenv(1, P)\n"
|
||||
|
||||
if err := L.DoString(pluginDef + string(data)); err != nil {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
loadedPlugins = append(loadedPlugins, pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pluginName := range preInstalledPlugins {
|
||||
alreadyExists := false
|
||||
for _, pl := range loadedPlugins {
|
||||
if pl == pluginName {
|
||||
alreadyExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyExists {
|
||||
plugin := "runtime/plugins/" + pluginName + "/" + pluginName + ".lua"
|
||||
data, err := Asset(plugin)
|
||||
if err != nil {
|
||||
TermMessage("Error loading pre-installed plugin: " + pluginName)
|
||||
continue
|
||||
}
|
||||
pluginDef := "\nlocal P = {}\n" + pluginName + " = P\nsetmetatable(" + pluginName + ", {__index = _G})\nsetfenv(1, P)\n"
|
||||
if err := L.DoString(pluginDef + string(data)); err != nil {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
|
||||
loadedPlugins = append(loadedPlugins, pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configDir + "/init.lua"); err == nil {
|
||||
pluginDef := "\nlocal P = {}\n" + "init" + " = P\nsetmetatable(" + "init" + ", {__index = _G})\nsetfenv(1, P)\n"
|
||||
data, _ := ioutil.ReadFile(configDir + "/init.lua")
|
||||
if err := L.DoString(pluginDef + string(data)); err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
loadedPlugins = append(loadedPlugins, "init")
|
||||
}
|
||||
}
|
||||
2873
cmd/micro/runtime.go
Normal file
2873
cmd/micro/runtime.go
Normal file
File diff suppressed because one or more lines are too long
135
cmd/micro/search.go
Normal file
135
cmd/micro/search.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
var (
|
||||
// What was the last search
|
||||
lastSearch string
|
||||
|
||||
// Where should we start the search down from (or up from)
|
||||
searchStart int
|
||||
|
||||
// Is there currently a search in progress
|
||||
searching bool
|
||||
|
||||
// Stores the history for searching
|
||||
searchHistory []string
|
||||
)
|
||||
|
||||
// BeginSearch starts a search
|
||||
func BeginSearch() {
|
||||
searchHistory = append(searchHistory, "")
|
||||
messenger.historyNum = len(searchHistory) - 1
|
||||
searching = true
|
||||
messenger.hasPrompt = true
|
||||
messenger.Message("Find: ")
|
||||
}
|
||||
|
||||
// EndSearch stops the current search
|
||||
func EndSearch() {
|
||||
searchHistory[len(searchHistory)-1] = messenger.response
|
||||
searching = false
|
||||
messenger.hasPrompt = false
|
||||
messenger.Clear()
|
||||
messenger.Reset()
|
||||
if lastSearch != "" {
|
||||
messenger.Message("^P Previous ^N Next")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSearchEvent takes an event and a view and will do a real time match from the messenger's output
|
||||
// to the current buffer. It searches down the buffer.
|
||||
func HandleSearchEvent(event tcell.Event, v *View) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape, tcell.KeyEnter:
|
||||
// Done
|
||||
EndSearch()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
messenger.HandleEvent(event, searchHistory)
|
||||
|
||||
if messenger.cursorx < 0 {
|
||||
// Done
|
||||
EndSearch()
|
||||
return
|
||||
}
|
||||
|
||||
if messenger.response == "" {
|
||||
v.Cursor.ResetSelection()
|
||||
// We don't end the search though
|
||||
return
|
||||
}
|
||||
|
||||
Search(messenger.response, v, true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Search searches in the view for the given regex. The down bool
|
||||
// specifies whether it should search down from the searchStart position
|
||||
// or up from there
|
||||
func Search(searchStr string, v *View, down bool) {
|
||||
if searchStr == "" {
|
||||
return
|
||||
}
|
||||
var str string
|
||||
var charPos int
|
||||
text := v.Buf.String()
|
||||
if down {
|
||||
str = string([]rune(text)[searchStart:])
|
||||
charPos = searchStart
|
||||
} else {
|
||||
str = string([]rune(text)[:searchStart])
|
||||
}
|
||||
r, err := regexp.Compile(searchStr)
|
||||
if v.Buf.Settings["ignorecase"].(bool) {
|
||||
r, err = regexp.Compile("(?i)" + searchStr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
matches := r.FindAllStringIndex(str, -1)
|
||||
var match []int
|
||||
if matches == nil {
|
||||
// Search the entire buffer now
|
||||
matches = r.FindAllStringIndex(text, -1)
|
||||
charPos = 0
|
||||
if matches == nil {
|
||||
v.Cursor.ResetSelection()
|
||||
return
|
||||
}
|
||||
|
||||
if !down {
|
||||
match = matches[len(matches)-1]
|
||||
} else {
|
||||
match = matches[0]
|
||||
}
|
||||
str = text
|
||||
}
|
||||
|
||||
if !down {
|
||||
match = matches[len(matches)-1]
|
||||
} else {
|
||||
match = matches[0]
|
||||
}
|
||||
|
||||
if match[0] == match[1] {
|
||||
return
|
||||
}
|
||||
|
||||
v.Cursor.SetSelectionStart(FromCharPos(charPos+runePos(match[0], str), v.Buf))
|
||||
v.Cursor.SetSelectionEnd(FromCharPos(charPos+runePos(match[1], str), v.Buf))
|
||||
v.Cursor.Loc = v.Cursor.CurSelection[1]
|
||||
if v.Relocate() {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
lastSearch = searchStr
|
||||
}
|
||||
329
cmd/micro/settings.go
Normal file
329
cmd/micro/settings.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yosuke-furukawa/json5/encoding/json5"
|
||||
"github.com/zyedidia/glob"
|
||||
)
|
||||
|
||||
// The options that the user can set
|
||||
var globalSettings map[string]interface{}
|
||||
|
||||
// InitGlobalSettings initializes the options map and sets all options to their default values
|
||||
func InitGlobalSettings() {
|
||||
defaults := DefaultGlobalSettings()
|
||||
var parsed map[string]interface{}
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
writeSettings := false
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if !strings.HasPrefix(string(input), "null") {
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json:", err.Error())
|
||||
}
|
||||
} else {
|
||||
writeSettings = true
|
||||
}
|
||||
}
|
||||
|
||||
globalSettings = make(map[string]interface{})
|
||||
for k, v := range defaults {
|
||||
globalSettings[k] = v
|
||||
}
|
||||
for k, v := range parsed {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
globalSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings {
|
||||
err := WriteSettings(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitLocalSettings scans the json in settings.json and sets the options locally based
|
||||
// on whether the buffer matches the glob
|
||||
func InitLocalSettings(buf *Buffer) {
|
||||
var parsed map[string]interface{}
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range parsed {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
g, err := glob.Compile(k)
|
||||
if err != nil {
|
||||
TermMessage("Error with glob setting ", k, ": ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if g.MatchString(buf.Path) {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
buf.Settings[k1] = v1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSettings writes the settings to the specified filename as JSON
|
||||
func WriteSettings(filename string) error {
|
||||
var err error
|
||||
if _, e := os.Stat(configDir); e == nil {
|
||||
parsed := make(map[string]interface{})
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
for k, v := range globalSettings {
|
||||
parsed[k] = v
|
||||
}
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if string(input) != "null" {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json:", err.Error())
|
||||
}
|
||||
|
||||
for k, v := range parsed {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if _, ok := globalSettings[k]; ok {
|
||||
parsed[k] = globalSettings[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json5.MarshalIndent(parsed, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AddOption creates a new option. This is meant to be called by plugins to add options.
|
||||
func AddOption(name string, value interface{}) {
|
||||
globalSettings[name] = value
|
||||
err := WriteSettings(configDir + "/settings.json")
|
||||
if err != nil {
|
||||
TermMessage("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalOption returns the global value of the given option
|
||||
func GetGlobalOption(name string) interface{} {
|
||||
return globalSettings[name]
|
||||
}
|
||||
|
||||
// GetLocalOption returns the local value of the given option
|
||||
func GetLocalOption(name string, buf *Buffer) interface{} {
|
||||
return buf.Settings[name]
|
||||
}
|
||||
|
||||
// GetOption returns the value of the given option
|
||||
// If there is a local version of the option, it returns that
|
||||
// otherwise it will return the global version
|
||||
func GetOption(name string) interface{} {
|
||||
if GetLocalOption(name, CurView().Buf) != nil {
|
||||
return GetLocalOption(name, CurView().Buf)
|
||||
}
|
||||
return GetGlobalOption(name)
|
||||
}
|
||||
|
||||
// DefaultGlobalSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultGlobalSettings() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"autoindent": true,
|
||||
"colorscheme": "zenburn",
|
||||
"cursorline": true,
|
||||
"ignorecase": false,
|
||||
"indentchar": " ",
|
||||
"infobar": true,
|
||||
"ruler": true,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollspeed": float64(2),
|
||||
"scrollmargin": float64(3),
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultLocalSettings returns the default local settings
|
||||
// Note that filetype is a local only option
|
||||
func DefaultLocalSettings() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"autoindent": true,
|
||||
"cursorline": true,
|
||||
"filetype": "Unknown",
|
||||
"ignorecase": false,
|
||||
"indentchar": " ",
|
||||
"ruler": true,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollspeed": float64(2),
|
||||
"scrollmargin": float64(3),
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
}
|
||||
}
|
||||
|
||||
// SetOption attempts to set the given option to the value
|
||||
// By default it will set the option as global, but if the option
|
||||
// is local only it will set the local version
|
||||
// Use setlocal to force an option to be set locally
|
||||
func SetOption(option, value string) error {
|
||||
if option == "colorscheme" {
|
||||
if !ColorschemeExists(value) {
|
||||
return errors.New(value + " is not a valid colorscheme")
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := globalSettings[option]; !ok {
|
||||
if _, ok := CurView().Buf.Settings[option]; !ok {
|
||||
return errors.New("Invalid option")
|
||||
}
|
||||
SetLocalOption(option, value, CurView())
|
||||
return nil
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(globalSettings[option]).Kind()
|
||||
if kind == reflect.Bool {
|
||||
b, err := ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
globalSettings[option] = b
|
||||
} else if kind == reflect.String {
|
||||
globalSettings[option] = value
|
||||
} else if kind == reflect.Float64 {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
globalSettings[option] = float64(i)
|
||||
}
|
||||
|
||||
if option == "colorscheme" {
|
||||
LoadSyntaxFiles()
|
||||
for _, tab := range tabs {
|
||||
for _, view := range tab.views {
|
||||
view.Buf.UpdateRules()
|
||||
if view.Buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if option == "infobar" {
|
||||
for _, tab := range tabs {
|
||||
tab.Resize()
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := CurView().Buf.Settings[option]; ok {
|
||||
for _, tab := range tabs {
|
||||
for _, view := range tab.views {
|
||||
SetLocalOption(option, value, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocalOption sets the local version of this option
|
||||
func SetLocalOption(option, value string, view *View) error {
|
||||
buf := view.Buf
|
||||
if _, ok := buf.Settings[option]; !ok {
|
||||
return errors.New("Invalid option")
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(buf.Settings[option]).Kind()
|
||||
if kind == reflect.Bool {
|
||||
b, err := ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
buf.Settings[option] = b
|
||||
} else if kind == reflect.String {
|
||||
buf.Settings[option] = value
|
||||
} else if kind == reflect.Float64 {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
buf.Settings[option] = float64(i)
|
||||
}
|
||||
|
||||
if option == "statusline" {
|
||||
view.ToggleStatusLine()
|
||||
if buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
}
|
||||
|
||||
if option == "filetype" {
|
||||
LoadSyntaxFiles()
|
||||
buf.UpdateRules()
|
||||
if buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOptionAndSettings sets the given option and saves the option setting to the settings config file
|
||||
func SetOptionAndSettings(option, value string) {
|
||||
filename := configDir + "/settings.json"
|
||||
|
||||
err := SetOption(option, value)
|
||||
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = WriteSettings(filename)
|
||||
if err != nil {
|
||||
messenger.Error("Error writing to settings.json: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
205
cmd/micro/split_tree.go
Normal file
205
cmd/micro/split_tree.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package main
|
||||
|
||||
type SplitType bool
|
||||
|
||||
const (
|
||||
VerticalSplit = false
|
||||
HorizontalSplit = true
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
VSplit(buf *Buffer)
|
||||
HSplit(buf *Buffer)
|
||||
String() string
|
||||
}
|
||||
|
||||
type LeafNode struct {
|
||||
view *View
|
||||
|
||||
parent *SplitTree
|
||||
}
|
||||
|
||||
func NewLeafNode(v *View, parent *SplitTree) *LeafNode {
|
||||
n := new(LeafNode)
|
||||
n.view = v
|
||||
n.view.splitNode = n
|
||||
n.parent = parent
|
||||
return n
|
||||
}
|
||||
|
||||
type SplitTree struct {
|
||||
kind SplitType
|
||||
|
||||
parent *SplitTree
|
||||
children []Node
|
||||
|
||||
x int
|
||||
y int
|
||||
|
||||
width int
|
||||
height int
|
||||
|
||||
tabNum int
|
||||
}
|
||||
|
||||
func (l *LeafNode) VSplit(buf *Buffer) {
|
||||
tab := tabs[l.parent.tabNum]
|
||||
if l.parent.kind == VerticalSplit {
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
l.parent.children = append(l.parent.children, NewLeafNode(newView, l.parent))
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
} else {
|
||||
s := new(SplitTree)
|
||||
s.kind = VerticalSplit
|
||||
s.parent = l.parent
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
s.children = []Node{l, NewLeafNode(newView, s)}
|
||||
l.parent.children[search(l.parent.children, l)] = s
|
||||
l.parent = s
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LeafNode) HSplit(buf *Buffer) {
|
||||
tab := tabs[l.parent.tabNum]
|
||||
if l.parent.kind == HorizontalSplit {
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
l.parent.children = append(l.parent.children, NewLeafNode(newView, l.parent))
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
} else {
|
||||
s := new(SplitTree)
|
||||
s.kind = HorizontalSplit
|
||||
s.parent = l.parent
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
s.children = []Node{l, NewLeafNode(newView, s)}
|
||||
l.parent.children[search(l.parent.children, l)] = s
|
||||
l.parent = s
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LeafNode) Delete() {
|
||||
i := search(l.parent.children, l)
|
||||
|
||||
copy(l.parent.children[i:], l.parent.children[i+1:])
|
||||
l.parent.children[len(l.parent.children)-1] = nil
|
||||
l.parent.children = l.parent.children[:len(l.parent.children)-1]
|
||||
|
||||
tab := tabs[l.parent.tabNum]
|
||||
j := findView(tab.views, l.view)
|
||||
copy(tab.views[j:], tab.views[j+1:])
|
||||
tab.views[len(tab.views)-1] = nil // or the zero value of T
|
||||
tab.views = tab.views[:len(tab.views)-1]
|
||||
|
||||
for i, v := range tab.views {
|
||||
v.Num = i
|
||||
}
|
||||
if tab.curView > 0 {
|
||||
tab.curView--
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SplitTree) Cleanup() {
|
||||
for i, node := range s.children {
|
||||
if n, ok := node.(*SplitTree); ok {
|
||||
if len(n.children) == 1 {
|
||||
if child, ok := n.children[0].(*LeafNode); ok {
|
||||
s.children[i] = child
|
||||
child.parent = s
|
||||
continue
|
||||
}
|
||||
}
|
||||
n.Cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SplitTree) ResizeSplits() {
|
||||
for i, node := range s.children {
|
||||
if n, ok := node.(*LeafNode); ok {
|
||||
if s.kind == VerticalSplit {
|
||||
n.view.width = s.width / len(s.children)
|
||||
n.view.height = s.height
|
||||
|
||||
n.view.x = s.x + n.view.width*i
|
||||
n.view.y = s.y
|
||||
} else {
|
||||
n.view.height = s.height / len(s.children)
|
||||
n.view.width = s.width
|
||||
|
||||
n.view.y = s.y + n.view.height*i
|
||||
n.view.x = s.x
|
||||
}
|
||||
if n.view.Buf.Settings["statusline"].(bool) {
|
||||
n.view.height--
|
||||
}
|
||||
|
||||
n.view.ToggleTabbar()
|
||||
n.view.matches = Match(n.view)
|
||||
} else if n, ok := node.(*SplitTree); ok {
|
||||
if s.kind == VerticalSplit {
|
||||
n.width = s.width / len(s.children)
|
||||
n.height = s.height
|
||||
|
||||
n.x = s.x + n.width*i
|
||||
n.y = s.y
|
||||
} else {
|
||||
n.height = s.height / len(s.children)
|
||||
n.width = s.width
|
||||
|
||||
n.y = s.y + n.height*i
|
||||
n.x = s.x
|
||||
}
|
||||
n.ResizeSplits()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LeafNode) String() string {
|
||||
return l.view.Buf.Name
|
||||
}
|
||||
|
||||
func search(haystack []Node, needle Node) int {
|
||||
for i, x := range haystack {
|
||||
if x == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func findView(haystack []*View, needle *View) int {
|
||||
for i, x := range haystack {
|
||||
if x == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *SplitTree) VSplit(buf *Buffer) {}
|
||||
func (s *SplitTree) HSplit(buf *Buffer) {}
|
||||
|
||||
func (s *SplitTree) String() string {
|
||||
str := "["
|
||||
for _, child := range s.children {
|
||||
str += child.String() + ", "
|
||||
}
|
||||
return str + "]"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package buffer
|
||||
package main
|
||||
|
||||
// TEStack is a simple implementation of a LIFO stack for text events
|
||||
type TEStack struct {
|
||||
// Stack is a simple implementation of a LIFO stack for text events
|
||||
type Stack struct {
|
||||
Top *Element
|
||||
Size int
|
||||
}
|
||||
@@ -13,19 +13,19 @@ type Element struct {
|
||||
}
|
||||
|
||||
// Len returns the stack's length
|
||||
func (s *TEStack) Len() int {
|
||||
func (s *Stack) Len() int {
|
||||
return s.Size
|
||||
}
|
||||
|
||||
// Push a new element onto the stack
|
||||
func (s *TEStack) Push(value *TextEvent) {
|
||||
func (s *Stack) Push(value *TextEvent) {
|
||||
s.Top = &Element{value, s.Top}
|
||||
s.Size++
|
||||
}
|
||||
|
||||
// Pop removes the top element from the stack and returns its value
|
||||
// If the stack is empty, return nil
|
||||
func (s *TEStack) Pop() (value *TextEvent) {
|
||||
func (s *Stack) Pop() (value *TextEvent) {
|
||||
if s.Size > 0 {
|
||||
value, s.Top = s.Top.Value, s.Top.Next
|
||||
s.Size--
|
||||
@@ -35,7 +35,7 @@ func (s *TEStack) Pop() (value *TextEvent) {
|
||||
}
|
||||
|
||||
// Peek returns the top element of the stack without removing it
|
||||
func (s *TEStack) Peek() *TextEvent {
|
||||
func (s *Stack) Peek() *TextEvent {
|
||||
if s.Size > 0 {
|
||||
return s.Top.Value
|
||||
}
|
||||
65
cmd/micro/statusline.go
Normal file
65
cmd/micro/statusline.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Statusline represents the information line at the bottom
|
||||
// of each view
|
||||
// It gives information such as filename, whether the file has been
|
||||
// modified, filetype, cursor location
|
||||
type Statusline struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
// Display draws the statusline to the screen
|
||||
func (sline *Statusline) Display() {
|
||||
// We'll draw the line at the lowest line in the view
|
||||
y := sline.view.height + sline.view.y
|
||||
|
||||
file := sline.view.Buf.Name
|
||||
|
||||
// If the buffer is dirty (has been modified) write a little '+'
|
||||
if sline.view.Buf.IsModified {
|
||||
file += " +"
|
||||
}
|
||||
|
||||
// Add one to cursor.x and cursor.y because (0,0) is the top left,
|
||||
// but users will be used to (1,1) (first line,first column)
|
||||
// We use GetVisualX() here because otherwise we get the column number in runes
|
||||
// so a '\t' is only 1, when it should be tabSize
|
||||
columnNum := strconv.Itoa(sline.view.Cursor.GetVisualX() + 1)
|
||||
lineNum := strconv.Itoa(sline.view.Cursor.Y + 1)
|
||||
|
||||
file += " (" + lineNum + "," + columnNum + ")"
|
||||
|
||||
// Add the filetype
|
||||
file += " " + sline.view.Buf.FileType()
|
||||
|
||||
rightText := helpBinding + " for help "
|
||||
if sline.view.Help {
|
||||
rightText = helpBinding + " to close help "
|
||||
}
|
||||
|
||||
statusLineStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
// Maybe there is a unicode filename?
|
||||
fileRunes := []rune(file)
|
||||
viewX := sline.view.x
|
||||
if viewX != 0 {
|
||||
screen.SetContent(viewX, y, ' ', nil, statusLineStyle)
|
||||
viewX++
|
||||
}
|
||||
for x := 0; x < sline.view.width; x++ {
|
||||
if x < len(fileRunes) {
|
||||
screen.SetContent(viewX+x, y, fileRunes[x], nil, statusLineStyle)
|
||||
} else if x >= sline.view.width-len(rightText) && x < len(rightText)+sline.view.width-len(rightText) {
|
||||
screen.SetContent(viewX+x, y, []rune(rightText)[x-sline.view.width+len(rightText)], nil, statusLineStyle)
|
||||
} else {
|
||||
screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
162
cmd/micro/tab.go
Normal file
162
cmd/micro/tab.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type Tab struct {
|
||||
// This contains all the views in this tab
|
||||
// There is generally only one view per tab, but you can have
|
||||
// multiple views with splits
|
||||
views []*View
|
||||
// This is the current view for this tab
|
||||
curView int
|
||||
// Generally this is the name of the current view's buffer
|
||||
name string
|
||||
|
||||
tree *SplitTree
|
||||
}
|
||||
|
||||
// NewTabFromView creates a new tab and puts the given view in the tab
|
||||
func NewTabFromView(v *View) *Tab {
|
||||
t := new(Tab)
|
||||
t.views = append(t.views, v)
|
||||
t.views[0].Num = 0
|
||||
|
||||
t.tree = new(SplitTree)
|
||||
t.tree.kind = VerticalSplit
|
||||
t.tree.children = []Node{NewLeafNode(t.views[0], t.tree)}
|
||||
|
||||
w, h := screen.Size()
|
||||
t.tree.width = w
|
||||
t.tree.height = h
|
||||
|
||||
if globalSettings["infobar"].(bool) {
|
||||
t.tree.height--
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// SetNum sets all this tab's views to have the correct tab number
|
||||
func (t *Tab) SetNum(num int) {
|
||||
for _, v := range t.views {
|
||||
v.TabNum = num
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tab) Cleanup() {
|
||||
t.tree.Cleanup()
|
||||
}
|
||||
|
||||
func (t *Tab) Resize() {
|
||||
w, h := screen.Size()
|
||||
t.tree.width = w
|
||||
t.tree.height = h
|
||||
|
||||
if globalSettings["infobar"].(bool) {
|
||||
t.tree.height--
|
||||
}
|
||||
|
||||
t.tree.ResizeSplits()
|
||||
}
|
||||
|
||||
// CurView returns the current view
|
||||
func CurView() *View {
|
||||
curTab := tabs[curTab]
|
||||
return curTab.views[curTab.curView]
|
||||
}
|
||||
|
||||
// TabbarString returns the string that should be displayed in the tabbar
|
||||
// It also returns a map containing which indicies correspond to which tab number
|
||||
// This is useful when we know that the mouse click has occurred at an x location
|
||||
// but need to know which tab that corresponds to to accurately change the tab
|
||||
func TabbarString() (string, map[int]int) {
|
||||
str := ""
|
||||
indicies := make(map[int]int)
|
||||
for i, t := range tabs {
|
||||
if i == curTab {
|
||||
str += "["
|
||||
} else {
|
||||
str += " "
|
||||
}
|
||||
str += t.views[t.curView].Buf.Name
|
||||
if i == curTab {
|
||||
str += "]"
|
||||
} else {
|
||||
str += " "
|
||||
}
|
||||
indicies[len(str)-1] = i + 1
|
||||
str += " "
|
||||
}
|
||||
return str, indicies
|
||||
}
|
||||
|
||||
// TabbarHandleMouseEvent checks the given mouse event if it is clicking on the tabbar
|
||||
// If it is it changes the current tab accordingly
|
||||
// This function returns true if the tab is changed
|
||||
func TabbarHandleMouseEvent(event tcell.Event) bool {
|
||||
// There is no tabbar displayed if there are less than 2 tabs
|
||||
if len(tabs) <= 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
button := e.Buttons()
|
||||
// Must be a left click
|
||||
if button == tcell.Button1 {
|
||||
x, y := e.Position()
|
||||
if y != 0 {
|
||||
return false
|
||||
}
|
||||
str, indicies := TabbarString()
|
||||
if x >= len(str) {
|
||||
return false
|
||||
}
|
||||
var tabnum int
|
||||
var keys []int
|
||||
for k := range indicies {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
for _, k := range keys {
|
||||
if x <= k {
|
||||
tabnum = indicies[k] - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
curTab = tabnum
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs
|
||||
func DisplayTabs() {
|
||||
if len(tabs) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
str, _ := TabbarString()
|
||||
|
||||
tabBarStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["tabbar"]; ok {
|
||||
tabBarStyle = style
|
||||
}
|
||||
|
||||
// Maybe there is a unicode filename?
|
||||
fileRunes := []rune(str)
|
||||
w, _ := screen.Size()
|
||||
for x := 0; x < w; x++ {
|
||||
if x < len(fileRunes) {
|
||||
screen.SetContent(x, 0, fileRunes[x], nil, tabBarStyle)
|
||||
} else {
|
||||
screen.SetContent(x, 0, ' ', nil, tabBarStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
296
cmd/micro/util.go
Normal file
296
cmd/micro/util.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// Util.go is a collection of utility functions that are used throughout
|
||||
// the program
|
||||
|
||||
// Count returns the length of a string in runes
|
||||
// This is exactly equivalent to utf8.RuneCountInString(), just less characters
|
||||
func Count(s string) int {
|
||||
return utf8.RuneCountInString(s)
|
||||
}
|
||||
|
||||
// NumOccurrences counts the number of occurences of a byte in a string
|
||||
func NumOccurrences(s string, c byte) int {
|
||||
var n int
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Spaces returns a string with n spaces
|
||||
func Spaces(n int) string {
|
||||
var str string
|
||||
for i := 0; i < n; i++ {
|
||||
str += " "
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Min takes the min of two ints
|
||||
func Min(a, b int) int {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Max takes the max of two ints
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// IsWordChar returns whether or not the string is a 'word character'
|
||||
// If it is a unicode character, then it does not match
|
||||
// Word characters are defined as [A-Za-z0-9_]
|
||||
func IsWordChar(str string) bool {
|
||||
if len(str) > 1 {
|
||||
// Unicode
|
||||
return false
|
||||
}
|
||||
c := str[0]
|
||||
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
|
||||
}
|
||||
|
||||
// IsWhitespace returns true if the given rune is a space, tab, or newline
|
||||
func IsWhitespace(c rune) bool {
|
||||
return c == ' ' || c == '\t' || c == '\n'
|
||||
}
|
||||
|
||||
// Contains returns whether or not a string array contains a given string
|
||||
func Contains(list []string, a string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Insert makes a simple insert into a string at the given position
|
||||
func Insert(str string, pos int, value string) string {
|
||||
return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
|
||||
}
|
||||
|
||||
// GetLeadingWhitespace returns the leading whitespace of the given string
|
||||
func GetLeadingWhitespace(str string) string {
|
||||
ws := ""
|
||||
for _, c := range str {
|
||||
if c == ' ' || c == '\t' {
|
||||
ws += string(c)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
// IsSpaces checks if a given string is only spaces
|
||||
func IsSpaces(str string) bool {
|
||||
for _, c := range str {
|
||||
if c != ' ' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSpacesOrTabs checks if a given string contains only spaces and tabs
|
||||
func IsSpacesOrTabs(str string) bool {
|
||||
for _, c := range str {
|
||||
if c != ' ' && c != '\t' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
|
||||
// as 'true' and 'false' respectively
|
||||
func ParseBool(str string) (bool, error) {
|
||||
if str == "on" {
|
||||
return true, nil
|
||||
}
|
||||
if str == "off" {
|
||||
return false, nil
|
||||
}
|
||||
return strconv.ParseBool(str)
|
||||
}
|
||||
|
||||
// EscapePath replaces every path separator in a given path with a %
|
||||
func EscapePath(path string) string {
|
||||
path = filepath.ToSlash(path)
|
||||
return strings.Replace(path, "/", "%", -1)
|
||||
}
|
||||
|
||||
// GetModTime returns the last modification time for a given file
|
||||
// It also returns a boolean if there was a problem accessing the file
|
||||
func GetModTime(path string) (time.Time, bool) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return time.Now(), false
|
||||
}
|
||||
return info.ModTime(), true
|
||||
}
|
||||
|
||||
// StringWidth returns the width of a string where tabs count as `tabsize` width
|
||||
func StringWidth(str string, tabsize int) int {
|
||||
sw := runewidth.StringWidth(str)
|
||||
sw += NumOccurrences(str, '\t') * (tabsize - 1)
|
||||
return sw
|
||||
}
|
||||
|
||||
// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
|
||||
// that have a width larger than 1 (this also counts tabs as `tabsize` width)
|
||||
func WidthOfLargeRunes(str string, tabsize int) int {
|
||||
count := 0
|
||||
for _, ch := range str {
|
||||
var w int
|
||||
if ch == '\t' {
|
||||
w = tabsize
|
||||
} else {
|
||||
w = runewidth.RuneWidth(ch)
|
||||
}
|
||||
if w > 1 {
|
||||
count += (w - 1)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// RunePos returns the rune index of a given byte index
|
||||
// This could cause problems if the byte index is between code points
|
||||
func runePos(p int, str string) int {
|
||||
return utf8.RuneCountInString(str[:p])
|
||||
}
|
||||
|
||||
func lcs(a, b string) string {
|
||||
arunes := []rune(a)
|
||||
brunes := []rune(b)
|
||||
|
||||
lcs := ""
|
||||
for i, r := range arunes {
|
||||
if i >= len(brunes) {
|
||||
break
|
||||
}
|
||||
if r == brunes[i] {
|
||||
lcs += string(r)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return lcs
|
||||
}
|
||||
|
||||
func CommonSubstring(arr ...string) string {
|
||||
commonStr := arr[0]
|
||||
|
||||
for _, str := range arr[1:] {
|
||||
commonStr = lcs(commonStr, str)
|
||||
}
|
||||
|
||||
return commonStr
|
||||
}
|
||||
|
||||
// Abs is a simple absolute value function for ints
|
||||
func Abs(n int) int {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// FuncName returns the name of a given function object
|
||||
func FuncName(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
// SplitCommandArgs seperates multiple command arguments which may be quoted.
|
||||
// The returned slice contains at least one string
|
||||
func SplitCommandArgs(input string) []string {
|
||||
var result []string
|
||||
curArg := new(bytes.Buffer)
|
||||
inQuote := false
|
||||
escape := false
|
||||
|
||||
appendResult := func() {
|
||||
str := curArg.String()
|
||||
inQuote = false
|
||||
escape = false
|
||||
if strings.HasPrefix(str, `"`) && strings.HasSuffix(str, `"`) {
|
||||
if unquoted, err := strconv.Unquote(str); err == nil {
|
||||
str = unquoted
|
||||
}
|
||||
}
|
||||
result = append(result, str)
|
||||
curArg.Reset()
|
||||
}
|
||||
|
||||
for _, r := range input {
|
||||
if r == ' ' && !inQuote {
|
||||
appendResult()
|
||||
} else {
|
||||
curArg.WriteRune(r)
|
||||
|
||||
if r == '"' && !inQuote {
|
||||
inQuote = true
|
||||
} else {
|
||||
if inQuote && !escape {
|
||||
if r == '"' {
|
||||
inQuote = false
|
||||
}
|
||||
if r == '\\' {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
escape = false
|
||||
}
|
||||
appendResult()
|
||||
return result
|
||||
}
|
||||
|
||||
// JoinCommandArgs joins multiple command arguments and quote the strings if needed.
|
||||
func JoinCommandArgs(args ...string) string {
|
||||
buf := new(bytes.Buffer)
|
||||
first := true
|
||||
for _, arg := range args {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
buf.WriteRune(' ')
|
||||
}
|
||||
quoted := strconv.Quote(arg)
|
||||
if quoted[1:len(quoted)-1] != arg || strings.ContainsRune(arg, ' ') {
|
||||
buf.WriteString(quoted)
|
||||
} else {
|
||||
buf.WriteString(arg)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
113
cmd/micro/util_test.go
Normal file
113
cmd/micro/util_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNumOccurences(t *testing.T) {
|
||||
var tests = []struct {
|
||||
inputStr string
|
||||
inputChar byte
|
||||
want int
|
||||
}{
|
||||
{"aaaa", 'a', 4},
|
||||
{"\trfd\ta", '\t', 2},
|
||||
{"∆ƒ\tø ® \t\t", '\t', 3},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := NumOccurrences(test.inputStr, test.inputChar); got != test.want {
|
||||
t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaces(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input int
|
||||
want string
|
||||
}{
|
||||
{4, " "},
|
||||
{0, ""},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := Spaces(test.input); got != test.want {
|
||||
t.Errorf("Spaces(%d) = \"%s\"", test.input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWordChar(t *testing.T) {
|
||||
if IsWordChar("t") == false {
|
||||
t.Errorf("IsWordChar(t) = false")
|
||||
}
|
||||
if IsWordChar("T") == false {
|
||||
t.Errorf("IsWordChar(T) = false")
|
||||
}
|
||||
if IsWordChar("5") == false {
|
||||
t.Errorf("IsWordChar(5) = false")
|
||||
}
|
||||
if IsWordChar("_") == false {
|
||||
t.Errorf("IsWordChar(_) = false")
|
||||
}
|
||||
if IsWordChar("~") == true {
|
||||
t.Errorf("IsWordChar(~) = true")
|
||||
}
|
||||
if IsWordChar(" ") == true {
|
||||
t.Errorf("IsWordChar( ) = true")
|
||||
}
|
||||
if IsWordChar("ß") == true {
|
||||
t.Errorf("IsWordChar(ß) = true")
|
||||
}
|
||||
if IsWordChar(")") == true {
|
||||
t.Errorf("IsWordChar()) = true")
|
||||
}
|
||||
if IsWordChar("\n") == true {
|
||||
t.Errorf("IsWordChar(\n)) = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinAndSplitCommandArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
Query []string
|
||||
Wanted string
|
||||
}{
|
||||
{[]string{`test case`}, `"test case"`},
|
||||
{[]string{`quote "test"`}, `"quote \"test\""`},
|
||||
{[]string{`slash\\\ test`}, `"slash\\\\\\ test"`},
|
||||
{[]string{`path 1`, `path\" 2`}, `"path 1" "path\\\" 2"`},
|
||||
{[]string{`foo`}, `foo`},
|
||||
{[]string{`foo\"bar`}, `"foo\\\"bar"`},
|
||||
{[]string{``}, ``},
|
||||
{[]string{`"`}, `"\""`},
|
||||
{[]string{`a`, ``}, `a `},
|
||||
{[]string{``, ``, ``, ``}, ` `},
|
||||
{[]string{"\n"}, `"\n"`},
|
||||
{[]string{"foo\tbar"}, `"foo\tbar"`},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
if result := JoinCommandArgs(test.Query...); test.Wanted != result {
|
||||
t.Errorf("JoinCommandArgs failed at Test %d\nGot: %q", i, result)
|
||||
}
|
||||
|
||||
if result := SplitCommandArgs(test.Wanted); !reflect.DeepEqual(test.Query, result) {
|
||||
t.Errorf("SplitCommandArgs failed at Test %d\nGot: `%q`", i, result)
|
||||
}
|
||||
}
|
||||
|
||||
splitTests := []struct {
|
||||
Query string
|
||||
Wanted []string
|
||||
}{
|
||||
{`"hallo""Welt"`, []string{`"hallo""Welt"`}},
|
||||
{`\"`, []string{`\"`}},
|
||||
{`"\"`, []string{`"\"`}},
|
||||
}
|
||||
|
||||
for i, test := range splitTests {
|
||||
if result := SplitCommandArgs(test.Query); !reflect.DeepEqual(test.Wanted, result) {
|
||||
t.Errorf("SplitCommandArgs failed at Split-Test %d\nGot: `%q`", i, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
818
cmd/micro/view.go
Normal file
818
cmd/micro/view.go
Normal file
@@ -0,0 +1,818 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// The View struct stores information about a view into a buffer.
|
||||
// It stores information about the cursor, and the viewport
|
||||
// that the user sees the buffer from.
|
||||
type View struct {
|
||||
// A pointer to the buffer's cursor for ease of access
|
||||
Cursor *Cursor
|
||||
|
||||
// The topmost line, used for vertical scrolling
|
||||
Topline int
|
||||
// The leftmost column, used for horizontal scrolling
|
||||
leftCol int
|
||||
|
||||
// Percentage of the terminal window that this view takes up (from 0 to 100)
|
||||
widthPercent int
|
||||
heightPercent int
|
||||
|
||||
// Specifies whether or not this view holds a help buffer
|
||||
Help bool
|
||||
|
||||
// Actual with and height
|
||||
width int
|
||||
height int
|
||||
|
||||
// Where this view is located
|
||||
x, y int
|
||||
|
||||
// How much to offset because of line numbers
|
||||
lineNumOffset int
|
||||
|
||||
// Holds the list of gutter messages
|
||||
messages map[string][]GutterMessage
|
||||
|
||||
// This is the index of this view in the views array
|
||||
Num int
|
||||
// What tab is this view stored in
|
||||
TabNum int
|
||||
|
||||
// The buffer
|
||||
Buf *Buffer
|
||||
// The statusline
|
||||
sline Statusline
|
||||
|
||||
// Since tcell doesn't differentiate between a mouse release event
|
||||
// and a mouse move event with no keys pressed, we need to keep
|
||||
// track of whether or not the mouse was pressed (or not released) last event to determine
|
||||
// mouse release events
|
||||
mouseReleased bool
|
||||
|
||||
// This stores when the last click was
|
||||
// This is useful for detecting double and triple clicks
|
||||
lastClickTime time.Time
|
||||
|
||||
// lastCutTime stores when the last ctrl+k was issued.
|
||||
// It is used for clearing the clipboard to replace it with fresh cut lines.
|
||||
lastCutTime time.Time
|
||||
|
||||
// freshClip returns true if the clipboard has never been pasted.
|
||||
freshClip bool
|
||||
|
||||
// Was the last mouse event actually a double click?
|
||||
// Useful for detecting triple clicks -- if a double click is detected
|
||||
// but the last mouse event was actually a double click, it's a triple click
|
||||
doubleClick bool
|
||||
// Same here, just to keep track for mouse move events
|
||||
tripleClick bool
|
||||
|
||||
// Syntax highlighting matches
|
||||
matches SyntaxMatches
|
||||
|
||||
splitNode *LeafNode
|
||||
}
|
||||
|
||||
// NewView returns a new fullscreen view
|
||||
func NewView(buf *Buffer) *View {
|
||||
screenW, screenH := screen.Size()
|
||||
return NewViewWidthHeight(buf, screenW, screenH)
|
||||
}
|
||||
|
||||
// NewViewWidthHeight returns a new view with the specified width and height
|
||||
// Note that w and h are raw column and row values
|
||||
func NewViewWidthHeight(buf *Buffer, w, h int) *View {
|
||||
v := new(View)
|
||||
|
||||
v.x, v.y = 0, 0
|
||||
|
||||
v.width = w
|
||||
v.height = h
|
||||
|
||||
v.ToggleTabbar()
|
||||
|
||||
v.OpenBuffer(buf)
|
||||
|
||||
v.messages = make(map[string][]GutterMessage)
|
||||
|
||||
v.sline = Statusline{
|
||||
view: v,
|
||||
}
|
||||
|
||||
if v.Buf.Settings["statusline"].(bool) {
|
||||
v.height--
|
||||
}
|
||||
|
||||
for _, pl := range loadedPlugins {
|
||||
_, err := Call(pl+".onViewOpen", v)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *View) ToggleStatusLine() {
|
||||
if v.Buf.Settings["statusline"].(bool) {
|
||||
v.height--
|
||||
} else {
|
||||
v.height++
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) ToggleTabbar() {
|
||||
if len(tabs) > 1 {
|
||||
if v.y == 0 {
|
||||
// Include one line for the tab bar at the top
|
||||
v.height--
|
||||
v.y = 1
|
||||
}
|
||||
} else {
|
||||
if v.y == 1 {
|
||||
v.y = 0
|
||||
v.height++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) paste(clip string) {
|
||||
leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
|
||||
|
||||
if v.Cursor.HasSelection() {
|
||||
v.Cursor.DeleteSelection()
|
||||
v.Cursor.ResetSelection()
|
||||
}
|
||||
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
|
||||
v.Buf.Insert(v.Cursor.Loc, clip)
|
||||
v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
|
||||
v.freshClip = false
|
||||
messenger.Message("Pasted clipboard")
|
||||
}
|
||||
|
||||
// ScrollUp scrolls the view up n lines (if possible)
|
||||
func (v *View) ScrollUp(n int) {
|
||||
// Try to scroll by n but if it would overflow, scroll by 1
|
||||
if v.Topline-n >= 0 {
|
||||
v.Topline -= n
|
||||
} else if v.Topline > 0 {
|
||||
v.Topline--
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollDown scrolls the view down n lines (if possible)
|
||||
func (v *View) ScrollDown(n int) {
|
||||
// Try to scroll by n but if it would overflow, scroll by 1
|
||||
if v.Topline+n <= v.Buf.NumLines-v.height {
|
||||
v.Topline += n
|
||||
} else if v.Topline < v.Buf.NumLines-v.height {
|
||||
v.Topline++
|
||||
}
|
||||
}
|
||||
|
||||
// CanClose returns whether or not the view can be closed
|
||||
// If there are unsaved changes, the user will be asked if the view can be closed
|
||||
// causing them to lose the unsaved changes
|
||||
// The message is what to print after saying "You have unsaved changes. "
|
||||
func (v *View) CanClose() bool {
|
||||
if v.Buf.IsModified {
|
||||
char, canceled := messenger.LetterPrompt("Save changes to "+v.Buf.Name+" before closing? (y,n,esc) ", 'y', 'n')
|
||||
if !canceled {
|
||||
if char == 'y' {
|
||||
v.Save(true)
|
||||
return true
|
||||
} else if char == 'n' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OpenBuffer opens a new buffer in this view.
|
||||
// This resets the topline, event handler and cursor.
|
||||
func (v *View) OpenBuffer(buf *Buffer) {
|
||||
screen.Clear()
|
||||
v.CloseBuffer()
|
||||
v.Buf = buf
|
||||
v.Cursor = &buf.Cursor
|
||||
v.Topline = 0
|
||||
v.leftCol = 0
|
||||
v.Cursor.ResetSelection()
|
||||
v.Relocate()
|
||||
v.Center(false)
|
||||
v.messages = make(map[string][]GutterMessage)
|
||||
|
||||
v.matches = Match(v)
|
||||
|
||||
// Set mouseReleased to true because we assume the mouse is not being pressed when
|
||||
// the editor is opened
|
||||
v.mouseReleased = true
|
||||
v.lastClickTime = time.Time{}
|
||||
}
|
||||
|
||||
// CloseBuffer performs any closing functions on the buffer
|
||||
func (v *View) CloseBuffer() {
|
||||
if v.Buf != nil {
|
||||
v.Buf.Serialize()
|
||||
}
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer
|
||||
func (v *View) ReOpen() {
|
||||
if v.CanClose() {
|
||||
screen.Clear()
|
||||
v.Buf.ReOpen()
|
||||
v.Relocate()
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
|
||||
// HSplit opens a horizontal split with the given buffer
|
||||
func (v *View) HSplit(buf *Buffer) bool {
|
||||
v.splitNode.HSplit(buf)
|
||||
tabs[v.TabNum].Resize()
|
||||
return false
|
||||
}
|
||||
|
||||
// VSplit opens a vertical split with the given buffer
|
||||
func (v *View) VSplit(buf *Buffer) bool {
|
||||
v.splitNode.VSplit(buf)
|
||||
tabs[v.TabNum].Resize()
|
||||
return false
|
||||
}
|
||||
|
||||
// Relocate moves the view window so that the cursor is in view
|
||||
// This is useful if the user has scrolled far away, and then starts typing
|
||||
func (v *View) Relocate() bool {
|
||||
ret := false
|
||||
cy := v.Cursor.Y
|
||||
scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
|
||||
if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
|
||||
v.Topline = cy - scrollmargin
|
||||
ret = true
|
||||
} else if cy < v.Topline {
|
||||
v.Topline = cy
|
||||
ret = true
|
||||
}
|
||||
if cy > v.Topline+v.height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
|
||||
v.Topline = cy - v.height + 1 + scrollmargin
|
||||
ret = true
|
||||
} else if cy >= v.Buf.NumLines-scrollmargin && cy > v.height {
|
||||
v.Topline = v.Buf.NumLines - v.height
|
||||
ret = true
|
||||
}
|
||||
|
||||
cx := v.Cursor.GetVisualX()
|
||||
if cx < v.leftCol {
|
||||
v.leftCol = cx
|
||||
ret = true
|
||||
}
|
||||
if cx+v.lineNumOffset+1 > v.leftCol+v.width {
|
||||
v.leftCol = cx - v.width + v.lineNumOffset + 1
|
||||
ret = true
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// MoveToMouseClick moves the cursor to location x, y assuming x, y were given
|
||||
// by a mouse click
|
||||
func (v *View) MoveToMouseClick(x, y int) {
|
||||
if y-v.Topline > v.height-1 {
|
||||
v.ScrollDown(1)
|
||||
y = v.height + v.Topline - 1
|
||||
}
|
||||
if y >= v.Buf.NumLines {
|
||||
y = v.Buf.NumLines - 1
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
}
|
||||
if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
|
||||
x = v.Cursor.GetCharPosInLine(y, x)
|
||||
if x > Count(v.Buf.Line(y)) {
|
||||
x = Count(v.Buf.Line(y))
|
||||
}
|
||||
v.Cursor.X = x
|
||||
v.Cursor.Y = y
|
||||
v.Cursor.LastVisualX = v.Cursor.GetVisualX()
|
||||
}
|
||||
|
||||
// HandleEvent handles an event passed by the main loop
|
||||
func (v *View) HandleEvent(event tcell.Event) {
|
||||
// This bool determines whether the view is relocated at the end of the function
|
||||
// By default it's true because most events should cause a relocate
|
||||
relocate := true
|
||||
|
||||
v.Buf.CheckModTime()
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
// Window resized
|
||||
tabs[v.TabNum].Resize()
|
||||
case *tcell.EventKey:
|
||||
if e.Key() == tcell.KeyRune && (e.Modifiers() == 0 || e.Modifiers() == tcell.ModShift) {
|
||||
// Insert a character
|
||||
if v.Cursor.HasSelection() {
|
||||
v.Cursor.DeleteSelection()
|
||||
v.Cursor.ResetSelection()
|
||||
}
|
||||
v.Buf.Insert(v.Cursor.Loc, string(e.Rune()))
|
||||
v.Cursor.Right()
|
||||
|
||||
for _, pl := range loadedPlugins {
|
||||
_, err := Call(pl+".onRune", string(e.Rune()), v)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
if recordingMacro {
|
||||
curMacro = append(curMacro, e.Rune())
|
||||
}
|
||||
} else {
|
||||
for key, actions := range bindings {
|
||||
if e.Key() == key.keyCode {
|
||||
if e.Key() == tcell.KeyRune {
|
||||
if e.Rune() != key.r {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if e.Modifiers() == key.modifiers {
|
||||
relocate = false
|
||||
for _, action := range actions {
|
||||
relocate = action(v, true) || relocate
|
||||
funcName := FuncName(action)
|
||||
if funcName != "main.(*View).ToggleMacro" && funcName != "main.(*View).PlayMacro" {
|
||||
if recordingMacro {
|
||||
curMacro = append(curMacro, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *tcell.EventPaste:
|
||||
if !PreActionCall("Paste", v) {
|
||||
break
|
||||
}
|
||||
|
||||
leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
|
||||
|
||||
if v.Cursor.HasSelection() {
|
||||
v.Cursor.DeleteSelection()
|
||||
v.Cursor.ResetSelection()
|
||||
}
|
||||
clip := e.Text()
|
||||
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
|
||||
v.Buf.Insert(v.Cursor.Loc, clip)
|
||||
v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
|
||||
v.freshClip = false
|
||||
messenger.Message("Pasted clipboard")
|
||||
|
||||
PostActionCall("Paste", v)
|
||||
case *tcell.EventMouse:
|
||||
x, y := e.Position()
|
||||
x -= v.lineNumOffset - v.leftCol + v.x
|
||||
y += v.Topline - v.y
|
||||
// Don't relocate for mouse events
|
||||
relocate = false
|
||||
|
||||
button := e.Buttons()
|
||||
|
||||
switch button {
|
||||
case tcell.Button1:
|
||||
// Left click
|
||||
if v.mouseReleased {
|
||||
v.MoveToMouseClick(x, y)
|
||||
if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
|
||||
if v.doubleClick {
|
||||
// Triple click
|
||||
v.lastClickTime = time.Now()
|
||||
|
||||
v.tripleClick = true
|
||||
v.doubleClick = false
|
||||
|
||||
v.Cursor.SelectLine()
|
||||
} else {
|
||||
// Double click
|
||||
v.lastClickTime = time.Now()
|
||||
|
||||
v.doubleClick = true
|
||||
v.tripleClick = false
|
||||
|
||||
v.Cursor.SelectWord()
|
||||
}
|
||||
} else {
|
||||
v.doubleClick = false
|
||||
v.tripleClick = false
|
||||
v.lastClickTime = time.Now()
|
||||
|
||||
v.Cursor.OrigSelection[0] = v.Cursor.Loc
|
||||
v.Cursor.CurSelection[0] = v.Cursor.Loc
|
||||
v.Cursor.CurSelection[1] = v.Cursor.Loc
|
||||
}
|
||||
v.mouseReleased = false
|
||||
} else if !v.mouseReleased {
|
||||
v.MoveToMouseClick(x, y)
|
||||
if v.tripleClick {
|
||||
v.Cursor.AddLineToSelection()
|
||||
} else if v.doubleClick {
|
||||
v.Cursor.AddWordToSelection()
|
||||
} else {
|
||||
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
|
||||
}
|
||||
}
|
||||
case tcell.Button2:
|
||||
// Middle mouse button was clicked,
|
||||
// We should paste primary
|
||||
v.PastePrimary(true)
|
||||
case tcell.ButtonNone:
|
||||
// Mouse event with no click
|
||||
if !v.mouseReleased {
|
||||
// Mouse was just released
|
||||
|
||||
// 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 !v.doubleClick && !v.tripleClick {
|
||||
v.MoveToMouseClick(x, y)
|
||||
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
|
||||
}
|
||||
v.mouseReleased = true
|
||||
}
|
||||
case tcell.WheelUp:
|
||||
// Scroll up
|
||||
scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
|
||||
v.ScrollUp(scrollspeed)
|
||||
case tcell.WheelDown:
|
||||
// Scroll down
|
||||
scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
|
||||
v.ScrollDown(scrollspeed)
|
||||
}
|
||||
}
|
||||
|
||||
if relocate {
|
||||
v.Relocate()
|
||||
}
|
||||
if v.Buf.Settings["syntax"].(bool) {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
|
||||
// GutterMessage creates a message in this view's gutter
|
||||
func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
|
||||
lineN--
|
||||
gutterMsg := GutterMessage{
|
||||
lineNum: lineN,
|
||||
msg: msg,
|
||||
kind: kind,
|
||||
}
|
||||
for _, v := range v.messages {
|
||||
for _, gmsg := range v {
|
||||
if gmsg.lineNum == lineN {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
messages := v.messages[section]
|
||||
v.messages[section] = append(messages, gutterMsg)
|
||||
}
|
||||
|
||||
// ClearGutterMessages clears all gutter messages from a given section
|
||||
func (v *View) ClearGutterMessages(section string) {
|
||||
v.messages[section] = []GutterMessage{}
|
||||
}
|
||||
|
||||
// ClearAllGutterMessages clears all the gutter messages
|
||||
func (v *View) ClearAllGutterMessages() {
|
||||
for k := range v.messages {
|
||||
v.messages[k] = []GutterMessage{}
|
||||
}
|
||||
}
|
||||
|
||||
// Opens the given help page in a new horizontal split
|
||||
func (v *View) openHelp(helpPage string) {
|
||||
if v.Help {
|
||||
helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md")
|
||||
helpBuffer.Name = "Help"
|
||||
v.OpenBuffer(helpBuffer)
|
||||
} else {
|
||||
helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md")
|
||||
helpBuffer.Name = "Help"
|
||||
v.HSplit(helpBuffer)
|
||||
CurView().Help = true
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) drawCell(x, y int, ch rune, combc []rune, style tcell.Style) {
|
||||
if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
|
||||
screen.SetContent(x, y, ch, combc, style)
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayView renders the view to the screen
|
||||
func (v *View) DisplayView() {
|
||||
// The charNum we are currently displaying
|
||||
// starts at the start of the viewport
|
||||
charNum := Loc{0, v.Topline}
|
||||
|
||||
// Convert the length of buffer to a string, and get the length of the string
|
||||
// We are going to have to offset by that amount
|
||||
maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
|
||||
|
||||
if v.Buf.Settings["ruler"] == true {
|
||||
// + 1 for the little space after the line number
|
||||
v.lineNumOffset = maxLineLength + 1
|
||||
} else {
|
||||
v.lineNumOffset = 0
|
||||
}
|
||||
|
||||
// We need to add to the line offset if there are gutter messages
|
||||
var hasGutterMessages bool
|
||||
for _, v := range v.messages {
|
||||
if len(v) > 0 {
|
||||
hasGutterMessages = true
|
||||
}
|
||||
}
|
||||
if hasGutterMessages {
|
||||
v.lineNumOffset += 2
|
||||
}
|
||||
|
||||
if v.x != 0 {
|
||||
// One space for the extra split divider
|
||||
v.lineNumOffset++
|
||||
}
|
||||
|
||||
// These represent the current screen coordinates
|
||||
screenX, screenY := 0, 0
|
||||
|
||||
highlightStyle := defStyle
|
||||
|
||||
// ViewLine is the current line from the top of the viewport
|
||||
for viewLine := 0; viewLine < v.height; viewLine++ {
|
||||
screenY = v.y + viewLine
|
||||
screenX = v.x
|
||||
|
||||
// This is the current line number of the buffer that we are drawing
|
||||
curLineN := viewLine + v.Topline
|
||||
|
||||
if v.x != 0 {
|
||||
// Draw the split divider
|
||||
v.drawCell(screenX, screenY, '|', nil, defStyle.Reverse(true))
|
||||
screenX++
|
||||
}
|
||||
|
||||
// If the buffer is smaller than the view height we have to clear all this space
|
||||
if curLineN >= v.Buf.NumLines {
|
||||
for i := screenX; i < v.x+v.width; i++ {
|
||||
v.drawCell(i, screenY, ' ', nil, defStyle)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
line := v.Buf.Line(curLineN)
|
||||
|
||||
// If there are gutter messages we need to display the '>>' symbol here
|
||||
if hasGutterMessages {
|
||||
// msgOnLine stores whether or not there is a gutter message on this line in particular
|
||||
msgOnLine := false
|
||||
for k := range v.messages {
|
||||
for _, msg := range v.messages[k] {
|
||||
if msg.lineNum == curLineN {
|
||||
msgOnLine = true
|
||||
gutterStyle := defStyle
|
||||
switch msg.kind {
|
||||
case GutterInfo:
|
||||
if style, ok := colorscheme["gutter-info"]; ok {
|
||||
gutterStyle = style
|
||||
}
|
||||
case GutterWarning:
|
||||
if style, ok := colorscheme["gutter-warning"]; ok {
|
||||
gutterStyle = style
|
||||
}
|
||||
case GutterError:
|
||||
if style, ok := colorscheme["gutter-error"]; ok {
|
||||
gutterStyle = style
|
||||
}
|
||||
}
|
||||
v.drawCell(screenX, screenY, '>', nil, gutterStyle)
|
||||
screenX++
|
||||
v.drawCell(screenX, screenY, '>', nil, gutterStyle)
|
||||
screenX++
|
||||
if v.Cursor.Y == curLineN && !messenger.hasPrompt {
|
||||
messenger.Message(msg.msg)
|
||||
messenger.gutterMessage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is no message on this line we just display an empty offset
|
||||
if !msgOnLine {
|
||||
v.drawCell(screenX, screenY, ' ', nil, defStyle)
|
||||
screenX++
|
||||
v.drawCell(screenX, screenY, ' ', nil, defStyle)
|
||||
screenX++
|
||||
if v.Cursor.Y == curLineN && messenger.gutterMessage {
|
||||
messenger.Reset()
|
||||
messenger.gutterMessage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v.Buf.Settings["ruler"] == true {
|
||||
// Write the line number
|
||||
lineNumStyle := defStyle
|
||||
if style, ok := colorscheme["line-number"]; ok {
|
||||
lineNumStyle = style
|
||||
}
|
||||
if style, ok := colorscheme["current-line-number"]; ok {
|
||||
if curLineN == v.Cursor.Y && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() {
|
||||
lineNumStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
lineNum := strconv.Itoa(curLineN + 1)
|
||||
|
||||
// Write the spaces before the line number if necessary
|
||||
for i := 0; i < maxLineLength-len(lineNum); i++ {
|
||||
v.drawCell(screenX, screenY, ' ', nil, lineNumStyle)
|
||||
screenX++
|
||||
}
|
||||
// Write the actual line number
|
||||
for _, ch := range lineNum {
|
||||
v.drawCell(screenX, screenY, ch, nil, lineNumStyle)
|
||||
screenX++
|
||||
}
|
||||
|
||||
// Write the extra space
|
||||
v.drawCell(screenX, screenY, ' ', nil, lineNumStyle)
|
||||
screenX++
|
||||
}
|
||||
|
||||
// Now we actually draw the line
|
||||
colN := 0
|
||||
for _, ch := range line {
|
||||
lineStyle := defStyle
|
||||
|
||||
if v.Buf.Settings["syntax"].(bool) {
|
||||
// Syntax highlighting is enabled
|
||||
highlightStyle = v.matches[viewLine][colN]
|
||||
}
|
||||
|
||||
if v.Cursor.HasSelection() &&
|
||||
(charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
|
||||
charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
|
||||
// The current character is selected
|
||||
lineStyle = defStyle.Reverse(true)
|
||||
|
||||
if style, ok := colorscheme["selection"]; ok {
|
||||
lineStyle = style
|
||||
}
|
||||
} else {
|
||||
lineStyle = highlightStyle
|
||||
}
|
||||
|
||||
// We need to display the background of the linestyle with the correct color if cursorline is enabled
|
||||
// and this is the current view and there is no selection on this line and the cursor is on this line
|
||||
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
|
||||
if style, ok := colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := style.Decompose()
|
||||
lineStyle = lineStyle.Background(fg)
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\t' {
|
||||
// If the character we are displaying is a tab, we need to do a bunch of special things
|
||||
|
||||
// First the user may have configured an `indent-char` to be displayed to show that this
|
||||
// is a tab character
|
||||
lineIndentStyle := defStyle
|
||||
if style, ok := colorscheme["indent-char"]; ok {
|
||||
lineIndentStyle = style
|
||||
}
|
||||
if v.Cursor.HasSelection() &&
|
||||
(charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
|
||||
charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
|
||||
|
||||
lineIndentStyle = defStyle.Reverse(true)
|
||||
|
||||
if style, ok := colorscheme["selection"]; ok {
|
||||
lineIndentStyle = style
|
||||
}
|
||||
}
|
||||
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
|
||||
if style, ok := colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := style.Decompose()
|
||||
lineIndentStyle = lineIndentStyle.Background(fg)
|
||||
}
|
||||
}
|
||||
// Here we get the indent char
|
||||
indentChar := []rune(v.Buf.Settings["indentchar"].(string))
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, indentChar[0], nil, lineIndentStyle)
|
||||
}
|
||||
// Now the tab has to be displayed as a bunch of spaces
|
||||
tabSize := int(v.Buf.Settings["tabsize"].(float64))
|
||||
for i := 0; i < tabSize-1; i++ {
|
||||
screenX++
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, ' ', nil, lineStyle)
|
||||
}
|
||||
}
|
||||
} else if runewidth.RuneWidth(ch) > 1 {
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX, screenY, ch, nil, lineStyle)
|
||||
}
|
||||
for i := 0; i < runewidth.RuneWidth(ch)-1; i++ {
|
||||
screenX++
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, '<', nil, lineStyle)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, ch, nil, lineStyle)
|
||||
}
|
||||
}
|
||||
charNum = charNum.Move(1, v.Buf)
|
||||
screenX++
|
||||
colN++
|
||||
}
|
||||
// Here we are at a newline
|
||||
|
||||
// The newline may be selected, in which case we should draw the selection style
|
||||
// with a space to represent it
|
||||
if v.Cursor.HasSelection() &&
|
||||
(charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
|
||||
charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
|
||||
|
||||
selectStyle := defStyle.Reverse(true)
|
||||
|
||||
if style, ok := colorscheme["selection"]; ok {
|
||||
selectStyle = style
|
||||
}
|
||||
v.drawCell(screenX, screenY, ' ', nil, selectStyle)
|
||||
screenX++
|
||||
}
|
||||
|
||||
charNum = charNum.Move(1, v.Buf)
|
||||
|
||||
for i := 0; i < v.width; i++ {
|
||||
lineStyle := defStyle
|
||||
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
|
||||
if style, ok := colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := style.Decompose()
|
||||
lineStyle = lineStyle.Background(fg)
|
||||
}
|
||||
}
|
||||
if screenX-v.x-v.leftCol+i >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol+i, screenY, ' ', nil, lineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayCursor draws the current buffer's cursor to the screen
|
||||
func (v *View) DisplayCursor() {
|
||||
// Don't draw the cursor if it is out of the viewport or if it has a selection
|
||||
if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
|
||||
screen.HideCursor()
|
||||
} else {
|
||||
screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.Y-v.Topline+v.y)
|
||||
}
|
||||
}
|
||||
|
||||
// Display renders the view, the cursor, and statusline
|
||||
func (v *View) Display() {
|
||||
v.DisplayView()
|
||||
if v.Num == tabs[curTab].curView {
|
||||
v.DisplayCursor()
|
||||
}
|
||||
_, screenH := screen.Size()
|
||||
if v.Buf.Settings["statusline"].(bool) {
|
||||
v.sline.Display()
|
||||
} else if (v.y + v.height) != screenH-1 {
|
||||
for x := 0; x < v.width; x++ {
|
||||
screen.SetContent(v.x+x, v.y+v.height, '-', nil, defStyle.Reverse(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>io.github.zyedidia.micro</id>
|
||||
<launchable type="desktop-id">micro.desktop</launchable>
|
||||
<name>Micro Text Editor</name>
|
||||
<summary>A modern and intuitive terminal-based text editor</summary>
|
||||
<description>
|
||||
<p>
|
||||
micro is a terminal-based text editor that aims to be easy to use and
|
||||
intuitive, while also taking advantage of the capabilities of modern terminals.
|
||||
It comes as a single, batteries-included, static binary with no dependencies;
|
||||
you can download and use it right now!
|
||||
</p>
|
||||
<p>
|
||||
As its name indicates, micro aims to be somewhat of a successor to the nano
|
||||
editor by being easy to install and use. It strives to be enjoyable as a full-time
|
||||
editor for people who prefer to work in a terminal, or those who regularly
|
||||
edit files over SSH.
|
||||
</p>
|
||||
</description>
|
||||
<metadata_license>MIT</metadata_license>
|
||||
<project_license>MIT</project_license>
|
||||
<categories>
|
||||
<category>Development</category>
|
||||
<category>TextEditor</category>
|
||||
</categories>
|
||||
<releases>
|
||||
<release version="2.0.15" date="2025-12-31"/>
|
||||
<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"/>
|
||||
</releases>
|
||||
<provides>
|
||||
<binary>micro</binary>
|
||||
<id>com.github.zyedidia.micro</id>
|
||||
</provides>
|
||||
<developer_name>Zachary Yedidia</developer_name>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Micro Text Editor editing its source code</caption>
|
||||
<image type="source">https://raw.githubusercontent.com/micro-editor/micro/master/assets/micro-solarized.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1" />
|
||||
<url type="homepage">https://micro-editor.github.io</url>
|
||||
<url type="bugtracker">https://github.com/micro-editor/micro/issues</url>
|
||||
<url type="faq">https://micro-editor.github.io/about.html</url>
|
||||
<url type="help">https://micro-editor.github.io/about.html</url>
|
||||
<url type="contact">https://github.com/zyedidia</url>
|
||||
<url type="vcs-browser">https://github.com/micro-editor/micro</url>
|
||||
<url type="contribute">https://github.com/micro-editor/micro#contributing</url>
|
||||
</component>
|
||||
367
data/micro.json
367
data/micro.json
@@ -1,367 +0,0 @@
|
||||
{
|
||||
"$comment": "https://github.com/micro-editor/micro",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "options",
|
||||
"description": "A micro editor config schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"autoindent": {
|
||||
"description": "Whether to use the same indentation as a previous line\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"autosave": {
|
||||
"description": "A delay between automatic saves\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"autosu": {
|
||||
"description": "Whether attempt to use super user privileges\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"backup": {
|
||||
"description": "Whether to backup all open buffers\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"backupdir": {
|
||||
"description": "A directory to store backups\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"basename": {
|
||||
"description": "Whether to show a basename instead of a full path\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"clipboard": {
|
||||
"description": "A way to access the system clipboard\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"external",
|
||||
"terminal",
|
||||
"internal"
|
||||
],
|
||||
"default": "external"
|
||||
},
|
||||
"colorcolumn": {
|
||||
"description": "A position to display a column\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0
|
||||
},
|
||||
"colorscheme": {
|
||||
"description": "A color scheme\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"atom-dark",
|
||||
"bubblegum",
|
||||
"cmc-16",
|
||||
"cmc-tc",
|
||||
"darcula",
|
||||
"default",
|
||||
"dracula-tc",
|
||||
"dukedark-tc",
|
||||
"dukelight-tc",
|
||||
"dukeubuntu-tc",
|
||||
"geany",
|
||||
"gotham",
|
||||
"gruvbox",
|
||||
"gruvbox-tc",
|
||||
"material-tc",
|
||||
"monokai-dark",
|
||||
"monokai",
|
||||
"one-dark",
|
||||
"railscast",
|
||||
"simple",
|
||||
"solarized",
|
||||
"solarized-tc",
|
||||
"sunny-day",
|
||||
"twilight",
|
||||
"zenburn"
|
||||
],
|
||||
"default": "default"
|
||||
},
|
||||
"cursorline": {
|
||||
"description": "Whether to highlight a line with a cursor with a different color\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"diffgutter": {
|
||||
"description": "Whether to display diff inticators before lines\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"divchars": {
|
||||
"description": "Divider chars for vertical and horizontal splits\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "|-"
|
||||
},
|
||||
"divreverse": {
|
||||
"description": "Whether to use inversed color scheme colors for splits\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"encoding": {
|
||||
"description": "An encoding used to open and save files\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "utf-8"
|
||||
},
|
||||
"eofnewline": {
|
||||
"description": "Whether to add a missing trailing new line\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"fastdirty": {
|
||||
"description": "Whether to use a fast algorithm to determine whether a file is changed\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"fileformat": {
|
||||
"description": "A line ending format\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"unix",
|
||||
"dos"
|
||||
],
|
||||
"default": "unix"
|
||||
},
|
||||
"filetype": {
|
||||
"description": "A filetype for the current buffer\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "unknown"
|
||||
},
|
||||
"hlsearch": {
|
||||
"description": "Whether to highlight all instances of a searched text after a successful search\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"incsearch": {
|
||||
"description": "Whether to enable an incremental search in `Find` prompt\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"ignorecase": {
|
||||
"description": "Whether to perform case-insensitive searches\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"indentchar": {
|
||||
"description": "An indentation character\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"maxLength": 1,
|
||||
"default": " "
|
||||
},
|
||||
"infobar": {
|
||||
"description": "Whether to enable a line at the bottom where messages are printed\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"keepautoindent": {
|
||||
"description": "Whether add a whitespace while using autoindent\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"keymenu": {
|
||||
"description": "Whether to display nano-style key menu at the bottom\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"matchbrace": {
|
||||
"description": "Whether to show matching braces\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"matchbracestyle": {
|
||||
"description": "Whether to underline or highlight matching braces\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"underline",
|
||||
"highlight"
|
||||
],
|
||||
"default": "underline"
|
||||
},
|
||||
"mkparents": {
|
||||
"description": "Whether to create missing directories\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"mouse": {
|
||||
"description": "Whether to enable mouse support\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"paste": {
|
||||
"description": "Whether to treat characters sent from the terminal in a single chunk as a paste event\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"parsecursor": {
|
||||
"description": "Whether to extract a line number and a column to open files with from file names\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"permbackup": {
|
||||
"description": "Whether to permanently save backups\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"pluginchannels": {
|
||||
"description": "A file with list of plugin channels\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"
|
||||
},
|
||||
"pluginrepos": {
|
||||
"description": "Plugin repositories\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"description": "A pluging repository\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"readonly": {
|
||||
"description": "Whether to forbid buffer editing\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"rmtrailingws": {
|
||||
"description": "Whether to remove trailing whitespaces\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ruler": {
|
||||
"description": "Whether to display line numbers\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"relativeruler": {
|
||||
"description": "Whether to display relative line numbers\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"savecursor": {
|
||||
"description": "Whether to save cursor position in files\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"savehistory": {
|
||||
"description": "Whether to save command history between closing and re-opening editor\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"saveundo": {
|
||||
"description": "Whether to save undo after closing file\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"scrollbar": {
|
||||
"description": "Whether to save undo after closing file\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"scrollmargin": {
|
||||
"description": "A margin at which a view starts scrolling when a cursor approaches an edge of a view\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "integer",
|
||||
"default": 3
|
||||
},
|
||||
"scrollspeed": {
|
||||
"description": "Line count to scroll for one scroll event\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "integer",
|
||||
"default": 2
|
||||
},
|
||||
"smartpaste": {
|
||||
"description": "Whether to add a leading whitespace while pasting multiple lines\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"softwrap": {
|
||||
"description": "Whether to wrap long lines\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"splitbottom": {
|
||||
"description": "Whether to create a new horizontal split below the current one\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"splitright": {
|
||||
"description": "Whether to create a new vertical split right of the current one\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"statusformatl": {
|
||||
"description": "Format string of left-justified part of the statusline\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)"
|
||||
},
|
||||
"statusformatr": {
|
||||
"description": "Format string of right-justified part of the statusline\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help"
|
||||
},
|
||||
"statusline": {
|
||||
"description": "Whether to display a status line\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"sucmd": {
|
||||
"description": "A super user command\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "sudo",
|
||||
"examples": [
|
||||
"sudo",
|
||||
"doas"
|
||||
]
|
||||
},
|
||||
"syntax": {
|
||||
"description": "Whether to enable a syntax highlighting\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"tabmovement": {
|
||||
"description": "Whether to navigate spaces at the beginning of lines as if they are tabs\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"tabhighlight": {
|
||||
"description": "Whether to invert tab character colors\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"tabreverse": {
|
||||
"description": "Whether to reverse tab bar colors\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"tabsize": {
|
||||
"description": "A tab size\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "integer",
|
||||
"default": 4
|
||||
},
|
||||
"tabstospaces": {
|
||||
"description": "Whether to use spaces instead of tabs\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"useprimary": {
|
||||
"description": "Whether to use primary clipboard to copy selections in the background\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"wordwrap": {
|
||||
"description": "Whether to wrap long lines by words\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"xterm": {
|
||||
"description": "Whether to assume that the current terminal is `xterm`\nhttps://github.com/micro-editor/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
40
go.mod
40
go.mod
@@ -1,40 +0,0 @@
|
||||
module github.com/micro-editor/micro/v2
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
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.13
|
||||
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
github.com/zyedidia/clipper v0.1.1
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
|
||||
golang.org/x/text v0.4.0
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
layeh.com/gopher-luar v1.0.11
|
||||
)
|
||||
|
||||
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.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/kballard/go-shellquote => github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5
|
||||
|
||||
replace layeh.com/gopher-luar v1.0.11 => github.com/layeh/gopher-luar v1.0.11
|
||||
|
||||
go 1.19
|
||||
76
go.sum
76
go.sum
@@ -1,76 +0,0 @@
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.13 h1:xyuSpBhSBsUH+bs7FER9IV2/TsQpBmCFiNWJVAEdT68=
|
||||
github.com/micro-editor/tcell/v2 v2.0.13/go.mod h1:ixpjICpoGp83FZVoLYFJPBwCAslHeTnvgPdhJVPLyy0=
|
||||
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5 h1:czSkYUNmHuWS2lv8VreufENEXZNOCGZcXd744YKf8yM=
|
||||
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5/go.mod h1:OszIG7ockt4osicVHq6gI2QmV4PBDK6H5/Bj8GDGv4Q=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
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/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw=
|
||||
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/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
|
||||
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
|
||||
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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/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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
//go:build plan9 || nacl || windows
|
||||
|
||||
package action
|
||||
|
||||
func (*BufPane) Suspend() bool {
|
||||
InfoBar.Error("Suspend is only supported on BSD/Linux")
|
||||
return false
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
)
|
||||
|
||||
// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs.
|
||||
// This only works on linux and has no default binding.
|
||||
// This code was adapted from the suspend code in nsf/godit
|
||||
func (*BufPane) Suspend() bool {
|
||||
screenb := screen.TempFini()
|
||||
|
||||
// suspend the process
|
||||
pid := syscall.Getpid()
|
||||
err := syscall.Kill(pid, syscall.SIGSTOP)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
screen.TempStart(screenb)
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/micro-editor/json5"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
var Binder = map[string]func(e Event, action string){
|
||||
"command": InfoMapEvent,
|
||||
"buffer": BufMapEvent,
|
||||
"terminal": TermMapEvent,
|
||||
}
|
||||
|
||||
func writeFile(name string, txt []byte) error {
|
||||
return util.SafeWrite(name, txt, false)
|
||||
}
|
||||
|
||||
func createBindingsIfNotExist(fname string) {
|
||||
if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) {
|
||||
writeFile(fname, []byte("{}"))
|
||||
}
|
||||
}
|
||||
|
||||
// InitBindings intializes the bindings map by reading from bindings.json
|
||||
func InitBindings() {
|
||||
var parsed map[string]any
|
||||
|
||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||
createBindingsIfNotExist(filename)
|
||||
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading bindings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for p, bind := range Binder {
|
||||
defaults := DefaultBindings(p)
|
||||
|
||||
for k, v := range defaults {
|
||||
BindKey(k, v, bind)
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range parsed {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
BindKey(k, val, Binder["buffer"])
|
||||
case map[string]any:
|
||||
bind, ok := Binder[k]
|
||||
if !ok || bind == nil {
|
||||
screen.TermMessage(fmt.Sprintf("%s is not a valid pane type", k))
|
||||
continue
|
||||
}
|
||||
for e, a := range val {
|
||||
s, ok := a.(string)
|
||||
if !ok {
|
||||
screen.TermMessage("Error reading bindings.json: non-string and non-map entry", k)
|
||||
} else {
|
||||
BindKey(e, s, bind)
|
||||
}
|
||||
}
|
||||
default:
|
||||
screen.TermMessage("Error reading bindings.json: non-string and non-map entry", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BindKey(k, v string, bind func(e Event, a string)) {
|
||||
event, err := findEvent(k)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(k, "\x1b") {
|
||||
screen.RegisterRawSeq(k)
|
||||
}
|
||||
|
||||
bind(event, v)
|
||||
|
||||
// switch e := event.(type) {
|
||||
// case KeyEvent:
|
||||
// InfoMapKey(e, v)
|
||||
// case KeySequenceEvent:
|
||||
// InfoMapKey(e, v)
|
||||
// case MouseEvent:
|
||||
// InfoMapMouse(e, v)
|
||||
// case RawEvent:
|
||||
// InfoMapKey(e, v)
|
||||
// }
|
||||
}
|
||||
|
||||
var r = regexp.MustCompile("<(.+?)>")
|
||||
|
||||
func findEvents(k string) (b KeySequenceEvent, ok bool, err error) {
|
||||
var events []Event = nil
|
||||
for len(k) > 0 {
|
||||
groups := r.FindStringSubmatchIndex(k)
|
||||
|
||||
if len(groups) > 3 {
|
||||
if events == nil {
|
||||
events = make([]Event, 0, 3)
|
||||
}
|
||||
|
||||
e, ok := findSingleEvent(k[groups[2]:groups[3]])
|
||||
if !ok {
|
||||
return KeySequenceEvent{}, false, errors.New("Invalid event " + k[groups[2]:groups[3]])
|
||||
}
|
||||
|
||||
events = append(events, e)
|
||||
|
||||
k = k[groups[3]+1:]
|
||||
} else {
|
||||
return KeySequenceEvent{}, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return KeySequenceEvent{events}, true, nil
|
||||
}
|
||||
|
||||
// findSingleEvent will find binding Key 'b' using string 'k'
|
||||
func findSingleEvent(k string) (b Event, ok bool) {
|
||||
modifiers := tcell.ModNone
|
||||
|
||||
// First, we'll strip off all the modifiers in the name and add them to the
|
||||
// ModMask
|
||||
modSearch:
|
||||
for {
|
||||
switch {
|
||||
case strings.HasPrefix(k, "-") && k != "-":
|
||||
// We optionally support dashes between modifiers
|
||||
k = k[1:]
|
||||
case strings.HasPrefix(k, "Ctrl") && k != "CtrlH":
|
||||
// CtrlH technically does not have a 'Ctrl' modifier because it is really backspace
|
||||
k = k[4:]
|
||||
modifiers |= tcell.ModCtrl
|
||||
case strings.HasPrefix(k, "Alt"):
|
||||
k = k[3:]
|
||||
modifiers |= tcell.ModAlt
|
||||
case strings.HasPrefix(k, "Shift"):
|
||||
k = k[5:]
|
||||
modifiers |= tcell.ModShift
|
||||
case strings.HasPrefix(k, "\x1b"):
|
||||
return RawEvent{
|
||||
esc: k,
|
||||
}, true
|
||||
default:
|
||||
break modSearch
|
||||
}
|
||||
}
|
||||
|
||||
if k == "" {
|
||||
return KeyEvent{}, false
|
||||
}
|
||||
|
||||
// Control is handled in a special way, since the terminal sends explicitly
|
||||
// marked escape sequences for control keys
|
||||
// We should check for Control keys first
|
||||
if modifiers&tcell.ModCtrl != 0 {
|
||||
// see if the key is in bindingKeys with the Ctrl prefix.
|
||||
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
|
||||
if code, ok := keyEvents["Ctrl"+k]; ok {
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingKeys
|
||||
if code, ok := keyEvents[k]; ok {
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
}, 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
|
||||
if code, ok := mouseEvents[k]; ok {
|
||||
return MouseEvent{
|
||||
btn: code,
|
||||
mod: modifiers,
|
||||
state: mstate,
|
||||
}, true
|
||||
}
|
||||
|
||||
// If we were given one character, then we've got a rune.
|
||||
if len(k) == 1 {
|
||||
return KeyEvent{
|
||||
code: tcell.KeyRune,
|
||||
mod: modifiers,
|
||||
r: rune(k[0]),
|
||||
}, true
|
||||
}
|
||||
|
||||
// We don't know what happened.
|
||||
return KeyEvent{}, false
|
||||
}
|
||||
|
||||
func findEvent(k string) (Event, error) {
|
||||
var event Event
|
||||
event, ok, err := findEvents(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
event, ok = findSingleEvent(k)
|
||||
if !ok {
|
||||
return nil, errors.New(k + " is not a bindable event")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// TryBindKeyPlug tries to bind a key for the plugin without writing to bindings.json.
|
||||
// This operation can be rejected by lockbindings to prevent unexpected actions by the user.
|
||||
func TryBindKeyPlug(k, v string, overwrite bool) (bool, error) {
|
||||
if l, ok := config.GlobalSettings["lockbindings"]; ok && l.(bool) {
|
||||
return false, errors.New("bindings is locked by the user")
|
||||
}
|
||||
return TryBindKey(k, v, overwrite, false)
|
||||
}
|
||||
|
||||
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
|
||||
// Returns true if the keybinding already existed or is binded successfully and a possible error
|
||||
func TryBindKey(k, v string, overwrite bool, writeToFile bool) (bool, error) {
|
||||
var e error
|
||||
var parsed map[string]any
|
||||
|
||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||
createBindingsIfNotExist(filename)
|
||||
if _, e = os.Stat(filename); e == nil {
|
||||
input, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
return false, errors.New("Error reading bindings.json: " + err.Error())
|
||||
}
|
||||
|
||||
key, err := findEvent(k)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
found := false
|
||||
var ev string
|
||||
for ev = range parsed {
|
||||
if e, err := findEvent(ev); err == nil {
|
||||
if eventsEqual(e, key) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if overwrite {
|
||||
parsed[ev] = v
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
parsed[k] = v
|
||||
}
|
||||
|
||||
BindKey(k, v, Binder["buffer"])
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
txt = append(txt, '\n')
|
||||
|
||||
if writeToFile {
|
||||
return true, writeFile(filename, txt)
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, e
|
||||
}
|
||||
|
||||
// UnbindKey removes the binding for a key from the bindings.json file
|
||||
func UnbindKey(k string) error {
|
||||
var e error
|
||||
var parsed map[string]any
|
||||
|
||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||
createBindingsIfNotExist(filename)
|
||||
if _, e = os.Stat(filename); e == nil {
|
||||
input, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.New("Error reading bindings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
return errors.New("Error reading bindings.json: " + err.Error())
|
||||
}
|
||||
|
||||
key, err := findEvent(k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for ev := range parsed {
|
||||
if e, err := findEvent(ev); err == nil {
|
||||
if eventsEqual(e, key) {
|
||||
delete(parsed, ev)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(k, "\x1b") {
|
||||
screen.UnregisterRawSeq(k)
|
||||
}
|
||||
|
||||
defaults := DefaultBindings("buffer")
|
||||
if a, ok := defaults[k]; ok {
|
||||
BindKey(k, a, Binder["buffer"])
|
||||
} else if _, ok := config.Bindings["buffer"][k]; ok {
|
||||
BufUnmap(key)
|
||||
delete(config.Bindings["buffer"], k)
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
txt = append(txt, '\n')
|
||||
return writeFile(filename, txt)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var mouseEvents = map[string]tcell.ButtonMask{
|
||||
"MouseLeft": tcell.ButtonPrimary,
|
||||
"MouseMiddle": tcell.ButtonMiddle,
|
||||
"MouseRight": tcell.ButtonSecondary,
|
||||
"MouseWheelUp": tcell.WheelUp,
|
||||
"MouseWheelDown": tcell.WheelDown,
|
||||
"MouseWheelLeft": tcell.WheelLeft,
|
||||
"MouseWheelRight": tcell.WheelRight,
|
||||
}
|
||||
|
||||
var keyEvents = map[string]tcell.Key{
|
||||
"Up": tcell.KeyUp,
|
||||
"Down": tcell.KeyDown,
|
||||
"Right": tcell.KeyRight,
|
||||
"Left": tcell.KeyLeft,
|
||||
"UpLeft": tcell.KeyUpLeft,
|
||||
"UpRight": tcell.KeyUpRight,
|
||||
"DownLeft": tcell.KeyDownLeft,
|
||||
"DownRight": tcell.KeyDownRight,
|
||||
"Center": tcell.KeyCenter,
|
||||
"PageUp": tcell.KeyPgUp,
|
||||
"PageDown": tcell.KeyPgDn,
|
||||
"Home": tcell.KeyHome,
|
||||
"End": tcell.KeyEnd,
|
||||
"Insert": tcell.KeyInsert,
|
||||
"Delete": tcell.KeyDelete,
|
||||
"Help": tcell.KeyHelp,
|
||||
"Exit": tcell.KeyExit,
|
||||
"Clear": tcell.KeyClear,
|
||||
"Cancel": tcell.KeyCancel,
|
||||
"Print": tcell.KeyPrint,
|
||||
"Pause": tcell.KeyPause,
|
||||
"Backtab": tcell.KeyBacktab,
|
||||
"F1": tcell.KeyF1,
|
||||
"F2": tcell.KeyF2,
|
||||
"F3": tcell.KeyF3,
|
||||
"F4": tcell.KeyF4,
|
||||
"F5": tcell.KeyF5,
|
||||
"F6": tcell.KeyF6,
|
||||
"F7": tcell.KeyF7,
|
||||
"F8": tcell.KeyF8,
|
||||
"F9": tcell.KeyF9,
|
||||
"F10": tcell.KeyF10,
|
||||
"F11": tcell.KeyF11,
|
||||
"F12": tcell.KeyF12,
|
||||
"F13": tcell.KeyF13,
|
||||
"F14": tcell.KeyF14,
|
||||
"F15": tcell.KeyF15,
|
||||
"F16": tcell.KeyF16,
|
||||
"F17": tcell.KeyF17,
|
||||
"F18": tcell.KeyF18,
|
||||
"F19": tcell.KeyF19,
|
||||
"F20": tcell.KeyF20,
|
||||
"F21": tcell.KeyF21,
|
||||
"F22": tcell.KeyF22,
|
||||
"F23": tcell.KeyF23,
|
||||
"F24": tcell.KeyF24,
|
||||
"F25": tcell.KeyF25,
|
||||
"F26": tcell.KeyF26,
|
||||
"F27": tcell.KeyF27,
|
||||
"F28": tcell.KeyF28,
|
||||
"F29": tcell.KeyF29,
|
||||
"F30": tcell.KeyF30,
|
||||
"F31": tcell.KeyF31,
|
||||
"F32": tcell.KeyF32,
|
||||
"F33": tcell.KeyF33,
|
||||
"F34": tcell.KeyF34,
|
||||
"F35": tcell.KeyF35,
|
||||
"F36": tcell.KeyF36,
|
||||
"F37": tcell.KeyF37,
|
||||
"F38": tcell.KeyF38,
|
||||
"F39": tcell.KeyF39,
|
||||
"F40": tcell.KeyF40,
|
||||
"F41": tcell.KeyF41,
|
||||
"F42": tcell.KeyF42,
|
||||
"F43": tcell.KeyF43,
|
||||
"F44": tcell.KeyF44,
|
||||
"F45": tcell.KeyF45,
|
||||
"F46": tcell.KeyF46,
|
||||
"F47": tcell.KeyF47,
|
||||
"F48": tcell.KeyF48,
|
||||
"F49": tcell.KeyF49,
|
||||
"F50": tcell.KeyF50,
|
||||
"F51": tcell.KeyF51,
|
||||
"F52": tcell.KeyF52,
|
||||
"F53": tcell.KeyF53,
|
||||
"F54": tcell.KeyF54,
|
||||
"F55": tcell.KeyF55,
|
||||
"F56": tcell.KeyF56,
|
||||
"F57": tcell.KeyF57,
|
||||
"F58": tcell.KeyF58,
|
||||
"F59": tcell.KeyF59,
|
||||
"F60": tcell.KeyF60,
|
||||
"F61": tcell.KeyF61,
|
||||
"F62": tcell.KeyF62,
|
||||
"F63": tcell.KeyF63,
|
||||
"F64": tcell.KeyF64,
|
||||
"CtrlSpace": tcell.KeyCtrlSpace,
|
||||
"CtrlA": tcell.KeyCtrlA,
|
||||
"CtrlB": tcell.KeyCtrlB,
|
||||
"CtrlC": tcell.KeyCtrlC,
|
||||
"CtrlD": tcell.KeyCtrlD,
|
||||
"CtrlE": tcell.KeyCtrlE,
|
||||
"CtrlF": tcell.KeyCtrlF,
|
||||
"CtrlG": tcell.KeyCtrlG,
|
||||
"CtrlH": tcell.KeyCtrlH,
|
||||
"CtrlI": tcell.KeyCtrlI,
|
||||
"CtrlJ": tcell.KeyCtrlJ,
|
||||
"CtrlK": tcell.KeyCtrlK,
|
||||
"CtrlL": tcell.KeyCtrlL,
|
||||
"CtrlM": tcell.KeyCtrlM,
|
||||
"CtrlN": tcell.KeyCtrlN,
|
||||
"CtrlO": tcell.KeyCtrlO,
|
||||
"CtrlP": tcell.KeyCtrlP,
|
||||
"CtrlQ": tcell.KeyCtrlQ,
|
||||
"CtrlR": tcell.KeyCtrlR,
|
||||
"CtrlS": tcell.KeyCtrlS,
|
||||
"CtrlT": tcell.KeyCtrlT,
|
||||
"CtrlU": tcell.KeyCtrlU,
|
||||
"CtrlV": tcell.KeyCtrlV,
|
||||
"CtrlW": tcell.KeyCtrlW,
|
||||
"CtrlX": tcell.KeyCtrlX,
|
||||
"CtrlY": tcell.KeyCtrlY,
|
||||
"CtrlZ": tcell.KeyCtrlZ,
|
||||
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
|
||||
"CtrlBackslash": tcell.KeyCtrlBackslash,
|
||||
"CtrlRightSq": tcell.KeyCtrlRightSq,
|
||||
"CtrlCarat": tcell.KeyCtrlCarat,
|
||||
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
|
||||
"Tab": tcell.KeyTab,
|
||||
"Esc": tcell.KeyEsc,
|
||||
"Escape": tcell.KeyEscape,
|
||||
"Enter": tcell.KeyEnter,
|
||||
"Backspace": tcell.KeyBackspace2,
|
||||
"OldBackspace": tcell.KeyBackspace,
|
||||
|
||||
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
|
||||
"PgUp": tcell.KeyPgUp,
|
||||
"PgDown": tcell.KeyPgDn,
|
||||
}
|
||||
@@ -1,933 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
ulua "github.com/micro-editor/micro/v2/internal/lua"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type BufAction any
|
||||
|
||||
// BufKeyAction represents an action bound to a key.
|
||||
type BufKeyAction func(*BufPane) bool
|
||||
|
||||
// BufMouseAction is an action that must be bound to a mouse event.
|
||||
type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
|
||||
|
||||
// BufBindings stores the bindings for the buffer pane type.
|
||||
var BufBindings *KeyTree
|
||||
|
||||
// BufKeyActionGeneral makes a general pane action from a BufKeyAction.
|
||||
func BufKeyActionGeneral(a BufKeyAction) PaneKeyAction {
|
||||
return func(p Pane) bool {
|
||||
return a(p.(*BufPane))
|
||||
}
|
||||
}
|
||||
|
||||
// BufMouseActionGeneral makes a general pane mouse action from a BufKeyAction.
|
||||
func BufMouseActionGeneral(a BufMouseAction) PaneMouseAction {
|
||||
return func(p Pane, me *tcell.EventMouse) bool {
|
||||
return a(p.(*BufPane), me)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
BufBindings = NewKeyTree()
|
||||
}
|
||||
|
||||
// LuaAction makes an action from a lua function. It returns either a BufKeyAction
|
||||
// or a BufMouseAction depending on the event type.
|
||||
func LuaAction(fn string, k Event) BufAction {
|
||||
luaFn := strings.Split(fn, ".")
|
||||
if len(luaFn) <= 1 {
|
||||
return nil
|
||||
}
|
||||
plName, plFn := luaFn[0], luaFn[1]
|
||||
pl := config.FindPlugin(plName)
|
||||
if pl == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var action BufAction
|
||||
switch k.(type) {
|
||||
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||
action = BufKeyAction(func(h *BufPane) bool {
|
||||
val, err := pl.Call(plFn, luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
if v, ok := val.(lua.LBool); !ok {
|
||||
return false
|
||||
} else {
|
||||
return bool(v)
|
||||
}
|
||||
})
|
||||
case MouseEvent:
|
||||
action = BufMouseAction(func(h *BufPane, te *tcell.EventMouse) bool {
|
||||
val, err := pl.Call(plFn, luar.New(ulua.L, h), luar.New(ulua.L, te))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
if v, ok := val.(lua.LBool); !ok {
|
||||
return false
|
||||
} else {
|
||||
return bool(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
// BufMapEvent maps an event to an action
|
||||
func BufMapEvent(k Event, action string) {
|
||||
config.Bindings["buffer"][k.Name()] = action
|
||||
|
||||
var actionfns []BufAction
|
||||
var names []string
|
||||
var types []byte
|
||||
for i := 0; ; i++ {
|
||||
if action == "" {
|
||||
break
|
||||
}
|
||||
|
||||
idx := util.IndexAnyUnquoted(action, "&|,")
|
||||
a := action
|
||||
if idx >= 0 {
|
||||
a = action[:idx]
|
||||
types = append(types, action[idx])
|
||||
action = action[idx+1:]
|
||||
} else {
|
||||
types = append(types, ' ')
|
||||
action = ""
|
||||
}
|
||||
|
||||
var afn BufAction
|
||||
if strings.HasPrefix(a, "command:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandAction(a)
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "command-edit:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandEditAction(a)
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "lua:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = LuaAction(a, k)
|
||||
if afn == nil {
|
||||
screen.TermMessage("Lua Error:", a, "does not exist")
|
||||
continue
|
||||
}
|
||||
split := strings.SplitN(a, ".", 2)
|
||||
if len(split) > 1 {
|
||||
a = strings.Title(split[0]) + strings.Title(split[1])
|
||||
} else {
|
||||
a = strings.Title(a)
|
||||
}
|
||||
|
||||
names = append(names, a)
|
||||
} else if f, ok := BufKeyActions[a]; ok {
|
||||
afn = f
|
||||
names = append(names, a)
|
||||
} else if f, ok := BufMouseActions[a]; ok {
|
||||
afn = f
|
||||
names = append(names, a)
|
||||
} else {
|
||||
screen.TermMessage("Error in bindings: action", a, "does not exist")
|
||||
continue
|
||||
}
|
||||
actionfns = append(actionfns, afn)
|
||||
}
|
||||
bufAction := func(h *BufPane, te *tcell.EventMouse) bool {
|
||||
for i, a := range actionfns {
|
||||
var success bool
|
||||
if _, ok := MultiActions[names[i]]; ok {
|
||||
success = true
|
||||
for _, c := range h.Buf.GetCursors() {
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
success = success && h.execAction(a, names[i], te)
|
||||
}
|
||||
} 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
|
||||
h = MainTab().CurPane()
|
||||
if h == nil {
|
||||
// stop, in case the current pane is not a BufPane
|
||||
break
|
||||
}
|
||||
|
||||
if (!success && types[i] == '&') || (success && types[i] == '|') {
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
switch e := k.(type) {
|
||||
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||
BufBindings.RegisterKeyBinding(e, BufKeyActionGeneral(func(h *BufPane) bool {
|
||||
return bufAction(h, nil)
|
||||
}))
|
||||
case MouseEvent:
|
||||
BufBindings.RegisterMouseBinding(e, BufMouseActionGeneral(bufAction))
|
||||
}
|
||||
}
|
||||
|
||||
// BufUnmap unmaps a key or mouse event from any action
|
||||
func BufUnmap(k Event) {
|
||||
// TODO
|
||||
// delete(BufKeyBindings, k)
|
||||
//
|
||||
// switch e := k.(type) {
|
||||
// case MouseEvent:
|
||||
// delete(BufMouseBindings, e)
|
||||
// }
|
||||
}
|
||||
|
||||
// The BufPane connects the buffer and the window
|
||||
// It provides a cursor (or multiple) and defines a set of actions
|
||||
// that can be taken on the buffer
|
||||
// The ActionHandler can access the window for necessary info about
|
||||
// visual positions for mouse clicks and scrolling
|
||||
type BufPane struct {
|
||||
display.BWindow
|
||||
|
||||
// Buf is the buffer this BufPane views
|
||||
Buf *buffer.Buffer
|
||||
// Bindings stores the association of key events and actions
|
||||
bindings *KeyTree
|
||||
|
||||
// Cursor is the currently active buffer cursor
|
||||
Cursor *buffer.Cursor
|
||||
|
||||
// Since tcell doesn't differentiate between a mouse press event
|
||||
// and a mouse move event with button pressed (nor between a mouse
|
||||
// release event and a mouse move event with no buttons pressed),
|
||||
// we need to keep track of whether or not the mouse was previously
|
||||
// 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
|
||||
|
||||
// This stores when the last click was
|
||||
// This is useful for detecting double and triple clicks
|
||||
lastClickTime time.Time
|
||||
lastLoc buffer.Loc
|
||||
|
||||
// freshClip returns true if one or more lines have been cut to the clipboard
|
||||
// and have never been pasted yet.
|
||||
freshClip bool
|
||||
|
||||
// Was the last mouse event actually a double click?
|
||||
// Useful for detecting triple clicks -- if a double click is detected
|
||||
// but the last mouse event was actually a double click, it's a triple click
|
||||
DoubleClick bool
|
||||
// Same here, just to keep track for mouse move events
|
||||
TripleClick bool
|
||||
|
||||
// Should the current multiple cursor selection search based on word or
|
||||
// based on selection (false for selection, true for word)
|
||||
multiWord bool
|
||||
|
||||
splitID uint64
|
||||
tab *Tab
|
||||
|
||||
// remember original location of a search in case the search is canceled
|
||||
searchOrig buffer.Loc
|
||||
|
||||
// The pane may not yet be fully initialized after its creation
|
||||
// since we may not know the window geometry yet. In such case we finish
|
||||
// its initialization a bit later, after the initial resize.
|
||||
initialized bool
|
||||
}
|
||||
|
||||
func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
|
||||
h := new(BufPane)
|
||||
h.Buf = buf
|
||||
h.BWindow = win
|
||||
h.tab = tab
|
||||
|
||||
h.Cursor = h.Buf.GetActiveCursor()
|
||||
h.mousePressed = make(map[MouseEvent]bool)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// NewBufPane creates a new buffer pane with the given window.
|
||||
func NewBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
|
||||
h := newBufPane(buf, win, tab)
|
||||
h.finishInitialize()
|
||||
return h
|
||||
}
|
||||
|
||||
// NewBufPaneFromBuf constructs a new pane from the given buffer and automatically
|
||||
// creates a buf window.
|
||||
func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
|
||||
w := display.NewBufWindow(0, 0, 0, 0, buf)
|
||||
h := newBufPane(buf, w, tab)
|
||||
// Postpone finishing initializing the pane until we know the actual geometry
|
||||
// of the buf window.
|
||||
return h
|
||||
}
|
||||
|
||||
// TODO: make sure splitID and tab are set before finishInitialize is called
|
||||
func (h *BufPane) finishInitialize() {
|
||||
h.initialRelocate()
|
||||
h.initialized = true
|
||||
|
||||
err := config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Resize resizes the pane
|
||||
func (h *BufPane) Resize(width, height int) {
|
||||
h.BWindow.Resize(width, height)
|
||||
if !h.initialized {
|
||||
h.finishInitialize()
|
||||
}
|
||||
}
|
||||
|
||||
// SetTab sets this pane's tab.
|
||||
func (h *BufPane) SetTab(t *Tab) {
|
||||
h.tab = t
|
||||
}
|
||||
|
||||
// Tab returns this pane's tab.
|
||||
func (h *BufPane) Tab() *Tab {
|
||||
return h.tab
|
||||
}
|
||||
|
||||
func (h *BufPane) ResizePane(size int) {
|
||||
n := h.tab.GetNode(h.splitID)
|
||||
n.ResizeSplit(size)
|
||||
h.tab.Resize()
|
||||
}
|
||||
|
||||
// PluginCB calls all plugin callbacks with a certain name and displays an
|
||||
// error if there is one and returns the aggregate boolean response.
|
||||
// The bufpane is passed as the first argument to the callbacks,
|
||||
// optional args are passed as the next arguments.
|
||||
func (h *BufPane) PluginCB(cb string, args ...any) bool {
|
||||
largs := []lua.LValue{luar.New(ulua.L, h)}
|
||||
for _, a := range args {
|
||||
largs = append(largs, luar.New(ulua.L, a))
|
||||
}
|
||||
|
||||
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, largs...)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (h *BufPane) resetMouse() {
|
||||
for me := range h.mousePressed {
|
||||
delete(h.mousePressed, me)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenBuffer opens the given buffer in this pane.
|
||||
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
||||
h.Buf.Close()
|
||||
h.Buf = b
|
||||
h.BWindow.SetBuffer(b)
|
||||
h.Cursor = b.GetActiveCursor()
|
||||
h.Resize(h.GetView().Width, h.GetView().Height)
|
||||
h.initialRelocate()
|
||||
// Set mouseReleased to true because we assume the mouse is not being
|
||||
// pressed when the editor is opened
|
||||
h.resetMouse()
|
||||
h.lastClickTime = time.Time{}
|
||||
}
|
||||
|
||||
// GotoLoc moves the cursor to a new location and adjusts the view accordingly.
|
||||
// Use GotoLoc when the new location may be far away from the current location.
|
||||
func (h *BufPane) GotoLoc(loc buffer.Loc) {
|
||||
sloc := h.SLocFromLoc(loc)
|
||||
d := h.Diff(h.SLocFromLoc(h.Cursor.Loc), sloc)
|
||||
|
||||
h.Cursor.GotoLoc(loc)
|
||||
|
||||
// If the new location is far away from the previous one,
|
||||
// ensure the cursor is at 25% of the window height
|
||||
height := h.BufView().Height
|
||||
if util.Abs(d) >= height {
|
||||
v := h.GetView()
|
||||
v.StartLine = h.Scroll(sloc, -height/4)
|
||||
h.ScrollAdjust()
|
||||
v.StartCol = 0
|
||||
}
|
||||
h.Relocate()
|
||||
}
|
||||
|
||||
func (h *BufPane) initialRelocate() {
|
||||
sloc := h.SLocFromLoc(h.Cursor.Loc)
|
||||
height := h.BufView().Height
|
||||
|
||||
// If the initial cursor location is far away from the beginning
|
||||
// of the buffer, ensure the cursor is at 25% of the window height
|
||||
v := h.GetView()
|
||||
if h.Diff(display.SLoc{0, 0}, sloc) < height {
|
||||
v.StartLine = display.SLoc{0, 0}
|
||||
} else {
|
||||
v.StartLine = h.Scroll(sloc, -height/4)
|
||||
h.ScrollAdjust()
|
||||
}
|
||||
v.StartCol = 0
|
||||
h.Relocate()
|
||||
}
|
||||
|
||||
// ID returns this pane's split id.
|
||||
func (h *BufPane) ID() uint64 {
|
||||
return h.splitID
|
||||
}
|
||||
|
||||
// SetID sets the split ID of this pane.
|
||||
func (h *BufPane) SetID(i uint64) {
|
||||
h.splitID = i
|
||||
}
|
||||
|
||||
// Name returns the BufPane's name.
|
||||
func (h *BufPane) Name() string {
|
||||
n := h.Buf.GetName()
|
||||
if h.Buf.Modified() {
|
||||
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 {
|
||||
reloadSetting := h.Buf.Settings["reload"]
|
||||
return reloadSetting.(string)
|
||||
}
|
||||
|
||||
// HandleEvent executes the tcell event properly
|
||||
func (h *BufPane) HandleEvent(event tcell.Event) {
|
||||
if h.Buf.ExternallyModified() && !h.Buf.ReloadDisabled {
|
||||
reload := h.getReloadSetting()
|
||||
|
||||
if reload == "prompt" {
|
||||
InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n,esc)", func(yes, canceled bool) {
|
||||
if canceled {
|
||||
h.Buf.DisableReload()
|
||||
}
|
||||
if !yes || canceled {
|
||||
h.Buf.UpdateModTime()
|
||||
} else {
|
||||
h.ReOpen()
|
||||
}
|
||||
})
|
||||
} else if reload == "auto" {
|
||||
h.ReOpen()
|
||||
} else if reload == "disabled" {
|
||||
h.Buf.DisableReload()
|
||||
} else {
|
||||
InfoBar.Message("Invalid reload setting")
|
||||
}
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventRaw:
|
||||
re := RawEvent{
|
||||
esc: e.EscSeq(),
|
||||
}
|
||||
h.DoKeyEvent(re)
|
||||
case *tcell.EventPaste:
|
||||
h.paste(e.Text())
|
||||
h.Relocate()
|
||||
case *tcell.EventKey:
|
||||
ke := keyEvent(e)
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
if !done && e.Key() == tcell.KeyRune {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
}
|
||||
case *tcell.EventMouse:
|
||||
if e.Buttons() != tcell.ButtonNone {
|
||||
me := MouseEvent{
|
||||
btn: e.Buttons(),
|
||||
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)
|
||||
} 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()
|
||||
|
||||
if h.IsActive() {
|
||||
// Display any gutter messages for this line
|
||||
c := h.Buf.GetActiveCursor()
|
||||
none := true
|
||||
for _, m := range h.Buf.Messages {
|
||||
if c.Y == m.Start.Y || c.Y == m.End.Y {
|
||||
InfoBar.GutterMessage(m.Msg)
|
||||
none = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if none && InfoBar.HasGutter {
|
||||
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.
|
||||
func (h *BufPane) Bindings() *KeyTree {
|
||||
if h.bindings != nil {
|
||||
return h.bindings
|
||||
}
|
||||
return BufBindings
|
||||
}
|
||||
|
||||
// DoKeyEvent executes a key event by finding the action it is bound
|
||||
// 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 {
|
||||
binds := h.Bindings()
|
||||
action, more := binds.NextEvent(e, nil)
|
||||
if action != nil && !more {
|
||||
action(h)
|
||||
binds.ResetEvents()
|
||||
return true
|
||||
} else if action == nil && !more {
|
||||
binds.ResetEvents()
|
||||
}
|
||||
return more
|
||||
}
|
||||
|
||||
func (h *BufPane) execAction(action BufAction, name string, te *tcell.EventMouse) bool {
|
||||
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
|
||||
h.Buf.HasSuggestions = false
|
||||
}
|
||||
|
||||
if !h.PluginCB("pre"+name, te) {
|
||||
return false
|
||||
}
|
||||
|
||||
var success bool
|
||||
switch a := action.(type) {
|
||||
case BufKeyAction:
|
||||
success = a(h)
|
||||
case BufMouseAction:
|
||||
success = a(h, te)
|
||||
}
|
||||
success = success && h.PluginCB("on"+name, te)
|
||||
|
||||
if _, ok := MultiActions[name]; ok {
|
||||
if recordingMacro {
|
||||
if name != "ToggleMacro" && name != "PlayMacro" {
|
||||
curmacro = append(curmacro, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
func (h *BufPane) completeAction(action string) {
|
||||
h.PluginCB("on" + action)
|
||||
}
|
||||
|
||||
func (h *BufPane) HasKeyEvent(e Event) bool {
|
||||
// TODO
|
||||
return true
|
||||
// _, ok := BufKeyBindings[e]
|
||||
// return ok
|
||||
}
|
||||
|
||||
// DoMouseEvent executes a mouse event by finding the action it is bound
|
||||
// to and executing it
|
||||
func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
|
||||
binds := h.Bindings()
|
||||
action, _ := binds.NextEvent(e, te)
|
||||
if action != nil {
|
||||
action(h)
|
||||
binds.ResetEvents()
|
||||
return true
|
||||
}
|
||||
// TODO
|
||||
return false
|
||||
|
||||
// if action, ok := BufMouseBindings[e]; ok {
|
||||
// if action(h, te) {
|
||||
// h.Relocate()
|
||||
// }
|
||||
// return true
|
||||
// } else if h.HasKeyEvent(e) {
|
||||
// return h.DoKeyEvent(e)
|
||||
// }
|
||||
// return false
|
||||
}
|
||||
|
||||
// DoRuneInsert inserts a given rune into the current buffer
|
||||
// (possibly multiple times for multiple cursors)
|
||||
func (h *BufPane) DoRuneInsert(r rune) {
|
||||
cursors := h.Buf.GetCursors()
|
||||
for _, c := range cursors {
|
||||
// Insert a character
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
if !h.PluginCB("preRune", string(r)) {
|
||||
continue
|
||||
}
|
||||
if c.HasSelection() {
|
||||
c.DeleteSelection()
|
||||
c.ResetSelection()
|
||||
}
|
||||
|
||||
if h.Buf.OverwriteMode {
|
||||
next := c.Loc
|
||||
next.X++
|
||||
h.Buf.Replace(c.Loc, next, string(r))
|
||||
} else {
|
||||
h.Buf.Insert(c.Loc, string(r))
|
||||
}
|
||||
if recordingMacro {
|
||||
curmacro = append(curmacro, r)
|
||||
}
|
||||
h.Relocate()
|
||||
h.PluginCB("onRune", string(r))
|
||||
}
|
||||
}
|
||||
|
||||
// VSplitIndex opens the given buffer in a vertical split on the given side.
|
||||
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
|
||||
e := NewBufPaneFromBuf(buf, h.tab)
|
||||
e.splitID = h.tab.GetNode(h.splitID).VSplit(right)
|
||||
currentPaneIdx := h.tab.GetPane(h.splitID)
|
||||
if right {
|
||||
currentPaneIdx++
|
||||
}
|
||||
h.tab.AddPane(e, currentPaneIdx)
|
||||
h.tab.Resize()
|
||||
h.tab.SetActive(currentPaneIdx)
|
||||
return e
|
||||
}
|
||||
|
||||
// HSplitIndex opens the given buffer in a horizontal split on the given side.
|
||||
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
|
||||
e := NewBufPaneFromBuf(buf, h.tab)
|
||||
e.splitID = h.tab.GetNode(h.splitID).HSplit(bottom)
|
||||
currentPaneIdx := h.tab.GetPane(h.splitID)
|
||||
if bottom {
|
||||
currentPaneIdx++
|
||||
}
|
||||
h.tab.AddPane(e, currentPaneIdx)
|
||||
h.tab.Resize()
|
||||
h.tab.SetActive(currentPaneIdx)
|
||||
return e
|
||||
}
|
||||
|
||||
// VSplitBuf opens the given buffer in a new vertical split.
|
||||
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
|
||||
return h.VSplitIndex(buf, h.Buf.Settings["splitright"].(bool))
|
||||
}
|
||||
|
||||
// HSplitBuf opens the given buffer in a new horizontal split.
|
||||
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
|
||||
return h.HSplitIndex(buf, h.Buf.Settings["splitbottom"].(bool))
|
||||
}
|
||||
|
||||
// Close this pane.
|
||||
func (h *BufPane) Close() {
|
||||
h.Buf.Close()
|
||||
}
|
||||
|
||||
// SetActive marks this pane as active.
|
||||
func (h *BufPane) SetActive(b bool) {
|
||||
if h.IsActive() == b {
|
||||
return
|
||||
}
|
||||
|
||||
h.BWindow.SetActive(b)
|
||||
if b {
|
||||
// Display any gutter messages for this line
|
||||
c := h.Buf.GetActiveCursor()
|
||||
none := true
|
||||
for _, m := range h.Buf.Messages {
|
||||
if c.Y == m.Start.Y || c.Y == m.End.Y {
|
||||
InfoBar.GutterMessage(m.Msg)
|
||||
none = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if none && InfoBar.HasGutter {
|
||||
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
|
||||
var BufKeyActions = map[string]BufKeyAction{
|
||||
"CursorUp": (*BufPane).CursorUp,
|
||||
"CursorDown": (*BufPane).CursorDown,
|
||||
"CursorPageUp": (*BufPane).CursorPageUp,
|
||||
"CursorPageDown": (*BufPane).CursorPageDown,
|
||||
"CursorLeft": (*BufPane).CursorLeft,
|
||||
"CursorRight": (*BufPane).CursorRight,
|
||||
"CursorStart": (*BufPane).CursorStart,
|
||||
"CursorEnd": (*BufPane).CursorEnd,
|
||||
"CursorToViewTop": (*BufPane).CursorToViewTop,
|
||||
"CursorToViewCenter": (*BufPane).CursorToViewCenter,
|
||||
"CursorToViewBottom": (*BufPane).CursorToViewBottom,
|
||||
"SelectToStart": (*BufPane).SelectToStart,
|
||||
"SelectToEnd": (*BufPane).SelectToEnd,
|
||||
"SelectUp": (*BufPane).SelectUp,
|
||||
"SelectDown": (*BufPane).SelectDown,
|
||||
"SelectLeft": (*BufPane).SelectLeft,
|
||||
"SelectRight": (*BufPane).SelectRight,
|
||||
"WordRight": (*BufPane).WordRight,
|
||||
"WordLeft": (*BufPane).WordLeft,
|
||||
"SubWordRight": (*BufPane).SubWordRight,
|
||||
"SubWordLeft": (*BufPane).SubWordLeft,
|
||||
"SelectWordRight": (*BufPane).SelectWordRight,
|
||||
"SelectWordLeft": (*BufPane).SelectWordLeft,
|
||||
"SelectSubWordRight": (*BufPane).SelectSubWordRight,
|
||||
"SelectSubWordLeft": (*BufPane).SelectSubWordLeft,
|
||||
"DeleteWordRight": (*BufPane).DeleteWordRight,
|
||||
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
|
||||
"DeleteSubWordRight": (*BufPane).DeleteSubWordRight,
|
||||
"DeleteSubWordLeft": (*BufPane).DeleteSubWordLeft,
|
||||
"SelectLine": (*BufPane).SelectLine,
|
||||
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
|
||||
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
|
||||
"SelectToStartOfTextToggle": (*BufPane).SelectToStartOfTextToggle,
|
||||
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
|
||||
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
|
||||
"ParagraphNext": (*BufPane).ParagraphNext,
|
||||
"SelectToParagraphPrevious": (*BufPane).SelectToParagraphPrevious,
|
||||
"SelectToParagraphNext": (*BufPane).SelectToParagraphNext,
|
||||
"InsertNewline": (*BufPane).InsertNewline,
|
||||
"Backspace": (*BufPane).Backspace,
|
||||
"Delete": (*BufPane).Delete,
|
||||
"InsertTab": (*BufPane).InsertTab,
|
||||
"Save": (*BufPane).Save,
|
||||
"SaveAll": (*BufPane).SaveAll,
|
||||
"SaveAs": (*BufPane).SaveAs,
|
||||
"Find": (*BufPane).Find,
|
||||
"FindLiteral": (*BufPane).FindLiteral,
|
||||
"FindNext": (*BufPane).FindNext,
|
||||
"FindPrevious": (*BufPane).FindPrevious,
|
||||
"DiffNext": (*BufPane).DiffNext,
|
||||
"DiffPrevious": (*BufPane).DiffPrevious,
|
||||
"Center": (*BufPane).Center,
|
||||
"Undo": (*BufPane).Undo,
|
||||
"Redo": (*BufPane).Redo,
|
||||
"Copy": (*BufPane).Copy,
|
||||
"CopyLine": (*BufPane).CopyLine,
|
||||
"Cut": (*BufPane).Cut,
|
||||
"CutLine": (*BufPane).CutLine,
|
||||
"Duplicate": (*BufPane).Duplicate,
|
||||
"DuplicateLine": (*BufPane).DuplicateLine,
|
||||
"DeleteLine": (*BufPane).DeleteLine,
|
||||
"MoveLinesUp": (*BufPane).MoveLinesUp,
|
||||
"MoveLinesDown": (*BufPane).MoveLinesDown,
|
||||
"IndentSelection": (*BufPane).IndentSelection,
|
||||
"OutdentSelection": (*BufPane).OutdentSelection,
|
||||
"Autocomplete": (*BufPane).Autocomplete,
|
||||
"CycleAutocompleteBack": (*BufPane).CycleAutocompleteBack,
|
||||
"OutdentLine": (*BufPane).OutdentLine,
|
||||
"IndentLine": (*BufPane).IndentLine,
|
||||
"Paste": (*BufPane).Paste,
|
||||
"PastePrimary": (*BufPane).PastePrimary,
|
||||
"SelectAll": (*BufPane).SelectAll,
|
||||
"OpenFile": (*BufPane).OpenFile,
|
||||
"Start": (*BufPane).Start,
|
||||
"End": (*BufPane).End,
|
||||
"PageUp": (*BufPane).PageUp,
|
||||
"PageDown": (*BufPane).PageDown,
|
||||
"SelectPageUp": (*BufPane).SelectPageUp,
|
||||
"SelectPageDown": (*BufPane).SelectPageDown,
|
||||
"HalfPageUp": (*BufPane).HalfPageUp,
|
||||
"HalfPageDown": (*BufPane).HalfPageDown,
|
||||
"StartOfText": (*BufPane).StartOfText,
|
||||
"StartOfTextToggle": (*BufPane).StartOfTextToggle,
|
||||
"StartOfLine": (*BufPane).StartOfLine,
|
||||
"EndOfLine": (*BufPane).EndOfLine,
|
||||
"ToggleHelp": (*BufPane).ToggleHelp,
|
||||
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
|
||||
"ToggleDiffGutter": (*BufPane).ToggleDiffGutter,
|
||||
"ToggleRuler": (*BufPane).ToggleRuler,
|
||||
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
|
||||
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
|
||||
"ResetSearch": (*BufPane).ResetSearch,
|
||||
"ClearStatus": (*BufPane).ClearStatus,
|
||||
"ShellMode": (*BufPane).ShellMode,
|
||||
"CommandMode": (*BufPane).CommandMode,
|
||||
"ToggleOverwriteMode": (*BufPane).ToggleOverwriteMode,
|
||||
"Escape": (*BufPane).Escape,
|
||||
"Quit": (*BufPane).Quit,
|
||||
"QuitAll": (*BufPane).QuitAll,
|
||||
"ForceQuit": (*BufPane).ForceQuit,
|
||||
"AddTab": (*BufPane).AddTab,
|
||||
"PreviousTab": (*BufPane).PreviousTab,
|
||||
"NextTab": (*BufPane).NextTab,
|
||||
"FirstTab": (*BufPane).FirstTab,
|
||||
"LastTab": (*BufPane).LastTab,
|
||||
"NextSplit": (*BufPane).NextSplit,
|
||||
"PreviousSplit": (*BufPane).PreviousSplit,
|
||||
"FirstSplit": (*BufPane).FirstSplit,
|
||||
"LastSplit": (*BufPane).LastSplit,
|
||||
"Unsplit": (*BufPane).Unsplit,
|
||||
"VSplit": (*BufPane).VSplitAction,
|
||||
"HSplit": (*BufPane).HSplitAction,
|
||||
"ToggleMacro": (*BufPane).ToggleMacro,
|
||||
"PlayMacro": (*BufPane).PlayMacro,
|
||||
"Suspend": (*BufPane).Suspend,
|
||||
"ScrollUp": (*BufPane).ScrollUpAction,
|
||||
"ScrollDown": (*BufPane).ScrollDownAction,
|
||||
"SpawnMultiCursor": (*BufPane).SpawnMultiCursor,
|
||||
"SpawnMultiCursorUp": (*BufPane).SpawnMultiCursorUp,
|
||||
"SpawnMultiCursorDown": (*BufPane).SpawnMultiCursorDown,
|
||||
"SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
|
||||
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
|
||||
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
|
||||
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
|
||||
"SkipMultiCursorBack": (*BufPane).SkipMultiCursorBack,
|
||||
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
|
||||
"JumpLine": (*BufPane).JumpLine,
|
||||
"Deselect": (*BufPane).Deselect,
|
||||
"ClearInfo": (*BufPane).ClearInfo,
|
||||
"None": (*BufPane).None,
|
||||
|
||||
// This was changed to InsertNewline but I don't want to break backwards compatibility
|
||||
"InsertEnter": (*BufPane).InsertNewline,
|
||||
}
|
||||
|
||||
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
|
||||
var BufMouseActions = map[string]BufMouseAction{
|
||||
"MousePress": (*BufPane).MousePress,
|
||||
"MouseDrag": (*BufPane).MouseDrag,
|
||||
"MouseRelease": (*BufPane).MouseRelease,
|
||||
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
|
||||
}
|
||||
|
||||
// MultiActions is a list of actions that should be executed multiple
|
||||
// times if there are multiple cursors (one per cursor)
|
||||
// Generally actions that modify global editor state like quitting or
|
||||
// saving should not be included in this list
|
||||
var MultiActions = map[string]bool{
|
||||
"CursorUp": true,
|
||||
"CursorDown": true,
|
||||
"CursorPageUp": true,
|
||||
"CursorPageDown": true,
|
||||
"CursorLeft": true,
|
||||
"CursorRight": true,
|
||||
"CursorStart": true,
|
||||
"CursorEnd": true,
|
||||
"SelectToStart": true,
|
||||
"SelectToEnd": true,
|
||||
"SelectUp": true,
|
||||
"SelectDown": true,
|
||||
"SelectLeft": true,
|
||||
"SelectRight": true,
|
||||
"WordRight": true,
|
||||
"WordLeft": true,
|
||||
"SubWordRight": true,
|
||||
"SubWordLeft": true,
|
||||
"SelectWordRight": true,
|
||||
"SelectWordLeft": true,
|
||||
"SelectSubWordRight": true,
|
||||
"SelectSubWordLeft": true,
|
||||
"DeleteWordRight": true,
|
||||
"DeleteWordLeft": true,
|
||||
"DeleteSubWordRight": true,
|
||||
"DeleteSubWordLeft": true,
|
||||
"SelectLine": true,
|
||||
"SelectToStartOfLine": true,
|
||||
"SelectToStartOfText": true,
|
||||
"SelectToStartOfTextToggle": true,
|
||||
"SelectToEndOfLine": true,
|
||||
"ParagraphPrevious": true,
|
||||
"ParagraphNext": true,
|
||||
"InsertNewline": true,
|
||||
"Backspace": true,
|
||||
"Delete": true,
|
||||
"InsertTab": true,
|
||||
"FindNext": true,
|
||||
"FindPrevious": true,
|
||||
"CopyLine": true,
|
||||
"Copy": true,
|
||||
"Cut": true,
|
||||
"CutLine": true,
|
||||
"Duplicate": true,
|
||||
"DuplicateLine": true,
|
||||
"DeleteLine": true,
|
||||
"MoveLinesUp": true,
|
||||
"MoveLinesDown": true,
|
||||
"IndentSelection": true,
|
||||
"OutdentSelection": true,
|
||||
"OutdentLine": true,
|
||||
"IndentLine": true,
|
||||
"Paste": true,
|
||||
"PastePrimary": true,
|
||||
"SelectPageUp": true,
|
||||
"SelectPageDown": true,
|
||||
"StartOfLine": true,
|
||||
"StartOfText": true,
|
||||
"StartOfTextToggle": true,
|
||||
"EndOfLine": true,
|
||||
"JumpToMatchingBrace": true,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
package action
|
||||
|
||||
var termdefaults = map[string]string{
|
||||
"<Ctrl-q><Ctrl-q>": "Exit",
|
||||
"<Ctrl-e><Ctrl-e>": "CommandMode",
|
||||
"<Ctrl-w><Ctrl-w>": "NextSplit|FirstSplit",
|
||||
}
|
||||
|
||||
// DefaultBindings returns a map containing micro's default keybindings
|
||||
func DefaultBindings(pane string) map[string]string {
|
||||
switch pane {
|
||||
case "command":
|
||||
return infodefaults
|
||||
case "buffer":
|
||||
return bufdefaults
|
||||
case "terminal":
|
||||
return termdefaults
|
||||
default:
|
||||
return map[string]string{}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package action
|
||||
|
||||
var bufdefaults = map[string]string{
|
||||
"Up": "CursorUp",
|
||||
"Down": "CursorDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"AltLeft": "WordLeft",
|
||||
"AltRight": "WordRight",
|
||||
"AltUp": "MoveLinesUp",
|
||||
"AltDown": "MoveLinesDown",
|
||||
"AltShiftRight": "SelectWordRight",
|
||||
"AltShiftLeft": "SelectWordLeft",
|
||||
"CtrlLeft": "StartOfTextToggle",
|
||||
"CtrlRight": "EndOfLine",
|
||||
"CtrlShiftLeft": "SelectToStartOfTextToggle",
|
||||
"ShiftHome": "SelectToStartOfTextToggle",
|
||||
"CtrlShiftRight": "SelectToEndOfLine",
|
||||
"ShiftEnd": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Alt-{": "ParagraphPrevious",
|
||||
"Alt-}": "ParagraphNext",
|
||||
"Enter": "InsertNewline",
|
||||
"CtrlH": "Backspace",
|
||||
"Backspace": "Backspace",
|
||||
"OldBackspace": "Backspace",
|
||||
"Alt-CtrlH": "DeleteWordLeft",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Tab": "Autocomplete|IndentSelection|InsertTab",
|
||||
"Backtab": "CycleAutocompleteBack|OutdentSelection|OutdentLine",
|
||||
"Ctrl-o": "OpenFile",
|
||||
"Ctrl-s": "Save",
|
||||
"Ctrl-f": "Find",
|
||||
"Alt-F": "FindLiteral",
|
||||
"Ctrl-n": "FindNext",
|
||||
"Ctrl-p": "FindPrevious",
|
||||
"Alt-[": "DiffPrevious|CursorStart",
|
||||
"Alt-]": "DiffNext|CursorEnd",
|
||||
"Ctrl-z": "Undo",
|
||||
"Ctrl-y": "Redo",
|
||||
"Ctrl-c": "Copy|CopyLine",
|
||||
"Ctrl-x": "Cut|CutLine",
|
||||
"Ctrl-k": "CutLine",
|
||||
"Ctrl-d": "Duplicate|DuplicateLine",
|
||||
"Ctrl-v": "Paste",
|
||||
"Ctrl-a": "SelectAll",
|
||||
"Ctrl-t": "AddTab",
|
||||
"Alt-,": "PreviousTab|LastTab",
|
||||
"Alt-.": "NextTab|FirstTab",
|
||||
"Home": "StartOfTextToggle",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"PageUp": "CursorPageUp",
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab|LastTab",
|
||||
"CtrlPageDown": "NextTab|FirstTab",
|
||||
"ShiftPageUp": "SelectPageUp",
|
||||
"ShiftPageDown": "SelectPageDown",
|
||||
"Ctrl-g": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"Ctrl-r": "ToggleRuler",
|
||||
"Ctrl-l": "command-edit:goto ",
|
||||
"Delete": "Delete",
|
||||
"Ctrl-b": "ShellMode",
|
||||
"Ctrl-q": "Quit",
|
||||
"Ctrl-e": "CommandMode",
|
||||
"Ctrl-w": "NextSplit|FirstSplit",
|
||||
"Ctrl-u": "ToggleMacro",
|
||||
"Ctrl-j": "PlayMacro",
|
||||
"Insert": "ToggleOverwriteMode",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfText",
|
||||
"Alt-e": "EndOfLine",
|
||||
// "Alt-p": "CursorUp",
|
||||
// "Alt-n": "CursorDown",
|
||||
|
||||
// Integration with file managers
|
||||
"F2": "Save",
|
||||
"F3": "Find",
|
||||
"F4": "Quit",
|
||||
"F7": "Find",
|
||||
"F10": "Quit",
|
||||
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
"AltShiftUp": "SpawnMultiCursorUp",
|
||||
"AltShiftDown": "SpawnMultiCursorDown",
|
||||
"Alt-m": "SpawnMultiCursorSelect",
|
||||
"Alt-p": "RemoveMultiCursor",
|
||||
"Alt-c": "RemoveAllMultiCursors",
|
||||
"Alt-x": "SkipMultiCursor",
|
||||
}
|
||||
|
||||
var infodefaults = map[string]string{
|
||||
"Up": "HistoryUp",
|
||||
"Down": "HistoryDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"AltLeft": "WordLeft",
|
||||
"AltRight": "WordRight",
|
||||
"AltUp": "CursorStart",
|
||||
"AltDown": "CursorEnd",
|
||||
"AltShiftRight": "SelectWordRight",
|
||||
"AltShiftLeft": "SelectWordLeft",
|
||||
"CtrlLeft": "StartOfTextToggle",
|
||||
"CtrlRight": "EndOfLine",
|
||||
"CtrlShiftLeft": "SelectToStartOfTextToggle",
|
||||
"ShiftHome": "SelectToStartOfTextToggle",
|
||||
"CtrlShiftRight": "SelectToEndOfLine",
|
||||
"ShiftEnd": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Enter": "ExecuteCommand",
|
||||
"CtrlH": "Backspace",
|
||||
"Backspace": "Backspace",
|
||||
"OldBackspace": "Backspace",
|
||||
"Alt-CtrlH": "DeleteWordLeft",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Tab": "CommandComplete",
|
||||
"Backtab": "CycleAutocompleteBack",
|
||||
"Ctrl-z": "Undo",
|
||||
"Ctrl-y": "Redo",
|
||||
"Ctrl-c": "Copy",
|
||||
"Ctrl-x": "Cut",
|
||||
"Ctrl-k": "CutLine",
|
||||
"Ctrl-v": "Paste",
|
||||
"Home": "StartOfTextToggle",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"Delete": "Delete",
|
||||
"Ctrl-q": "AbortCommand",
|
||||
"Ctrl-e": "EndOfLine",
|
||||
"Ctrl-a": "StartOfLine",
|
||||
"Ctrl-w": "DeleteWordLeft",
|
||||
"Insert": "ToggleOverwriteMode",
|
||||
"Ctrl-b": "WordLeft",
|
||||
"Ctrl-f": "WordRight",
|
||||
"Ctrl-d": "DeleteWordLeft",
|
||||
"Ctrl-m": "ExecuteCommand",
|
||||
"Ctrl-n": "HistoryDown",
|
||||
"Ctrl-p": "HistoryUp",
|
||||
"Ctrl-u": "SelectToStart",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfText",
|
||||
"Alt-e": "EndOfLine",
|
||||
|
||||
// Integration with file managers
|
||||
"F10": "AbortCommand",
|
||||
"Esc": "AbortCommand",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
//go:build !darwin
|
||||
// +build !darwin
|
||||
|
||||
package action
|
||||
|
||||
var bufdefaults = map[string]string{
|
||||
"Up": "CursorUp",
|
||||
"Down": "CursorDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"CtrlLeft": "WordLeft",
|
||||
"CtrlRight": "WordRight",
|
||||
"AltUp": "MoveLinesUp",
|
||||
"AltDown": "MoveLinesDown",
|
||||
"CtrlShiftRight": "SelectWordRight",
|
||||
"CtrlShiftLeft": "SelectWordLeft",
|
||||
"AltLeft": "StartOfTextToggle",
|
||||
"AltRight": "EndOfLine",
|
||||
"AltShiftLeft": "SelectToStartOfTextToggle",
|
||||
"ShiftHome": "SelectToStartOfTextToggle",
|
||||
"AltShiftRight": "SelectToEndOfLine",
|
||||
"ShiftEnd": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Alt-{": "ParagraphPrevious",
|
||||
"Alt-}": "ParagraphNext",
|
||||
"Enter": "InsertNewline",
|
||||
"CtrlH": "Backspace",
|
||||
"Backspace": "Backspace",
|
||||
"OldBackspace": "Backspace",
|
||||
"Alt-CtrlH": "DeleteWordLeft",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Tab": "Autocomplete|IndentSelection|InsertTab",
|
||||
"Backtab": "CycleAutocompleteBack|OutdentSelection|OutdentLine",
|
||||
"Ctrl-o": "OpenFile",
|
||||
"Ctrl-s": "Save",
|
||||
"Ctrl-f": "Find",
|
||||
"Alt-F": "FindLiteral",
|
||||
"Ctrl-n": "FindNext",
|
||||
"Ctrl-p": "FindPrevious",
|
||||
"Alt-[": "DiffPrevious|CursorStart",
|
||||
"Alt-]": "DiffNext|CursorEnd",
|
||||
"Ctrl-z": "Undo",
|
||||
"Ctrl-y": "Redo",
|
||||
"Ctrl-c": "Copy|CopyLine",
|
||||
"Ctrl-x": "Cut|CutLine",
|
||||
"Ctrl-k": "CutLine",
|
||||
"Ctrl-d": "Duplicate|DuplicateLine",
|
||||
"Ctrl-v": "Paste",
|
||||
"Ctrl-a": "SelectAll",
|
||||
"Ctrl-t": "AddTab",
|
||||
"Alt-,": "PreviousTab|LastTab",
|
||||
"Alt-.": "NextTab|FirstTab",
|
||||
"Home": "StartOfTextToggle",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"PageUp": "CursorPageUp",
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab|LastTab",
|
||||
"CtrlPageDown": "NextTab|FirstTab",
|
||||
"ShiftPageUp": "SelectPageUp",
|
||||
"ShiftPageDown": "SelectPageDown",
|
||||
"Ctrl-g": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"Ctrl-r": "ToggleRuler",
|
||||
"Ctrl-l": "command-edit:goto ",
|
||||
"Delete": "Delete",
|
||||
"Ctrl-b": "ShellMode",
|
||||
"Ctrl-q": "Quit",
|
||||
"Ctrl-e": "CommandMode",
|
||||
"Ctrl-w": "NextSplit|FirstSplit",
|
||||
"Ctrl-u": "ToggleMacro",
|
||||
"Ctrl-j": "PlayMacro",
|
||||
"Insert": "ToggleOverwriteMode",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfText",
|
||||
"Alt-e": "EndOfLine",
|
||||
// "Alt-p": "CursorUp",
|
||||
// "Alt-n": "CursorDown",
|
||||
|
||||
// Integration with file managers
|
||||
"F2": "Save",
|
||||
"F3": "Find",
|
||||
"F4": "Quit",
|
||||
"F7": "Find",
|
||||
"F10": "Quit",
|
||||
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
"Alt-m": "SpawnMultiCursorSelect",
|
||||
"AltShiftUp": "SpawnMultiCursorUp",
|
||||
"AltShiftDown": "SpawnMultiCursorDown",
|
||||
"Alt-p": "RemoveMultiCursor",
|
||||
"Alt-c": "RemoveAllMultiCursors",
|
||||
"Alt-x": "SkipMultiCursor",
|
||||
}
|
||||
|
||||
var infodefaults = map[string]string{
|
||||
"Up": "HistoryUp",
|
||||
"Down": "HistoryDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"AltLeft": "StartOfTextToggle",
|
||||
"AltRight": "EndOfLine",
|
||||
"AltUp": "CursorStart",
|
||||
"AltDown": "CursorEnd",
|
||||
"AltShiftRight": "SelectWordRight",
|
||||
"AltShiftLeft": "SelectWordLeft",
|
||||
"CtrlLeft": "WordLeft",
|
||||
"CtrlRight": "WordRight",
|
||||
"CtrlShiftLeft": "SelectToStartOfTextToggle",
|
||||
"ShiftHome": "SelectToStartOfTextToggle",
|
||||
"CtrlShiftRight": "SelectToEndOfLine",
|
||||
"ShiftEnd": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Enter": "ExecuteCommand",
|
||||
"CtrlH": "Backspace",
|
||||
"Backspace": "Backspace",
|
||||
"OldBackspace": "Backspace",
|
||||
"Alt-CtrlH": "DeleteWordLeft",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Tab": "CommandComplete",
|
||||
"Backtab": "CycleAutocompleteBack",
|
||||
"Ctrl-z": "Undo",
|
||||
"Ctrl-y": "Redo",
|
||||
"Ctrl-c": "Copy",
|
||||
"Ctrl-x": "Cut",
|
||||
"Ctrl-k": "CutLine",
|
||||
"Ctrl-v": "Paste",
|
||||
"Home": "StartOfTextToggle",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"Delete": "Delete",
|
||||
"Ctrl-q": "AbortCommand",
|
||||
"Ctrl-e": "EndOfLine",
|
||||
"Ctrl-a": "StartOfLine",
|
||||
"Ctrl-w": "DeleteWordLeft",
|
||||
"Insert": "ToggleOverwriteMode",
|
||||
"Ctrl-b": "WordLeft",
|
||||
"Ctrl-f": "WordRight",
|
||||
"Ctrl-d": "DeleteWordLeft",
|
||||
"Ctrl-m": "ExecuteCommand",
|
||||
"Ctrl-n": "HistoryDown",
|
||||
"Ctrl-p": "HistoryUp",
|
||||
"Ctrl-u": "SelectToStart",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfText",
|
||||
"Alt-e": "EndOfLine",
|
||||
|
||||
// Integration with file managers
|
||||
"F10": "AbortCommand",
|
||||
"Esc": "AbortCommand",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
type Event interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// RawEvent is simply an escape code
|
||||
// We allow users to directly bind escape codes
|
||||
// to get around some of a limitations of terminals
|
||||
type RawEvent struct {
|
||||
esc string
|
||||
}
|
||||
|
||||
func (r RawEvent) Name() string {
|
||||
return r.esc
|
||||
}
|
||||
|
||||
// KeyEvent is a key event containing a key code,
|
||||
// some possible modifiers (alt, ctrl, etc...) and
|
||||
// a rune if it was simply a character press
|
||||
// Note: to be compatible with tcell events,
|
||||
// for ctrl keys r=code
|
||||
type KeyEvent struct {
|
||||
code tcell.Key
|
||||
mod tcell.ModMask
|
||||
r rune
|
||||
any bool
|
||||
}
|
||||
|
||||
func metaToAlt(mod tcell.ModMask) tcell.ModMask {
|
||||
if mod&tcell.ModMeta != 0 {
|
||||
mod &= ^tcell.ModMeta
|
||||
mod |= tcell.ModAlt
|
||||
}
|
||||
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 {
|
||||
if k.any {
|
||||
return "<any>"
|
||||
}
|
||||
s := ""
|
||||
m := []string{}
|
||||
if k.mod&tcell.ModShift != 0 {
|
||||
m = append(m, "Shift")
|
||||
}
|
||||
if k.mod&tcell.ModAlt != 0 {
|
||||
m = append(m, "Alt")
|
||||
}
|
||||
if k.mod&tcell.ModMeta != 0 {
|
||||
m = append(m, "Meta")
|
||||
}
|
||||
if k.mod&tcell.ModCtrl != 0 {
|
||||
m = append(m, "Ctrl")
|
||||
}
|
||||
|
||||
ok := false
|
||||
if s, ok = tcell.KeyNames[k.code]; !ok {
|
||||
if k.code == tcell.KeyRune {
|
||||
s = string(k.r)
|
||||
} else {
|
||||
s = fmt.Sprintf("Key[%d]", k.code)
|
||||
}
|
||||
}
|
||||
if len(m) != 0 {
|
||||
if k.mod&tcell.ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") {
|
||||
s = s[5:]
|
||||
if len(s) == 1 {
|
||||
s = strings.ToLower(s)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", strings.Join(m, "-"), s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// A KeySequence defines a list of consecutive
|
||||
// events. All events in the sequence must be KeyEvents
|
||||
// or MouseEvents.
|
||||
type KeySequenceEvent struct {
|
||||
keys []Event
|
||||
}
|
||||
|
||||
func (k KeySequenceEvent) Name() string {
|
||||
buf := bytes.Buffer{}
|
||||
for _, e := range k.keys {
|
||||
buf.WriteByte('<')
|
||||
buf.WriteString(e.Name())
|
||||
buf.WriteByte('>')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type MouseState int
|
||||
|
||||
const (
|
||||
MousePress = iota
|
||||
MouseDrag
|
||||
MouseRelease
|
||||
)
|
||||
|
||||
// MouseEvent is a mouse event with a mouse button and
|
||||
// any possible key modifiers
|
||||
type MouseEvent struct {
|
||||
btn tcell.ButtonMask
|
||||
mod tcell.ModMask
|
||||
state MouseState
|
||||
}
|
||||
|
||||
func (m MouseEvent) Name() string {
|
||||
mod := ""
|
||||
if m.mod&tcell.ModShift != 0 {
|
||||
mod = "Shift-"
|
||||
}
|
||||
if m.mod&tcell.ModAlt != 0 {
|
||||
mod = "Alt-"
|
||||
}
|
||||
if m.mod&tcell.ModMeta != 0 {
|
||||
mod = "Meta-"
|
||||
}
|
||||
if m.mod&tcell.ModCtrl != 0 {
|
||||
mod = "Ctrl-"
|
||||
}
|
||||
|
||||
state := ""
|
||||
switch m.state {
|
||||
case MouseDrag:
|
||||
state = "Drag"
|
||||
case MouseRelease:
|
||||
state = "Release"
|
||||
}
|
||||
|
||||
for k, v := range mouseEvents {
|
||||
if v == m.btn {
|
||||
return fmt.Sprintf("%s%s%s", mod, k, state)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ConstructEvent takes a tcell event and returns a micro
|
||||
// event. Note that tcell events can't express certain
|
||||
// micro events such as key sequences. This function is
|
||||
// mostly used for debugging/raw panes or constructing
|
||||
// intermediate micro events while parsing a sequence.
|
||||
func ConstructEvent(event tcell.Event) (Event, error) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
return keyEvent(e), nil
|
||||
case *tcell.EventRaw:
|
||||
return RawEvent{
|
||||
esc: e.EscSeq(),
|
||||
}, nil
|
||||
case *tcell.EventMouse:
|
||||
return MouseEvent{
|
||||
btn: e.Buttons(),
|
||||
mod: metaToAlt(e.Modifiers()),
|
||||
}, nil
|
||||
}
|
||||
return nil, errors.New("No micro event equivalent")
|
||||
}
|
||||
|
||||
// A Handler will take a tcell event and execute it
|
||||
// appropriately
|
||||
type Handler interface {
|
||||
HandleEvent(tcell.Event)
|
||||
HandleCommand(string)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package action
|
||||
|
||||
import "github.com/micro-editor/micro/v2/internal/buffer"
|
||||
|
||||
// InfoBar is the global info bar.
|
||||
var InfoBar *InfoPane
|
||||
|
||||
// LogBufPane is a global log buffer.
|
||||
var LogBufPane *BufPane
|
||||
|
||||
// InitGlobals initializes the log buffer and the info bar
|
||||
func InitGlobals() {
|
||||
InfoBar = NewInfoBar()
|
||||
buffer.LogBuf = buffer.NewBufferFromString("", "", buffer.BTLog)
|
||||
buffer.LogBuf.SetName("Log")
|
||||
}
|
||||
|
||||
// GetInfoBar returns the infobar pane
|
||||
func GetInfoBar() *InfoPane {
|
||||
return InfoBar
|
||||
}
|
||||
|
||||
// WriteLog writes a string to the log buffer
|
||||
func WriteLog(s string) {
|
||||
buffer.WriteLog(s)
|
||||
if LogBufPane != nil {
|
||||
LogBufPane.CursorEnd()
|
||||
}
|
||||
}
|
||||
|
||||
// OpenLogBuf opens the log buffer from the current bufpane
|
||||
// If the current bufpane is a log buffer nothing happens,
|
||||
// otherwise the log buffer is opened in a horizontal split
|
||||
func (h *BufPane) OpenLogBuf() {
|
||||
LogBufPane = h.HSplitBuf(buffer.LogBuf)
|
||||
LogBufPane.CursorEnd()
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/micro-editor/micro/v2/pkg/highlight"
|
||||
)
|
||||
|
||||
// This file is meant (for now) for autocompletion in command mode, not
|
||||
// while coding. This helps micro autocomplete commands and then filenames
|
||||
// for example with `vsplit filename`.
|
||||
|
||||
// CommandComplete autocompletes commands
|
||||
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// HelpComplete autocompletes help topics
|
||||
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
|
||||
for _, file := range config.ListRuntimeFiles(config.RTHelp) {
|
||||
topic := file.Name()
|
||||
if strings.HasPrefix(topic, input) {
|
||||
suggestions = append(suggestions, topic)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// colorschemeComplete tab-completes names of colorschemes.
|
||||
// This is just a heper value for OptionValueComplete
|
||||
func colorschemeComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
files := config.ListRuntimeFiles(config.RTColorscheme)
|
||||
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), input) {
|
||||
suggestions = append(suggestions, f.Name())
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
// filetypeComplete autocompletes filetype
|
||||
func filetypeComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
|
||||
// We cannot match filetypes just by names of syntax files,
|
||||
// since those names may be different from the actual filetype values
|
||||
// specified inside syntax files (e.g. "c++" filetype in cpp.yaml).
|
||||
// So we need to parse filetype values out of those files.
|
||||
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
header, err := highlight.MakeHeaderYaml(data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Prevent duplicated defaults
|
||||
if header.FileType == "off" || header.FileType == "unknown" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(header.FileType, input) {
|
||||
suggestions = append(suggestions, header.FileType)
|
||||
}
|
||||
}
|
||||
headerLoop:
|
||||
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
|
||||
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
for option := range config.GlobalSettings {
|
||||
if strings.HasPrefix(option, input) {
|
||||
suggestions = append(suggestions, option)
|
||||
}
|
||||
}
|
||||
// for option := range localSettings {
|
||||
// if strings.HasPrefix(option, input) && !contains(suggestions, option) {
|
||||
// suggestions = append(suggestions, option)
|
||||
// }
|
||||
// }
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// OptionValueComplete completes values for various options
|
||||
func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
if len(args) >= 2 {
|
||||
// localSettings := config.DefaultLocalSettings()
|
||||
for option := range config.GlobalSettings {
|
||||
if option == string(args[len(args)-2]) {
|
||||
completeValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// for option := range localSettings {
|
||||
// if option == string(args[len(args)-2]) {
|
||||
// completeValue = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
}
|
||||
if !completeValue {
|
||||
return OptionComplete(b)
|
||||
}
|
||||
|
||||
inputOpt := string(args[len(args)-2])
|
||||
|
||||
inputOpt = strings.TrimSpace(inputOpt)
|
||||
var suggestions []string
|
||||
// localSettings := config.DefaultLocalSettings()
|
||||
var optionVal any
|
||||
for k, option := range config.GlobalSettings {
|
||||
if k == inputOpt {
|
||||
optionVal = option
|
||||
}
|
||||
}
|
||||
// for k, option := range localSettings {
|
||||
// if k == inputOpt {
|
||||
// optionVal = option
|
||||
// }
|
||||
// }
|
||||
|
||||
switch optionVal.(type) {
|
||||
case bool:
|
||||
if strings.HasPrefix("on", input) {
|
||||
suggestions = append(suggestions, "on")
|
||||
} else if strings.HasPrefix("true", input) {
|
||||
suggestions = append(suggestions, "true")
|
||||
}
|
||||
if strings.HasPrefix("off", input) {
|
||||
suggestions = append(suggestions, "off")
|
||||
} else if strings.HasPrefix("false", input) {
|
||||
suggestions = append(suggestions, "false")
|
||||
}
|
||||
case string:
|
||||
switch inputOpt {
|
||||
case "colorscheme":
|
||||
_, suggestions = colorschemeComplete(input)
|
||||
case "filetype":
|
||||
_, suggestions = filetypeComplete(input)
|
||||
case "sucmd":
|
||||
if strings.HasPrefix("sudo", input) {
|
||||
suggestions = append(suggestions, "sudo")
|
||||
}
|
||||
if strings.HasPrefix("doas", input) {
|
||||
suggestions = append(suggestions, "doas")
|
||||
}
|
||||
default:
|
||||
if choices, ok := config.OptionChoices[inputOpt]; ok {
|
||||
for _, choice := range choices {
|
||||
if strings.HasPrefix(choice, input) {
|
||||
suggestions = append(suggestions, choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(suggestions)
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginCmdComplete autocompletes the plugin command
|
||||
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
for _, cmd := range PluginCmds {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginComplete completes values for the plugin command
|
||||
func PluginComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
if len(args) >= 2 {
|
||||
for _, cmd := range PluginCmds {
|
||||
if cmd == string(args[len(args)-2]) {
|
||||
completeValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !completeValue {
|
||||
return PluginCmdComplete(b)
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, pl := range config.Plugins {
|
||||
if strings.HasPrefix(pl.Name, input) {
|
||||
suggestions = append(suggestions, pl.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(suggestions)
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginNameComplete completes with the names of loaded plugins
|
||||
// func PluginNameComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
// c := b.GetActiveCursor()
|
||||
// input, argstart := buffer.GetArg(b)
|
||||
//
|
||||
// var suggestions []string
|
||||
// for _, pp := range config.GetAllPluginPackages(nil) {
|
||||
// if strings.HasPrefix(pp.Name, input) {
|
||||
// suggestions = append(suggestions, pp.Name)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// sort.Strings(suggestions)
|
||||
// completions := make([]string, len(suggestions))
|
||||
// for i := range suggestions {
|
||||
// completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
// }
|
||||
// return completions, suggestions
|
||||
// }
|
||||
|
||||
// // MakeCompletion registers a function from a plugin for autocomplete commands
|
||||
// func MakeCompletion(function string) Completion {
|
||||
// pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
|
||||
// return Completion(-len(pluginCompletions))
|
||||
// }
|
||||
@@ -1,241 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
"github.com/micro-editor/micro/v2/internal/info"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
type InfoKeyAction func(*InfoPane)
|
||||
|
||||
var InfoBindings *KeyTree
|
||||
var InfoBufBindings *KeyTree
|
||||
|
||||
func init() {
|
||||
InfoBindings = NewKeyTree()
|
||||
InfoBufBindings = NewKeyTree()
|
||||
}
|
||||
|
||||
func InfoMapEvent(k Event, action string) {
|
||||
config.Bindings["command"][k.Name()] = action
|
||||
|
||||
switch e := k.(type) {
|
||||
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||
infoMapKey(e, action)
|
||||
case MouseEvent:
|
||||
infoMapMouse(e, action)
|
||||
}
|
||||
}
|
||||
|
||||
func infoMapKey(k Event, action string) {
|
||||
if f, ok := InfoKeyActions[action]; ok {
|
||||
InfoBindings.RegisterKeyBinding(k, InfoKeyActionGeneral(f))
|
||||
} else if f, ok := BufKeyActions[action]; ok {
|
||||
InfoBufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(f))
|
||||
}
|
||||
}
|
||||
|
||||
func infoMapMouse(k MouseEvent, action string) {
|
||||
// TODO: map mouse
|
||||
if f, ok := BufMouseActions[action]; ok {
|
||||
InfoBufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f))
|
||||
} else {
|
||||
infoMapKey(k, action)
|
||||
}
|
||||
}
|
||||
|
||||
func InfoKeyActionGeneral(a InfoKeyAction) PaneKeyAction {
|
||||
return func(p Pane) bool {
|
||||
a(p.(*InfoPane))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
type InfoPane struct {
|
||||
*BufPane
|
||||
*info.InfoBuf
|
||||
}
|
||||
|
||||
func NewInfoPane(ib *info.InfoBuf, w display.BWindow, tab *Tab) *InfoPane {
|
||||
ip := new(InfoPane)
|
||||
ip.InfoBuf = ib
|
||||
ip.BufPane = NewBufPane(ib.Buffer, w, tab)
|
||||
ip.BufPane.bindings = InfoBufBindings
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewInfoBar() *InfoPane {
|
||||
ib := info.NewBuffer()
|
||||
w := display.NewInfoWindow(ib)
|
||||
return NewInfoPane(ib, w, nil)
|
||||
}
|
||||
|
||||
func (h *InfoPane) Close() {
|
||||
h.InfoBuf.Close()
|
||||
h.BufPane.Close()
|
||||
}
|
||||
|
||||
func (h *InfoPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
// TODO
|
||||
case *tcell.EventKey:
|
||||
ke := keyEvent(e)
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
hasYN := h.HasYN
|
||||
if e.Key() == tcell.KeyRune && hasYN {
|
||||
y := e.Rune() == 'y' || e.Rune() == 'Y'
|
||||
n := e.Rune() == 'n' || e.Rune() == 'N'
|
||||
if y || n {
|
||||
h.YNResp = y
|
||||
h.DonePrompt(false)
|
||||
|
||||
InfoBindings.ResetEvents()
|
||||
InfoBufBindings.ResetEvents()
|
||||
}
|
||||
}
|
||||
if e.Key() == tcell.KeyRune && !done && !hasYN {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
done = true
|
||||
}
|
||||
if done && h.HasPrompt && !hasYN {
|
||||
resp := string(h.LineBytes(0))
|
||||
hist := h.History[h.PromptType]
|
||||
if resp != hist[h.HistoryNum] {
|
||||
h.HistoryNum = len(hist) - 1
|
||||
hist[h.HistoryNum] = resp
|
||||
h.HistorySearch = false
|
||||
}
|
||||
if h.EventCallback != nil {
|
||||
h.EventCallback(resp)
|
||||
}
|
||||
}
|
||||
default:
|
||||
h.BufPane.HandleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
action, more := InfoBindings.NextEvent(e, nil)
|
||||
if action != nil && !more {
|
||||
action(h)
|
||||
InfoBindings.ResetEvents()
|
||||
|
||||
return true
|
||||
} else if action == nil && !more {
|
||||
InfoBindings.ResetEvents()
|
||||
// return false //TODO:?
|
||||
}
|
||||
|
||||
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)
|
||||
if action != nil && !more {
|
||||
action(h.BufPane)
|
||||
InfoBufBindings.ResetEvents()
|
||||
return true
|
||||
} else if action == nil && !more {
|
||||
InfoBufBindings.ResetEvents()
|
||||
}
|
||||
}
|
||||
|
||||
return more
|
||||
}
|
||||
|
||||
// HistoryUp cycles history up
|
||||
func (h *InfoPane) HistoryUp() {
|
||||
h.UpHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// HistoryDown cycles history down
|
||||
func (h *InfoPane) HistoryDown() {
|
||||
h.DownHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// HistorySearchUp fetches the previous history item beginning with the text
|
||||
// in the infobuffer before cursor
|
||||
func (h *InfoPane) HistorySearchUp() {
|
||||
h.SearchUpHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// HistorySearchDown fetches the next history item beginning with the text
|
||||
// in the infobuffer before cursor
|
||||
func (h *InfoPane) HistorySearchDown() {
|
||||
h.SearchDownHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// Autocomplete begins autocompletion
|
||||
func (h *InfoPane) CommandComplete() {
|
||||
b := h.Buf
|
||||
if b.HasSuggestions {
|
||||
b.CycleAutocomplete(true)
|
||||
return
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(0)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
cmd := string(args[0])
|
||||
|
||||
if h.PromptType == "Command" {
|
||||
if len(args) == 1 {
|
||||
b.Autocomplete(CommandComplete)
|
||||
} else if action, ok := commands[cmd]; ok {
|
||||
if action.completer != nil {
|
||||
b.Autocomplete(action.completer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// by default use filename autocompletion
|
||||
b.Autocomplete(buffer.FileComplete)
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteCommand completes the prompt
|
||||
func (h *InfoPane) ExecuteCommand() {
|
||||
if !h.HasYN {
|
||||
h.DonePrompt(false)
|
||||
}
|
||||
}
|
||||
|
||||
// AbortCommand cancels the prompt
|
||||
func (h *InfoPane) AbortCommand() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
|
||||
// InfoKeyActions contains the list of all possible key actions the infopane could execute
|
||||
var InfoKeyActions = map[string]InfoKeyAction{
|
||||
"HistoryUp": (*InfoPane).HistoryUp,
|
||||
"HistoryDown": (*InfoPane).HistoryDown,
|
||||
"HistorySearchUp": (*InfoPane).HistorySearchUp,
|
||||
"HistorySearchDown": (*InfoPane).HistorySearchDown,
|
||||
"CommandComplete": (*InfoPane).CommandComplete,
|
||||
"ExecuteCommand": (*InfoPane).ExecuteCommand,
|
||||
"AbortCommand": (*InfoPane).AbortCommand,
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
type PaneKeyAction func(Pane) bool
|
||||
type PaneMouseAction func(Pane, *tcell.EventMouse) bool
|
||||
type PaneKeyAnyAction func(Pane, []KeyEvent) bool
|
||||
|
||||
// A KeyTreeNode stores a single node in the KeyTree (trie). The
|
||||
// children are stored as a map, and any node may store a list of
|
||||
// actions (the list will be nil if no actions correspond to a certain
|
||||
// node)
|
||||
type KeyTreeNode struct {
|
||||
children map[Event]*KeyTreeNode
|
||||
|
||||
// Only one of these actions may be active in the current
|
||||
// mode, and only one will be returned. If multiple actions
|
||||
// are active, it is undefined which one will be the one
|
||||
// returned.
|
||||
actions []TreeAction
|
||||
}
|
||||
|
||||
func NewKeyTreeNode() *KeyTreeNode {
|
||||
n := new(KeyTreeNode)
|
||||
n.children = make(map[Event]*KeyTreeNode)
|
||||
n.actions = []TreeAction{}
|
||||
return n
|
||||
}
|
||||
|
||||
// A TreeAction stores an action, and a set of mode constraints for
|
||||
// the action to be active.
|
||||
type TreeAction struct {
|
||||
// only one of these can be non-nil
|
||||
action PaneKeyAction
|
||||
any PaneKeyAnyAction
|
||||
mouse PaneMouseAction
|
||||
|
||||
modes []ModeConstraint
|
||||
}
|
||||
|
||||
// A KeyTree is a data structure for storing keybindings. It maps
|
||||
// key events to actions, and maintains a set of currently enabled
|
||||
// modes, which affects the action that is returned for a key event.
|
||||
// The tree acts like a Trie for Events to handle sequence events.
|
||||
type KeyTree struct {
|
||||
root *KeyTreeNode
|
||||
modes map[string]bool
|
||||
|
||||
cursor KeyTreeCursor
|
||||
}
|
||||
|
||||
// A KeyTreeCursor keeps track of the current location within the
|
||||
// tree, and stores any information from previous events that may
|
||||
// be needed to execute the action (values of wildcard events or
|
||||
// mouse events)
|
||||
type KeyTreeCursor struct {
|
||||
node *KeyTreeNode
|
||||
|
||||
recordedEvents []Event
|
||||
wildcards []KeyEvent
|
||||
mouseInfo *tcell.EventMouse
|
||||
}
|
||||
|
||||
// MakeClosure uses the information stored in a key tree cursor to construct
|
||||
// a PaneKeyAction from a TreeAction (which may have a PaneKeyAction, PaneMouseAction,
|
||||
// or AnyAction)
|
||||
func (k *KeyTreeCursor) MakeClosure(a TreeAction) PaneKeyAction {
|
||||
if a.action != nil {
|
||||
return a.action
|
||||
} else if a.any != nil {
|
||||
return func(p Pane) bool {
|
||||
return a.any(p, k.wildcards)
|
||||
}
|
||||
} else if a.mouse != nil {
|
||||
return func(p Pane) bool {
|
||||
return a.mouse(p, k.mouseInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewKeyTree allocates and returns an empty key tree
|
||||
func NewKeyTree() *KeyTree {
|
||||
root := NewKeyTreeNode()
|
||||
tree := new(KeyTree)
|
||||
|
||||
tree.root = root
|
||||
tree.modes = make(map[string]bool)
|
||||
tree.cursor = KeyTreeCursor{
|
||||
node: root,
|
||||
wildcards: []KeyEvent{},
|
||||
mouseInfo: nil,
|
||||
}
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
// A ModeConstraint specifies that an action can only be executed
|
||||
// while a certain mode is enabled or disabled.
|
||||
type ModeConstraint struct {
|
||||
mode string
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// RegisterKeyBinding registers a PaneKeyAction with an Event.
|
||||
func (k *KeyTree) RegisterKeyBinding(e Event, a PaneKeyAction) {
|
||||
k.registerBinding(e, TreeAction{
|
||||
action: a,
|
||||
any: nil,
|
||||
mouse: nil,
|
||||
modes: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterKeyAnyBinding registers a PaneKeyAnyAction with an Event.
|
||||
// The event should contain an "any" event.
|
||||
func (k *KeyTree) RegisterKeyAnyBinding(e Event, a PaneKeyAnyAction) {
|
||||
k.registerBinding(e, TreeAction{
|
||||
action: nil,
|
||||
any: a,
|
||||
mouse: nil,
|
||||
modes: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterMouseBinding registers a PaneMouseAction with an Event.
|
||||
// The event should contain a mouse event.
|
||||
func (k *KeyTree) RegisterMouseBinding(e Event, a PaneMouseAction) {
|
||||
k.registerBinding(e, TreeAction{
|
||||
action: nil,
|
||||
any: nil,
|
||||
mouse: a,
|
||||
modes: nil,
|
||||
})
|
||||
}
|
||||
|
||||
func (k *KeyTree) registerBinding(e Event, a TreeAction) {
|
||||
switch ev := e.(type) {
|
||||
case KeyEvent, MouseEvent, RawEvent:
|
||||
newNode, ok := k.root.children[e]
|
||||
if !ok {
|
||||
newNode = NewKeyTreeNode()
|
||||
k.root.children[e] = newNode
|
||||
}
|
||||
// newNode.actions = append(newNode.actions, a)
|
||||
newNode.actions = []TreeAction{a}
|
||||
case KeySequenceEvent:
|
||||
n := k.root
|
||||
for _, key := range ev.keys {
|
||||
newNode, ok := n.children[key]
|
||||
if !ok {
|
||||
newNode = NewKeyTreeNode()
|
||||
n.children[key] = newNode
|
||||
}
|
||||
|
||||
n = newNode
|
||||
}
|
||||
// n.actions = append(n.actions, a)
|
||||
n.actions = []TreeAction{a}
|
||||
}
|
||||
}
|
||||
|
||||
// NextEvent returns the action for the current sequence where e is the next
|
||||
// event. Even if the action was registered as a PaneKeyAnyAction or PaneMouseAction,
|
||||
// it will be returned as a PaneKeyAction closure where the appropriate arguments
|
||||
// have been provided.
|
||||
// If no action is associated with the given Event, or mode constraints are not
|
||||
// met for that action, nil is returned.
|
||||
// A boolean is returned to indicate if there is a conflict with this action. A
|
||||
// conflict occurs when there is an active action for this event but there are
|
||||
// bindings associated with further sequences starting with this event. The
|
||||
// calling function can decide what to do about the conflict (e.g. use a
|
||||
// timeout).
|
||||
func (k *KeyTree) NextEvent(e Event, mouse *tcell.EventMouse) (PaneKeyAction, bool) {
|
||||
n := k.cursor.node
|
||||
c, ok := n.children[e]
|
||||
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
more := len(c.children) > 0
|
||||
|
||||
k.cursor.node = c
|
||||
|
||||
k.cursor.recordedEvents = append(k.cursor.recordedEvents, e)
|
||||
|
||||
switch ev := e.(type) {
|
||||
case KeyEvent:
|
||||
if ev.any {
|
||||
k.cursor.wildcards = append(k.cursor.wildcards, ev)
|
||||
}
|
||||
case MouseEvent:
|
||||
k.cursor.mouseInfo = mouse
|
||||
}
|
||||
|
||||
if len(c.actions) > 0 {
|
||||
// check if actions are active
|
||||
for _, a := range c.actions {
|
||||
active := true
|
||||
for _, mc := range a.modes {
|
||||
// if any mode constraint is not met, the action is not active
|
||||
hasMode := k.modes[mc.mode]
|
||||
if hasMode != mc.disabled {
|
||||
active = false
|
||||
}
|
||||
}
|
||||
|
||||
if active {
|
||||
// the first active action to be found is returned
|
||||
return k.cursor.MakeClosure(a), more
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, more
|
||||
}
|
||||
|
||||
// ResetEvents sets the current sequence back to the initial value.
|
||||
func (k *KeyTree) ResetEvents() {
|
||||
k.cursor.node = k.root
|
||||
k.cursor.wildcards = []KeyEvent{}
|
||||
k.cursor.recordedEvents = []Event{}
|
||||
k.cursor.mouseInfo = nil
|
||||
}
|
||||
|
||||
// RecordedEventsStr returns the list of recorded events as a string
|
||||
func (k *KeyTree) RecordedEventsStr() string {
|
||||
buf := &bytes.Buffer{}
|
||||
for _, e := range k.cursor.recordedEvents {
|
||||
buf.WriteString(e.Name())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// DeleteBinding removes any currently active actions associated with the
|
||||
// given event.
|
||||
func (k *KeyTree) DeleteBinding(e Event) {
|
||||
|
||||
}
|
||||
|
||||
// DeleteAllBindings removes all actions associated with the given event,
|
||||
// regardless of whether they are active or not.
|
||||
func (k *KeyTree) DeleteAllBindings(e Event) {
|
||||
|
||||
}
|
||||
|
||||
// SetMode enables or disabled a given mode
|
||||
func (k *KeyTree) SetMode(mode string, en bool) {
|
||||
k.modes[mode] = en
|
||||
}
|
||||
|
||||
// HasMode returns if the given mode is currently active
|
||||
func (k *KeyTree) HasMode(mode string) bool {
|
||||
return k.modes[mode]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
)
|
||||
|
||||
// A Pane is a general interface for a window in the editor.
|
||||
type Pane interface {
|
||||
Handler
|
||||
display.Window
|
||||
ID() uint64
|
||||
SetID(i uint64)
|
||||
Name() string
|
||||
Close()
|
||||
SetTab(t *Tab)
|
||||
Tab() *Tab
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
type RawPane struct {
|
||||
*BufPane
|
||||
}
|
||||
|
||||
func NewRawPaneFromWin(b *buffer.Buffer, win display.BWindow, tab *Tab) *RawPane {
|
||||
rh := new(RawPane)
|
||||
rh.BufPane = NewBufPane(b, win, tab)
|
||||
|
||||
return rh
|
||||
}
|
||||
|
||||
func NewRawPane(tab *Tab) *RawPane {
|
||||
b := buffer.NewBufferFromString("", "", buffer.BTRaw)
|
||||
b.SetName("Raw event viewer")
|
||||
|
||||
w := display.NewBufWindow(0, 0, 0, 0, b)
|
||||
|
||||
return NewRawPaneFromWin(b, w, tab)
|
||||
}
|
||||
|
||||
func (h *RawPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if e.Key() == tcell.KeyCtrlQ {
|
||||
h.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
h.Buf.Insert(h.Cursor.Loc, reflect.TypeOf(event).String()[7:])
|
||||
|
||||
e, err := ConstructEvent(event)
|
||||
if err == nil {
|
||||
h.Buf.Insert(h.Cursor.Loc, fmt.Sprintf(": %s", e.Name()))
|
||||
}
|
||||
|
||||
h.Buf.Insert(h.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq()))
|
||||
|
||||
h.Relocate()
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/buffer"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
ulua "github.com/micro-editor/micro/v2/internal/lua"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/views"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
// The TabList is a list of tabs and a window to display the tab bar
|
||||
// at the top of the screen
|
||||
type TabList struct {
|
||||
*display.TabWindow
|
||||
List []*Tab
|
||||
}
|
||||
|
||||
// NewTabList creates a TabList from a list of buffers by creating a Tab
|
||||
// for each buffer
|
||||
func NewTabList(bufs []*buffer.Buffer) *TabList {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
tl := new(TabList)
|
||||
tl.List = make([]*Tab, len(bufs))
|
||||
if len(bufs) > 1 {
|
||||
for i, b := range bufs {
|
||||
tl.List[i] = NewTabFromBuffer(0, 1, w, h-1-iOffset, b)
|
||||
}
|
||||
} else {
|
||||
tl.List[0] = NewTabFromBuffer(0, 0, w, h-iOffset, bufs[0])
|
||||
}
|
||||
tl.TabWindow = display.NewTabWindow(w, 0)
|
||||
tl.Names = make([]string, len(bufs))
|
||||
|
||||
return tl
|
||||
}
|
||||
|
||||
// UpdateNames makes sure that the list of names the tab window has access to is
|
||||
// correct
|
||||
func (t *TabList) UpdateNames() {
|
||||
t.Names = t.Names[:0]
|
||||
for _, p := range t.List {
|
||||
t.Names = append(t.Names, p.Panes[p.active].Name())
|
||||
}
|
||||
}
|
||||
|
||||
// AddTab adds a new tab to this TabList
|
||||
func (t *TabList) AddTab(p *Tab) {
|
||||
t.List = append(t.List, p)
|
||||
t.Resize()
|
||||
t.UpdateNames()
|
||||
}
|
||||
|
||||
// RemoveTab removes a tab with the given id from the TabList
|
||||
func (t *TabList) RemoveTab(id uint64) {
|
||||
for i, p := range t.List {
|
||||
if len(p.Panes) == 0 {
|
||||
continue
|
||||
}
|
||||
if p.Panes[0].ID() == id {
|
||||
copy(t.List[i:], t.List[i+1:])
|
||||
t.List[len(t.List)-1] = nil
|
||||
t.List = t.List[:len(t.List)-1]
|
||||
if t.Active() >= len(t.List) {
|
||||
t.SetActive(len(t.List) - 1)
|
||||
}
|
||||
t.Resize()
|
||||
t.UpdateNames()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize resizes all elements within the tab list
|
||||
// One thing to note is that when there is only 1 tab
|
||||
// the tab bar should not be drawn so resizing must take
|
||||
// that into account
|
||||
func (t *TabList) Resize() {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
InfoBar.Resize(w, h-1)
|
||||
if len(t.List) > 1 {
|
||||
for _, p := range t.List {
|
||||
p.Y = 1
|
||||
p.Node.Resize(w, h-1-iOffset)
|
||||
p.Resize()
|
||||
}
|
||||
} else if len(t.List) == 1 {
|
||||
t.List[0].Y = 0
|
||||
t.List[0].Node.Resize(w, h-iOffset)
|
||||
t.List[0].Resize()
|
||||
}
|
||||
t.TabWindow.Resize(w, h)
|
||||
}
|
||||
|
||||
// HandleEvent checks for a resize event or a mouse event on the tab bar
|
||||
// otherwise it will forward the event to the currently active tab
|
||||
func (t *TabList) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
t.Resize()
|
||||
case *tcell.EventMouse:
|
||||
mx, my := e.Position()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
if my == t.Y && len(t.List) > 1 {
|
||||
if mx == 0 {
|
||||
t.Scroll(-4)
|
||||
} else if mx == t.Width-1 {
|
||||
t.Scroll(4)
|
||||
} else {
|
||||
ind := t.LocFromVisual(buffer.Loc{mx, my})
|
||||
if ind != -1 {
|
||||
t.SetActive(ind)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
if t.List[t.Active()].release {
|
||||
// Mouse release received, while already released
|
||||
t.ResetMouse()
|
||||
return
|
||||
}
|
||||
case tcell.WheelUp:
|
||||
if my == t.Y && len(t.List) > 1 {
|
||||
t.Scroll(4)
|
||||
return
|
||||
}
|
||||
case tcell.WheelDown:
|
||||
if my == t.Y && len(t.List) > 1 {
|
||||
t.Scroll(-4)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.List[t.Active()].HandleEvent(event)
|
||||
}
|
||||
|
||||
// Display updates the names and then displays the tab bar
|
||||
func (t *TabList) Display() {
|
||||
t.UpdateNames()
|
||||
if len(t.List) > 1 {
|
||||
t.TabWindow.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
|
||||
var Tabs *TabList
|
||||
|
||||
func InitTabs(bufs []*buffer.Buffer) {
|
||||
multiopen := config.GetGlobalOption("multiopen").(string)
|
||||
if multiopen == "tab" {
|
||||
Tabs = NewTabList(bufs)
|
||||
} else {
|
||||
Tabs = NewTabList(bufs[:1])
|
||||
for _, b := range bufs[1:] {
|
||||
if multiopen == "vsplit" {
|
||||
MainTab().CurPane().VSplitBuf(b)
|
||||
} else { // default hsplit
|
||||
MainTab().CurPane().HSplitBuf(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.RestartCallback = Tabs.ResetMouse
|
||||
}
|
||||
|
||||
func MainTab() *Tab {
|
||||
return Tabs.List[Tabs.Active()]
|
||||
}
|
||||
|
||||
// A Tab represents a single tab
|
||||
// It consists of a list of edit panes (the open buffers),
|
||||
// a split tree (stored as just the root node), and a uiwindow
|
||||
// to display the UI elements like the borders between splits
|
||||
type Tab struct {
|
||||
*views.Node
|
||||
*display.UIWindow
|
||||
|
||||
isActive bool
|
||||
|
||||
Panes []Pane
|
||||
active int
|
||||
|
||||
resizing *views.Node // node currently being resized
|
||||
// captures whether the mouse is released
|
||||
release bool
|
||||
}
|
||||
|
||||
// NewTabFromBuffer creates a new tab from the given buffer
|
||||
func NewTabFromBuffer(x, y, width, height int, b *buffer.Buffer) *Tab {
|
||||
t := new(Tab)
|
||||
t.Node = views.NewRoot(x, y, width, height)
|
||||
t.UIWindow = display.NewUIWindow(t.Node)
|
||||
t.release = true
|
||||
|
||||
e := NewBufPaneFromBuf(b, t)
|
||||
e.SetID(t.ID())
|
||||
|
||||
t.Panes = append(t.Panes, e)
|
||||
return t
|
||||
}
|
||||
|
||||
func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
|
||||
t := new(Tab)
|
||||
t.Node = views.NewRoot(x, y, width, height)
|
||||
t.UIWindow = display.NewUIWindow(t.Node)
|
||||
t.release = true
|
||||
pane.SetTab(t)
|
||||
pane.SetID(t.ID())
|
||||
|
||||
t.Panes = append(t.Panes, pane)
|
||||
return t
|
||||
}
|
||||
|
||||
// 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
|
||||
// is interacting with the UI (resizing splits) then the event is consumed here
|
||||
// If the event is a mouse press event in a pane, that pane will become active
|
||||
// and get the event
|
||||
func (t *Tab) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
mx, my := e.Position()
|
||||
btn := e.Buttons()
|
||||
switch {
|
||||
case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone:
|
||||
// button press or drag
|
||||
wasReleased := t.release
|
||||
t.release = false
|
||||
|
||||
if btn == tcell.Button1 {
|
||||
if t.resizing != nil {
|
||||
var size int
|
||||
if t.resizing.Kind == views.STVert {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wasReleased {
|
||||
for i, p := range t.Panes {
|
||||
v := p.GetView()
|
||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||
if inpane {
|
||||
t.SetActive(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case btn == tcell.ButtonNone:
|
||||
// button release
|
||||
t.release = true
|
||||
if t.resizing != nil {
|
||||
t.resizing = nil
|
||||
return
|
||||
}
|
||||
default:
|
||||
// wheel move
|
||||
for _, p := range t.Panes {
|
||||
v := p.GetView()
|
||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||
if inpane {
|
||||
p.HandleEvent(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
t.Panes[t.active].HandleEvent(event)
|
||||
}
|
||||
|
||||
// SetActive changes the currently active pane to the specified index
|
||||
func (t *Tab) SetActive(i int) {
|
||||
t.active = i
|
||||
for j, p := range t.Panes {
|
||||
if j == i {
|
||||
p.SetActive(true)
|
||||
} else {
|
||||
p.SetActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddPane adds a pane at a given index
|
||||
func (t *Tab) AddPane(pane Pane, i int) {
|
||||
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
|
||||
func (t *Tab) GetPane(splitid uint64) int {
|
||||
for i, p := range t.Panes {
|
||||
if p.ID() == splitid {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Remove pane removes the pane with the given index
|
||||
func (t *Tab) RemovePane(i int) {
|
||||
copy(t.Panes[i:], t.Panes[i+1:])
|
||||
t.Panes[len(t.Panes)-1] = nil
|
||||
t.Panes = t.Panes[:len(t.Panes)-1]
|
||||
}
|
||||
|
||||
// Resize resizes all panes according to their corresponding split nodes
|
||||
func (t *Tab) Resize() {
|
||||
for _, p := range t.Panes {
|
||||
n := t.GetNode(p.ID())
|
||||
pv := p.GetView()
|
||||
offset := 0
|
||||
if n.X != 0 {
|
||||
offset = 1
|
||||
}
|
||||
pv.X, pv.Y = n.X+offset, n.Y
|
||||
p.SetView(pv)
|
||||
p.Resize(n.W-offset, n.H)
|
||||
}
|
||||
}
|
||||
|
||||
// CurPane returns the currently active pane
|
||||
func (t *Tab) CurPane() *BufPane {
|
||||
p, ok := t.Panes[t.active].(*BufPane)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/micro-editor/micro/v2/internal/shell"
|
||||
)
|
||||
|
||||
// TermEmuSupported is a constant that marks if the terminal emulator is supported
|
||||
const TermEmuSupported = true
|
||||
|
||||
// RunTermEmulator starts a terminal emulator from a bufpane with the given input (command)
|
||||
// if wait is true it will wait for the user to exit by pressing enter once the executable has terminated
|
||||
// if getOutput is true it will redirect the stdout of the process to a pipe which will be passed to the
|
||||
// callback which is a function that takes a string and a list of optional user arguments
|
||||
func RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool, callback func(out string, userargs []any), userargs []any) error {
|
||||
args, err := shellquote.Split(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := new(shell.Terminal)
|
||||
err = t.Start(args, getOutput, wait, callback, userargs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.AddTab()
|
||||
id := MainTab().Panes[0].ID()
|
||||
|
||||
v := h.GetView()
|
||||
|
||||
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MainTab().Panes[0] = tp
|
||||
MainTab().SetActive(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//go:build plan9 || nacl || windows
|
||||
|
||||
package action
|
||||
|
||||
import "errors"
|
||||
|
||||
// TermEmuSupported is a constant that marks if the terminal emulator is supported
|
||||
const TermEmuSupported = false
|
||||
|
||||
// RunTermEmulator returns an error for unsupported systems (non-unix systems
|
||||
func RunTermEmulator(input string, wait bool, getOutput bool, callback func(out string, userargs []any), userargs []any) error {
|
||||
return errors.New("Unsupported operating system")
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/clipboard"
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/display"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/shell"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
"github.com/micro-editor/terminal"
|
||||
)
|
||||
|
||||
type TermKeyAction func(*TermPane)
|
||||
|
||||
var TermBindings *KeyTree
|
||||
|
||||
func init() {
|
||||
TermBindings = NewKeyTree()
|
||||
}
|
||||
|
||||
func TermKeyActionGeneral(a TermKeyAction) PaneKeyAction {
|
||||
return func(p Pane) bool {
|
||||
a(p.(*TermPane))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func TermMapEvent(k Event, action string) {
|
||||
config.Bindings["terminal"][k.Name()] = action
|
||||
|
||||
switch e := k.(type) {
|
||||
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||
termMapKey(e, action)
|
||||
case MouseEvent:
|
||||
termMapMouse(e, action)
|
||||
}
|
||||
}
|
||||
|
||||
func termMapKey(k Event, action string) {
|
||||
if f, ok := TermKeyActions[action]; ok {
|
||||
TermBindings.RegisterKeyBinding(k, TermKeyActionGeneral(f))
|
||||
}
|
||||
}
|
||||
|
||||
func termMapMouse(k MouseEvent, action string) {
|
||||
// TODO: map mouse
|
||||
termMapKey(k, action)
|
||||
}
|
||||
|
||||
type TermPane struct {
|
||||
*shell.Terminal
|
||||
display.Window
|
||||
|
||||
mouseReleased bool
|
||||
id uint64
|
||||
tab *Tab
|
||||
}
|
||||
|
||||
func NewTermPane(x, y, w, h int, t *shell.Terminal, id uint64, tab *Tab) (*TermPane, error) {
|
||||
if !TermEmuSupported {
|
||||
return nil, errors.New("Terminal emulator is not supported on this system")
|
||||
}
|
||||
|
||||
th := new(TermPane)
|
||||
th.Terminal = t
|
||||
th.id = id
|
||||
th.mouseReleased = true
|
||||
th.Window = display.NewTermWindow(x, y, w, h, t)
|
||||
th.tab = tab
|
||||
return th, nil
|
||||
}
|
||||
|
||||
func (t *TermPane) ID() uint64 {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *TermPane) SetID(i uint64) {
|
||||
t.id = i
|
||||
}
|
||||
|
||||
func (t *TermPane) Name() string {
|
||||
return t.Terminal.Name()
|
||||
}
|
||||
|
||||
func (t *TermPane) SetTab(tab *Tab) {
|
||||
t.tab = tab
|
||||
}
|
||||
|
||||
func (t *TermPane) Tab() *Tab {
|
||||
return t.tab
|
||||
}
|
||||
|
||||
func (t *TermPane) Close() {}
|
||||
|
||||
// Quit closes this termpane
|
||||
func (t *TermPane) Quit() {
|
||||
t.Close()
|
||||
if len(MainTab().Panes) > 1 {
|
||||
t.Unsplit()
|
||||
} else if len(Tabs.List) > 1 {
|
||||
Tabs.RemoveTab(t.id)
|
||||
} else {
|
||||
screen.Screen.Fini()
|
||||
InfoBar.Close()
|
||||
runtime.Goexit()
|
||||
}
|
||||
}
|
||||
|
||||
// Unsplit removes this split
|
||||
func (t *TermPane) Unsplit() {
|
||||
n := MainTab().GetNode(t.id)
|
||||
n.Unsplit()
|
||||
|
||||
MainTab().RemovePane(MainTab().GetPane(t.id))
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
}
|
||||
|
||||
// HandleEvent handles a tcell event by forwarding it to the terminal emulator
|
||||
// If the event is a mouse event and the program running in the emulator
|
||||
// does not have mouse support, the emulator will support selections and
|
||||
// copy-paste
|
||||
func (t *TermPane) HandleEvent(event tcell.Event) {
|
||||
if e, ok := event.(*tcell.EventKey); ok {
|
||||
ke := keyEvent(e)
|
||||
action, more := TermBindings.NextEvent(ke, nil)
|
||||
|
||||
if !more {
|
||||
if action != nil {
|
||||
action(t)
|
||||
TermBindings.ResetEvents()
|
||||
return
|
||||
}
|
||||
TermBindings.ResetEvents()
|
||||
}
|
||||
|
||||
if more {
|
||||
return
|
||||
}
|
||||
|
||||
if t.Status == shell.TTDone {
|
||||
switch e.Key() {
|
||||
case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter:
|
||||
t.Close()
|
||||
t.Quit()
|
||||
default:
|
||||
}
|
||||
}
|
||||
if e.Key() == tcell.KeyCtrlC && t.HasSelection() {
|
||||
clipboard.Write(t.GetSelection(t.GetView().Width), clipboard.ClipboardReg)
|
||||
InfoBar.Message("Copied selection to clipboard")
|
||||
} else if t.Status != shell.TTDone {
|
||||
t.WriteString(event.EscSeq())
|
||||
}
|
||||
} else if _, ok := event.(*tcell.EventPaste); ok {
|
||||
if t.Status != shell.TTDone {
|
||||
t.WriteString(event.EscSeq())
|
||||
}
|
||||
} else if e, ok := event.(*tcell.EventMouse); !ok || t.State.Mode(terminal.ModeMouseMask) {
|
||||
// t.WriteString(event.EscSeq())
|
||||
} else {
|
||||
x, y := e.Position()
|
||||
v := t.GetView()
|
||||
x -= v.X
|
||||
y -= v.Y
|
||||
|
||||
if e.Buttons() == tcell.Button1 {
|
||||
if !t.mouseReleased {
|
||||
// drag
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
} else {
|
||||
t.Selection[0].X = x
|
||||
t.Selection[0].Y = y
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
}
|
||||
|
||||
t.mouseReleased = false
|
||||
} else if e.Buttons() == tcell.ButtonNone {
|
||||
if !t.mouseReleased {
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
}
|
||||
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 {
|
||||
t.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
// Exit closes the termpane
|
||||
func (t *TermPane) Exit() {
|
||||
t.Terminal.Close()
|
||||
t.Quit()
|
||||
}
|
||||
|
||||
// CommandMode opens the termpane's command mode
|
||||
func (t *TermPane) CommandMode() {
|
||||
InfoBar.Prompt("> ", "", "TerminalCommand", nil, func(resp string, canceled bool) {
|
||||
if !canceled {
|
||||
t.HandleCommand(resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NextSplit moves to the next split
|
||||
func (t *TermPane) NextSplit() {
|
||||
a := t.tab.active
|
||||
if a < len(t.tab.Panes)-1 {
|
||||
a++
|
||||
} else {
|
||||
a = 0
|
||||
}
|
||||
|
||||
t.tab.SetActive(a)
|
||||
}
|
||||
|
||||
// HandleCommand handles a command for the term pane
|
||||
func (t *TermPane) HandleCommand(input string) {
|
||||
InfoBar.Error("Commands are unsupported in term for now")
|
||||
}
|
||||
|
||||
// TermKeyActions contains the list of all possible key actions the termpane could execute
|
||||
var TermKeyActions = map[string]TermKeyAction{
|
||||
"Exit": (*TermPane).Exit,
|
||||
"CommandMode": (*TermPane).CommandMode,
|
||||
"NextSplit": (*TermPane).NextSplit,
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
// A Completer is a function that takes a buffer and returns info
|
||||
// describing what autocompletions should be inserted at the current
|
||||
// cursor location
|
||||
// It returns a list of string suggestions which will be inserted at
|
||||
// the current cursor location if selected as well as a list of
|
||||
// suggestion names which can be displayed in an autocomplete box or
|
||||
// other UI element
|
||||
type Completer func(*Buffer) ([]string, []string)
|
||||
|
||||
func (b *Buffer) GetSuggestions() {
|
||||
|
||||
}
|
||||
|
||||
// Autocomplete starts the autocomplete process
|
||||
func (b *Buffer) Autocomplete(c Completer) bool {
|
||||
b.Completions, b.Suggestions = c(b)
|
||||
if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 {
|
||||
return false
|
||||
}
|
||||
b.CurSuggestion = -1
|
||||
b.CycleAutocomplete(true)
|
||||
return true
|
||||
}
|
||||
|
||||
// CycleAutocomplete moves to the next suggestion
|
||||
func (b *Buffer) CycleAutocomplete(forward bool) {
|
||||
prevSuggestion := b.CurSuggestion
|
||||
|
||||
if forward {
|
||||
b.CurSuggestion++
|
||||
} else {
|
||||
b.CurSuggestion--
|
||||
}
|
||||
if b.CurSuggestion >= len(b.Suggestions) {
|
||||
b.CurSuggestion = 0
|
||||
} else if b.CurSuggestion < 0 {
|
||||
b.CurSuggestion = len(b.Suggestions) - 1
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
start := c.Loc
|
||||
end := c.Loc
|
||||
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
|
||||
start = end.Move(-util.CharacterCountInString(b.Completions[prevSuggestion]), b)
|
||||
}
|
||||
|
||||
b.Replace(start, end, b.Completions[b.CurSuggestion])
|
||||
if len(b.Suggestions) > 1 {
|
||||
b.HasSuggestions = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetWord gets the most recent word separated by any separator
|
||||
// (whitespace, punctuation, any non alphanumeric character)
|
||||
func (b *Buffer) GetWord() ([]byte, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
if c.X == 0 || util.IsWhitespace(b.RuneAt(c.Loc.Move(-1, b))) {
|
||||
return []byte{}, -1
|
||||
}
|
||||
|
||||
if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) {
|
||||
return []byte{}, c.X
|
||||
}
|
||||
|
||||
args := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||
input := args[len(args)-1]
|
||||
return input, c.X - util.CharacterCount(input)
|
||||
}
|
||||
|
||||
// GetArg gets the most recent word (separated by ' ' only)
|
||||
func (b *Buffer) GetArg() (string, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
input := string(args[len(args)-1])
|
||||
argstart := 0
|
||||
for i, a := range args {
|
||||
if i == len(args)-1 {
|
||||
break
|
||||
}
|
||||
argstart += util.CharacterCount(a) + 1
|
||||
}
|
||||
|
||||
return input, argstart
|
||||
}
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
sep := string(os.PathSeparator)
|
||||
dirs := strings.Split(input, sep)
|
||||
|
||||
var files []fs.DirEntry
|
||||
var err error
|
||||
if len(dirs) > 1 {
|
||||
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
||||
|
||||
directories, _ = util.ReplaceHome(directories)
|
||||
files, err = os.ReadDir(directories)
|
||||
} else {
|
||||
files, err = os.ReadDir(".")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() {
|
||||
name += sep
|
||||
}
|
||||
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
var complete string
|
||||
if len(dirs) > 1 {
|
||||
complete = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[i]
|
||||
} else {
|
||||
complete = suggestions[i]
|
||||
}
|
||||
completions[i] = util.SliceEndStr(complete, c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// BufferComplete autocompletes based on previous words in the buffer
|
||||
func BufferComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := b.GetWord()
|
||||
|
||||
if argstart == -1 {
|
||||
return []string{}, []string{}
|
||||
}
|
||||
|
||||
inputLen := util.CharacterCount(input)
|
||||
|
||||
suggestionsSet := make(map[string]struct{})
|
||||
|
||||
var suggestions []string
|
||||
for i := c.Y; i >= 0; i-- {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
if _, ok := suggestionsSet[strw]; !ok {
|
||||
suggestionsSet[strw] = struct{}{}
|
||||
suggestions = append(suggestions, strw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := c.Y + 1; i < b.LinesNum(); i++ {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
if _, ok := suggestionsSet[strw]; !ok {
|
||||
suggestionsSet[strw] = struct{}{}
|
||||
suggestions = append(suggestions, strw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(suggestions) > 1 {
|
||||
suggestions = append(suggestions, string(input))
|
||||
}
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
const BackupMsg = `A backup was detected for:
|
||||
|
||||
%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
|
||||
|
||||
* 'recover' will apply the backup as unsaved changes to the current buffer.
|
||||
When the buffer is closed, the backup will be removed.
|
||||
* 'ignore' will ignore the backup, discarding its changes. The backup file
|
||||
will be removed.
|
||||
* 'abort' will abort the open operation, and instead open an empty buffer.
|
||||
|
||||
Options: [r]ecover, [i]gnore, [a]bort: `
|
||||
|
||||
const backupSeconds = 8
|
||||
|
||||
type backupRequestType int
|
||||
|
||||
const (
|
||||
backupCreate = iota
|
||||
backupRemove
|
||||
)
|
||||
|
||||
type backupRequest struct {
|
||||
buf *SharedBuffer
|
||||
reqType backupRequestType
|
||||
}
|
||||
|
||||
var requestedBackups map[*SharedBuffer]bool
|
||||
|
||||
func init() {
|
||||
requestedBackups = make(map[*SharedBuffer]bool)
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) RequestBackup() {
|
||||
backupRequestChan <- backupRequest{buf: b, reqType: backupCreate}
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) CancelBackup() {
|
||||
backupRequestChan <- backupRequest{buf: b, reqType: backupRemove}
|
||||
}
|
||||
|
||||
func handleBackupRequest(br backupRequest) {
|
||||
switch br.reqType {
|
||||
case backupCreate:
|
||||
// schedule periodic backup
|
||||
requestedBackups[br.buf] = true
|
||||
case backupRemove:
|
||||
br.buf.RemoveBackup()
|
||||
delete(requestedBackups, br.buf)
|
||||
}
|
||||
}
|
||||
|
||||
func periodicBackup() {
|
||||
for buf := range requestedBackups {
|
||||
err := buf.Backup()
|
||||
if err == nil {
|
||||
delete(requestedBackups, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) backupDir() string {
|
||||
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
|
||||
if backupdir == "" || err != nil {
|
||||
backupdir = filepath.Join(config.ConfigDir, "backups")
|
||||
}
|
||||
return backupdir
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) keepBackup() bool {
|
||||
return b.forceKeepBackup || b.Settings["permbackup"].(bool)
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) writeBackup(path string) (string, string, error) {
|
||||
backupdir := b.backupDir()
|
||||
if _, err := os.Stat(backupdir); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return "", "", err
|
||||
}
|
||||
if err = os.Mkdir(backupdir, os.ModePerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
name, resolveName := util.DetermineEscapePath(backupdir, path)
|
||||
tmp := name + util.BackupSuffix
|
||||
|
||||
_, err := b.overwriteFile(tmp)
|
||||
if err != nil {
|
||||
os.Remove(tmp)
|
||||
return name, resolveName, err
|
||||
}
|
||||
err = os.Rename(tmp, name)
|
||||
if err != nil {
|
||||
os.Remove(tmp)
|
||||
return name, resolveName, err
|
||||
}
|
||||
|
||||
if resolveName != "" {
|
||||
err = util.SafeWrite(resolveName, []byte(path), true)
|
||||
if err != nil {
|
||||
return name, resolveName, err
|
||||
}
|
||||
}
|
||||
|
||||
return name, resolveName, nil
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) removeBackup(path string, resolveName string) {
|
||||
os.Remove(path)
|
||||
if resolveName != "" {
|
||||
os.Remove(resolveName)
|
||||
}
|
||||
}
|
||||
|
||||
// Backup saves the buffer to the backups directory
|
||||
func (b *SharedBuffer) Backup() error {
|
||||
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _, err := b.writeBackup(b.AbsPath)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveBackup removes any backup file associated with this buffer
|
||||
func (b *SharedBuffer) RemoveBackup() {
|
||||
if b.keepBackup() || b.Path == "" || b.Type != BTDefault {
|
||||
return
|
||||
}
|
||||
f, resolveName := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
|
||||
b.removeBackup(f, resolveName)
|
||||
}
|
||||
|
||||
// ApplyBackup applies the corresponding backup file to this buffer (if one exists)
|
||||
// Returns true if a backup was applied
|
||||
func (b *SharedBuffer) ApplyBackup(fsize int64) (bool, bool) {
|
||||
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
|
||||
backupfile, resolveName := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
|
||||
if info, err := os.Stat(backupfile); err == nil {
|
||||
backup, err := os.Open(backupfile)
|
||||
if err == nil {
|
||||
defer backup.Close()
|
||||
t := info.ModTime()
|
||||
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)
|
||||
|
||||
if choice%3 == 0 {
|
||||
// recover
|
||||
b.LineArray = NewLineArray(uint64(fsize), FFAuto, backup)
|
||||
b.setModified()
|
||||
return true, true
|
||||
} else if choice%3 == 1 {
|
||||
// delete
|
||||
b.removeBackup(backupfile, resolveName)
|
||||
} else if choice%3 == 2 {
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,323 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
ulua "github.com/micro-editor/micro/v2/internal/lua"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type operation struct {
|
||||
start Loc
|
||||
end Loc
|
||||
text []string
|
||||
}
|
||||
|
||||
func init() {
|
||||
ulua.L = lua.NewState()
|
||||
config.InitRuntimeFiles(false)
|
||||
config.InitGlobalSettings()
|
||||
config.GlobalSettings["backup"] = false
|
||||
config.GlobalSettings["fastdirty"] = true
|
||||
}
|
||||
|
||||
func check(t *testing.T, before []string, operations []operation, after []string) {
|
||||
assert := assert.New(t)
|
||||
|
||||
b := NewBufferFromString(strings.Join(before, "\n"), "", BTDefault)
|
||||
|
||||
assert.NotEqual("", b.GetName())
|
||||
assert.Equal(false, b.ExternallyModified())
|
||||
assert.Equal(false, b.Modified())
|
||||
assert.Equal(1, b.NumCursors())
|
||||
|
||||
checkText := func(lines []string) {
|
||||
assert.Equal([]byte(strings.Join(lines, "\n")), b.Bytes())
|
||||
assert.Equal(len(lines), b.LinesNum())
|
||||
for i, s := range lines {
|
||||
assert.Equal(s, b.Line(i))
|
||||
assert.Equal([]byte(s), b.LineBytes(i))
|
||||
}
|
||||
}
|
||||
|
||||
checkText(before)
|
||||
|
||||
var cursors []*Cursor
|
||||
|
||||
for _, op := range operations {
|
||||
cursor := NewCursor(b, op.start)
|
||||
cursor.SetSelectionStart(op.start)
|
||||
cursor.SetSelectionEnd(op.end)
|
||||
b.AddCursor(cursor)
|
||||
cursors = append(cursors, cursor)
|
||||
}
|
||||
|
||||
assert.Equal(1+len(operations), b.NumCursors())
|
||||
|
||||
for i, op := range operations {
|
||||
cursor := cursors[i]
|
||||
b.SetCurCursor(cursor.Num)
|
||||
cursor.DeleteSelection()
|
||||
b.Insert(cursor.Loc, strings.Join(op.text, "\n"))
|
||||
}
|
||||
|
||||
checkText(after)
|
||||
|
||||
// must have exactly two events per operation (delete and insert)
|
||||
for range operations {
|
||||
b.UndoOneEvent()
|
||||
b.UndoOneEvent()
|
||||
}
|
||||
|
||||
checkText(before)
|
||||
|
||||
for i, op := range operations {
|
||||
cursor := cursors[i]
|
||||
if op.start == op.end {
|
||||
assert.Equal(op.start, cursor.Loc)
|
||||
} else {
|
||||
assert.Equal(op.start, cursor.CurSelection[0])
|
||||
assert.Equal(op.end, cursor.CurSelection[1])
|
||||
}
|
||||
}
|
||||
|
||||
for range operations {
|
||||
b.RedoOneEvent()
|
||||
b.RedoOneEvent()
|
||||
}
|
||||
|
||||
checkText(after)
|
||||
|
||||
b.Close()
|
||||
}
|
||||
|
||||
const maxLineLength = 200
|
||||
|
||||
var alphabet = []rune(" abcdeäم📚")
|
||||
|
||||
func randomString(length int) string {
|
||||
runes := make([]rune, length)
|
||||
for i := range runes {
|
||||
runes[i] = alphabet[rand.Intn(len(alphabet))]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func randomText(nLines int) string {
|
||||
lines := make([]string, nLines)
|
||||
for i := range lines {
|
||||
lines[i] = randomString(rand.Intn(maxLineLength + 1))
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func benchCreateAndClose(testingB *testing.B, nLines int) {
|
||||
rand.Seed(int64(nLines))
|
||||
|
||||
text := randomText(nLines)
|
||||
|
||||
testingB.ResetTimer()
|
||||
|
||||
for i := 0; i < testingB.N; i++ {
|
||||
b := NewBufferFromString(text, "", BTDefault)
|
||||
b.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func benchRead(testingB *testing.B, nLines int) {
|
||||
rand.Seed(int64(nLines))
|
||||
|
||||
b := NewBufferFromString(randomText(nLines), "", BTDefault)
|
||||
|
||||
testingB.ResetTimer()
|
||||
|
||||
for i := 0; i < testingB.N; i++ {
|
||||
b.Bytes()
|
||||
for j := 0; j < b.LinesNum(); j++ {
|
||||
b.Line(j)
|
||||
b.LineBytes(j)
|
||||
}
|
||||
}
|
||||
|
||||
testingB.StopTimer()
|
||||
|
||||
b.Close()
|
||||
}
|
||||
|
||||
func benchEdit(testingB *testing.B, nLines, nCursors int) {
|
||||
rand.Seed(int64(nLines + nCursors))
|
||||
|
||||
b := NewBufferFromString(randomText(nLines), "", BTDefault)
|
||||
|
||||
regionSize := nLines / nCursors
|
||||
|
||||
operations := make([]operation, nCursors)
|
||||
for i := range operations {
|
||||
startLine := (i * regionSize) + rand.Intn(regionSize-5)
|
||||
startColumn := rand.Intn(util.CharacterCountInString(b.Line(startLine)) + 1)
|
||||
endLine := startLine + 1 + rand.Intn(5)
|
||||
endColumn := rand.Intn(util.CharacterCountInString(b.Line(endLine)) + 1)
|
||||
|
||||
operations[i] = operation{
|
||||
start: Loc{startColumn, startLine},
|
||||
end: Loc{endColumn, endLine},
|
||||
text: []string{randomText(2 + rand.Intn(4))},
|
||||
}
|
||||
}
|
||||
|
||||
testingB.ResetTimer()
|
||||
|
||||
for i := 0; i < testingB.N; i++ {
|
||||
b.SetCursors([]*Cursor{})
|
||||
|
||||
var cursors []*Cursor
|
||||
|
||||
for _, op := range operations {
|
||||
cursor := NewCursor(b, op.start)
|
||||
cursor.SetSelectionStart(op.start)
|
||||
cursor.SetSelectionEnd(op.end)
|
||||
b.AddCursor(cursor)
|
||||
cursors = append(cursors, cursor)
|
||||
}
|
||||
|
||||
for j, op := range operations {
|
||||
cursor := cursors[j]
|
||||
b.SetCurCursor(cursor.Num)
|
||||
cursor.DeleteSelection()
|
||||
b.Insert(cursor.Loc, op.text[0])
|
||||
}
|
||||
|
||||
for b.UndoStack.Peek() != nil {
|
||||
b.UndoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
testingB.StopTimer()
|
||||
|
||||
b.Close()
|
||||
}
|
||||
|
||||
func BenchmarkCreateAndClose10Lines(b *testing.B) {
|
||||
benchCreateAndClose(b, 10)
|
||||
}
|
||||
|
||||
func BenchmarkCreateAndClose100Lines(b *testing.B) {
|
||||
benchCreateAndClose(b, 100)
|
||||
}
|
||||
|
||||
func BenchmarkCreateAndClose1000Lines(b *testing.B) {
|
||||
benchCreateAndClose(b, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkCreateAndClose10000Lines(b *testing.B) {
|
||||
benchCreateAndClose(b, 10000)
|
||||
}
|
||||
|
||||
func BenchmarkCreateAndClose100000Lines(b *testing.B) {
|
||||
benchCreateAndClose(b, 100000)
|
||||
}
|
||||
|
||||
func BenchmarkCreateAndClose1000000Lines(b *testing.B) {
|
||||
benchCreateAndClose(b, 1000000)
|
||||
}
|
||||
|
||||
func BenchmarkRead10Lines(b *testing.B) {
|
||||
benchRead(b, 10)
|
||||
}
|
||||
|
||||
func BenchmarkRead100Lines(b *testing.B) {
|
||||
benchRead(b, 100)
|
||||
}
|
||||
|
||||
func BenchmarkRead1000Lines(b *testing.B) {
|
||||
benchRead(b, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkRead10000Lines(b *testing.B) {
|
||||
benchRead(b, 10000)
|
||||
}
|
||||
|
||||
func BenchmarkRead100000Lines(b *testing.B) {
|
||||
benchRead(b, 100000)
|
||||
}
|
||||
|
||||
func BenchmarkRead1000000Lines(b *testing.B) {
|
||||
benchRead(b, 1000000)
|
||||
}
|
||||
|
||||
func BenchmarkEdit10Lines1Cursor(b *testing.B) {
|
||||
benchEdit(b, 10, 1)
|
||||
}
|
||||
|
||||
func BenchmarkEdit100Lines1Cursor(b *testing.B) {
|
||||
benchEdit(b, 100, 1)
|
||||
}
|
||||
|
||||
func BenchmarkEdit100Lines10Cursors(b *testing.B) {
|
||||
benchEdit(b, 100, 10)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000Lines1Cursor(b *testing.B) {
|
||||
benchEdit(b, 1000, 1)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000Lines10Cursors(b *testing.B) {
|
||||
benchEdit(b, 1000, 10)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000Lines100Cursors(b *testing.B) {
|
||||
benchEdit(b, 1000, 100)
|
||||
}
|
||||
|
||||
func BenchmarkEdit10000Lines1Cursor(b *testing.B) {
|
||||
benchEdit(b, 10000, 1)
|
||||
}
|
||||
|
||||
func BenchmarkEdit10000Lines10Cursors(b *testing.B) {
|
||||
benchEdit(b, 10000, 10)
|
||||
}
|
||||
|
||||
func BenchmarkEdit10000Lines100Cursors(b *testing.B) {
|
||||
benchEdit(b, 10000, 100)
|
||||
}
|
||||
|
||||
func BenchmarkEdit10000Lines1000Cursors(b *testing.B) {
|
||||
benchEdit(b, 10000, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkEdit100000Lines1Cursor(b *testing.B) {
|
||||
benchEdit(b, 100000, 1)
|
||||
}
|
||||
|
||||
func BenchmarkEdit100000Lines10Cursors(b *testing.B) {
|
||||
benchEdit(b, 100000, 10)
|
||||
}
|
||||
|
||||
func BenchmarkEdit100000Lines100Cursors(b *testing.B) {
|
||||
benchEdit(b, 100000, 100)
|
||||
}
|
||||
|
||||
func BenchmarkEdit100000Lines1000Cursors(b *testing.B) {
|
||||
benchEdit(b, 100000, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000000Lines1Cursor(b *testing.B) {
|
||||
benchEdit(b, 1000000, 1)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000000Lines10Cursors(b *testing.B) {
|
||||
benchEdit(b, 1000000, 10)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000000Lines100Cursors(b *testing.B) {
|
||||
benchEdit(b, 1000000, 100)
|
||||
}
|
||||
|
||||
func BenchmarkEdit1000000Lines1000Cursors(b *testing.B) {
|
||||
benchEdit(b, 1000000, 1000)
|
||||
}
|
||||
@@ -1,627 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/micro-editor/micro/v2/internal/clipboard"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
// InBounds returns whether the given location is a valid character position in the given buffer
|
||||
func InBounds(pos Loc, buf *Buffer) bool {
|
||||
if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > util.CharacterCount(buf.LineBytes(pos.Y)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// The Cursor struct stores the location of the cursor in the buffer
|
||||
// as well as the selection
|
||||
type Cursor struct {
|
||||
buf *Buffer
|
||||
Loc
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
CurSelection [2]Loc
|
||||
// The original selection as a range of character numbers
|
||||
// This is used for line and word selection where it is necessary
|
||||
// to know what the original selection was
|
||||
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)
|
||||
Num int
|
||||
}
|
||||
|
||||
func NewCursor(b *Buffer, l Loc) *Cursor {
|
||||
c := &Cursor{
|
||||
buf: b,
|
||||
Loc: l,
|
||||
|
||||
NewTrailingWsY: -1,
|
||||
}
|
||||
c.StoreVisualX()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Cursor) SetBuf(b *Buffer) {
|
||||
c.buf = b
|
||||
}
|
||||
|
||||
func (c *Cursor) Buf() *Buffer {
|
||||
return c.buf
|
||||
}
|
||||
|
||||
// Goto puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) Goto(b Cursor) {
|
||||
c.X, c.Y = b.X, b.Y
|
||||
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// GotoLoc puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) GotoLoc(l Loc) {
|
||||
c.X, c.Y = l.X, l.Y
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// GetVisualX returns the x value of the cursor in visual spaces
|
||||
func (c *Cursor) GetVisualX(wrap bool) int {
|
||||
if wrap && c.buf.GetVisualX != nil {
|
||||
return c.buf.GetVisualX(c.Loc)
|
||||
}
|
||||
|
||||
if c.X <= 0 {
|
||||
c.X = 0
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes := c.buf.LineBytes(c.Y)
|
||||
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
||||
|
||||
return util.StringWidth(bytes, c.X, tabsize)
|
||||
}
|
||||
|
||||
// GetCharPosInLine gets the char position of a visual x y
|
||||
// coordinate (this is necessary because tabs are 1 char but
|
||||
// 4 visual spaces)
|
||||
func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
|
||||
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
||||
return util.GetCharPosInLine(b, visualPos, tabsize)
|
||||
}
|
||||
|
||||
// Start moves the cursor to the start of the line it is on
|
||||
func (c *Cursor) Start() {
|
||||
c.X = 0
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// StartOfText moves the cursor to the first non-whitespace rune of
|
||||
// the line it is on
|
||||
func (c *Cursor) StartOfText() {
|
||||
c.Start()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
break
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// IsStartOfText returns whether the cursor is at the first
|
||||
// non-whitespace rune of the line it is on
|
||||
func (c *Cursor) IsStartOfText() bool {
|
||||
x := 0
|
||||
for util.IsWhitespace(c.RuneUnder(x)) {
|
||||
if x == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
break
|
||||
}
|
||||
x++
|
||||
}
|
||||
return c.X == x
|
||||
}
|
||||
|
||||
// End moves the cursor to the end of the line it is on
|
||||
func (c *Cursor) End() {
|
||||
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// CopySelection copies the user's selection to either "primary"
|
||||
// or "clipboard"
|
||||
func (c *Cursor) CopySelection(target clipboard.Register) {
|
||||
if c.HasSelection() {
|
||||
if target != clipboard.PrimaryReg || c.buf.Settings["useprimary"].(bool) {
|
||||
clipboard.WriteMulti(string(c.GetSelection()), target, c.Num, c.buf.NumCursors())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResetSelection resets the user's selection
|
||||
func (c *Cursor) ResetSelection() {
|
||||
c.CurSelection[0] = c.buf.Start()
|
||||
c.CurSelection[1] = c.buf.Start()
|
||||
}
|
||||
|
||||
// SetSelectionStart sets the start of the selection
|
||||
func (c *Cursor) SetSelectionStart(pos Loc) {
|
||||
c.CurSelection[0] = pos
|
||||
}
|
||||
|
||||
// SetSelectionEnd sets the end of the selection
|
||||
func (c *Cursor) SetSelectionEnd(pos Loc) {
|
||||
c.CurSelection[1] = pos
|
||||
}
|
||||
|
||||
// HasSelection returns whether or not the user has selected anything
|
||||
func (c *Cursor) HasSelection() bool {
|
||||
return c.CurSelection[0] != c.CurSelection[1]
|
||||
}
|
||||
|
||||
// DeleteSelection deletes the currently selected text
|
||||
func (c *Cursor) DeleteSelection() {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
|
||||
c.Loc = c.CurSelection[1]
|
||||
} else if !c.HasSelection() {
|
||||
return
|
||||
} else {
|
||||
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
|
||||
c.Loc = c.CurSelection[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Deselect closes the cursor's current selection
|
||||
// Start indicates whether the cursor should be placed
|
||||
// at the start or end of the selection
|
||||
func (c *Cursor) Deselect(start bool) {
|
||||
if c.HasSelection() {
|
||||
if start {
|
||||
c.Loc = c.CurSelection[0]
|
||||
} else {
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
c.ResetSelection()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// GetSelection returns the cursor's selection
|
||||
func (c *Cursor) GetSelection() []byte {
|
||||
if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
|
||||
}
|
||||
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// SelectLine selects the current line
|
||||
func (c *Cursor) SelectLine() {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.End()
|
||||
if len(c.buf.lines)-1 > c.Y {
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
} else {
|
||||
c.SetSelectionEnd(c.Loc)
|
||||
}
|
||||
|
||||
c.OrigSelection = c.CurSelection
|
||||
}
|
||||
|
||||
// AddLineToSelection adds the current line to the selection
|
||||
func (c *Cursor) AddLineToSelection() {
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
c.End()
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
}
|
||||
}
|
||||
|
||||
// UpN moves the cursor up N lines (if possible)
|
||||
func (c *Cursor) UpN(amount int) {
|
||||
proposedY := c.Y - amount
|
||||
if proposedY < 0 {
|
||||
proposedY = 0
|
||||
} else if proposedY >= len(c.buf.lines) {
|
||||
proposedY = len(c.buf.lines) - 1
|
||||
}
|
||||
|
||||
bytes := c.buf.LineBytes(proposedY)
|
||||
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
|
||||
|
||||
if c.X > util.CharacterCount(bytes) || (amount < 0 && proposedY == c.Y) {
|
||||
c.X = util.CharacterCount(bytes)
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
if c.X < 0 || (amount > 0 && proposedY == c.Y) {
|
||||
c.X = 0
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
c.Y = proposedY
|
||||
}
|
||||
|
||||
// DownN moves the cursor down N lines (if possible)
|
||||
func (c *Cursor) DownN(amount int) {
|
||||
c.UpN(-amount)
|
||||
}
|
||||
|
||||
// Up moves the cursor up one line (if possible)
|
||||
func (c *Cursor) Up() {
|
||||
c.UpN(1)
|
||||
}
|
||||
|
||||
// Down moves the cursor down one line (if possible)
|
||||
func (c *Cursor) Down() {
|
||||
c.DownN(1)
|
||||
}
|
||||
|
||||
// Left moves the cursor left one cell (if possible) or to
|
||||
// the previous line if it is at the beginning
|
||||
func (c *Cursor) Left() {
|
||||
if c.Loc == c.buf.Start() {
|
||||
return
|
||||
}
|
||||
if c.X > 0 {
|
||||
c.X--
|
||||
} else {
|
||||
c.Up()
|
||||
c.End()
|
||||
}
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Right moves the cursor right one cell (if possible) or
|
||||
// to the next line if it is at the end
|
||||
func (c *Cursor) Right() {
|
||||
if c.Loc == c.buf.End() {
|
||||
return
|
||||
}
|
||||
if c.X < util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
c.X++
|
||||
} else {
|
||||
c.Down()
|
||||
c.Start()
|
||||
}
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Relocate makes sure that the cursor is inside the bounds
|
||||
// of the buffer If it isn't, it moves it to be within the
|
||||
// buffer's lines
|
||||
func (c *Cursor) Relocate() {
|
||||
if c.Y < 0 {
|
||||
c.Y = 0
|
||||
} else if c.Y >= len(c.buf.lines) {
|
||||
c.Y = len(c.buf.lines) - 1
|
||||
}
|
||||
|
||||
if c.X < 0 {
|
||||
c.X = 0
|
||||
} else if c.X > util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
|
||||
}
|
||||
}
|
||||
|
||||
// SelectWord selects the word the cursor is currently on
|
||||
func (c *Cursor) SelectWord() {
|
||||
if len(c.buf.LineBytes(c.Y)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.OrigSelection = c.CurSelection
|
||||
return
|
||||
}
|
||||
|
||||
forward, backward := c.X, c.X
|
||||
|
||||
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.OrigSelection[0] = c.CurSelection[0]
|
||||
|
||||
lineLen := util.CharacterCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.OrigSelection[1] = c.CurSelection[1]
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// AddWordToSelection adds the word the cursor is currently on
|
||||
// to the selection
|
||||
func (c *Cursor) AddWordToSelection() {
|
||||
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
return
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
backward := c.X
|
||||
|
||||
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
forward := c.X
|
||||
|
||||
lineLen := util.CharacterCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// SelectTo selects from the current cursor location to the given
|
||||
// location
|
||||
func (c *Cursor) SelectTo(loc Loc) {
|
||||
if loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
c.SetSelectionEnd(loc)
|
||||
} else {
|
||||
c.SetSelectionStart(loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[0])
|
||||
}
|
||||
}
|
||||
|
||||
// WordRight moves the cursor one word to the right
|
||||
func (c *Cursor) WordRight() {
|
||||
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
c.Right()
|
||||
return
|
||||
}
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
return
|
||||
}
|
||||
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()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// WordLeft moves the cursor one word to the left
|
||||
func (c *Cursor) WordLeft() {
|
||||
if c.X == 0 {
|
||||
c.Left()
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
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()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
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
|
||||
func (c *Cursor) RuneUnder(x int) rune {
|
||||
line := c.buf.LineBytes(c.Y)
|
||||
if len(line) == 0 || x >= util.CharacterCount(line) {
|
||||
return '\n'
|
||||
} else if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
i := 0
|
||||
for len(line) > 0 {
|
||||
r, _, size := util.DecodeCharacter(line)
|
||||
line = line[size:]
|
||||
|
||||
if i == x {
|
||||
return r
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
func (c *Cursor) StoreVisualX() {
|
||||
c.LastVisualX = c.GetVisualX(false)
|
||||
c.LastWrappedVisualX = c.GetVisualX(true)
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
ulua "github.com/micro-editor/micro/v2/internal/lua"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
const (
|
||||
// Opposite and undoing events must have opposite values
|
||||
|
||||
// TextEventInsert represents an insertion event
|
||||
TextEventInsert = 1
|
||||
// TextEventRemove represents a deletion event
|
||||
TextEventRemove = -1
|
||||
// TextEventReplace represents a replace event
|
||||
TextEventReplace = 0
|
||||
|
||||
undoThreshold = 1000 // If two events are less than n milliseconds apart, undo both of them
|
||||
)
|
||||
|
||||
// TextEvent holds data for a manipulation on some text that can be undone
|
||||
type TextEvent struct {
|
||||
C Cursor
|
||||
|
||||
EventType int
|
||||
Deltas []Delta
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// A Delta is a change to the buffer
|
||||
type Delta struct {
|
||||
Text []byte
|
||||
Start Loc
|
||||
End Loc
|
||||
}
|
||||
|
||||
// DoTextEvent runs a text event
|
||||
func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
|
||||
oldl := eh.buf.LinesNum()
|
||||
|
||||
if useUndo {
|
||||
eh.Execute(t)
|
||||
} else {
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
if len(t.Deltas) != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
text := t.Deltas[0].Text
|
||||
start := t.Deltas[0].Start
|
||||
lastnl := -1
|
||||
var endX int
|
||||
var textX int
|
||||
if t.EventType == TextEventInsert {
|
||||
linecount := eh.buf.LinesNum() - oldl
|
||||
textcount := util.CharacterCount(text)
|
||||
lastnl = bytes.LastIndex(text, []byte{'\n'})
|
||||
if lastnl >= 0 {
|
||||
endX = util.CharacterCount(text[lastnl+1:])
|
||||
textX = endX
|
||||
} else {
|
||||
endX = start.X + textcount
|
||||
textX = textcount
|
||||
}
|
||||
t.Deltas[0].End = clamp(Loc{endX, start.Y + linecount}, eh.buf.LineArray)
|
||||
}
|
||||
end := t.Deltas[0].End
|
||||
|
||||
for _, c := range eh.cursors {
|
||||
move := func(loc Loc) Loc {
|
||||
if t.EventType == TextEventInsert {
|
||||
if start.Y != loc.Y && loc.GreaterThan(start) {
|
||||
loc.Y += end.Y - start.Y
|
||||
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
|
||||
loc.Y += end.Y - start.Y
|
||||
if lastnl >= 0 {
|
||||
loc.X += textX - start.X
|
||||
} else {
|
||||
loc.X += textX
|
||||
}
|
||||
}
|
||||
return loc
|
||||
} else {
|
||||
if loc.Y != end.Y && loc.GreaterThan(end) {
|
||||
loc.Y -= end.Y - start.Y
|
||||
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
||||
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
}
|
||||
c.Loc = move(c.Loc)
|
||||
c.CurSelection[0] = move(c.CurSelection[0])
|
||||
c.CurSelection[1] = move(c.CurSelection[1])
|
||||
c.OrigSelection[0] = move(c.OrigSelection[0])
|
||||
c.OrigSelection[1] = move(c.OrigSelection[1])
|
||||
c.Relocate()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
if useUndo {
|
||||
eh.updateTrailingWs(t)
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||
if t.EventType == TextEventInsert {
|
||||
for _, d := range t.Deltas {
|
||||
buf.insert(d.Start, d.Text)
|
||||
}
|
||||
} else if t.EventType == TextEventRemove {
|
||||
for i, d := range t.Deltas {
|
||||
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
||||
}
|
||||
} else if t.EventType == TextEventReplace {
|
||||
for i, d := range t.Deltas {
|
||||
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
||||
buf.insert(d.Start, d.Text)
|
||||
t.Deltas[i].Start = d.Start
|
||||
t.Deltas[i].End = Loc{d.Start.X + util.CharacterCount(d.Text), d.Start.Y}
|
||||
}
|
||||
for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 {
|
||||
t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UndoTextEvent undoes a text event
|
||||
func (eh *EventHandler) UndoTextEvent(t *TextEvent) {
|
||||
t.EventType = -t.EventType
|
||||
eh.DoTextEvent(t, false)
|
||||
}
|
||||
|
||||
// EventHandler executes text manipulations and allows undoing and redoing
|
||||
type EventHandler struct {
|
||||
buf *SharedBuffer
|
||||
cursors []*Cursor
|
||||
active int
|
||||
UndoStack *TEStack
|
||||
RedoStack *TEStack
|
||||
}
|
||||
|
||||
// NewEventHandler returns a new EventHandler
|
||||
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
|
||||
eh := new(EventHandler)
|
||||
eh.UndoStack = new(TEStack)
|
||||
eh.RedoStack = new(TEStack)
|
||||
eh.buf = buf
|
||||
eh.cursors = cursors
|
||||
return eh
|
||||
}
|
||||
|
||||
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
|
||||
// the buffer equal to that string
|
||||
// This means that we can transform the buffer into any string and still preserve undo/redo
|
||||
// through insert and delete events
|
||||
func (eh *EventHandler) ApplyDiff(new string) {
|
||||
differ := dmp.New()
|
||||
diff := differ.DiffMain(string(eh.buf.Bytes()), new, false)
|
||||
loc := eh.buf.Start()
|
||||
for _, d := range diff {
|
||||
if d.Type == dmp.DiffDelete {
|
||||
eh.Remove(loc, loc.MoveLA(util.CharacterCountInString(d.Text), eh.buf.LineArray))
|
||||
} else {
|
||||
if d.Type == dmp.DiffInsert {
|
||||
eh.Insert(loc, d.Text)
|
||||
}
|
||||
loc = loc.MoveLA(util.CharacterCountInString(d.Text), eh.buf.LineArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert creates an insert text event and executes it
|
||||
func (eh *EventHandler) Insert(start Loc, textStr string) {
|
||||
text := []byte(textStr)
|
||||
eh.InsertBytes(start, text)
|
||||
}
|
||||
|
||||
// InsertBytes creates an insert text event and executes it
|
||||
func (eh *EventHandler) InsertBytes(start Loc, text []byte) {
|
||||
if len(text) == 0 {
|
||||
return
|
||||
}
|
||||
start = clamp(start, eh.buf.LineArray)
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventInsert,
|
||||
Deltas: []Delta{{text, start, Loc{0, 0}}},
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.DoTextEvent(e, true)
|
||||
}
|
||||
|
||||
// Remove creates a remove text event and executes it
|
||||
func (eh *EventHandler) Remove(start, end Loc) {
|
||||
if start == end {
|
||||
return
|
||||
}
|
||||
start = clamp(start, eh.buf.LineArray)
|
||||
end = clamp(end, eh.buf.LineArray)
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventRemove,
|
||||
Deltas: []Delta{{[]byte{}, start, end}},
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.DoTextEvent(e, true)
|
||||
}
|
||||
|
||||
// MultipleReplace creates an multiple insertions executes them
|
||||
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventReplace,
|
||||
Deltas: deltas,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
}
|
||||
|
||||
// Replace deletes from start to end and replaces it with the given string
|
||||
func (eh *EventHandler) Replace(start, end Loc, replace string) {
|
||||
eh.Remove(start, end)
|
||||
eh.Insert(start, replace)
|
||||
}
|
||||
|
||||
// Execute a textevent and add it to the undo stack
|
||||
func (eh *EventHandler) Execute(t *TextEvent) {
|
||||
if eh.RedoStack.Len() > 0 {
|
||||
eh.RedoStack = new(TEStack)
|
||||
}
|
||||
eh.UndoStack.Push(t)
|
||||
|
||||
b, err := config.RunPluginFnBool(nil, "onBeforeTextEvent", luar.New(ulua.L, eh.buf), luar.New(ulua.L, t))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
// Undo the first event in the undo stack. Returns false if the stack is empty.
|
||||
func (eh *EventHandler) Undo() bool {
|
||||
t := eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
endTime := startTime - (startTime % undoThreshold)
|
||||
|
||||
for {
|
||||
t = eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
||||
break
|
||||
}
|
||||
|
||||
eh.UndoOneEvent()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UndoOneEvent undoes one event
|
||||
func (eh *EventHandler) UndoOneEvent() {
|
||||
// This event should be undone
|
||||
// Pop it off the stack
|
||||
t := eh.UndoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
// Undo it
|
||||
// Modifies the text event
|
||||
eh.UndoTextEvent(t)
|
||||
|
||||
// Set the cursor in the right place
|
||||
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
|
||||
eh.cursors[t.C.Num].Goto(t.C)
|
||||
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
|
||||
}
|
||||
|
||||
// Push it to the redo stack
|
||||
eh.RedoStack.Push(t)
|
||||
}
|
||||
|
||||
// Redo the first event in the redo stack. Returns false if the stack is empty.
|
||||
func (eh *EventHandler) Redo() bool {
|
||||
t := eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
endTime := startTime - (startTime % undoThreshold) + undoThreshold
|
||||
|
||||
for {
|
||||
t = eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
||||
break
|
||||
}
|
||||
|
||||
eh.RedoOneEvent()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RedoOneEvent redoes one event
|
||||
func (eh *EventHandler) RedoOneEvent() {
|
||||
t := eh.RedoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
|
||||
eh.cursors[t.C.Num].Goto(t.C)
|
||||
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
|
||||
}
|
||||
|
||||
// Modifies the text event
|
||||
eh.UndoTextEvent(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
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"github.com/micro-editor/micro/v2/pkg/highlight"
|
||||
)
|
||||
|
||||
// Finds the byte index of the nth rune in a byte slice
|
||||
func runeToByteIndex(n int, txt []byte) int {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
i := 0
|
||||
for len(txt) > 0 {
|
||||
_, _, size := util.DecodeCharacter(txt)
|
||||
|
||||
txt = txt[size:]
|
||||
count += size
|
||||
i++
|
||||
|
||||
if i == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// A searchState contains the search match info for a single line
|
||||
type searchState struct {
|
||||
search string
|
||||
useRegex bool
|
||||
ignorecase bool
|
||||
match [][2]int
|
||||
done bool
|
||||
}
|
||||
|
||||
// A Line contains the data in bytes as well as a highlight state, match
|
||||
// and a flag for whether the highlighting needs to be updated
|
||||
type Line struct {
|
||||
data []byte
|
||||
|
||||
state highlight.State
|
||||
match highlight.LineMatch
|
||||
lock sync.Mutex
|
||||
|
||||
// The search states for the line, used for highlighting of search matches,
|
||||
// separately from the syntax highlighting.
|
||||
// A map is used because the line array may be shared between multiple buffers
|
||||
// (multiple instances of the same file opened in different edit panes)
|
||||
// which have distinct searches, so in the general case there are multiple
|
||||
// searches per a line, one search per a Buffer containing this line.
|
||||
search map[*Buffer]*searchState
|
||||
}
|
||||
|
||||
const (
|
||||
// Line ending file formats
|
||||
FFAuto = 0 // Autodetect format
|
||||
FFUnix = 1 // LF line endings (unix style '\n')
|
||||
FFDos = 2 // CRLF line endings (dos style '\r\n')
|
||||
)
|
||||
|
||||
type FileFormat byte
|
||||
|
||||
// A LineArray simply stores and array of lines and makes it easy to insert
|
||||
// and delete in it
|
||||
type LineArray struct {
|
||||
lines []Line
|
||||
Endings FileFormat
|
||||
initsize uint64
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Append efficiently appends lines together
|
||||
// It allocates an additional 10000 lines if the original estimate
|
||||
// is incorrect
|
||||
func Append(slice []Line, data ...Line) []Line {
|
||||
l := len(slice)
|
||||
if l+len(data) > cap(slice) { // reallocate
|
||||
newSlice := make([]Line, (l+len(data))+10000)
|
||||
copy(newSlice, slice)
|
||||
slice = newSlice
|
||||
}
|
||||
slice = slice[0 : l+len(data)]
|
||||
for i, c := range data {
|
||||
slice[l+i] = c
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// NewLineArray returns a new line array from an array of bytes
|
||||
func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray {
|
||||
la := new(LineArray)
|
||||
|
||||
la.lines = make([]Line, 0, 1000)
|
||||
la.initsize = size
|
||||
|
||||
br := bufio.NewReader(reader)
|
||||
var loaded int
|
||||
|
||||
la.Endings = endings
|
||||
|
||||
n := 0
|
||||
for {
|
||||
data, err := br.ReadBytes('\n')
|
||||
// Detect the line ending by checking to see if there is a '\r' char
|
||||
// before the '\n'
|
||||
// Even if the file format is set to DOS, the '\r' is removed so
|
||||
// that all lines end with '\n'
|
||||
dlen := len(data)
|
||||
if dlen > 1 && data[dlen-2] == '\r' {
|
||||
data = append(data[:dlen-2], '\n')
|
||||
if la.Endings == FFAuto {
|
||||
la.Endings = FFDos
|
||||
}
|
||||
dlen = len(data)
|
||||
} else if dlen > 0 {
|
||||
if la.Endings == FFAuto {
|
||||
la.Endings = FFUnix
|
||||
}
|
||||
}
|
||||
|
||||
// If we are loading a large file (greater than 1000) we use the file
|
||||
// size and the length of the first 1000 lines to try to estimate
|
||||
// how many lines will need to be allocated for the rest of the file
|
||||
// We add an extra 10000 to the original estimate to be safe and give
|
||||
// plenty of room for expansion
|
||||
if n >= 1000 && loaded >= 0 {
|
||||
totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
|
||||
newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
|
||||
copy(newSlice, la.lines)
|
||||
la.lines = newSlice
|
||||
loaded = -1
|
||||
}
|
||||
|
||||
// Counter for the number of bytes in the first 1000 lines
|
||||
if loaded >= 0 {
|
||||
loaded += dlen
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
la.lines = Append(la.lines, Line{
|
||||
data: data,
|
||||
state: nil,
|
||||
match: nil,
|
||||
})
|
||||
}
|
||||
// Last line was read
|
||||
break
|
||||
} else {
|
||||
la.lines = Append(la.lines, Line{
|
||||
data: data[:dlen-1],
|
||||
state: nil,
|
||||
match: nil,
|
||||
})
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
return la
|
||||
}
|
||||
|
||||
// Bytes returns the string that should be written to disk when
|
||||
// the line array is saved
|
||||
func (la *LineArray) Bytes() []byte {
|
||||
b := new(bytes.Buffer)
|
||||
// initsize should provide a good estimate
|
||||
b.Grow(int(la.initsize + 4096))
|
||||
for i, l := range la.lines {
|
||||
b.Write(l.data)
|
||||
if i != len(la.lines)-1 {
|
||||
if la.Endings == FFDos {
|
||||
b.WriteByte('\r')
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// newlineBelow adds a newline below the given line number
|
||||
func (la *LineArray) newlineBelow(y int) {
|
||||
la.lines = append(la.lines, Line{
|
||||
data: []byte{' '},
|
||||
state: nil,
|
||||
match: nil,
|
||||
})
|
||||
copy(la.lines[y+2:], la.lines[y+1:])
|
||||
la.lines[y+1] = Line{
|
||||
data: []byte{},
|
||||
state: la.lines[y].state,
|
||||
match: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Inserts a byte array at a given location
|
||||
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
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') {
|
||||
la.split(Loc{x, y})
|
||||
x = 0
|
||||
y++
|
||||
|
||||
if value[i] == '\r' {
|
||||
i++
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
la.insertByte(Loc{x, y}, value[i])
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// InsertByte inserts a byte at a given location
|
||||
func (la *LineArray) insertByte(pos Loc, value byte) {
|
||||
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
|
||||
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
|
||||
la.lines[pos.Y].data[pos.X] = value
|
||||
}
|
||||
|
||||
// joinLines joins the two lines a and b
|
||||
func (la *LineArray) joinLines(a, b int) {
|
||||
la.lines[a].data = append(la.lines[a].data, la.lines[b].data...)
|
||||
la.deleteLine(b)
|
||||
}
|
||||
|
||||
// split splits a line at a given position
|
||||
func (la *LineArray) split(pos Loc) {
|
||||
la.newlineBelow(pos.Y)
|
||||
la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...)
|
||||
la.lines[pos.Y+1].state = la.lines[pos.Y].state
|
||||
la.lines[pos.Y].state = nil
|
||||
la.lines[pos.Y].match = nil
|
||||
la.lines[pos.Y+1].match = nil
|
||||
la.deleteToEnd(Loc{pos.X, pos.Y})
|
||||
}
|
||||
|
||||
// removes from start to end
|
||||
func (la *LineArray) remove(start, end Loc) []byte {
|
||||
la.lock.Lock()
|
||||
defer la.lock.Unlock()
|
||||
|
||||
sub := la.Substr(start, end)
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||
if start.Y == end.Y {
|
||||
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
|
||||
} else {
|
||||
la.deleteLines(start.Y+1, end.Y-1)
|
||||
la.deleteToEnd(Loc{startX, start.Y})
|
||||
la.deleteFromStart(Loc{endX - 1, start.Y + 1})
|
||||
la.joinLines(start.Y, start.Y+1)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// deleteToEnd deletes from the end of a line to the position
|
||||
func (la *LineArray) deleteToEnd(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
|
||||
}
|
||||
|
||||
// deleteFromStart deletes from the start of a line to the position
|
||||
func (la *LineArray) deleteFromStart(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
|
||||
}
|
||||
|
||||
// deleteLine deletes the line number
|
||||
func (la *LineArray) deleteLine(y int) {
|
||||
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
|
||||
}
|
||||
|
||||
func (la *LineArray) deleteLines(y1, y2 int) {
|
||||
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
|
||||
}
|
||||
|
||||
// Substr returns the string representation between two locations
|
||||
func (la *LineArray) Substr(start, end Loc) []byte {
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||
if start.Y == end.Y {
|
||||
src := la.lines[start.Y].data[startX:endX]
|
||||
dest := make([]byte, len(src))
|
||||
copy(dest, src)
|
||||
return dest
|
||||
}
|
||||
str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y))
|
||||
str = append(str, la.lines[start.Y].data[startX:]...)
|
||||
str = append(str, '\n')
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
str = append(str, la.lines[i].data...)
|
||||
str = append(str, '\n')
|
||||
}
|
||||
str = append(str, la.lines[end.Y].data[:endX]...)
|
||||
return str
|
||||
}
|
||||
|
||||
// LinesNum returns the number of lines in the buffer
|
||||
func (la *LineArray) LinesNum() int {
|
||||
return len(la.lines)
|
||||
}
|
||||
|
||||
// Start returns the start of the buffer
|
||||
func (la *LineArray) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (la *LineArray) End() Loc {
|
||||
numlines := len(la.lines)
|
||||
return Loc{util.CharacterCount(la.lines[numlines-1].data), numlines - 1}
|
||||
}
|
||||
|
||||
// LineBytes returns line n as an array of bytes
|
||||
func (la *LineArray) LineBytes(lineN int) []byte {
|
||||
if lineN >= len(la.lines) || lineN < 0 {
|
||||
return []byte{}
|
||||
}
|
||||
return la.lines[lineN].data
|
||||
}
|
||||
|
||||
// State gets the highlight state for the given line number
|
||||
func (la *LineArray) State(lineN int) highlight.State {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
return la.lines[lineN].state
|
||||
}
|
||||
|
||||
// SetState sets the highlight state at the given line number
|
||||
func (la *LineArray) SetState(lineN int, s highlight.State) {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
la.lines[lineN].state = s
|
||||
}
|
||||
|
||||
// SetMatch sets the match at the given line number
|
||||
func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
la.lines[lineN].match = m
|
||||
}
|
||||
|
||||
// Match retrieves the match for the given line number
|
||||
func (la *LineArray) Match(lineN int) highlight.LineMatch {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
return la.lines[lineN].match
|
||||
}
|
||||
|
||||
// Locks the whole LineArray
|
||||
func (la *LineArray) Lock() {
|
||||
la.lock.Lock()
|
||||
}
|
||||
|
||||
// Unlocks the whole LineArray
|
||||
func (la *LineArray) Unlock() {
|
||||
la.lock.Unlock()
|
||||
}
|
||||
|
||||
// SearchMatch returns true if the location `pos` is within a match
|
||||
// of the last search for the buffer `b`.
|
||||
// It is used for efficient highlighting of search matches (separately
|
||||
// from the syntax highlighting).
|
||||
// SearchMatch searches for the matches if it is called first time
|
||||
// for the given line or if the line was modified. Otherwise the
|
||||
// previously found matches are used.
|
||||
//
|
||||
// The buffer `b` needs to be passed because the line array may be shared
|
||||
// between multiple buffers (multiple instances of the same file opened
|
||||
// in different edit panes) which have distinct searches, so SearchMatch
|
||||
// needs to know which search to match against.
|
||||
func (la *LineArray) SearchMatch(b *Buffer, pos Loc) bool {
|
||||
if b.LastSearch == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
lineN := pos.Y
|
||||
if la.lines[lineN].search == nil {
|
||||
la.lines[lineN].search = make(map[*Buffer]*searchState)
|
||||
}
|
||||
s, ok := la.lines[lineN].search[b]
|
||||
if !ok {
|
||||
// Note: here is a small harmless leak: when the buffer `b` is closed,
|
||||
// `s` is not deleted from the map. It means that the buffer
|
||||
// will not be garbage-collected until the line array is garbage-collected,
|
||||
// i.e. until all the buffers sharing this file are closed.
|
||||
s = new(searchState)
|
||||
la.lines[lineN].search[b] = s
|
||||
}
|
||||
if !ok || s.search != b.LastSearch || s.useRegex != b.LastSearchRegex ||
|
||||
s.ignorecase != b.Settings["ignorecase"].(bool) {
|
||||
s.search = b.LastSearch
|
||||
s.useRegex = b.LastSearchRegex
|
||||
s.ignorecase = b.Settings["ignorecase"].(bool)
|
||||
s.done = false
|
||||
}
|
||||
|
||||
if !s.done {
|
||||
s.match = nil
|
||||
start := Loc{0, lineN}
|
||||
end := Loc{util.CharacterCount(la.lines[lineN].data), lineN}
|
||||
for start.X < end.X {
|
||||
m, found, _ := b.FindNext(b.LastSearch, start, end, start, true, b.LastSearchRegex)
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
s.match = append(s.match, [2]int{m[0].X, m[1].X})
|
||||
|
||||
start.X = m[1].X
|
||||
if m[1].X == m[0].X {
|
||||
start.X = m[1].X + 1
|
||||
}
|
||||
}
|
||||
|
||||
s.done = true
|
||||
}
|
||||
|
||||
for _, m := range s.match {
|
||||
if pos.X >= m[0] && pos.X < m[1] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// invalidateSearchMatches marks search matches for the given line as outdated.
|
||||
// It is called when the line is modified.
|
||||
func (la *LineArray) invalidateSearchMatches(lineN int) {
|
||||
if la.lines[lineN].search != nil {
|
||||
for _, s := range la.lines[lineN].search {
|
||||
s.done = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var unicode_txt = `An preost wes on leoden, Laȝamon was ihoten
|
||||
He wes Leovenaðes sone -- liðe him be Drihten.
|
||||
He wonede at Ernleȝe at æðelen are chirechen,
|
||||
Uppen Sevarne staþe, sel þar him þuhte,
|
||||
Onfest Radestone, þer he bock radde.`
|
||||
|
||||
var la *LineArray
|
||||
|
||||
func init() {
|
||||
reader := strings.NewReader(unicode_txt)
|
||||
la = NewLineArray(uint64(len(unicode_txt)), FFAuto, reader)
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
la.insert(Loc{17, 1}, []byte{'\n'})
|
||||
assert.Equal(t, len(la.lines), 6)
|
||||
sub1 := la.Substr(Loc{0, 1}, Loc{17, 1})
|
||||
sub2 := la.Substr(Loc{0, 2}, Loc{30, 2})
|
||||
|
||||
assert.Equal(t, []byte("He wes Leovenaðes"), sub1)
|
||||
assert.Equal(t, []byte(" sone -- liðe him be Drihten."), sub2)
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
la.remove(Loc{47, 1}, Loc{0, 2})
|
||||
assert.Equal(t, len(la.lines), 5)
|
||||
sub := la.Substr(Loc{0, 1}, Loc{47, 1})
|
||||
bytes := la.Bytes()
|
||||
|
||||
assert.Equal(t, []byte("He wes Leovenaðes sone -- liðe him be Drihten."), sub)
|
||||
assert.Equal(t, unicode_txt, string(bytes))
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
la.insert(Loc{20, 3}, []byte(" foobar"))
|
||||
sub1 := la.Substr(Loc{0, 3}, Loc{50, 3})
|
||||
|
||||
assert.Equal(t, []byte("Uppen Sevarne staþe, foobar sel þar him þuhte,"), sub1)
|
||||
|
||||
la.insert(Loc{25, 2}, []byte("H̼̥̯͇͙̕͘͞e̸̦̞̠̣̰͙̼̥̦̼̖̬͕͕̰̯̫͇̕ĺ̜̠̩̯̯͙̼̭̠͕̮̞͜l̶͓̫̞̮͈͞ͅo̸͔͙̳̠͈̮̼̳͙̥̲͜͠"))
|
||||
|
||||
sub2 := la.Substr(Loc{0, 2}, Loc{60, 2})
|
||||
assert.Equal(t, []byte("He wonede at Ernleȝe at æH̼̥̯͇͙̕͘͞e̸̦̞̠̣̰͙̼̥̦̼̖̬͕͕̰̯̫͇̕ĺ̜̠̩̯̯͙̼̭̠͕̮̞͜l̶͓̫̞̮͈͞ͅo̸͔͙̳̠͈̮̼̳͙̥̲͜͠ðelen are chirechen,"), sub2)
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
la.remove(Loc{20, 3}, Loc{27, 3})
|
||||
la.remove(Loc{25, 2}, Loc{30, 2})
|
||||
|
||||
bytes := la.Bytes()
|
||||
assert.Equal(t, unicode_txt, string(bytes))
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
// Loc stores a location
|
||||
type Loc struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// LessThan returns true if b is smaller
|
||||
func (l Loc) LessThan(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
return l.Y == b.Y && l.X < b.X
|
||||
}
|
||||
|
||||
// GreaterThan returns true if b is bigger
|
||||
func (l Loc) GreaterThan(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
return l.Y == b.Y && l.X > b.X
|
||||
}
|
||||
|
||||
// GreaterEqual returns true if b is greater than or equal to b
|
||||
func (l Loc) GreaterEqual(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
return l == b
|
||||
}
|
||||
|
||||
// LessEqual returns true if b is less than or equal to b
|
||||
func (l Loc) LessEqual(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
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
|
||||
|
||||
// Diff returns the distance between two locations
|
||||
func DiffLA(a, b Loc, buf *LineArray) int {
|
||||
if a.Y == b.Y {
|
||||
if a.X > b.X {
|
||||
return a.X - b.X
|
||||
}
|
||||
return b.X - a.X
|
||||
}
|
||||
|
||||
// Make sure a is guaranteed to be less than b
|
||||
if b.LessThan(a) {
|
||||
a, b = b, a
|
||||
}
|
||||
|
||||
loc := 0
|
||||
for i := a.Y + 1; i < b.Y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += util.CharacterCount(buf.LineBytes(i)) + 1
|
||||
}
|
||||
loc += util.CharacterCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
|
||||
return loc
|
||||
}
|
||||
|
||||
// This moves the location one character to the right
|
||||
func (l Loc) right(buf *LineArray) Loc {
|
||||
if l == buf.End() {
|
||||
return Loc{l.X + 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X < util.CharacterCount(buf.LineBytes(l.Y)) {
|
||||
res = Loc{l.X + 1, l.Y}
|
||||
} else {
|
||||
res = Loc{0, l.Y + 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// This moves the given location one character to the left
|
||||
func (l Loc) left(buf *LineArray) Loc {
|
||||
if l == buf.Start() {
|
||||
return Loc{l.X - 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X > 0 {
|
||||
res = Loc{l.X - 1, l.Y}
|
||||
} else {
|
||||
res = Loc{util.CharacterCount(buf.LineBytes(l.Y - 1)), l.Y - 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// MoveLA moves the cursor n characters to the left or right
|
||||
// It moves the cursor left if n is negative
|
||||
func (l Loc) MoveLA(n int, buf *LineArray) Loc {
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
l = l.right(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
for i := 0; i < util.Abs(n); i++ {
|
||||
l = l.left(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Diff returns the difference between two locs
|
||||
func (l Loc) Diff(b Loc, buf *Buffer) int {
|
||||
return DiffLA(l, b, buf.LineArray)
|
||||
}
|
||||
|
||||
// Move moves a loc n characters
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
return l.MoveLA(n, buf.LineArray)
|
||||
}
|
||||
|
||||
// ByteOffset is just like ToCharPos except it counts bytes instead of runes
|
||||
func ByteOffset(pos Loc, buf *Buffer) int {
|
||||
x, y := pos.X, pos.Y
|
||||
loc := 0
|
||||
for i := 0; i < y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += len(buf.Line(i)) + 1
|
||||
}
|
||||
loc += len(buf.Line(y)[:x])
|
||||
return loc
|
||||
}
|
||||
|
||||
// clamps a loc within a buffer
|
||||
func clamp(pos Loc, la *LineArray) Loc {
|
||||
return pos.Clamp(la.Start(), la.End())
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
type MsgType int
|
||||
|
||||
const (
|
||||
MTInfo = iota
|
||||
MTWarning
|
||||
MTError
|
||||
)
|
||||
|
||||
// Message represents the information for a gutter message
|
||||
type Message struct {
|
||||
// The Msg iteslf
|
||||
Msg string
|
||||
// Start and End locations for the message
|
||||
Start, End Loc
|
||||
// The Kind stores the message type
|
||||
Kind MsgType
|
||||
// The Owner of the message
|
||||
Owner string
|
||||
}
|
||||
|
||||
// NewMessage creates a new gutter message
|
||||
func NewMessage(owner string, msg string, start, end Loc, kind MsgType) *Message {
|
||||
return &Message{
|
||||
Msg: msg,
|
||||
Start: start,
|
||||
End: end,
|
||||
Kind: kind,
|
||||
Owner: owner,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMessageAtLine creates a new gutter message at a given line
|
||||
func NewMessageAtLine(owner string, msg string, line int, kind MsgType) *Message {
|
||||
start := Loc{-1, line - 1}
|
||||
end := start
|
||||
return NewMessage(owner, msg, start, end, kind)
|
||||
}
|
||||
|
||||
func (m *Message) Style() tcell.Style {
|
||||
switch m.Kind {
|
||||
case MTInfo:
|
||||
if style, ok := config.Colorscheme["gutter-info"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTWarning:
|
||||
if style, ok := config.Colorscheme["gutter-warning"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTError:
|
||||
if style, ok := config.Colorscheme["gutter-error"]; ok {
|
||||
return style
|
||||
}
|
||||
}
|
||||
return config.DefStyle
|
||||
}
|
||||
|
||||
func (b *Buffer) AddMessage(m *Message) {
|
||||
b.Messages = append(b.Messages, m)
|
||||
}
|
||||
|
||||
func (b *Buffer) removeMsg(i int) {
|
||||
copy(b.Messages[i:], b.Messages[i+1:])
|
||||
b.Messages[len(b.Messages)-1] = nil
|
||||
b.Messages = b.Messages[:len(b.Messages)-1]
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearMessages(owner string) {
|
||||
for i := len(b.Messages) - 1; i >= 0; i-- {
|
||||
if b.Messages[i].Owner == owner {
|
||||
b.removeMsg(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearAllMessages() {
|
||||
b.Messages = make([]*Message, 0)
|
||||
}
|
||||
|
||||
type Messager interface {
|
||||
Message(msg ...any)
|
||||
}
|
||||
|
||||
var prompt Messager
|
||||
|
||||
func SetMessager(m Messager) {
|
||||
prompt = m
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// LargeFileThreshold is the number of bytes when fastdirty is forced
|
||||
// because hashing is too slow
|
||||
const LargeFileThreshold = 50000
|
||||
|
||||
type wrappedFile struct {
|
||||
name string
|
||||
writeCloser io.WriteCloser
|
||||
withSudo bool
|
||||
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 backupRequest
|
||||
|
||||
func init() {
|
||||
// Both saveRequestChan and backupRequestChan need to be non-buffered
|
||||
// so the save/backup goroutine receives both save and backup requests
|
||||
// in the same order the main goroutine sends them.
|
||||
saveRequestChan = make(chan saveRequest)
|
||||
backupRequestChan = make(chan backupRequest)
|
||||
|
||||
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 br := <-backupRequestChan:
|
||||
handleBackupRequest(br)
|
||||
case <-backupTicker.C:
|
||||
periodicBackup()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func openFile(name string, withSudo bool) (wrappedFile, error) {
|
||||
var err error
|
||||
var writeCloser io.WriteCloser
|
||||
var screenb bool
|
||||
var cmd *exec.Cmd
|
||||
var sigChan chan os.Signal
|
||||
|
||||
if withSudo {
|
||||
conv := "notrunc"
|
||||
// TODO: both platforms do not support dd with conv=fsync yet
|
||||
if !(runtime.GOOS == "illumos" || runtime.GOOS == "netbsd") {
|
||||
conv += ",fsync"
|
||||
}
|
||||
|
||||
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "conv="+conv, "of="+name)
|
||||
writeCloser, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return wrappedFile{}, err
|
||||
}
|
||||
|
||||
sigChan = make(chan os.Signal, 1)
|
||||
signal.Reset(os.Interrupt)
|
||||
signal.Notify(sigChan, os.Interrupt)
|
||||
|
||||
screenb = screen.TempFini()
|
||||
// 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
|
||||
// is too small to handle the full file contents all at once
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
screen.TempStart(screenb)
|
||||
|
||||
signal.Notify(util.Sigterm, os.Interrupt)
|
||||
signal.Stop(sigChan)
|
||||
|
||||
return wrappedFile{}, err
|
||||
}
|
||||
} else {
|
||||
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
|
||||
if err != nil {
|
||||
return wrappedFile{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedFile{name, writeCloser, withSudo, screenb, cmd, sigChan}, nil
|
||||
}
|
||||
|
||||
func (wf wrappedFile) Truncate() error {
|
||||
if wf.withSudo {
|
||||
// we don't need to stop the screen here, since it is still stopped
|
||||
// by openFile()
|
||||
// truncate might not be available on every platfom, so use dd instead
|
||||
cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "count=0", "of="+wf.name)
|
||||
return cmd.Run()
|
||||
}
|
||||
return wf.writeCloser.(*os.File).Truncate(0)
|
||||
}
|
||||
|
||||
func (wf wrappedFile) Write(b *SharedBuffer) (int, error) {
|
||||
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
|
||||
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
if len(b.lines) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// end of line
|
||||
var eol []byte
|
||||
if b.Endings == FFDos {
|
||||
eol = []byte{'\r', '\n'}
|
||||
} else {
|
||||
eol = []byte{'\n'}
|
||||
}
|
||||
|
||||
err := wf.Truncate()
|
||||
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
|
||||
err := wf.cmd.Wait()
|
||||
screen.TempStart(wf.screenb)
|
||||
|
||||
signal.Notify(util.Sigterm, os.Interrupt)
|
||||
signal.Stop(wf.sigChan)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *SharedBuffer) 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
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// AutoSave saves the buffer to its default path
|
||||
func (b *Buffer) AutoSave() error {
|
||||
if !b.Modified() {
|
||||
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
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
return b.saveToFile(filename, false, false)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
return b.saveToFile(filename, true, false)
|
||||
}
|
||||
|
||||
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error {
|
||||
var err error
|
||||
if b.Type.Readonly {
|
||||
return errors.New("Cannot save readonly buffer")
|
||||
}
|
||||
if b.Type.Scratch {
|
||||
return errors.New("Cannot save scratch buffer")
|
||||
}
|
||||
|
||||
if !autoSave && b.Settings["rmtrailingws"].(bool) {
|
||||
for i, l := range b.lines {
|
||||
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||
|
||||
linelen := util.CharacterCount(l.data)
|
||||
b.Remove(Loc{leftover, i}, Loc{linelen, i})
|
||||
}
|
||||
|
||||
b.RelocateCursors()
|
||||
}
|
||||
|
||||
if b.Settings["eofnewline"].(bool) {
|
||||
end := b.End()
|
||||
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
|
||||
b.insert(end, []byte{'\n'})
|
||||
}
|
||||
}
|
||||
|
||||
filename, err = util.ReplaceHome(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newFile := false
|
||||
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
|
||||
if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||
// Check if the parent dirs don't exist
|
||||
if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) {
|
||||
// Prompt to make sure they want to create the dirs that are missing
|
||||
if b.Settings["mkparents"].(bool) {
|
||||
// Create all leading dir(s) since they don't exist
|
||||
if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
|
||||
// If there was an error creating the dirs
|
||||
return mkdirallErr
|
||||
}
|
||||
} else {
|
||||
return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveResponseChan := make(chan saveResponse)
|
||||
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
|
||||
result := <-saveResponseChan
|
||||
err = result.err
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrOverwrite) {
|
||||
screen.TermMessage(err)
|
||||
err = errors.Unwrap(err)
|
||||
|
||||
b.UpdateModTime()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if result.size > LargeFileThreshold {
|
||||
// For large files 'fastdirty' needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
b.calcHash(&b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
newPath := b.Path != filename
|
||||
if newPath {
|
||||
b.RemoveBackup()
|
||||
}
|
||||
|
||||
b.Path = filename
|
||||
b.AbsPath = absFilename
|
||||
b.isModified = false
|
||||
b.UpdateModTime()
|
||||
|
||||
if newPath {
|
||||
// need to update glob-based and filetype-based settings
|
||||
b.ReloadSettings(true)
|
||||
}
|
||||
|
||||
err = b.Serialize()
|
||||
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 *SharedBuffer) 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)
|
||||
}
|
||||
}()
|
||||
|
||||
// Try to backup first before writing
|
||||
backupName, resolveName, err := b.writeBackup(path)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Backup saved, so cancel pending periodic backup, if any
|
||||
delete(requestedBackups, b)
|
||||
|
||||
b.forceKeepBackup = true
|
||||
size := 0
|
||||
{
|
||||
// If we failed to write or close, keep the backup we made
|
||||
size, err = file.Write(b)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return 0, util.OverwriteError{err, backupName}
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return 0, util.OverwriteError{err, backupName}
|
||||
}
|
||||
}
|
||||
b.forceKeepBackup = false
|
||||
|
||||
if !b.keepBackup() {
|
||||
b.removeBackup(backupName, resolveName)
|
||||
}
|
||||
|
||||
return size, err
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/micro-editor/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 != 0 {
|
||||
re, err := regexp.Compile(r.String() + `\E`)
|
||||
if err == nil {
|
||||
// r contains \Q without closing \E
|
||||
r = re
|
||||
}
|
||||
|
||||
if padMode == padStart {
|
||||
r = regexp.MustCompile(".(?:" + r.String() + ")")
|
||||
} else if padMode == padEnd {
|
||||
r = regexp.MustCompile("(?:" + r.String() + ").")
|
||||
} else {
|
||||
// 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) {
|
||||
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
|
||||
if start.Y > b.LinesNum()-1 {
|
||||
start.X = lastcn - 1
|
||||
}
|
||||
if end.Y > b.LinesNum()-1 {
|
||||
end.X = lastcn
|
||||
}
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l, charpos, padMode, rPadded := findLineParams(b, start, end, i, r)
|
||||
|
||||
match := rPadded.FindIndex(l)
|
||||
|
||||
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}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
|
||||
if start.Y > b.LinesNum()-1 {
|
||||
start.X = lastcn - 1
|
||||
}
|
||||
if end.Y > b.LinesNum()-1 {
|
||||
end.X = lastcn
|
||||
}
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := end.Y; i >= start.Y; i-- {
|
||||
charCount := util.CharacterCount(b.LineBytes(i))
|
||||
from := Loc{0, i}.Clamp(start, end)
|
||||
to := Loc{charCount, i}.Clamp(start, end)
|
||||
|
||||
allMatches := b.findAll(r, from, to)
|
||||
if allMatches != nil {
|
||||
match := allMatches[len(allMatches)-1]
|
||||
return [2]Loc{match[0], match[1]}, true
|
||||
}
|
||||
}
|
||||
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
|
||||
// It returns the start and end location of the match (if found) and
|
||||
// a boolean indicating if it was found
|
||||
// May also return an error if the search regex is invalid
|
||||
func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bool) ([2]Loc, bool, error) {
|
||||
if s == "" {
|
||||
return [2]Loc{}, false, nil
|
||||
}
|
||||
|
||||
var r *regexp.Regexp
|
||||
var err error
|
||||
|
||||
if !useRegex {
|
||||
s = regexp.QuoteMeta(s)
|
||||
}
|
||||
|
||||
if b.Settings["ignorecase"].(bool) {
|
||||
r, err = regexp.Compile("(?i)" + s)
|
||||
} else {
|
||||
r, err = regexp.Compile(s)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return [2]Loc{}, false, err
|
||||
}
|
||||
|
||||
var found bool
|
||||
var l [2]Loc
|
||||
if down {
|
||||
l, found = b.findDown(r, from, end)
|
||||
if !found {
|
||||
l, found = b.findDown(r, start, end)
|
||||
}
|
||||
} else {
|
||||
l, found = b.findUp(r, from, start)
|
||||
if !found {
|
||||
l, found = b.findUp(r, end, start)
|
||||
}
|
||||
}
|
||||
return l, found, nil
|
||||
}
|
||||
|
||||
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
||||
// and returns the number of replacements made and the number of characters
|
||||
// added or removed on the last line of the range
|
||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) {
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
charsEnd := util.CharacterCount(b.LineBytes(end.Y))
|
||||
found := 0
|
||||
var deltas []Delta
|
||||
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.LineBytes(i)
|
||||
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
|
||||
|
||||
from := Loc{0, i}.Clamp(start, end)
|
||||
to := Loc{charCount, i}.Clamp(start, end)
|
||||
matches := b.findAll(search, from, to)
|
||||
found += len(matches)
|
||||
|
||||
for j := len(matches) - 1; j >= 0; j-- {
|
||||
// if we counted upwards, the different deltas would interfere
|
||||
match := matches[j]
|
||||
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}})
|
||||
}
|
||||
}
|
||||
|
||||
b.MultipleReplace(deltas)
|
||||
|
||||
return found, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
"github.com/micro-editor/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Loc
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to config.ConfigDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
|
||||
return nil
|
||||
}
|
||||
if b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := SerializedBuffer{
|
||||
Cursor: b.GetActiveCursor().Loc,
|
||||
ModTime: b.ModTime,
|
||||
}
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
buffer.EventHandler = b.EventHandler
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := gob.NewEncoder(&buf).Encode(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name, resolveName := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)
|
||||
err = util.SafeWrite(name, buf.Bytes(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resolveName != "" {
|
||||
err = util.SafeWrite(resolveName, []byte(b.AbsPath), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unserialize loads the buffer info from config.ConfigDir/buffers
|
||||
func (b *Buffer) Unserialize() error {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
if b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
name, _ := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)
|
||||
file, err := os.Open(name)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
return errors.New(err.Error() + "\nYou may want to remove the files in ~/.config/micro/buffers (these files\nstore the information for the 'saveundo' and 'savecursor' options) if\nthis problem persists.\nThis may be caused by upgrading to version 2.0, and removing the 'buffers'\ndirectory will reset the cursor and undo history and solve the problem.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.StartCursor = buffer.Cursor
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) && buffer.EventHandler != nil {
|
||||
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.buf = b.SharedBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"reflect"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/config"
|
||||
ulua "github.com/micro-editor/micro/v2/internal/lua"
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
func (b *Buffer) ReloadSettings(reloadFiletype bool) {
|
||||
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 any) {
|
||||
oldValue := b.Settings[option]
|
||||
if reflect.DeepEqual(oldValue, nativeValue) {
|
||||
return
|
||||
}
|
||||
|
||||
b.Settings[option] = nativeValue
|
||||
|
||||
if option == "fastdirty" {
|
||||
if !nativeValue.(bool) {
|
||||
if b.Size() > LargeFileThreshold {
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
if !b.isModified {
|
||||
b.calcHash(&b.origHash)
|
||||
} else {
|
||||
// prevent using an old stale origHash value
|
||||
b.origHash = [md5.Size]byte{}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if option == "statusline" {
|
||||
screen.Redraw()
|
||||
} else if option == "filetype" {
|
||||
b.ReloadSettings(false)
|
||||
} else if option == "fileformat" {
|
||||
switch b.Settings["fileformat"].(string) {
|
||||
case "unix":
|
||||
b.Endings = FFUnix
|
||||
case "dos":
|
||||
b.Endings = FFDos
|
||||
}
|
||||
b.setModified()
|
||||
} else if option == "syntax" {
|
||||
if !nativeValue.(bool) {
|
||||
b.ClearMatches()
|
||||
} else {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} 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.setModified()
|
||||
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
|
||||
b.Type.Readonly = nativeValue.(bool)
|
||||
} else if option == "hlsearch" {
|
||||
for _, buf := range OpenBuffers {
|
||||
if b.SharedBuffer == buf.SharedBuffer {
|
||||
buf.HighlightSearch = nativeValue.(bool)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, pl := range config.Plugins {
|
||||
if option == pl.Name {
|
||||
if nativeValue.(bool) {
|
||||
if !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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.doCallbacks(option, oldValue, nativeValue)
|
||||
}
|
||||
|
||||
func (b *Buffer) SetOptionNative(option string, nativeValue any) error {
|
||||
if err := config.OptionIsValid(option, nativeValue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.DoSetOptionNative(option, nativeValue)
|
||||
b.LocalSettings[option] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOption sets a given option to a value just for this buffer
|
||||
func (b *Buffer) SetOption(option, value string) error {
|
||||
if _, ok := b.Settings[option]; !ok {
|
||||
return config.ErrInvalidOption
|
||||
}
|
||||
|
||||
nativeValue, err := config.GetNativeValue(option, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.SetOptionNative(option, nativeValue)
|
||||
}
|
||||
|
||||
func (b *Buffer) doCallbacks(option string, oldValue any, newValue any) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
s := new(TEStack)
|
||||
e1 := &TextEvent{
|
||||
EventType: TextEventReplace,
|
||||
Time: time.Now(),
|
||||
}
|
||||
e2 := &TextEvent{
|
||||
EventType: TextEventInsert,
|
||||
Time: time.Now(),
|
||||
}
|
||||
s.Push(e1)
|
||||
s.Push(e2)
|
||||
|
||||
p := s.Peek()
|
||||
assert.Equal(t, p.EventType, TextEventInsert)
|
||||
p = s.Pop()
|
||||
assert.Equal(t, p.EventType, TextEventInsert)
|
||||
p = s.Peek()
|
||||
assert.Equal(t, p.EventType, TextEventReplace)
|
||||
p = s.Pop()
|
||||
assert.Equal(t, p.EventType, TextEventReplace)
|
||||
p = s.Pop()
|
||||
assert.Nil(t, p)
|
||||
p = s.Peek()
|
||||
assert.Nil(t, p)
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/zyedidia/clipper"
|
||||
)
|
||||
|
||||
type Method int
|
||||
|
||||
const (
|
||||
// External relies on external tools for accessing the clipboard
|
||||
// These include xclip, xsel, wl-clipboard for linux, pbcopy/pbpaste on Mac,
|
||||
// and Syscalls on Windows.
|
||||
External Method = iota
|
||||
// Terminal uses the terminal to manage the clipboard via OSC 52. Many
|
||||
// terminals do not support OSC 52, in which case this method won't work.
|
||||
Terminal
|
||||
// Internal just manages the clipboard with an internal buffer and doesn't
|
||||
// attempt to interface with the system clipboard
|
||||
Internal
|
||||
)
|
||||
|
||||
// CurrentMethod is the method used to store clipboard information
|
||||
var CurrentMethod Method = Internal
|
||||
|
||||
// A Register is a buffer used to store text. The system clipboard has the 'clipboard'
|
||||
// and 'primary' (linux-only) registers, but other registers may be used internal to micro.
|
||||
type Register int
|
||||
|
||||
const (
|
||||
// ClipboardReg is the main system clipboard
|
||||
ClipboardReg Register = -1
|
||||
// PrimaryReg is the system primary clipboard (linux only)
|
||||
PrimaryReg = -2
|
||||
)
|
||||
|
||||
var clipboard clipper.Clipboard
|
||||
|
||||
// Initialize attempts to initialize the clipboard using the given method
|
||||
func Initialize(m Method) error {
|
||||
var err error
|
||||
switch m {
|
||||
case External:
|
||||
clips := make([]clipper.Clipboard, 0, len(clipper.Clipboards)+1)
|
||||
clips = append(clips, &clipper.Custom{
|
||||
Name: "micro-clip",
|
||||
})
|
||||
clips = append(clips, clipper.Clipboards...)
|
||||
clipboard, err = clipper.GetClipboard(clips...)
|
||||
}
|
||||
if err != nil {
|
||||
CurrentMethod = Internal
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SetMethod changes the clipboard access method
|
||||
func SetMethod(m string) Method {
|
||||
switch m {
|
||||
case "internal":
|
||||
CurrentMethod = Internal
|
||||
case "external":
|
||||
CurrentMethod = External
|
||||
case "terminal":
|
||||
CurrentMethod = Terminal
|
||||
}
|
||||
return CurrentMethod
|
||||
}
|
||||
|
||||
// Read reads from a clipboard register
|
||||
func Read(r Register) (string, error) {
|
||||
return read(r, CurrentMethod)
|
||||
}
|
||||
|
||||
// Write writes text to a clipboard register
|
||||
func Write(text string, r Register) error {
|
||||
return write(text, r, CurrentMethod)
|
||||
}
|
||||
|
||||
// ReadMulti reads text from a clipboard register for a certain multi-cursor
|
||||
func ReadMulti(r Register, num, ncursors int) (string, error) {
|
||||
clip, err := Read(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ValidMulti(r, clip, ncursors) {
|
||||
return multi.getText(r, num), nil
|
||||
}
|
||||
return clip, nil
|
||||
}
|
||||
|
||||
// WriteMulti writes text to a clipboard register for a certain multi-cursor
|
||||
func WriteMulti(text string, r Register, num int, ncursors int) error {
|
||||
return writeMulti(text, r, num, ncursors, CurrentMethod)
|
||||
}
|
||||
|
||||
// ValidMulti checks if the internal multi-clipboard is valid and up-to-date
|
||||
// with the system clipboard
|
||||
func ValidMulti(r Register, clip string, ncursors int) bool {
|
||||
return multi.isValid(r, clip, ncursors)
|
||||
}
|
||||
|
||||
func writeMulti(text string, r Register, num int, ncursors int, m Method) error {
|
||||
multi.writeText(text, r, num, ncursors)
|
||||
return write(multi.getAllText(r), r, m)
|
||||
}
|
||||
|
||||
func read(r Register, m Method) (string, error) {
|
||||
switch m {
|
||||
case External:
|
||||
switch r {
|
||||
case ClipboardReg:
|
||||
b, e := clipboard.ReadAll(clipper.RegClipboard)
|
||||
return string(b), e
|
||||
case PrimaryReg:
|
||||
b, e := clipboard.ReadAll(clipper.RegPrimary)
|
||||
return string(b), e
|
||||
default:
|
||||
return internal.read(r), nil
|
||||
}
|
||||
case Internal:
|
||||
return internal.read(r), nil
|
||||
case Terminal:
|
||||
switch r {
|
||||
case ClipboardReg:
|
||||
// terminal paste works by sending an esc sequence to the
|
||||
// terminal to trigger a paste event
|
||||
return terminal.read("clipboard")
|
||||
case PrimaryReg:
|
||||
return terminal.read("primary")
|
||||
default:
|
||||
return internal.read(r), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("Invalid clipboard method")
|
||||
}
|
||||
|
||||
func write(text string, r Register, m Method) error {
|
||||
switch m {
|
||||
case External:
|
||||
switch r {
|
||||
case ClipboardReg:
|
||||
return clipboard.WriteAll(clipper.RegClipboard, []byte(text))
|
||||
case PrimaryReg:
|
||||
return clipboard.WriteAll(clipper.RegPrimary, []byte(text))
|
||||
default:
|
||||
internal.write(text, r)
|
||||
}
|
||||
case Internal:
|
||||
internal.write(text, r)
|
||||
case Terminal:
|
||||
switch r {
|
||||
case ClipboardReg:
|
||||
return terminal.write(text, "c")
|
||||
case PrimaryReg:
|
||||
return terminal.write(text, "p")
|
||||
default:
|
||||
internal.write(text, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
type internalClipboard map[Register]string
|
||||
|
||||
var internal internalClipboard
|
||||
|
||||
func init() {
|
||||
internal = make(internalClipboard)
|
||||
}
|
||||
|
||||
func (c internalClipboard) read(r Register) string {
|
||||
return c[r]
|
||||
}
|
||||
|
||||
func (c internalClipboard) write(text string, r Register) {
|
||||
c[r] = text
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// For storing multi cursor clipboard contents
|
||||
type multiClipboard map[Register][]string
|
||||
|
||||
var multi multiClipboard
|
||||
|
||||
func (c multiClipboard) getAllText(r Register) string {
|
||||
content := c[r]
|
||||
if content == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
for _, s := range content {
|
||||
buf.WriteString(s)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (c multiClipboard) getText(r Register, num int) string {
|
||||
content := c[r]
|
||||
if content == nil || len(content) <= num {
|
||||
return ""
|
||||
}
|
||||
|
||||
return content[num]
|
||||
}
|
||||
|
||||
// isValid checks if the text stored in this multi-clipboard is the same as the
|
||||
// text stored in the system clipboard (provided as an argument), and therefore
|
||||
// if it is safe to use the multi-clipboard for pasting instead of the system
|
||||
// clipboard.
|
||||
func (c multiClipboard) isValid(r Register, clipboard string, ncursors int) bool {
|
||||
content := c[r]
|
||||
if content == nil || len(content) != ncursors {
|
||||
return false
|
||||
}
|
||||
|
||||
return clipboard == c.getAllText(r)
|
||||
}
|
||||
|
||||
func (c multiClipboard) writeText(text string, r Register, num int, ncursors int) {
|
||||
content := c[r]
|
||||
if content == nil || len(content) != ncursors {
|
||||
content = make([]string, ncursors, ncursors)
|
||||
c[r] = content
|
||||
}
|
||||
|
||||
if num >= ncursors {
|
||||
return
|
||||
}
|
||||
|
||||
content[num] = text
|
||||
}
|
||||
|
||||
func init() {
|
||||
multi = make(multiClipboard)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/micro-editor/micro/v2/internal/screen"
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
type terminalClipboard struct{}
|
||||
|
||||
var terminal terminalClipboard
|
||||
|
||||
func (t terminalClipboard) read(reg string) (string, error) {
|
||||
screen.Screen.GetClipboard(reg)
|
||||
// wait at most 200ms for response
|
||||
for {
|
||||
select {
|
||||
case event := <-screen.Events:
|
||||
e, ok := event.(*tcell.EventPaste)
|
||||
if ok {
|
||||
return e.Text(), nil
|
||||
}
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
return "", errors.New("No clipboard received from terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t terminalClipboard) write(text, reg string) error {
|
||||
return screen.Screen.SetClipboard(text, reg)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var Autosave chan bool
|
||||
var autotime chan float64
|
||||
|
||||
func init() {
|
||||
Autosave = make(chan bool)
|
||||
autotime = make(chan float64)
|
||||
}
|
||||
|
||||
func SetAutoTime(a float64) {
|
||||
autotime <- a
|
||||
}
|
||||
|
||||
func StartAutoSave() {
|
||||
go func() {
|
||||
var a float64
|
||||
var t *time.Timer
|
||||
var elapsed <-chan time.Time
|
||||
for {
|
||||
select {
|
||||
case a = <-autotime:
|
||||
if t != nil {
|
||||
t.Stop()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/micro-editor/tcell/v2"
|
||||
)
|
||||
|
||||
// DefStyle is Micro's default style
|
||||
var DefStyle tcell.Style = tcell.StyleDefault
|
||||
|
||||
// Colorscheme is the current colorscheme
|
||||
var Colorscheme map[string]tcell.Style
|
||||
|
||||
// GetColor takes in a syntax group and returns the colorscheme's style for that group
|
||||
func GetColor(color string) tcell.Style {
|
||||
st := DefStyle
|
||||
if color == "" {
|
||||
return st
|
||||
}
|
||||
groups := strings.Split(color, ".")
|
||||
if len(groups) > 1 {
|
||||
curGroup := ""
|
||||
for i, g := range groups {
|
||||
if i != 0 {
|
||||
curGroup += "."
|
||||
}
|
||||
curGroup += g
|
||||
if style, ok := Colorscheme[curGroup]; ok {
|
||||
st = style
|
||||
}
|
||||
}
|
||||
} else if style, ok := Colorscheme[color]; ok {
|
||||
st = style
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
}
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
// ColorschemeExists checks if a given colorscheme exists
|
||||
func ColorschemeExists(colorschemeName string) bool {
|
||||
return FindRuntimeFile(RTColorscheme, colorschemeName) != nil
|
||||
}
|
||||
|
||||
// InitColorscheme picks and initializes the colorscheme when micro starts
|
||||
func InitColorscheme() error {
|
||||
Colorscheme = make(map[string]tcell.Style)
|
||||
DefStyle = tcell.StyleDefault
|
||||
|
||||
c, err := LoadDefaultColorscheme()
|
||||
if err == nil {
|
||||
Colorscheme = c
|
||||
} else {
|
||||
// The colorscheme setting seems broken (maybe because we have not validated
|
||||
// it earlier, see comment in verifySetting()). So reset it to the default
|
||||
// colorscheme and try again.
|
||||
GlobalSettings["colorscheme"] = DefaultGlobalOnlySettings["colorscheme"]
|
||||
if c, err2 := LoadDefaultColorscheme(); err2 == nil {
|
||||
Colorscheme = c
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
|
||||
func LoadDefaultColorscheme() (map[string]tcell.Style, error) {
|
||||
var parsedColorschemes []string
|
||||
return LoadColorscheme(GlobalSettings["colorscheme"].(string), &parsedColorschemes)
|
||||
}
|
||||
|
||||
// LoadColorscheme loads the given colorscheme from a directory
|
||||
func LoadColorscheme(colorschemeName string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
|
||||
c := make(map[string]tcell.Style)
|
||||
file := FindRuntimeFile(RTColorscheme, colorschemeName)
|
||||
if file == nil {
|
||||
return c, errors.New(colorschemeName + " is not a valid colorscheme")
|
||||
}
|
||||
if data, err := file.Data(); err != nil {
|
||||
return c, errors.New("Error loading colorscheme: " + err.Error())
|
||||
} else {
|
||||
var err error
|
||||
c, err = ParseColorscheme(file.Name(), string(data), parsedColorschemes)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
|
||||
// red background
|
||||
func ParseColorscheme(name string, text string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
|
||||
var err error
|
||||
colorParser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||
includeParser := regexp.MustCompile(`include\s+"(.*)"`)
|
||||
lines := strings.Split(text, "\n")
|
||||
c := make(map[string]tcell.Style)
|
||||
|
||||
if parsedColorschemes != nil {
|
||||
*parsedColorschemes = append(*parsedColorschemes, name)
|
||||
}
|
||||
|
||||
lineLoop:
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
strings.TrimSpace(line)[0] == '#' {
|
||||
// Ignore this line
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
link := string(matches[1])
|
||||
colors := string(matches[2])
|
||||
|
||||
style := StringToStyle(colors)
|
||||
c[link] = style
|
||||
|
||||
if link == "default" {
|
||||
DefStyle = style
|
||||
}
|
||||
} else {
|
||||
err = errors.New("Color-link statement is not valid: " + line)
|
||||
}
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
// StringToStyle returns a style from a string
|
||||
// The strings must be in the format "extra foregroundcolor,backgroundcolor"
|
||||
// The 'extra' can be bold, reverse, italic or underline
|
||||
func StringToStyle(str string) tcell.Style {
|
||||
var fg, bg string
|
||||
spaceSplit := strings.Split(str, " ")
|
||||
split := strings.Split(spaceSplit[len(spaceSplit)-1], ",")
|
||||
if len(split) > 1 {
|
||||
fg, bg = split[0], split[1]
|
||||
} else {
|
||||
fg = split[0]
|
||||
}
|
||||
fg = strings.TrimSpace(fg)
|
||||
bg = strings.TrimSpace(bg)
|
||||
|
||||
var fgColor, bgColor tcell.Color
|
||||
var ok bool
|
||||
if fg == "" || fg == "default" {
|
||||
fgColor, _, _ = DefStyle.Decompose()
|
||||
} else {
|
||||
fgColor, ok = StringToColor(fg)
|
||||
if !ok {
|
||||
fgColor, _, _ = DefStyle.Decompose()
|
||||
}
|
||||
}
|
||||
if bg == "" || bg == "default" {
|
||||
_, bgColor, _ = DefStyle.Decompose()
|
||||
} else {
|
||||
bgColor, ok = StringToColor(bg)
|
||||
if !ok {
|
||||
_, bgColor, _ = DefStyle.Decompose()
|
||||
}
|
||||
}
|
||||
|
||||
style := DefStyle.Foreground(fgColor).Background(bgColor)
|
||||
if strings.Contains(str, "bold") {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
if strings.Contains(str, "italic") {
|
||||
style = style.Italic(true)
|
||||
}
|
||||
if strings.Contains(str, "reverse") {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
if strings.Contains(str, "underline") {
|
||||
style = style.Underline(true)
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// StringToColor returns a tcell color from a string representation of a color
|
||||
// We accept either bright... or light... to mean the brighter version of a color
|
||||
func StringToColor(str string) (tcell.Color, bool) {
|
||||
switch str {
|
||||
case "black":
|
||||
return tcell.ColorBlack, true
|
||||
case "red":
|
||||
return tcell.ColorMaroon, true
|
||||
case "green":
|
||||
return tcell.ColorGreen, true
|
||||
case "yellow":
|
||||
return tcell.ColorOlive, true
|
||||
case "blue":
|
||||
return tcell.ColorNavy, true
|
||||
case "magenta":
|
||||
return tcell.ColorPurple, true
|
||||
case "cyan":
|
||||
return tcell.ColorTeal, true
|
||||
case "white":
|
||||
return tcell.ColorSilver, true
|
||||
case "brightblack", "lightblack":
|
||||
return tcell.ColorGray, true
|
||||
case "brightred", "lightred":
|
||||
return tcell.ColorRed, true
|
||||
case "brightgreen", "lightgreen":
|
||||
return tcell.ColorLime, true
|
||||
case "brightyellow", "lightyellow":
|
||||
return tcell.ColorYellow, true
|
||||
case "brightblue", "lightblue":
|
||||
return tcell.ColorBlue, true
|
||||
case "brightmagenta", "lightmagenta":
|
||||
return tcell.ColorFuchsia, true
|
||||
case "brightcyan", "lightcyan":
|
||||
return tcell.ColorAqua, true
|
||||
case "brightwhite", "lightwhite":
|
||||
return tcell.ColorWhite, true
|
||||
case "default":
|
||||
return tcell.ColorDefault, true
|
||||
default:
|
||||
// Check if this is a 256 color
|
||||
if num, err := strconv.Atoi(str); err == nil {
|
||||
return GetColor256(num), true
|
||||
}
|
||||
// Check if this is a truecolor hex value
|
||||
if len(str) == 7 && str[0] == '#' {
|
||||
return tcell.GetColor(str), true
|
||||
}
|
||||
return tcell.ColorDefault, false
|
||||
}
|
||||
}
|
||||
|
||||
// GetColor256 returns the tcell color for a number between 0 and 255
|
||||
func GetColor256(color int) tcell.Color {
|
||||
if color == 0 {
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
return tcell.PaletteColor(color)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user