mirror of
https://github.com/zyedidia/micro.git
synced 2026-04-01 23:49:49 +09:00
Compare commits
290 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c577049c | ||
|
|
cc195b6a96 | ||
|
|
f74eb23827 | ||
|
|
fa9bc6b98c | ||
|
|
63ffc40a6b | ||
|
|
a09c98a6dc | ||
|
|
7dc78b780a | ||
|
|
b80ea93486 | ||
|
|
33b7c9db7c | ||
|
|
8b31dc79bf | ||
|
|
4170df89eb | ||
|
|
832c7deaf8 | ||
|
|
a678d42861 | ||
|
|
0542765d95 | ||
|
|
10511c9baf | ||
|
|
a3071b173b | ||
|
|
724e29446f | ||
|
|
05529596cf | ||
|
|
f661b64e0a | ||
|
|
4663927098 | ||
|
|
395d848980 | ||
|
|
c741e36d2d | ||
|
|
521b63a28c | ||
|
|
62c1c667b8 | ||
|
|
c67a30e611 | ||
|
|
c51f848df2 | ||
|
|
7f6e5bc860 | ||
|
|
9afcb80c95 | ||
|
|
fd3a00226c | ||
|
|
f88ac6d4fe | ||
|
|
0b15b57e63 | ||
|
|
d31095fe8f | ||
|
|
7fe98ccfee | ||
|
|
93efc9eabe | ||
|
|
352fd2be22 | ||
|
|
c0f6b65ed6 | ||
|
|
e0f5361d97 | ||
|
|
3737979139 | ||
|
|
be69b2580b | ||
|
|
21bb61c5ff | ||
|
|
d744872f4c | ||
|
|
a9c359a719 | ||
|
|
1fae584eea | ||
|
|
0d51035acd | ||
|
|
658c20ff2a | ||
|
|
2259fd10af | ||
|
|
b8772b69c2 | ||
|
|
e042bb3514 | ||
|
|
d173e527ac | ||
|
|
f8e532b0d7 | ||
|
|
0373a63b31 | ||
|
|
765889f610 | ||
|
|
5c8bf6b3a6 | ||
|
|
aa9c476b1e | ||
|
|
2793c37a94 | ||
|
|
7e09a921e4 | ||
|
|
e2e8baf4f3 | ||
|
|
fc5d83f6c6 | ||
|
|
781f057e6f | ||
|
|
762e31f2fd | ||
|
|
1f71667616 | ||
|
|
a10624cc33 | ||
|
|
bbf6ec292e | ||
|
|
dc7759204b | ||
|
|
a84aa225ab | ||
|
|
882b98f3f1 | ||
|
|
0fa4a3a8db | ||
|
|
531c7d88e2 | ||
|
|
c58ed0e51a | ||
|
|
f475220e67 | ||
|
|
57375e0732 | ||
|
|
d98fafd2f9 | ||
|
|
f5a9744bde | ||
|
|
a3e25e3701 | ||
|
|
f0f4afa272 | ||
|
|
3fb34cf4f2 | ||
|
|
f05d3582b3 | ||
|
|
dc62dd9d82 | ||
|
|
26ae1b95cc | ||
|
|
0a6b32d775 | ||
|
|
badaba66f3 | ||
|
|
602acff42f | ||
|
|
ced6d9487a | ||
|
|
c701ba66af | ||
|
|
fc3a5cd038 | ||
|
|
c965447416 | ||
|
|
352580a50a | ||
|
|
55f45ce8ff | ||
|
|
650c0a8db0 | ||
|
|
bad1a4b8ca | ||
|
|
5540cae610 | ||
|
|
25c7fa55b1 | ||
|
|
a85696d5e0 | ||
|
|
9face7484e | ||
|
|
f4d576b6e0 | ||
|
|
9eb8782ff2 | ||
|
|
46e55c8e91 | ||
|
|
dd913df9e9 | ||
|
|
e9bd1b35f4 | ||
|
|
4911a56181 | ||
|
|
343812bd2e | ||
|
|
35630aa736 | ||
|
|
78fcf2fc31 | ||
|
|
5dbdf8c0e8 | ||
|
|
889a841575 | ||
|
|
917650826a | ||
|
|
b70f0eb113 | ||
|
|
5a159ce444 | ||
|
|
bca35a5939 | ||
|
|
1f51d0b9e2 | ||
|
|
0a1447b688 | ||
|
|
2ecdac8405 | ||
|
|
385437d400 | ||
|
|
1c35f3dc39 | ||
|
|
07cda68795 | ||
|
|
3919cf399f | ||
|
|
b05df07df2 | ||
|
|
8af890a0a3 | ||
|
|
ff5b147639 | ||
|
|
3f810c24d2 | ||
|
|
26fa15c147 | ||
|
|
147943837d | ||
|
|
24406a5ae8 | ||
|
|
8632b82cbe | ||
|
|
fade304667 | ||
|
|
5b3737fb2a | ||
|
|
36bf3f6619 | ||
|
|
8c7f63ac15 | ||
|
|
18f3e1bf89 | ||
|
|
e48575f349 | ||
|
|
eec068a4fc | ||
|
|
5510317942 | ||
|
|
169a9a65fa | ||
|
|
c3052b491f | ||
|
|
b929c61228 | ||
|
|
08c516c730 | ||
|
|
1bddc8d03e | ||
|
|
f9cad2e448 | ||
|
|
3aed20fde9 | ||
|
|
a436dae587 | ||
|
|
5610d01e08 | ||
|
|
0806addbd7 | ||
|
|
6cd39efddc | ||
|
|
089160a7e4 | ||
|
|
ed993a4021 | ||
|
|
4cafa601b5 | ||
|
|
87ee41ab27 | ||
|
|
8d8bc58f91 | ||
|
|
6ffabd626f | ||
|
|
2c53d1fcab | ||
|
|
f265179def | ||
|
|
390794213e | ||
|
|
430da61314 | ||
|
|
f386b29e16 | ||
|
|
4283881591 | ||
|
|
186817d0c4 | ||
|
|
2a1790d15a | ||
|
|
c6dc5a4b1f | ||
|
|
426aa9bb8b | ||
|
|
acb0d763df | ||
|
|
d1d38d1ed7 | ||
|
|
467c71dbb8 | ||
|
|
a3ca054371 | ||
|
|
b6dcbfa846 | ||
|
|
6e71e37568 | ||
|
|
dd7134a762 | ||
|
|
2830c4878e | ||
|
|
53d56d032c | ||
|
|
c493e14eb4 | ||
|
|
69dc54b407 | ||
|
|
c5d32f625b | ||
|
|
baca0e5cb2 | ||
|
|
d67ce731ed | ||
|
|
828871acdf | ||
|
|
dc833d3552 | ||
|
|
08028cf415 | ||
|
|
93dd8ca729 | ||
|
|
3d7024e059 | ||
|
|
b291f27c3f | ||
|
|
3903859970 | ||
|
|
839e86849e | ||
|
|
20bf7096b8 | ||
|
|
d96f060b4c | ||
|
|
08892b125f | ||
|
|
838f371486 | ||
|
|
fc7efbdbe9 | ||
|
|
2ab1b3132e | ||
|
|
d64c9443f5 | ||
|
|
ee6519f5cb | ||
|
|
984c32b513 | ||
|
|
1595c5ddda | ||
|
|
1021f61a81 | ||
|
|
053949eac6 | ||
|
|
9ee82a6cb3 | ||
|
|
66a3839589 | ||
|
|
b2a428f1cd | ||
|
|
5492d30953 | ||
|
|
6c3b5ad17c | ||
|
|
39e410aa46 | ||
|
|
3f4942cedb | ||
|
|
2b8d925925 | ||
|
|
0c923aa156 | ||
|
|
13483602d5 | ||
|
|
c2c2b2addf | ||
|
|
8b4e9d2c5e | ||
|
|
a57d29ada9 | ||
|
|
bb1f4dad77 | ||
|
|
426e6c600f | ||
|
|
9ab9f8bc1c | ||
|
|
f15db6aa30 | ||
|
|
4895a29be2 | ||
|
|
b518bda50c | ||
|
|
c64add289b | ||
|
|
5ae2799b70 | ||
|
|
16e38b988c | ||
|
|
8a3d83f7c7 | ||
|
|
55b251ffee | ||
|
|
8724709cf9 | ||
|
|
4a53419c62 | ||
|
|
8af304cc21 | ||
|
|
399134fe5b | ||
|
|
db26b5fee5 | ||
|
|
cb903f414c | ||
|
|
0a69cc68dc | ||
|
|
1d1b363fa7 | ||
|
|
4ffc2206ee | ||
|
|
9089e9ec83 | ||
|
|
c24604d1ab | ||
|
|
606bcecf03 | ||
|
|
94af5f13bd | ||
|
|
80db98dc81 | ||
|
|
e424537ff8 | ||
|
|
5bfda7b5f6 | ||
|
|
3dba23a348 | ||
|
|
00174bb376 | ||
|
|
c4c5b184c2 | ||
|
|
7b718cb87c | ||
|
|
6dc3df646b | ||
|
|
13d1407f60 | ||
|
|
53efce72fa | ||
|
|
f108c90643 | ||
|
|
b824e767d6 | ||
|
|
c52ccad14b | ||
|
|
104caf08dd | ||
|
|
64370b70d6 | ||
|
|
8368af3cc8 | ||
|
|
ca3a9d0794 | ||
|
|
bd306d67b4 | ||
|
|
a01ae92541 | ||
|
|
dcdd3e749a | ||
|
|
628d9bb37b | ||
|
|
bfc4b1d195 | ||
|
|
d2ee6107a3 | ||
|
|
69eaa9191a | ||
|
|
14dca7d349 | ||
|
|
fad4e449fb | ||
|
|
f0bc6281d4 | ||
|
|
fe4ade78df | ||
|
|
88b4498ce0 | ||
|
|
3fce03dfd0 | ||
|
|
15b36ce0d6 | ||
|
|
321322af31 | ||
|
|
0de16334d3 | ||
|
|
c15abea64c | ||
|
|
9fdea82542 | ||
|
|
eedebd80d4 | ||
|
|
e5026ef3fa | ||
|
|
af2ec9d540 | ||
|
|
59dda01cb7 | ||
|
|
fce8db80de | ||
|
|
e5a9b906f3 | ||
|
|
422305af99 | ||
|
|
4e383dd110 | ||
|
|
2d82362a66 | ||
|
|
359b58a89b | ||
|
|
a373d22939 | ||
|
|
12398916c7 | ||
|
|
c791cef9c6 | ||
|
|
3c16df87ee | ||
|
|
2d0d0416e7 | ||
|
|
2aa386f455 | ||
|
|
93151f8109 | ||
|
|
433879046e | ||
|
|
d8e9d61a95 | ||
|
|
6fa12743d6 | ||
|
|
dcc7205699 | ||
|
|
2d95064ff6 | ||
|
|
e5093892fd | ||
|
|
124fa9e2e7 | ||
|
|
34ac83b594 |
46
.github/workflows/nightly.yaml
vendored
Normal file
46
.github/workflows/nightly.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Nightly builds
|
||||
on:
|
||||
workflow_dispatch: # Allows manual trigger
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
nightly:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Setup
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Build
|
||||
run: tools/cross-compile.sh nightly
|
||||
|
||||
- name: Tag
|
||||
uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: nightly
|
||||
force_push_tag: true
|
||||
message: "Pre-release nightly"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: nightly
|
||||
tag_name: nightly
|
||||
files: binaries/*
|
||||
prerelease: true
|
||||
|
||||
- name: Cleanup
|
||||
run: rm -rf binaries
|
||||
36
.github/workflows/release.yaml
vendored
Normal file
36
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Release builds
|
||||
on:
|
||||
workflow_dispatch: # Allows manual trigger
|
||||
# push:
|
||||
# tags:
|
||||
# - 'v*.*.*' # automatically react on semantic versioned tags
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.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
|
||||
9
.github/workflows/test.yaml
vendored
9
.github/workflows/test.yaml
vendored
@@ -8,10 +8,15 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- uses: actions/checkout@v3
|
||||
cache: false
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
36
README.md
36
README.md
@@ -68,6 +68,7 @@ You can also check out the website for Micro at https://micro-editor.github.io.
|
||||
- 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
|
||||
@@ -137,24 +138,33 @@ for other operating systems. These packages are not guaranteed to be up-to-date.
|
||||
|
||||
<!-- * `apt install micro` (Ubuntu 20.04 `focal`, and Debian `unstable | testing | buster-backports`). At the moment, this package (2.0.1-1) is outdated and has a known bug where debug mode is enabled. -->
|
||||
|
||||
* Linux: Available in 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).
|
||||
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
|
||||
* Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop).
|
||||
* 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/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
|
||||
* distro-agnostic package managers:
|
||||
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
|
||||
* `flox install micro` (with [Flox](https://flox.dev))
|
||||
* Windows: [Chocolatey](https://chocolatey.org), [Scoop](https://scoop.sh/) and [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/winget/).
|
||||
* `choco install micro`.
|
||||
* `scoop install micro`.
|
||||
* `winget install zyedidia.micro`
|
||||
* OpenBSD: Available in the ports tree and also available as a binary package.
|
||||
* `pkd_add -v micro`.
|
||||
* `pkg_add -v micro`.
|
||||
* NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](http://www.pkgsrc.org/)-current:
|
||||
* `pkg_add micro`
|
||||
* macOS with [MacPorts](https://www.macports.org):
|
||||
* `sudo port install 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:**
|
||||
|
||||
|
||||
@@ -12,5 +12,4 @@ Keywords=text;editor;syntax;terminal;
|
||||
Exec=micro %F
|
||||
StartupNotify=false
|
||||
Terminal=true
|
||||
NoDisplay=true
|
||||
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
@@ -47,14 +48,18 @@ func luaImportMicro() *lua.LTable {
|
||||
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
|
||||
ulua.L.SetField(pkg, "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.Pane {
|
||||
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, "Lock", luar.New(ulua.L, &ulua.Lock))
|
||||
ulua.L.SetField(pkg, "After", luar.New(ulua.L, func(t time.Duration, f func()) {
|
||||
time.AfterFunc(t, func() {
|
||||
timerChan <- f
|
||||
})
|
||||
}))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||
"github.com/zyedidia/micro/v2/internal/clipboard"
|
||||
"github.com/zyedidia/micro/v2/internal/config"
|
||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||
"github.com/zyedidia/micro/v2/internal/screen"
|
||||
"github.com/zyedidia/micro/v2/internal/shell"
|
||||
"github.com/zyedidia/micro/v2/internal/util"
|
||||
@@ -31,9 +30,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Event channel
|
||||
autosave chan bool
|
||||
|
||||
// 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")
|
||||
@@ -44,8 +40,9 @@ var (
|
||||
flagClean = flag.Bool("clean", false, "Clean configuration directory")
|
||||
optionFlags map[string]*string
|
||||
|
||||
sigterm chan os.Signal
|
||||
sighup chan os.Signal
|
||||
sighup chan os.Signal
|
||||
|
||||
timerChan chan func()
|
||||
)
|
||||
|
||||
func InitFlags() {
|
||||
@@ -68,7 +65,7 @@ func InitFlags() {
|
||||
fmt.Println("-version")
|
||||
fmt.Println(" \tShow the version number and information")
|
||||
|
||||
fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n")
|
||||
fmt.Print("\nMicro's plugins can be managed at the command line with the following commands.\n")
|
||||
fmt.Println("-plugin install [PLUGIN]...")
|
||||
fmt.Println(" \tInstall plugin(s)")
|
||||
fmt.Println("-plugin remove [PLUGIN]...")
|
||||
@@ -256,7 +253,9 @@ func main() {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
config.InitRuntimeFiles()
|
||||
config.InitRuntimeFiles(true)
|
||||
config.InitPlugins()
|
||||
|
||||
err = config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
@@ -274,7 +273,12 @@ func main() {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
if err = config.OptionIsValid(k, nativeValue); err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
config.GlobalSettings[k] = nativeValue
|
||||
config.VolatileSettings[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,18 +356,20 @@ func main() {
|
||||
log.Println(clipErr, " or change 'clipboard' option")
|
||||
}
|
||||
|
||||
config.StartAutoSave()
|
||||
if a := config.GetGlobalOption("autosave").(float64); a > 0 {
|
||||
config.SetAutoTime(int(a))
|
||||
config.StartAutoSave()
|
||||
config.SetAutoTime(a)
|
||||
}
|
||||
|
||||
screen.Events = make(chan tcell.Event)
|
||||
|
||||
sigterm = make(chan os.Signal, 1)
|
||||
util.Sigterm = make(chan os.Signal, 1)
|
||||
sighup = make(chan os.Signal, 1)
|
||||
signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
|
||||
signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
|
||||
signal.Notify(sighup, syscall.SIGHUP)
|
||||
|
||||
timerChan = make(chan func())
|
||||
|
||||
// Here is the event loop which runs in a separate thread
|
||||
go func() {
|
||||
for {
|
||||
@@ -414,21 +420,20 @@ func DoEvent() {
|
||||
select {
|
||||
case f := <-shell.Jobs:
|
||||
// If a new job has finished while running in the background we should execute the callback
|
||||
ulua.Lock.Lock()
|
||||
f.Function(f.Output, f.Args)
|
||||
ulua.Lock.Unlock()
|
||||
case <-config.Autosave:
|
||||
ulua.Lock.Lock()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.Save()
|
||||
b.AutoSave()
|
||||
}
|
||||
ulua.Lock.Unlock()
|
||||
case <-shell.CloseTerms:
|
||||
action.Tabs.CloseTerms()
|
||||
case event = <-screen.Events:
|
||||
case <-screen.DrawChan():
|
||||
for len(screen.DrawChan()) > 0 {
|
||||
<-screen.DrawChan()
|
||||
}
|
||||
case f := <-timerChan:
|
||||
f()
|
||||
case <-sighup:
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if !b.Modified() {
|
||||
@@ -436,7 +441,7 @@ func DoEvent() {
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
case <-sigterm:
|
||||
case <-util.Sigterm:
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if !b.Modified() {
|
||||
b.Fini()
|
||||
@@ -468,13 +473,20 @@ func DoEvent() {
|
||||
return
|
||||
}
|
||||
|
||||
ulua.Lock.Lock()
|
||||
// if event != nil {
|
||||
if action.InfoBar.HasPrompt {
|
||||
action.InfoBar.HandleEvent(event)
|
||||
} else {
|
||||
action.Tabs.HandleEvent(event)
|
||||
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)
|
||||
} else {
|
||||
action.Tabs.HandleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
err := config.RunPluginFn("onAnyEvent")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
// }
|
||||
ulua.Lock.Unlock()
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ func startup(args []string) (tcell.SimulationScreen, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.InitRuntimeFiles()
|
||||
config.InitRuntimeFiles(true)
|
||||
config.InitPlugins()
|
||||
|
||||
err = config.ReadSettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -107,7 +109,10 @@ func handleEvent() {
|
||||
if e != nil {
|
||||
screen.Events <- e
|
||||
}
|
||||
DoEvent()
|
||||
|
||||
for len(screen.DrawChan()) > 0 || len(screen.Events) > 0 {
|
||||
DoEvent()
|
||||
}
|
||||
}
|
||||
|
||||
func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
|
||||
@@ -149,6 +154,16 @@ func openFile(file string) {
|
||||
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(name string, content string) (string, error) {
|
||||
testf, err := ioutil.TempFile("", name)
|
||||
if err != nil {
|
||||
@@ -188,14 +203,7 @@ func TestSimpleEdit(t *testing.T) {
|
||||
|
||||
openFile(file)
|
||||
|
||||
var buf *buffer.Buffer
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if b.Path == file {
|
||||
buf = b
|
||||
}
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
if findBuffer(file) == nil {
|
||||
t.Errorf("Could not find buffer %s", file)
|
||||
return
|
||||
}
|
||||
@@ -234,6 +242,11 @@ func TestMouse(t *testing.T) {
|
||||
|
||||
openFile(file)
|
||||
|
||||
if findBuffer(file) == nil {
|
||||
t.Errorf("Could not find buffer %s", file)
|
||||
return
|
||||
}
|
||||
|
||||
// buffer:
|
||||
// base content
|
||||
// the selections need to happen at different locations to avoid a double click
|
||||
@@ -297,6 +310,11 @@ func TestSearchAndReplace(t *testing.T) {
|
||||
|
||||
openFile(file)
|
||||
|
||||
if findBuffer(file) == nil {
|
||||
t.Errorf("Could not find buffer %s", file)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<category>TextEditor</category>
|
||||
</categories>
|
||||
<releases>
|
||||
<release version="2.0.14" date="2024-08-27"/>
|
||||
<release version="2.0.13" date="2023-10-22"/>
|
||||
<release version="2.0.12" date="2023-09-06"/>
|
||||
<release version="2.0.11" date="2022-08-01"/>
|
||||
</releases>
|
||||
<provides>
|
||||
|
||||
@@ -170,10 +170,19 @@
|
||||
"default": false
|
||||
},
|
||||
"matchbrace": {
|
||||
"description": "Whether to underline matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||
"description": "Whether to show matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"matchbracestyle": {
|
||||
"description": "Whether to underline or highlight matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"underline",
|
||||
"highlight"
|
||||
],
|
||||
"default": "underline"
|
||||
},
|
||||
"mkparents": {
|
||||
"description": "Whether to create missing directories\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "boolean",
|
||||
@@ -296,8 +305,8 @@
|
||||
},
|
||||
"statusline": {
|
||||
"description": "Whether to display a status line\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||
"type": "string",
|
||||
"default": "sudo"
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"sucmd": {
|
||||
"description": "A super user command\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
|
||||
@@ -355,4 +364,4 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,53 +51,47 @@ func (h *BufPane) ScrollAdjust() {
|
||||
func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
|
||||
b := h.Buf
|
||||
mx, my := e.Position()
|
||||
// ignore click on the status line
|
||||
if my >= h.BufView().Y+h.BufView().Height {
|
||||
return false
|
||||
}
|
||||
mouseLoc := h.LocFromVisual(buffer.Loc{mx, my})
|
||||
h.Cursor.Loc = mouseLoc
|
||||
if h.mouseReleased {
|
||||
if b.NumCursors() > 1 {
|
||||
b.ClearCursors()
|
||||
h.Relocate()
|
||||
h.Cursor = h.Buf.GetActiveCursor()
|
||||
h.Cursor.Loc = mouseLoc
|
||||
}
|
||||
if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) {
|
||||
if h.doubleClick {
|
||||
// Triple click
|
||||
h.lastClickTime = time.Now()
|
||||
|
||||
h.tripleClick = true
|
||||
h.doubleClick = false
|
||||
|
||||
h.Cursor.SelectLine()
|
||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
||||
} else {
|
||||
// Double click
|
||||
h.lastClickTime = time.Now()
|
||||
|
||||
h.doubleClick = true
|
||||
h.tripleClick = false
|
||||
|
||||
h.Cursor.SelectWord()
|
||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
||||
}
|
||||
} else {
|
||||
h.doubleClick = false
|
||||
h.tripleClick = false
|
||||
if b.NumCursors() > 1 {
|
||||
b.ClearCursors()
|
||||
h.Relocate()
|
||||
h.Cursor = h.Buf.GetActiveCursor()
|
||||
h.Cursor.Loc = mouseLoc
|
||||
}
|
||||
if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) {
|
||||
if h.doubleClick {
|
||||
// Triple click
|
||||
h.lastClickTime = time.Now()
|
||||
|
||||
h.Cursor.OrigSelection[0] = h.Cursor.Loc
|
||||
h.Cursor.CurSelection[0] = h.Cursor.Loc
|
||||
h.Cursor.CurSelection[1] = h.Cursor.Loc
|
||||
}
|
||||
h.mouseReleased = false
|
||||
} else if !h.mouseReleased {
|
||||
if h.tripleClick {
|
||||
h.Cursor.AddLineToSelection()
|
||||
} else if h.doubleClick {
|
||||
h.Cursor.AddWordToSelection()
|
||||
h.tripleClick = true
|
||||
h.doubleClick = false
|
||||
|
||||
h.Cursor.SelectLine()
|
||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
||||
} else {
|
||||
h.Cursor.SetSelectionEnd(h.Cursor.Loc)
|
||||
// Double click
|
||||
h.lastClickTime = time.Now()
|
||||
|
||||
h.doubleClick = true
|
||||
h.tripleClick = false
|
||||
|
||||
h.Cursor.SelectWord()
|
||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
||||
}
|
||||
} else {
|
||||
h.doubleClick = false
|
||||
h.tripleClick = false
|
||||
h.lastClickTime = time.Now()
|
||||
|
||||
h.Cursor.OrigSelection[0] = h.Cursor.Loc
|
||||
h.Cursor.CurSelection[0] = h.Cursor.Loc
|
||||
h.Cursor.CurSelection[1] = h.Cursor.Loc
|
||||
}
|
||||
|
||||
h.Cursor.StoreVisualX()
|
||||
@@ -106,6 +100,45 @@ func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *BufPane) MouseDrag(e *tcell.EventMouse) bool {
|
||||
mx, my := e.Position()
|
||||
// ignore drag on the status line
|
||||
if my >= h.BufView().Y+h.BufView().Height {
|
||||
return false
|
||||
}
|
||||
h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my})
|
||||
|
||||
if h.tripleClick {
|
||||
h.Cursor.AddLineToSelection()
|
||||
} else if h.doubleClick {
|
||||
h.Cursor.AddWordToSelection()
|
||||
} else {
|
||||
h.Cursor.SelectTo(h.Cursor.Loc)
|
||||
}
|
||||
|
||||
h.Cursor.StoreVisualX()
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *BufPane) MouseRelease(e *tcell.EventMouse) bool {
|
||||
// We could finish the selection based on the release location as in the
|
||||
// commented out code below, to allow text selections even in a terminal
|
||||
// that doesn't support mouse motion events. But when the mouse click is
|
||||
// within the scroll margin, that would cause a scroll and selection
|
||||
// even for a simple mouse click, which is not good.
|
||||
// if !h.doubleClick && !h.tripleClick {
|
||||
// mx, my := e.Position()
|
||||
// h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my})
|
||||
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
|
||||
// }
|
||||
|
||||
if h.Cursor.HasSelection() {
|
||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ScrollUpAction scrolls the view up
|
||||
func (h *BufPane) ScrollUpAction() bool {
|
||||
h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"]))
|
||||
@@ -176,7 +209,7 @@ func (h *BufPane) CursorUp() bool {
|
||||
|
||||
// CursorDown moves the cursor down
|
||||
func (h *BufPane) CursorDown() bool {
|
||||
h.Cursor.Deselect(true)
|
||||
h.Cursor.Deselect(false)
|
||||
h.MoveCursorDown(1)
|
||||
h.Relocate()
|
||||
return true
|
||||
@@ -211,7 +244,7 @@ func (h *BufPane) CursorLeft() bool {
|
||||
func (h *BufPane) CursorRight() bool {
|
||||
if h.Cursor.HasSelection() {
|
||||
h.Cursor.Deselect(false)
|
||||
h.Cursor.Loc = h.Cursor.Loc.Move(1, h.Buf)
|
||||
h.Cursor.Right()
|
||||
} else {
|
||||
tabstospaces := h.Buf.Settings["tabstospaces"].(bool)
|
||||
tabmovement := h.Buf.Settings["tabmovement"].(bool)
|
||||
@@ -250,6 +283,22 @@ func (h *BufPane) WordLeft() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SubWordRight moves the cursor one sub-word to the right
|
||||
func (h *BufPane) SubWordRight() bool {
|
||||
h.Cursor.Deselect(false)
|
||||
h.Cursor.SubWordRight()
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// SubWordLeft moves the cursor one sub-word to the left
|
||||
func (h *BufPane) SubWordLeft() bool {
|
||||
h.Cursor.Deselect(true)
|
||||
h.Cursor.SubWordLeft()
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectUp selects up one line
|
||||
func (h *BufPane) SelectUp() bool {
|
||||
if !h.Cursor.HasSelection() {
|
||||
@@ -326,6 +375,28 @@ func (h *BufPane) SelectWordLeft() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectSubWordRight selects the sub-word to the right of the cursor
|
||||
func (h *BufPane) SelectSubWordRight() bool {
|
||||
if !h.Cursor.HasSelection() {
|
||||
h.Cursor.OrigSelection[0] = h.Cursor.Loc
|
||||
}
|
||||
h.Cursor.SubWordRight()
|
||||
h.Cursor.SelectTo(h.Cursor.Loc)
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectSubWordLeft selects the sub-word to the left of the cursor
|
||||
func (h *BufPane) SelectSubWordLeft() bool {
|
||||
if !h.Cursor.HasSelection() {
|
||||
h.Cursor.OrigSelection[0] = h.Cursor.Loc
|
||||
}
|
||||
h.Cursor.SubWordLeft()
|
||||
h.Cursor.SelectTo(h.Cursor.Loc)
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// StartOfText moves the cursor to the start of the text of the line
|
||||
func (h *BufPane) StartOfText() bool {
|
||||
h.Cursor.Deselect(true)
|
||||
@@ -419,38 +490,92 @@ func (h *BufPane) SelectToEndOfLine() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ParagraphPrevious moves the cursor to the previous empty line, or beginning of the buffer if there's none
|
||||
func (h *BufPane) ParagraphPrevious() bool {
|
||||
func (h *BufPane) paragraphPrevious() {
|
||||
var line int
|
||||
// Skip to the first non-empty line
|
||||
for line = h.Cursor.Y; line > 0; line-- {
|
||||
if len(h.Buf.LineBytes(line)) == 0 && line != h.Cursor.Y {
|
||||
if len(h.Buf.LineBytes(line)) != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Find the first empty line
|
||||
for ; line > 0; line-- {
|
||||
if len(h.Buf.LineBytes(line)) == 0 {
|
||||
h.Cursor.X = 0
|
||||
h.Cursor.Y = line
|
||||
break
|
||||
}
|
||||
}
|
||||
// If no empty line found. move cursor to end of buffer
|
||||
// If no empty line was found, move the cursor to the start of the buffer
|
||||
if line == 0 {
|
||||
h.Cursor.Loc = h.Buf.Start()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BufPane) paragraphNext() {
|
||||
var line int
|
||||
// Skip to the first non-empty line
|
||||
for line = h.Cursor.Y; line < h.Buf.LinesNum(); line++ {
|
||||
if len(h.Buf.LineBytes(line)) != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Find the first empty line
|
||||
for ; line < h.Buf.LinesNum(); line++ {
|
||||
if len(h.Buf.LineBytes(line)) == 0 {
|
||||
h.Cursor.X = 0
|
||||
h.Cursor.Y = line
|
||||
break
|
||||
}
|
||||
}
|
||||
// If no empty line was found, move the cursor to the end of the buffer
|
||||
if line == h.Buf.LinesNum() {
|
||||
h.Cursor.Loc = h.Buf.End()
|
||||
}
|
||||
}
|
||||
|
||||
// ParagraphPrevious moves the cursor to the first empty line that comes before
|
||||
// the paragraph closest to the cursor, or beginning of the buffer if there
|
||||
// isn't a paragraph
|
||||
func (h *BufPane) ParagraphPrevious() bool {
|
||||
h.Cursor.Deselect(true)
|
||||
h.paragraphPrevious()
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// ParagraphNext moves the cursor to the next empty line, or end of the buffer if there's none
|
||||
// ParagraphNext moves the cursor to the first empty line that comes after the
|
||||
// paragraph closest to the cursor, or end of the buffer if there isn't a
|
||||
// paragraph
|
||||
func (h *BufPane) ParagraphNext() bool {
|
||||
var line int
|
||||
for line = h.Cursor.Y; line < h.Buf.LinesNum(); line++ {
|
||||
if len(h.Buf.LineBytes(line)) == 0 && line != h.Cursor.Y {
|
||||
h.Cursor.X = 0
|
||||
h.Cursor.Y = line
|
||||
break
|
||||
}
|
||||
h.Cursor.Deselect(true)
|
||||
h.paragraphNext()
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectToParagraphPrevious selects to the first empty line that comes before
|
||||
// the paragraph closest to the cursor, or beginning of the buffer if there
|
||||
// isn't a paragraph
|
||||
func (h *BufPane) SelectToParagraphPrevious() bool {
|
||||
if !h.Cursor.HasSelection() {
|
||||
h.Cursor.OrigSelection[0] = h.Cursor.Loc
|
||||
}
|
||||
// If no empty line found. move cursor to end of buffer
|
||||
if line == h.Buf.LinesNum() {
|
||||
h.Cursor.Loc = h.Buf.End()
|
||||
h.paragraphPrevious()
|
||||
h.Cursor.SelectTo(h.Cursor.Loc)
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectToParagraphNext selects to the first empty line that comes after the
|
||||
// paragraph closest to the cursor, or end of the buffer if there isn't a
|
||||
// paragraph
|
||||
func (h *BufPane) SelectToParagraphNext() bool {
|
||||
if !h.Cursor.HasSelection() {
|
||||
h.Cursor.OrigSelection[0] = h.Cursor.Loc
|
||||
}
|
||||
h.paragraphNext()
|
||||
h.Cursor.SelectTo(h.Cursor.Loc)
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
@@ -589,6 +714,28 @@ func (h *BufPane) DeleteWordLeft() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteSubWordRight deletes the sub-word to the right of the cursor
|
||||
func (h *BufPane) DeleteSubWordRight() bool {
|
||||
h.SelectSubWordRight()
|
||||
if h.Cursor.HasSelection() {
|
||||
h.Cursor.DeleteSelection()
|
||||
h.Cursor.ResetSelection()
|
||||
}
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteSubWordLeft deletes the sub-word to the left of the cursor
|
||||
func (h *BufPane) DeleteSubWordLeft() bool {
|
||||
h.SelectSubWordLeft()
|
||||
if h.Cursor.HasSelection() {
|
||||
h.Cursor.DeleteSelection()
|
||||
h.Cursor.ResetSelection()
|
||||
}
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
// Delete deletes the next character
|
||||
func (h *BufPane) Delete() bool {
|
||||
if h.Cursor.HasSelection() {
|
||||
@@ -712,8 +859,8 @@ func (h *BufPane) Autocomplete() bool {
|
||||
}
|
||||
r := h.Cursor.RuneUnder(h.Cursor.X)
|
||||
prev := h.Cursor.RuneUnder(h.Cursor.X - 1)
|
||||
if !util.IsAutocomplete(prev) || !util.IsNonAlphaNumeric(r) {
|
||||
// don't autocomplete if cursor is on alpha numeric character (middle of a word)
|
||||
if !util.IsAutocomplete(prev) || util.IsWordChar(r) {
|
||||
// don't autocomplete if cursor is within a word
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -793,25 +940,26 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool {
|
||||
filename := strings.Join(args, " ")
|
||||
fileinfo, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
|
||||
noPrompt := h.saveBufToFile(filename, action, callback)
|
||||
if noPrompt {
|
||||
h.completeAction(action)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
InfoBar.YNPrompt(
|
||||
fmt.Sprintf("the file %s already exists in the directory, would you like to overwrite? Y/n", fileinfo.Name()),
|
||||
func(yes, canceled bool) {
|
||||
if yes && !canceled {
|
||||
noPrompt := h.saveBufToFile(filename, action, callback)
|
||||
if noPrompt {
|
||||
h.completeAction(action)
|
||||
} else {
|
||||
InfoBar.YNPrompt(
|
||||
fmt.Sprintf("The file %s already exists in the directory, would you like to overwrite? Y/n", fileinfo.Name()),
|
||||
func(yes, canceled bool) {
|
||||
if yes && !canceled {
|
||||
noPrompt := h.saveBufToFile(filename, action, callback)
|
||||
if noPrompt {
|
||||
h.completeAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
return false
|
||||
@@ -953,6 +1101,9 @@ func (h *BufPane) find(useRegex bool) bool {
|
||||
}
|
||||
}
|
||||
pattern := string(h.Cursor.GetSelection())
|
||||
if useRegex && pattern != "" {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
}
|
||||
if eventCallback != nil && pattern != "" {
|
||||
eventCallback(pattern)
|
||||
}
|
||||
@@ -971,12 +1122,27 @@ func (h *BufPane) ToggleHighlightSearch() bool {
|
||||
|
||||
// UnhighlightSearch unhighlights all instances of the last used search term
|
||||
func (h *BufPane) UnhighlightSearch() bool {
|
||||
if !h.Buf.HighlightSearch {
|
||||
return false
|
||||
}
|
||||
h.Buf.HighlightSearch = false
|
||||
return true
|
||||
}
|
||||
|
||||
// ResetSearch resets the last used search term
|
||||
func (h *BufPane) ResetSearch() bool {
|
||||
if h.Buf.LastSearch != "" {
|
||||
h.Buf.LastSearch = ""
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FindNext searches forwards for the last used search term
|
||||
func (h *BufPane) FindNext() bool {
|
||||
if h.Buf.LastSearch == "" {
|
||||
return false
|
||||
}
|
||||
// If the cursor is at the start of a selection and we search we want
|
||||
// to search from the end of the selection in the case that
|
||||
// the selection is a search result in which case we wouldn't move at
|
||||
@@ -1003,6 +1169,9 @@ func (h *BufPane) FindNext() bool {
|
||||
|
||||
// FindPrevious searches backwards for the last used search term
|
||||
func (h *BufPane) FindPrevious() bool {
|
||||
if h.Buf.LastSearch == "" {
|
||||
return false
|
||||
}
|
||||
// If the cursor is at the end of a selection and we search we want
|
||||
// to search from the beginning of the selection in the case that
|
||||
// the selection is a search result in which case we wouldn't move at
|
||||
@@ -1051,7 +1220,9 @@ func (h *BufPane) DiffPrevious() bool {
|
||||
|
||||
// Undo undoes the last action
|
||||
func (h *BufPane) Undo() bool {
|
||||
h.Buf.Undo()
|
||||
if !h.Buf.Undo() {
|
||||
return false
|
||||
}
|
||||
InfoBar.Message("Undid action")
|
||||
h.Relocate()
|
||||
return true
|
||||
@@ -1059,7 +1230,9 @@ func (h *BufPane) Undo() bool {
|
||||
|
||||
// Redo redoes the last action
|
||||
func (h *BufPane) Redo() bool {
|
||||
h.Buf.Redo()
|
||||
if !h.Buf.Redo() {
|
||||
return false
|
||||
}
|
||||
InfoBar.Message("Redid action")
|
||||
h.Relocate()
|
||||
return true
|
||||
@@ -1273,9 +1446,13 @@ func (h *BufPane) PastePrimary() bool {
|
||||
|
||||
func (h *BufPane) paste(clip string) {
|
||||
if h.Buf.Settings["smartpaste"].(bool) {
|
||||
if h.Cursor.X > 0 && len(util.GetLeadingWhitespace([]byte(strings.TrimLeft(clip, "\r\n")))) == 0 {
|
||||
leadingWS := util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y))
|
||||
clip = strings.ReplaceAll(clip, "\n", "\n"+string(leadingWS))
|
||||
if h.Cursor.X > 0 {
|
||||
leadingPasteWS := string(util.GetLeadingWhitespace([]byte(clip)))
|
||||
if leadingPasteWS != " " && strings.Contains(clip, "\n"+leadingPasteWS) {
|
||||
leadingWS := string(util.GetLeadingWhitespace(h.Buf.LineBytes(h.Cursor.Y)))
|
||||
clip = strings.TrimPrefix(clip, leadingPasteWS)
|
||||
clip = strings.ReplaceAll(clip, "\n"+leadingPasteWS, "\n"+leadingWS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1293,21 +1470,19 @@ func (h *BufPane) paste(clip string) {
|
||||
// JumpToMatchingBrace moves the cursor to the matching brace if it is
|
||||
// currently on a brace
|
||||
func (h *BufPane) JumpToMatchingBrace() bool {
|
||||
for _, bp := range buffer.BracePairs {
|
||||
r := h.Cursor.RuneUnder(h.Cursor.X)
|
||||
rl := h.Cursor.RuneUnder(h.Cursor.X - 1)
|
||||
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
|
||||
matchingBrace, left, found := h.Buf.FindMatchingBrace(bp, h.Cursor.Loc)
|
||||
if found {
|
||||
if left {
|
||||
h.Cursor.GotoLoc(matchingBrace)
|
||||
} else {
|
||||
h.Cursor.GotoLoc(matchingBrace.Move(1, h.Buf))
|
||||
}
|
||||
h.Relocate()
|
||||
return true
|
||||
matchingBrace, left, found := h.Buf.FindMatchingBrace(h.Cursor.Loc)
|
||||
if found {
|
||||
if h.Buf.Settings["matchbraceleft"].(bool) {
|
||||
if left {
|
||||
h.Cursor.GotoLoc(matchingBrace)
|
||||
} else {
|
||||
h.Cursor.GotoLoc(matchingBrace.Move(1, h.Buf))
|
||||
}
|
||||
} else {
|
||||
h.Cursor.GotoLoc(matchingBrace)
|
||||
}
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1439,9 +1614,7 @@ func (h *BufPane) HalfPageDown() bool {
|
||||
func (h *BufPane) ToggleDiffGutter() bool {
|
||||
if !h.Buf.Settings["diffgutter"].(bool) {
|
||||
h.Buf.Settings["diffgutter"] = true
|
||||
h.Buf.UpdateDiff(func(synchronous bool) {
|
||||
screen.Redraw()
|
||||
})
|
||||
h.Buf.UpdateDiff()
|
||||
InfoBar.Message("Enabled diff gutter")
|
||||
} else {
|
||||
h.Buf.Settings["diffgutter"] = false
|
||||
@@ -1462,10 +1635,9 @@ func (h *BufPane) ToggleRuler() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ClearStatus clears the messenger bar
|
||||
// ClearStatus clears the infobar. It is an alias for ClearInfo.
|
||||
func (h *BufPane) ClearStatus() bool {
|
||||
InfoBar.Message("")
|
||||
return true
|
||||
return h.ClearInfo()
|
||||
}
|
||||
|
||||
// ToggleHelp toggles the help screen
|
||||
@@ -1520,12 +1692,18 @@ func (h *BufPane) Escape() bool {
|
||||
|
||||
// Deselect deselects on the current cursor
|
||||
func (h *BufPane) Deselect() bool {
|
||||
if !h.Cursor.HasSelection() {
|
||||
return false
|
||||
}
|
||||
h.Cursor.Deselect(true)
|
||||
return true
|
||||
}
|
||||
|
||||
// ClearInfo clears the infobar
|
||||
func (h *BufPane) ClearInfo() bool {
|
||||
if InfoBar.Msg == "" {
|
||||
return false
|
||||
}
|
||||
InfoBar.Message("")
|
||||
return true
|
||||
}
|
||||
@@ -1616,6 +1794,10 @@ func (h *BufPane) AddTab() bool {
|
||||
// PreviousTab switches to the previous tab in the tab list
|
||||
func (h *BufPane) PreviousTab() bool {
|
||||
tabsLen := len(Tabs.List)
|
||||
if tabsLen == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
a := Tabs.Active() + tabsLen
|
||||
Tabs.SetActive((a - 1) % tabsLen)
|
||||
|
||||
@@ -1624,8 +1806,13 @@ func (h *BufPane) PreviousTab() bool {
|
||||
|
||||
// NextTab switches to the next tab in the tab list
|
||||
func (h *BufPane) NextTab() bool {
|
||||
tabsLen := len(Tabs.List)
|
||||
if tabsLen == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
a := Tabs.Active()
|
||||
Tabs.SetActive((a + 1) % len(Tabs.List))
|
||||
Tabs.SetActive((a + 1) % tabsLen)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1661,6 +1848,10 @@ func (h *BufPane) Unsplit() bool {
|
||||
|
||||
// NextSplit changes the view to the next split
|
||||
func (h *BufPane) NextSplit() bool {
|
||||
if len(h.tab.Panes) == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
a := h.tab.active
|
||||
if a < len(h.tab.Panes)-1 {
|
||||
a++
|
||||
@@ -1675,6 +1866,10 @@ func (h *BufPane) NextSplit() bool {
|
||||
|
||||
// PreviousSplit changes the view to the previous split
|
||||
func (h *BufPane) PreviousSplit() bool {
|
||||
if len(h.tab.Panes) == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
a := h.tab.active
|
||||
if a > 0 {
|
||||
a--
|
||||
@@ -1711,7 +1906,7 @@ func (h *BufPane) PlayMacro() bool {
|
||||
switch t := action.(type) {
|
||||
case rune:
|
||||
h.DoRuneInsert(t)
|
||||
case func(*BufPane) bool:
|
||||
case BufKeyAction:
|
||||
t(h)
|
||||
}
|
||||
}
|
||||
@@ -1760,15 +1955,39 @@ func (h *BufPane) SpawnMultiCursor() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y less.
|
||||
func (h *BufPane) SpawnMultiCursorUp() bool {
|
||||
if h.Cursor.Y == 0 {
|
||||
return false
|
||||
}
|
||||
h.Cursor.GotoLoc(buffer.Loc{h.Cursor.X, h.Cursor.Y - 1})
|
||||
h.Cursor.Relocate()
|
||||
// SpawnMultiCursorUpN is not an action
|
||||
func (h *BufPane) SpawnMultiCursorUpN(n int) bool {
|
||||
lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
|
||||
var c *buffer.Cursor
|
||||
if !h.Buf.Settings["softwrap"].(bool) {
|
||||
if n > 0 && lastC.Y == 0 {
|
||||
return false
|
||||
}
|
||||
if n < 0 && lastC.Y+1 == h.Buf.LinesNum() {
|
||||
return false
|
||||
}
|
||||
|
||||
h.Buf.DeselectCursors()
|
||||
|
||||
c = buffer.NewCursor(h.Buf, buffer.Loc{lastC.X, lastC.Y - n})
|
||||
c.LastVisualX = lastC.LastVisualX
|
||||
c.X = c.GetCharPosInLine(h.Buf.LineBytes(c.Y), c.LastVisualX)
|
||||
c.Relocate()
|
||||
} else {
|
||||
vloc := h.VLocFromLoc(lastC.Loc)
|
||||
sloc := h.Scroll(vloc.SLoc, -n)
|
||||
if sloc == vloc.SLoc {
|
||||
return false
|
||||
}
|
||||
|
||||
h.Buf.DeselectCursors()
|
||||
|
||||
vloc.SLoc = sloc
|
||||
vloc.VisualX = lastC.LastVisualX
|
||||
c = buffer.NewCursor(h.Buf, h.LocFromVLoc(vloc))
|
||||
c.LastVisualX = lastC.LastVisualX
|
||||
}
|
||||
|
||||
c := buffer.NewCursor(h.Buf, buffer.Loc{h.Cursor.X, h.Cursor.Y + 1})
|
||||
h.Buf.AddCursor(c)
|
||||
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
|
||||
h.Buf.MergeCursors()
|
||||
@@ -1777,20 +1996,14 @@ func (h *BufPane) SpawnMultiCursorUp() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SpawnMultiCursorUp creates additional cursor, at the same X (if possible), one Y less.
|
||||
func (h *BufPane) SpawnMultiCursorUp() bool {
|
||||
return h.SpawnMultiCursorUpN(1)
|
||||
}
|
||||
|
||||
// SpawnMultiCursorDown creates additional cursor, at the same X (if possible), one Y more.
|
||||
func (h *BufPane) SpawnMultiCursorDown() bool {
|
||||
if h.Cursor.Y+1 == h.Buf.LinesNum() {
|
||||
return false
|
||||
}
|
||||
h.Cursor.GotoLoc(buffer.Loc{h.Cursor.X, h.Cursor.Y + 1})
|
||||
h.Cursor.Relocate()
|
||||
|
||||
c := buffer.NewCursor(h.Buf, buffer.Loc{h.Cursor.X, h.Cursor.Y - 1})
|
||||
h.Buf.AddCursor(c)
|
||||
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
|
||||
h.Buf.MergeCursors()
|
||||
h.Relocate()
|
||||
return true
|
||||
return h.SpawnMultiCursorUpN(-1)
|
||||
}
|
||||
|
||||
// SpawnMultiCursorSelect adds a cursor at the beginning of each line of a selection
|
||||
@@ -1827,11 +2040,27 @@ func (h *BufPane) SpawnMultiCursorSelect() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position
|
||||
// MouseMultiCursor is a mouse action which puts a new cursor at the mouse position,
|
||||
// or removes a cursor if it is already there
|
||||
func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool {
|
||||
b := h.Buf
|
||||
mx, my := e.Position()
|
||||
// ignore click on the status line
|
||||
if my >= h.BufView().Y+h.BufView().Height {
|
||||
return false
|
||||
}
|
||||
mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
|
||||
|
||||
if h.Buf.NumCursors() > 1 {
|
||||
cursors := h.Buf.GetCursors()
|
||||
for _, c := range cursors {
|
||||
if c.Loc == mouseLoc {
|
||||
h.Buf.RemoveCursor(c.Num)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c := buffer.NewCursor(b, mouseLoc)
|
||||
b.AddCursor(c)
|
||||
b.MergeCursors()
|
||||
@@ -1842,6 +2071,9 @@ func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool {
|
||||
// SkipMultiCursor moves the current multiple cursor to the next available position
|
||||
func (h *BufPane) SkipMultiCursor() bool {
|
||||
lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
|
||||
if !lastC.HasSelection() {
|
||||
return false
|
||||
}
|
||||
sel := lastC.GetSelection()
|
||||
searchStart := lastC.CurSelection[1]
|
||||
|
||||
@@ -1877,8 +2109,11 @@ func (h *BufPane) RemoveMultiCursor() bool {
|
||||
h.Buf.RemoveCursor(h.Buf.NumCursors() - 1)
|
||||
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
|
||||
h.Buf.UpdateCursors()
|
||||
} else {
|
||||
} else if h.multiWord {
|
||||
h.multiWord = false
|
||||
h.Cursor.Deselect(true)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
h.Relocate()
|
||||
return true
|
||||
@@ -1886,8 +2121,12 @@ func (h *BufPane) RemoveMultiCursor() bool {
|
||||
|
||||
// RemoveAllMultiCursors removes all cursors except the base cursor
|
||||
func (h *BufPane) RemoveAllMultiCursors() bool {
|
||||
h.Buf.ClearCursors()
|
||||
h.multiWord = false
|
||||
if h.Buf.NumCursors() > 1 || h.multiWord {
|
||||
h.Buf.ClearCursors()
|
||||
h.multiWord = false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
h.Relocate()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -88,6 +88,10 @@ func BindKey(k, v string, bind func(e Event, a string)) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(k, "\x1b") {
|
||||
screen.Screen.RegisterRawSeq(k)
|
||||
}
|
||||
|
||||
bind(event, v)
|
||||
|
||||
// switch e := event.(type) {
|
||||
@@ -153,7 +157,6 @@ modSearch:
|
||||
k = k[5:]
|
||||
modifiers |= tcell.ModShift
|
||||
case strings.HasPrefix(k, "\x1b"):
|
||||
screen.Screen.RegisterRawSeq(k)
|
||||
return RawEvent{
|
||||
esc: k,
|
||||
}, true
|
||||
@@ -173,39 +176,35 @@ modSearch:
|
||||
// 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 {
|
||||
var r tcell.Key
|
||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
||||
if code < 256 && code != 27 {
|
||||
r = code
|
||||
}
|
||||
// It is, we're done.
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
r: rune(r),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingKeys
|
||||
if code, ok := keyEvents[k]; ok {
|
||||
var r tcell.Key
|
||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
||||
if code < 256 && code != 27 {
|
||||
r = code
|
||||
}
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
r: rune(r),
|
||||
}, 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,
|
||||
btn: code,
|
||||
mod: modifiers,
|
||||
state: mstate,
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -239,6 +238,24 @@ func findEvent(k string) (Event, error) {
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func eventsEqual(e1 Event, e2 Event) bool {
|
||||
seq1, ok1 := e1.(KeySequenceEvent)
|
||||
seq2, ok2 := e2.(KeySequenceEvent)
|
||||
if ok1 && ok2 {
|
||||
if len(seq1.keys) != len(seq2.keys) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(seq1.keys); i++ {
|
||||
if seq1.keys[i] != seq2.keys[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return e1 == e2
|
||||
}
|
||||
|
||||
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
|
||||
// Returns true if the keybinding already existed and a possible error
|
||||
func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
||||
@@ -264,21 +281,23 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
||||
}
|
||||
|
||||
found := false
|
||||
for ev := range parsed {
|
||||
var ev string
|
||||
for ev = range parsed {
|
||||
if e, err := findEvent(ev); err == nil {
|
||||
if e == key {
|
||||
if overwrite {
|
||||
parsed[ev] = v
|
||||
}
|
||||
if eventsEqual(e, key) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found && !overwrite {
|
||||
return true, nil
|
||||
} else if !found {
|
||||
if found {
|
||||
if overwrite {
|
||||
parsed[ev] = v
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
parsed[k] = v
|
||||
}
|
||||
|
||||
@@ -315,13 +334,17 @@ func UnbindKey(k string) error {
|
||||
|
||||
for ev := range parsed {
|
||||
if e, err := findEvent(ev); err == nil {
|
||||
if e == key {
|
||||
if eventsEqual(e, key) {
|
||||
delete(parsed, ev)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(k, "\x1b") {
|
||||
screen.Screen.UnregisterRawSeq(k)
|
||||
}
|
||||
|
||||
defaults := DefaultBindings("buffer")
|
||||
if a, ok := defaults[k]; ok {
|
||||
BindKey(k, a, Binder["buffer"])
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||
"github.com/zyedidia/micro/v2/internal/clipboard"
|
||||
"github.com/zyedidia/micro/v2/internal/config"
|
||||
"github.com/zyedidia/micro/v2/internal/display"
|
||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||
@@ -17,6 +16,8 @@ import (
|
||||
"github.com/zyedidia/tcell/v2"
|
||||
)
|
||||
|
||||
type BufAction interface{}
|
||||
|
||||
// BufKeyAction represents an action bound to a key.
|
||||
type BufKeyAction func(*BufPane) bool
|
||||
|
||||
@@ -44,8 +45,9 @@ func init() {
|
||||
BufBindings = NewKeyTree()
|
||||
}
|
||||
|
||||
// LuaAction makes a BufKeyAction from a lua function.
|
||||
func LuaAction(fn string) func(*BufPane) bool {
|
||||
// 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
|
||||
@@ -55,33 +57,42 @@ func LuaAction(fn string) func(*BufPane) bool {
|
||||
if pl == nil {
|
||||
return nil
|
||||
}
|
||||
return 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// BufMapKey maps an event to an action
|
||||
// BufMapEvent maps an event to an action
|
||||
func BufMapEvent(k Event, action string) {
|
||||
config.Bindings["buffer"][k.Name()] = action
|
||||
|
||||
switch e := k.(type) {
|
||||
case KeyEvent, KeySequenceEvent, RawEvent:
|
||||
bufMapKey(e, action)
|
||||
case MouseEvent:
|
||||
bufMapMouse(e, action)
|
||||
}
|
||||
}
|
||||
|
||||
func bufMapKey(k Event, action string) {
|
||||
var actionfns []func(*BufPane) bool
|
||||
var actionfns []BufAction
|
||||
var names []string
|
||||
var types []byte
|
||||
for i := 0; ; i++ {
|
||||
@@ -102,7 +113,7 @@ func bufMapKey(k Event, action string) {
|
||||
action = ""
|
||||
}
|
||||
|
||||
var afn func(*BufPane) bool
|
||||
var afn BufAction
|
||||
if strings.HasPrefix(a, "command:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandAction(a)
|
||||
@@ -113,7 +124,7 @@ func bufMapKey(k Event, action string) {
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "lua:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = LuaAction(a)
|
||||
afn = LuaAction(a, k)
|
||||
if afn == nil {
|
||||
screen.TermMessage("Lua Error:", a, "does not exist")
|
||||
continue
|
||||
@@ -129,47 +140,52 @@ func bufMapKey(k Event, action string) {
|
||||
} 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) bool {
|
||||
cursors := h.Buf.GetCursors()
|
||||
success := true
|
||||
bufAction := func(h *BufPane, te *tcell.EventMouse) bool {
|
||||
for i, a := range actionfns {
|
||||
innerSuccess := true
|
||||
for j, c := range cursors {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
|
||||
innerSuccess = innerSuccess && h.execAction(a, names[i], j)
|
||||
} else {
|
||||
break
|
||||
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()
|
||||
success = innerSuccess
|
||||
if h == nil {
|
||||
// stop, in case the current pane is not a BufPane
|
||||
break
|
||||
}
|
||||
|
||||
if (!success && types[i] == '&') || (success && types[i] == '|') {
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
BufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(bufAction))
|
||||
}
|
||||
|
||||
// BufMapMouse maps a mouse event to an action
|
||||
func bufMapMouse(k MouseEvent, action string) {
|
||||
if f, ok := BufMouseActions[action]; ok {
|
||||
BufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f))
|
||||
} else {
|
||||
// TODO
|
||||
// delete(BufMouseBindings, k)
|
||||
bufMapKey(k, action)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,11 +216,15 @@ type BufPane struct {
|
||||
// Cursor is the currently active buffer cursor
|
||||
Cursor *buffer.Cursor
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
// We need to keep track of insert key press toggle
|
||||
isOverwriteMode bool
|
||||
@@ -250,7 +270,7 @@ func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
|
||||
h.tab = tab
|
||||
|
||||
h.Cursor = h.Buf.GetActiveCursor()
|
||||
h.mouseReleased = true
|
||||
h.mousePressed = make(map[MouseEvent]bool)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -276,7 +296,11 @@ func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
|
||||
func (h *BufPane) finishInitialize() {
|
||||
h.initialRelocate()
|
||||
h.initialized = true
|
||||
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
||||
|
||||
err := config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Resize resizes the pane
|
||||
@@ -304,7 +328,7 @@ func (h *BufPane) ResizePane(size int) {
|
||||
}
|
||||
|
||||
// PluginCB calls all plugin callbacks with a certain name and displays an
|
||||
// error if there is one and returns the aggregrate boolean response
|
||||
// error if there is one and returns the aggregate boolean response
|
||||
func (h *BufPane) PluginCB(cb string) bool {
|
||||
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
@@ -322,6 +346,12 @@ func (h *BufPane) PluginCBRune(cb string, r rune) bool {
|
||||
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()
|
||||
@@ -332,7 +362,7 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
||||
h.initialRelocate()
|
||||
// Set mouseReleased to true because we assume the mouse is not being
|
||||
// pressed when the editor is opened
|
||||
h.mouseReleased = true
|
||||
h.resetMouse()
|
||||
// Set isOverwriteMode to false, because we assume we are in the default
|
||||
// mode when editor is opened
|
||||
h.isOverwriteMode = false
|
||||
@@ -395,6 +425,12 @@ func (h *BufPane) Name() string {
|
||||
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)
|
||||
@@ -413,11 +449,11 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
|
||||
if !yes || canceled {
|
||||
h.Buf.UpdateModTime()
|
||||
} else {
|
||||
h.Buf.ReOpen()
|
||||
h.ReOpen()
|
||||
}
|
||||
})
|
||||
} else if reload == "auto" {
|
||||
h.Buf.ReOpen()
|
||||
h.ReOpen()
|
||||
} else if reload == "disabled" {
|
||||
h.Buf.DisableReload()
|
||||
} else {
|
||||
@@ -435,61 +471,44 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
|
||||
h.paste(e.Text())
|
||||
h.Relocate()
|
||||
case *tcell.EventKey:
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: metaToAlt(e.Modifiers()),
|
||||
r: e.Rune(),
|
||||
}
|
||||
ke := keyEvent(e)
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
if !done && e.Key() == tcell.KeyRune {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
}
|
||||
case *tcell.EventMouse:
|
||||
cancel := false
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
_, my := e.Position()
|
||||
if h.Buf.Type.Kind != buffer.BTInfo.Kind && h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
|
||||
cancel = true
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
// Mouse event with no click
|
||||
if !h.mouseReleased {
|
||||
// Mouse was just released
|
||||
|
||||
// mx, my := e.Position()
|
||||
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
|
||||
|
||||
// we could finish the selection based on the release location as described
|
||||
// below but when the mouse click is within the scroll margin this will
|
||||
// cause a scroll and selection even for a simple mouse click which is
|
||||
// not good
|
||||
// for terminals that don't support mouse motion events, selection via
|
||||
// the mouse won't work but this is ok
|
||||
|
||||
// Relocating here isn't really necessary because the cursor will
|
||||
// be in the right place from the last mouse event
|
||||
// However, if we are running in a terminal that doesn't support mouse motion
|
||||
// events, this still allows the user to make selections, except only after they
|
||||
// release the mouse
|
||||
|
||||
// if !h.doubleClick && !h.tripleClick {
|
||||
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
|
||||
// }
|
||||
if h.Cursor.HasSelection() {
|
||||
h.Cursor.CopySelection(clipboard.PrimaryReg)
|
||||
}
|
||||
h.mouseReleased = true
|
||||
}
|
||||
}
|
||||
|
||||
if !cancel {
|
||||
if e.Buttons() != tcell.ButtonNone {
|
||||
me := MouseEvent{
|
||||
btn: e.Buttons(),
|
||||
mod: metaToAlt(e.Modifiers()),
|
||||
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()
|
||||
@@ -509,6 +528,14 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
|
||||
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.
|
||||
@@ -520,7 +547,10 @@ func (h *BufPane) Bindings() *KeyTree {
|
||||
}
|
||||
|
||||
// DoKeyEvent executes a key event by finding the action it is bound
|
||||
// to and executing it (possibly multiple times for multiple cursors)
|
||||
// to and executing it (possibly multiple times for multiple cursors).
|
||||
// Returns true if the action was executed OR if there are more keys
|
||||
// remaining to process before executing an action (if this is a key
|
||||
// sequence event). Returns false if no action found.
|
||||
func (h *BufPane) DoKeyEvent(e Event) bool {
|
||||
binds := h.Bindings()
|
||||
action, more := binds.NextEvent(e, nil)
|
||||
@@ -534,30 +564,33 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
|
||||
return more
|
||||
}
|
||||
|
||||
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
|
||||
func (h *BufPane) execAction(action BufAction, name string, te *tcell.EventMouse) bool {
|
||||
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
|
||||
h.Buf.HasSuggestions = false
|
||||
}
|
||||
|
||||
_, isMulti := MultiActions[name]
|
||||
if (!isMulti && cursor == 0) || isMulti {
|
||||
if h.PluginCB("pre" + name) {
|
||||
success := action(h)
|
||||
success = success && h.PluginCB("on"+name)
|
||||
if !h.PluginCB("pre" + name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if isMulti {
|
||||
if recordingMacro {
|
||||
if name != "ToggleMacro" && name != "PlayMacro" {
|
||||
curmacro = append(curmacro, action)
|
||||
}
|
||||
}
|
||||
var success bool
|
||||
switch a := action.(type) {
|
||||
case BufKeyAction:
|
||||
success = a(h)
|
||||
case BufMouseAction:
|
||||
success = a(h, te)
|
||||
}
|
||||
success = success && h.PluginCB("on"+name)
|
||||
|
||||
if _, ok := MultiActions[name]; ok {
|
||||
if recordingMacro {
|
||||
if name != "ToggleMacro" && name != "PlayMacro" {
|
||||
curmacro = append(curmacro, action)
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return success
|
||||
}
|
||||
|
||||
func (h *BufPane) completeAction(action string) {
|
||||
@@ -663,6 +696,10 @@ func (h *BufPane) 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
|
||||
@@ -678,8 +715,12 @@ func (h *BufPane) SetActive(b bool) {
|
||||
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
|
||||
@@ -700,10 +741,16 @@ var BufKeyActions = map[string]BufKeyAction{
|
||||
"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,
|
||||
@@ -711,6 +758,8 @@ var BufKeyActions = map[string]BufKeyAction{
|
||||
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
|
||||
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
|
||||
"ParagraphNext": (*BufPane).ParagraphNext,
|
||||
"SelectToParagraphPrevious": (*BufPane).SelectToParagraphPrevious,
|
||||
"SelectToParagraphNext": (*BufPane).SelectToParagraphNext,
|
||||
"InsertNewline": (*BufPane).InsertNewline,
|
||||
"Backspace": (*BufPane).Backspace,
|
||||
"Delete": (*BufPane).Delete,
|
||||
@@ -763,6 +812,7 @@ var BufKeyActions = map[string]BufKeyAction{
|
||||
"ToggleRuler": (*BufPane).ToggleRuler,
|
||||
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
|
||||
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
|
||||
"ResetSearch": (*BufPane).ResetSearch,
|
||||
"ClearStatus": (*BufPane).ClearStatus,
|
||||
"ShellMode": (*BufPane).ShellMode,
|
||||
"CommandMode": (*BufPane).CommandMode,
|
||||
@@ -804,6 +854,8 @@ var BufKeyActions = map[string]BufKeyAction{
|
||||
// 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,
|
||||
}
|
||||
|
||||
@@ -828,10 +880,16 @@ var MultiActions = map[string]bool{
|
||||
"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,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -41,6 +42,7 @@ func InitCommands() {
|
||||
"unbind": {(*BufPane).UnbindCmd, nil},
|
||||
"quit": {(*BufPane).QuitCmd, nil},
|
||||
"goto": {(*BufPane).GotoCmd, nil},
|
||||
"jump": {(*BufPane).JumpCmd, nil},
|
||||
"save": {(*BufPane).SaveCmd, nil},
|
||||
"replace": {(*BufPane).ReplaceCmd, nil},
|
||||
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
|
||||
@@ -197,7 +199,7 @@ func (h *BufPane) TabMoveCmd(args []string) {
|
||||
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
|
||||
|
||||
activeTab := Tabs.List[idxFrom]
|
||||
Tabs.RemoveTab(activeTab.ID())
|
||||
Tabs.RemoveTab(activeTab.Panes[0].ID())
|
||||
Tabs.List = append(Tabs.List, nil)
|
||||
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
|
||||
Tabs.List[idxTo] = activeTab
|
||||
@@ -329,31 +331,84 @@ func (h *BufPane) ToggleLogCmd(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadCmd reloads all files (syntax files, colorschemes...)
|
||||
// ReloadCmd reloads all files (syntax files, colorschemes, plugins...)
|
||||
func (h *BufPane) ReloadCmd(args []string) {
|
||||
ReloadConfig()
|
||||
reloadRuntime(true)
|
||||
}
|
||||
|
||||
// ReloadConfig reloads only the configuration
|
||||
func ReloadConfig() {
|
||||
config.InitRuntimeFiles()
|
||||
reloadRuntime(false)
|
||||
}
|
||||
|
||||
func reloadRuntime(reloadPlugins bool) {
|
||||
if reloadPlugins {
|
||||
err := config.RunPluginFn("deinit")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
config.InitRuntimeFiles(true)
|
||||
|
||||
if reloadPlugins {
|
||||
config.InitPlugins()
|
||||
}
|
||||
|
||||
err := config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
} else {
|
||||
parsedSettings := config.ParsedSettings()
|
||||
defaultSettings := config.DefaultAllSettings()
|
||||
for k := range defaultSettings {
|
||||
if _, ok := config.VolatileSettings[k]; ok {
|
||||
// reload should not override volatile settings
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := parsedSettings[k]; ok {
|
||||
err = doSetGlobalOptionNative(k, parsedSettings[k])
|
||||
} else {
|
||||
err = doSetGlobalOptionNative(k, defaultSettings[k])
|
||||
}
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = config.InitGlobalSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
|
||||
if reloadPlugins {
|
||||
err = config.LoadAllPlugins()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
InitBindings()
|
||||
InitCommands()
|
||||
|
||||
if reloadPlugins {
|
||||
err = config.RunPluginFn("preinit")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
err = config.RunPluginFn("init")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
err = config.RunPluginFn("postinit")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
b.ReloadSettings(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,22 +418,24 @@ func (h *BufPane) ReopenCmd(args []string) {
|
||||
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
|
||||
if !canceled && yes {
|
||||
h.Save()
|
||||
h.Buf.ReOpen()
|
||||
h.ReOpen()
|
||||
} else if !canceled {
|
||||
h.Buf.ReOpen()
|
||||
h.ReOpen()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
h.Buf.ReOpen()
|
||||
h.ReOpen()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BufPane) openHelp(page string) error {
|
||||
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
||||
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
|
||||
return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err))
|
||||
} else {
|
||||
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
||||
helpBuffer.SetName("Help " + page)
|
||||
helpBuffer.SetOptionNative("hltaberrors", false)
|
||||
helpBuffer.SetOptionNative("hltrailingws", false)
|
||||
|
||||
if h.Buf.Type == buffer.BTHelp {
|
||||
h.OpenBuffer(helpBuffer)
|
||||
@@ -470,70 +527,86 @@ func (h *BufPane) NewTabCmd(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||
local := false
|
||||
for _, s := range config.LocalSettings {
|
||||
if s == option {
|
||||
local = true
|
||||
break
|
||||
}
|
||||
func doSetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||
if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !local {
|
||||
config.GlobalSettings[option] = nativeValue
|
||||
config.ModifiedSettings[option] = true
|
||||
config.GlobalSettings[option] = nativeValue
|
||||
config.ModifiedSettings[option] = true
|
||||
delete(config.VolatileSettings, option)
|
||||
|
||||
if option == "colorscheme" {
|
||||
// LoadSyntaxFiles()
|
||||
config.InitColorscheme()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "infobar" || option == "keymenu" {
|
||||
Tabs.Resize()
|
||||
} else if option == "mouse" {
|
||||
if !nativeValue.(bool) {
|
||||
screen.Screen.DisableMouse()
|
||||
} else {
|
||||
screen.Screen.EnableMouse()
|
||||
}
|
||||
} else if option == "autosave" {
|
||||
if nativeValue.(float64) > 0 {
|
||||
config.SetAutoTime(int(nativeValue.(float64)))
|
||||
config.StartAutoSave()
|
||||
} else {
|
||||
config.SetAutoTime(0)
|
||||
}
|
||||
} else if option == "paste" {
|
||||
screen.Screen.SetPaste(nativeValue.(bool))
|
||||
} else if option == "clipboard" {
|
||||
m := clipboard.SetMethod(nativeValue.(string))
|
||||
err := clipboard.Initialize(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if option == "colorscheme" {
|
||||
// LoadSyntaxFiles()
|
||||
config.InitColorscheme()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "infobar" || option == "keymenu" {
|
||||
Tabs.Resize()
|
||||
} else if option == "mouse" {
|
||||
if !nativeValue.(bool) {
|
||||
screen.Screen.DisableMouse()
|
||||
} else {
|
||||
for _, pl := range config.Plugins {
|
||||
if option == pl.Name {
|
||||
if nativeValue.(bool) && !pl.Loaded {
|
||||
pl.Load()
|
||||
_, err := pl.Call("init")
|
||||
if err != nil && err != config.ErrNoSuchFunction {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
} else if !nativeValue.(bool) && pl.Loaded {
|
||||
_, err := pl.Call("deinit")
|
||||
if err != nil && err != config.ErrNoSuchFunction {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
screen.Screen.EnableMouse()
|
||||
}
|
||||
} else if option == "autosave" {
|
||||
if nativeValue.(float64) > 0 {
|
||||
config.SetAutoTime(nativeValue.(float64))
|
||||
} else {
|
||||
config.SetAutoTime(0)
|
||||
}
|
||||
} else if option == "paste" {
|
||||
screen.Screen.SetPaste(nativeValue.(bool))
|
||||
} else if option == "clipboard" {
|
||||
m := clipboard.SetMethod(nativeValue.(string))
|
||||
err := clipboard.Initialize(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
for _, pl := range config.Plugins {
|
||||
if option == pl.Name {
|
||||
if nativeValue.(bool) && !pl.Loaded {
|
||||
pl.Load()
|
||||
_, err := pl.Call("init")
|
||||
if err != nil && err != config.ErrNoSuchFunction {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
} else if !nativeValue.(bool) && pl.Loaded {
|
||||
_, err := pl.Call("deinit")
|
||||
if err != nil && err != config.ErrNoSuchFunction {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||
if err := config.OptionIsValid(option, nativeValue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check for local option first...
|
||||
for _, s := range config.LocalSettings {
|
||||
if s == option {
|
||||
return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue)
|
||||
}
|
||||
}
|
||||
|
||||
// ...if it's not local continue with the globals...
|
||||
if err := doSetGlobalOptionNative(option, nativeValue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ...at last check the buffer locals
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.SetOptionNative(option, nativeValue)
|
||||
b.DoSetOptionNative(option, nativeValue)
|
||||
delete(b.LocalSettings, option)
|
||||
}
|
||||
|
||||
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
||||
@@ -560,16 +633,10 @@ func (h *BufPane) ResetCmd(args []string) {
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
defaults := config.DefaultAllSettings()
|
||||
|
||||
defaultGlobals := config.DefaultGlobalSettings()
|
||||
defaultLocals := config.DefaultCommonSettings()
|
||||
|
||||
if _, ok := defaultGlobals[option]; ok {
|
||||
SetGlobalOptionNative(option, defaultGlobals[option])
|
||||
return
|
||||
}
|
||||
if _, ok := defaultLocals[option]; ok {
|
||||
h.Buf.SetOptionNative(option, defaultLocals[option])
|
||||
if _, ok := defaults[option]; ok {
|
||||
SetGlobalOptionNative(option, defaults[option])
|
||||
return
|
||||
}
|
||||
InfoBar.Error(config.ErrInvalidOption)
|
||||
@@ -634,6 +701,11 @@ func (h *BufPane) ShowCmd(args []string) {
|
||||
InfoBar.Message(option)
|
||||
}
|
||||
|
||||
func parseKeyArg(arg string) string {
|
||||
// If this is a raw escape sequence, convert it to its raw byte form
|
||||
return strings.ReplaceAll(arg, "\\x1b", "\x1b")
|
||||
}
|
||||
|
||||
// ShowKeyCmd displays the action that a key is bound to
|
||||
func (h *BufPane) ShowKeyCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
@@ -641,7 +713,7 @@ func (h *BufPane) ShowKeyCmd(args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
event, err := findEvent(args[0])
|
||||
event, err := findEvent(parseKeyArg(args[0]))
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
@@ -660,7 +732,7 @@ func (h *BufPane) BindCmd(args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := TryBindKey(args[0], args[1], true)
|
||||
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
@@ -673,7 +745,7 @@ func (h *BufPane) UnbindCmd(args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := UnbindKey(args[0])
|
||||
err := UnbindKey(parseKeyArg(args[0]))
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
@@ -701,41 +773,67 @@ func (h *BufPane) QuitCmd(args []string) {
|
||||
// position in the buffer
|
||||
// For example: `goto line`, or `goto line:col`
|
||||
func (h *BufPane) GotoCmd(args []string) {
|
||||
line, col, err := h.parseLineCol(args)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if line < 0 {
|
||||
line = h.Buf.LinesNum() + 1 + line
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
||||
|
||||
h.RemoveAllMultiCursors()
|
||||
h.Cursor.Deselect(true)
|
||||
h.GotoLoc(buffer.Loc{col, line})
|
||||
}
|
||||
|
||||
// JumpCmd is a command that will send the cursor to a certain relative
|
||||
// position in the buffer
|
||||
// For example: `jump line`, `jump -line`, or `jump -line:col`
|
||||
func (h *BufPane) JumpCmd(args []string) {
|
||||
line, col, err := h.parseLineCol(args)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
line = h.Buf.GetActiveCursor().Y + 1 + line
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
||||
|
||||
h.RemoveAllMultiCursors()
|
||||
h.Cursor.Deselect(true)
|
||||
h.GotoLoc(buffer.Loc{col, line})
|
||||
}
|
||||
|
||||
// parseLineCol is a helper to parse the input of GotoCmd and JumpCmd
|
||||
func (h *BufPane) parseLineCol(args []string) (line int, col int, err error) {
|
||||
if len(args) <= 0 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return 0, 0, errors.New("Not enough arguments")
|
||||
}
|
||||
|
||||
line, col = 0, 0
|
||||
if strings.Contains(args[0], ":") {
|
||||
parts := strings.SplitN(args[0], ":", 2)
|
||||
line, err = strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
col, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
} else {
|
||||
h.RemoveAllMultiCursors()
|
||||
if strings.Contains(args[0], ":") {
|
||||
parts := strings.SplitN(args[0], ":", 2)
|
||||
line, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
col, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
if line < 0 {
|
||||
line = h.Buf.LinesNum() + 1 + line
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
||||
h.GotoLoc(buffer.Loc{col, line})
|
||||
} else {
|
||||
line, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
if line < 0 {
|
||||
line = h.Buf.LinesNum() + 1 + line
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
h.GotoLoc(buffer.Loc{0, line})
|
||||
line, err = strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return line, col, nil
|
||||
}
|
||||
|
||||
// SaveCmd saves the buffer optionally with an argument file name
|
||||
@@ -810,7 +908,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
|
||||
end = h.Cursor.CurSelection[1]
|
||||
}
|
||||
if all {
|
||||
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
|
||||
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex)
|
||||
} else {
|
||||
inRange := func(l buffer.Loc) bool {
|
||||
return l.GreaterEqual(start) && l.LessEqual(end)
|
||||
@@ -840,7 +938,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
|
||||
|
||||
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && yes {
|
||||
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
|
||||
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace, !noRegex)
|
||||
|
||||
searchLoc = locs[0]
|
||||
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
|
||||
|
||||
@@ -62,6 +62,8 @@ var bufdefaults = map[string]string{
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab",
|
||||
"CtrlPageDown": "NextTab",
|
||||
"ShiftPageUp": "SelectPageUp",
|
||||
"ShiftPageDown": "SelectPageDown",
|
||||
"Ctrl-g": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"Ctrl-r": "ToggleRuler",
|
||||
@@ -92,11 +94,13 @@ var bufdefaults = map[string]string{
|
||||
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
"AltShiftUp": "SpawnMultiCursorUp",
|
||||
@@ -175,8 +179,10 @@ var infodefaults = map[string]string{
|
||||
"Esc": "AbortCommand",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ var bufdefaults = map[string]string{
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab",
|
||||
"CtrlPageDown": "NextTab",
|
||||
"ShiftPageUp": "SelectPageUp",
|
||||
"ShiftPageDown": "SelectPageDown",
|
||||
"Ctrl-g": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"Ctrl-r": "ToggleRuler",
|
||||
@@ -95,11 +97,13 @@ var bufdefaults = map[string]string{
|
||||
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
"Alt-m": "SpawnMultiCursorSelect",
|
||||
@@ -178,8 +182,10 @@ var infodefaults = map[string]string{
|
||||
"Esc": "AbortCommand",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
}
|
||||
|
||||
@@ -44,6 +44,17 @@ func metaToAlt(mod tcell.ModMask) tcell.ModMask {
|
||||
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>"
|
||||
@@ -68,7 +79,7 @@ func (k KeyEvent) Name() string {
|
||||
if k.code == tcell.KeyRune {
|
||||
s = string(k.r)
|
||||
} else {
|
||||
s = fmt.Sprintf("Key[%d,%d]", k.code, int(k.r))
|
||||
s = fmt.Sprintf("Key[%d]", k.code)
|
||||
}
|
||||
}
|
||||
if len(m) != 0 {
|
||||
@@ -100,11 +111,20 @@ func (k KeySequenceEvent) Name() string {
|
||||
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
|
||||
btn tcell.ButtonMask
|
||||
mod tcell.ModMask
|
||||
state MouseState
|
||||
}
|
||||
|
||||
func (m MouseEvent) Name() string {
|
||||
@@ -122,9 +142,17 @@ func (m MouseEvent) Name() string {
|
||||
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", mod, k)
|
||||
return fmt.Sprintf("%s%s%s", mod, k, state)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -138,11 +166,7 @@ func (m MouseEvent) Name() string {
|
||||
func ConstructEvent(event tcell.Event) (Event, error) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
return KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: metaToAlt(e.Modifiers()),
|
||||
r: e.Rune(),
|
||||
}, nil
|
||||
return keyEvent(e), nil
|
||||
case *tcell.EventRaw:
|
||||
return RawEvent{
|
||||
esc: e.EscSeq(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||
"github.com/zyedidia/micro/v2/internal/config"
|
||||
"github.com/zyedidia/micro/v2/internal/util"
|
||||
"github.com/zyedidia/micro/v2/pkg/highlight"
|
||||
)
|
||||
|
||||
// This file is meant (for now) for autocompletion in command mode, not
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
// CommandComplete autocompletes commands
|
||||
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
@@ -38,7 +39,7 @@ func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
// HelpComplete autocompletes help topics
|
||||
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
|
||||
@@ -77,6 +78,63 @@ func colorschemeComplete(input string) (string, []string) {
|
||||
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
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
@@ -89,7 +147,7 @@ func contains(s []string, e string) bool {
|
||||
// OptionComplete autocompletes options
|
||||
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
for option := range config.GlobalSettings {
|
||||
@@ -116,7 +174,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := buffer.GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
@@ -172,13 +230,8 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
switch inputOpt {
|
||||
case "colorscheme":
|
||||
_, suggestions = colorschemeComplete(input)
|
||||
case "fileformat":
|
||||
if strings.HasPrefix("unix", input) {
|
||||
suggestions = append(suggestions, "unix")
|
||||
}
|
||||
if strings.HasPrefix("dos", input) {
|
||||
suggestions = append(suggestions, "dos")
|
||||
}
|
||||
case "filetype":
|
||||
_, suggestions = filetypeComplete(input)
|
||||
case "sucmd":
|
||||
if strings.HasPrefix("sudo", input) {
|
||||
suggestions = append(suggestions, "sudo")
|
||||
@@ -186,15 +239,13 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
if strings.HasPrefix("doas", input) {
|
||||
suggestions = append(suggestions, "doas")
|
||||
}
|
||||
case "clipboard":
|
||||
if strings.HasPrefix("external", input) {
|
||||
suggestions = append(suggestions, "external")
|
||||
}
|
||||
if strings.HasPrefix("internal", input) {
|
||||
suggestions = append(suggestions, "internal")
|
||||
}
|
||||
if strings.HasPrefix("terminal", input) {
|
||||
suggestions = append(suggestions, "terminal")
|
||||
default:
|
||||
if choices, ok := config.OptionChoices[inputOpt]; ok {
|
||||
for _, choice := range choices {
|
||||
if strings.HasPrefix(choice, input) {
|
||||
suggestions = append(suggestions, choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,7 +261,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
// PluginCmdComplete autocompletes the plugin command
|
||||
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
var suggestions []string
|
||||
for _, cmd := range PluginCmds {
|
||||
@@ -232,7 +283,7 @@ func PluginComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := buffer.GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
|
||||
@@ -83,22 +83,22 @@ func (h *InfoPane) Close() {
|
||||
|
||||
func (h *InfoPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
// TODO
|
||||
case *tcell.EventKey:
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: metaToAlt(e.Modifiers()),
|
||||
r: e.Rune(),
|
||||
}
|
||||
ke := keyEvent(e)
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
hasYN := h.HasYN
|
||||
if e.Key() == tcell.KeyRune && hasYN {
|
||||
if (e.Rune() == 'y' || e.Rune() == 'Y') && hasYN {
|
||||
h.YNResp = true
|
||||
h.DonePrompt(false)
|
||||
} else if (e.Rune() == 'n' || e.Rune() == 'N') && hasYN {
|
||||
h.YNResp = false
|
||||
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 {
|
||||
@@ -122,7 +122,10 @@ func (h *InfoPane) HandleEvent(event tcell.Event) {
|
||||
}
|
||||
}
|
||||
|
||||
// DoKeyEvent executes a key event for the command bar, doing any overridden actions
|
||||
// DoKeyEvent executes a key event for the command bar, doing any overridden actions.
|
||||
// Returns true if the action was executed OR if there are more keys remaining
|
||||
// to process before executing an action (if this is a key sequence event).
|
||||
// Returns false if no action found.
|
||||
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
||||
action, more := InfoBindings.NextEvent(e, nil)
|
||||
if action != nil && !more {
|
||||
@@ -136,11 +139,25 @@ func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
||||
}
|
||||
|
||||
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 {
|
||||
done := action(h.BufPane)
|
||||
action(h.BufPane)
|
||||
InfoBufBindings.ResetEvents()
|
||||
return done
|
||||
return true
|
||||
} else if action == nil && !more {
|
||||
InfoBufBindings.ResetEvents()
|
||||
}
|
||||
|
||||
@@ -107,30 +107,32 @@ func (t *TabList) HandleEvent(event tcell.Event) {
|
||||
mx, my := e.Position()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
if my == t.Y && mx == 0 {
|
||||
t.Scroll(-4)
|
||||
return
|
||||
} else if my == t.Y && mx == t.Width-1 {
|
||||
t.Scroll(4)
|
||||
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
|
||||
}
|
||||
if len(t.List) > 1 {
|
||||
ind := t.LocFromVisual(buffer.Loc{mx, my})
|
||||
if ind != -1 {
|
||||
t.SetActive(ind)
|
||||
return
|
||||
}
|
||||
if my == 0 {
|
||||
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 {
|
||||
if my == t.Y && len(t.List) > 1 {
|
||||
t.Scroll(4)
|
||||
return
|
||||
}
|
||||
case tcell.WheelDown:
|
||||
if my == t.Y {
|
||||
if my == t.Y && len(t.List) > 1 {
|
||||
t.Scroll(-4)
|
||||
return
|
||||
}
|
||||
@@ -147,6 +149,56 @@ func (t *TabList) Display() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TabList) SetActive(a int) {
|
||||
t.TabWindow.SetActive(a)
|
||||
|
||||
for i, p := range t.List {
|
||||
if i == a {
|
||||
if !p.isActive {
|
||||
p.isActive = true
|
||||
|
||||
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, p.CurPane()))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p.isActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResetMouse resets the mouse release state after the screen was stopped
|
||||
// or the pane changed.
|
||||
// This prevents situations in which mouse releases are received at the wrong place
|
||||
// and the mouse state is still pressed.
|
||||
func (t *TabList) ResetMouse() {
|
||||
for _, tab := range t.List {
|
||||
if !tab.release && tab.resizing != nil {
|
||||
tab.resizing = nil
|
||||
}
|
||||
|
||||
tab.release = true
|
||||
|
||||
for _, p := range tab.Panes {
|
||||
if bp, ok := p.(*BufPane); ok {
|
||||
bp.resetMouse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CloseTerms notifies term panes that a terminal job has finished.
|
||||
func (t *TabList) CloseTerms() {
|
||||
for _, tab := range t.List {
|
||||
for _, p := range tab.Panes {
|
||||
if tp, ok := p.(*TermPane); ok {
|
||||
tp.HandleTermClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs is the global tab list
|
||||
var Tabs *TabList
|
||||
|
||||
@@ -164,6 +216,8 @@ func InitTabs(bufs []*buffer.Buffer) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.RestartCallback = Tabs.ResetMouse
|
||||
}
|
||||
|
||||
func MainTab() *Tab {
|
||||
@@ -177,6 +231,9 @@ func MainTab() *Tab {
|
||||
type Tab struct {
|
||||
*views.Node
|
||||
*display.UIWindow
|
||||
|
||||
isActive bool
|
||||
|
||||
Panes []Pane
|
||||
active int
|
||||
|
||||
@@ -214,34 +271,40 @@ func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
|
||||
// HandleEvent takes a tcell event and usually dispatches it to the current
|
||||
// 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 event in a pane, that pane will become active and get
|
||||
// the event
|
||||
// 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()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
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 t.resizing != nil {
|
||||
var size int
|
||||
if t.resizing.Kind == views.STVert {
|
||||
size = mx - t.resizing.X
|
||||
} else {
|
||||
size = my - t.resizing.Y + 1
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
t.resizing.ResizeSplit(size)
|
||||
t.Resize()
|
||||
return
|
||||
}
|
||||
|
||||
if wasReleased {
|
||||
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
|
||||
if t.resizing != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
@@ -251,10 +314,15 @@ func (t *Tab) HandleEvent(event tcell.Event) {
|
||||
}
|
||||
}
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
t.resizing = nil
|
||||
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
|
||||
@@ -279,11 +347,6 @@ func (t *Tab) SetActive(i int) {
|
||||
p.SetActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, MainTab().CurPane()))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPane returns the pane with the given split index
|
||||
|
||||
@@ -81,6 +81,10 @@ 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
|
||||
}
|
||||
@@ -121,11 +125,7 @@ func (t *TermPane) Unsplit() {
|
||||
// copy-paste
|
||||
func (t *TermPane) HandleEvent(event tcell.Event) {
|
||||
if e, ok := event.(*tcell.EventKey); ok {
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: metaToAlt(e.Modifiers()),
|
||||
r: e.Rune(),
|
||||
}
|
||||
ke := keyEvent(e)
|
||||
action, more := TermBindings.NextEvent(ke, nil)
|
||||
|
||||
if !more {
|
||||
@@ -159,9 +159,9 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
|
||||
if t.Status != shell.TTDone {
|
||||
t.WriteString(event.EscSeq())
|
||||
}
|
||||
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
|
||||
} else if e, ok := event.(*tcell.EventMouse); !ok || t.State.Mode(terminal.ModeMouseMask) {
|
||||
// t.WriteString(event.EscSeq())
|
||||
} else if e != nil {
|
||||
} else {
|
||||
x, y := e.Position()
|
||||
v := t.GetView()
|
||||
x -= v.X
|
||||
@@ -188,7 +188,12 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (b *Buffer) CycleAutocomplete(forward bool) {
|
||||
|
||||
// GetWord gets the most recent word separated by any separator
|
||||
// (whitespace, punctuation, any non alphanumeric character)
|
||||
func GetWord(b *Buffer) ([]byte, int) {
|
||||
func (b *Buffer) GetWord() ([]byte, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
@@ -73,17 +73,17 @@ func GetWord(b *Buffer) ([]byte, int) {
|
||||
return []byte{}, -1
|
||||
}
|
||||
|
||||
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc.Move(-1, b))) {
|
||||
if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) {
|
||||
return []byte{}, c.X
|
||||
}
|
||||
|
||||
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
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 GetArg(b *Buffer) (string, int) {
|
||||
func (b *Buffer) GetArg() (string, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
@@ -104,7 +104,7 @@ func GetArg(b *Buffer) (string, int) {
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetArg(b)
|
||||
input, argstart := b.GetArg()
|
||||
|
||||
sep := string(os.PathSeparator)
|
||||
dirs := strings.Split(input, sep)
|
||||
@@ -153,7 +153,7 @@ func FileComplete(b *Buffer) ([]string, []string) {
|
||||
// BufferComplete autocompletes based on previous words in the buffer
|
||||
func BufferComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetWord(b)
|
||||
input, argstart := b.GetWord()
|
||||
|
||||
if argstart == -1 {
|
||||
return []string{}, []string{}
|
||||
@@ -166,7 +166,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
|
||||
var suggestions []string
|
||||
for i := c.Y; i >= 0; i-- {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
words := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
@@ -179,7 +179,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
|
||||
}
|
||||
for i := c.Y + 1; i < b.LinesNum(); i++ {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
words := bytes.FieldsFunc(l, util.IsNonWordChar)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
|
||||
@@ -57,17 +57,13 @@ var (
|
||||
BTLog = BufType{2, true, true, false}
|
||||
// BTScratch is a buffer that cannot be saved (for scratch work)
|
||||
BTScratch = BufType{3, false, true, false}
|
||||
// BTRaw is is a buffer that shows raw terminal events
|
||||
// BTRaw is a buffer that shows raw terminal events
|
||||
BTRaw = BufType{4, false, true, false}
|
||||
// BTInfo is a buffer for inputting information
|
||||
BTInfo = BufType{5, false, true, false}
|
||||
// BTStdout is a buffer that only writes to stdout
|
||||
// when closed
|
||||
BTStdout = BufType{6, false, true, true}
|
||||
|
||||
// ErrFileTooLarge is returned when the file is too large to hash
|
||||
// (fastdirty is automatically enabled)
|
||||
ErrFileTooLarge = errors.New("File is too large to hash")
|
||||
)
|
||||
|
||||
// SharedBuffer is a struct containing info that is shared among buffers
|
||||
@@ -90,6 +86,8 @@ type SharedBuffer struct {
|
||||
|
||||
// Settings customized by the user
|
||||
Settings map[string]interface{}
|
||||
// LocalSettings customized by the user for this buffer only
|
||||
LocalSettings map[string]bool
|
||||
|
||||
Suggestions []string
|
||||
Completions []string
|
||||
@@ -330,6 +328,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
||||
// assigning the filetype.
|
||||
settings := config.DefaultCommonSettings()
|
||||
b.Settings = config.DefaultCommonSettings()
|
||||
b.LocalSettings = make(map[string]bool)
|
||||
for k, v := range config.GlobalSettings {
|
||||
if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
|
||||
// make sure setting is not global-only
|
||||
@@ -368,6 +367,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
||||
case "dos":
|
||||
ff = FFDos
|
||||
}
|
||||
} else {
|
||||
// in case of autodetection treat as locally set
|
||||
b.LocalSettings["fileformat"] = true
|
||||
}
|
||||
|
||||
b.LineArray = NewLineArray(uint64(size), ff, reader)
|
||||
@@ -554,7 +556,11 @@ func (b *Buffer) ReOpen() error {
|
||||
|
||||
err = b.UpdateModTime()
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
calcHash(b, &b.origHash)
|
||||
if len(data) > LargeFileThreshold {
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
b.isModified = false
|
||||
b.RelocateCursors()
|
||||
@@ -568,6 +574,13 @@ func (b *Buffer) RelocateCursors() {
|
||||
}
|
||||
}
|
||||
|
||||
// DeselectCursors removes selection from all cursors
|
||||
func (b *Buffer) DeselectCursors() {
|
||||
for _, c := range b.cursors {
|
||||
c.Deselect(true)
|
||||
}
|
||||
}
|
||||
|
||||
// RuneAt returns the rune at a given location in the buffer
|
||||
func (b *Buffer) RuneAt(loc Loc) rune {
|
||||
line := b.LineBytes(loc.Y)
|
||||
@@ -642,39 +655,122 @@ func (b *Buffer) Size() int {
|
||||
}
|
||||
|
||||
// calcHash calculates md5 hash of all lines in the buffer
|
||||
func calcHash(b *Buffer, out *[md5.Size]byte) error {
|
||||
func calcHash(b *Buffer, out *[md5.Size]byte) {
|
||||
h := md5.New()
|
||||
|
||||
size := 0
|
||||
if len(b.lines) > 0 {
|
||||
n, e := h.Write(b.lines[0].data)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
h.Write(b.lines[0].data)
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
n, e = h.Write([]byte{'\n'})
|
||||
if e != nil {
|
||||
return e
|
||||
if b.Endings == FFDos {
|
||||
h.Write([]byte{'\r', '\n'})
|
||||
} else {
|
||||
h.Write([]byte{'\n'})
|
||||
}
|
||||
size += n
|
||||
n, e = h.Write(l.data)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
size += n
|
||||
h.Write(l.data)
|
||||
}
|
||||
}
|
||||
|
||||
if size > LargeFileThreshold {
|
||||
return ErrFileTooLarge
|
||||
}
|
||||
|
||||
h.Sum((*out)[:0])
|
||||
}
|
||||
|
||||
func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if header == nil {
|
||||
header, err = highlight.MakeHeaderYaml(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
syndef, err := highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return syndef
|
||||
}
|
||||
|
||||
// findRealRuntimeSyntaxDef finds a specific syntax definition
|
||||
// in the user's custom syntax files
|
||||
func findRealRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
|
||||
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||
if f.Name() == name {
|
||||
syndef := parseDefFromFile(f, header)
|
||||
if syndef != nil {
|
||||
return syndef
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findRuntimeSyntaxDef finds a specific syntax definition
|
||||
// in the built-in syntax files
|
||||
func findRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
if f.Name() == name {
|
||||
syndef := parseDefFromFile(f, header)
|
||||
if syndef != nil {
|
||||
return syndef
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveIncludes(syndef *highlight.Def) {
|
||||
includes := highlight.GetIncludes(syndef)
|
||||
if len(includes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var files []*highlight.File
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
header, err := highlight.MakeHeaderYaml(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, i := range includes {
|
||||
if header.FileType == i {
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
files = append(files, file)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(files) >= len(includes) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
highlight.ResolveIncludes(syndef, files)
|
||||
}
|
||||
|
||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||
// This is called when the colorscheme changes
|
||||
func (b *Buffer) UpdateRules() {
|
||||
@@ -683,13 +779,32 @@ func (b *Buffer) UpdateRules() {
|
||||
}
|
||||
ft := b.Settings["filetype"].(string)
|
||||
if ft == "off" {
|
||||
b.ClearMatches()
|
||||
b.SyntaxDef = nil
|
||||
return
|
||||
}
|
||||
|
||||
b.SyntaxDef = nil
|
||||
|
||||
// syntaxFileInfo is an internal helper structure
|
||||
// to store properties of one single syntax file
|
||||
type syntaxFileInfo struct {
|
||||
header *highlight.Header
|
||||
fileName string
|
||||
syntaxDef *highlight.Def
|
||||
}
|
||||
|
||||
fnameMatches := []syntaxFileInfo{}
|
||||
headerMatches := []syntaxFileInfo{}
|
||||
syntaxFile := ""
|
||||
foundDef := false
|
||||
var header *highlight.Header
|
||||
// search for the syntax file in the user's custom syntax files
|
||||
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
|
||||
if f.Name() == "default" {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
@@ -701,118 +816,145 @@ func (b *Buffer) UpdateRules() {
|
||||
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
|
||||
matchedFileType := false
|
||||
matchedFileName := false
|
||||
matchedFileHeader := false
|
||||
|
||||
if ft == "unknown" || ft == "" {
|
||||
if header.MatchFileName(b.Path) {
|
||||
matchedFileName = true
|
||||
}
|
||||
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
|
||||
matchedFileHeader = true
|
||||
}
|
||||
} else if header.FileType == ft {
|
||||
matchedFileType = true
|
||||
}
|
||||
|
||||
if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
|
||||
if matchedFileType || matchedFileName || matchedFileHeader {
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
syndef, err := highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
b.SyntaxDef = syndef
|
||||
syntaxFile = f.Name()
|
||||
foundDef = true
|
||||
break
|
||||
|
||||
if matchedFileType {
|
||||
b.SyntaxDef = syndef
|
||||
syntaxFile = f.Name()
|
||||
foundDef = true
|
||||
break
|
||||
}
|
||||
|
||||
if matchedFileName {
|
||||
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), syndef})
|
||||
} else if matchedFileHeader {
|
||||
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), syndef})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// search in the default syntax files
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
if !foundDef {
|
||||
// search for the syntax file in the built-in syntax files
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
header, err = highlight.MakeHeader(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading syntax header file", f.Name(), err)
|
||||
continue
|
||||
}
|
||||
header, err = highlight.MakeHeader(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading syntax header file", f.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ft == "unknown" || ft == "" {
|
||||
if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
|
||||
if ft == "unknown" || ft == "" {
|
||||
if header.MatchFileName(b.Path) {
|
||||
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), nil})
|
||||
}
|
||||
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
|
||||
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), nil})
|
||||
}
|
||||
} else if header.FileType == ft {
|
||||
syntaxFile = f.Name()
|
||||
break
|
||||
}
|
||||
} else if header.FileType == ft {
|
||||
syntaxFile = f.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if syntaxFile == "" {
|
||||
matches := fnameMatches
|
||||
if len(matches) == 0 {
|
||||
matches = headerMatches
|
||||
}
|
||||
|
||||
length := len(matches)
|
||||
if length > 0 {
|
||||
signatureMatch := false
|
||||
if length > 1 {
|
||||
// multiple matching syntax files found, try to resolve the ambiguity
|
||||
// using signatures
|
||||
detectlimit := util.IntOpt(b.Settings["detectlimit"])
|
||||
lineCount := len(b.lines)
|
||||
limit := lineCount
|
||||
if detectlimit > 0 && lineCount > detectlimit {
|
||||
limit = detectlimit
|
||||
}
|
||||
|
||||
matchLoop:
|
||||
for _, m := range matches {
|
||||
if m.header.HasFileSignature() {
|
||||
for i := 0; i < limit; i++ {
|
||||
if m.header.MatchFileSignature(b.lines[i].data) {
|
||||
syntaxFile = m.fileName
|
||||
if m.syntaxDef != nil {
|
||||
b.SyntaxDef = m.syntaxDef
|
||||
foundDef = true
|
||||
}
|
||||
header = m.header
|
||||
signatureMatch = true
|
||||
break matchLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if length == 1 || !signatureMatch {
|
||||
syntaxFile = matches[0].fileName
|
||||
if matches[0].syntaxDef != nil {
|
||||
b.SyntaxDef = matches[0].syntaxDef
|
||||
foundDef = true
|
||||
}
|
||||
header = matches[0].header
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if syntaxFile != "" && !foundDef {
|
||||
// we found a syntax file using a syntax header file
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
if f.Name() == syntaxFile {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
syndef, err := highlight.ParseDef(file, header)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
b.SyntaxDef = syndef
|
||||
break
|
||||
}
|
||||
}
|
||||
b.SyntaxDef = findRuntimeSyntaxDef(syntaxFile, header)
|
||||
}
|
||||
|
||||
if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
|
||||
includes := highlight.GetIncludes(b.SyntaxDef)
|
||||
|
||||
var files []*highlight.File
|
||||
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
|
||||
data, err := f.Data()
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
header, err := highlight.MakeHeaderYaml(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, i := range includes {
|
||||
if header.FileType == i {
|
||||
file, err := highlight.ParseFile(data)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
files = append(files, file)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(files) >= len(includes) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
highlight.ResolveIncludes(b.SyntaxDef, files)
|
||||
}
|
||||
|
||||
if b.Highlighter == nil || syntaxFile != "" {
|
||||
if b.SyntaxDef != nil {
|
||||
b.Settings["filetype"] = b.SyntaxDef.FileType
|
||||
}
|
||||
if b.SyntaxDef != nil {
|
||||
b.Settings["filetype"] = b.SyntaxDef.FileType
|
||||
} else {
|
||||
b.SyntaxDef = &highlight.EmptyDef
|
||||
// search for the default file in the user's custom syntax files
|
||||
b.SyntaxDef = findRealRuntimeSyntaxDef("default", nil)
|
||||
if b.SyntaxDef == nil {
|
||||
// search for the default file in the built-in syntax files
|
||||
b.SyntaxDef = findRuntimeSyntaxDef("default", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if b.SyntaxDef != nil {
|
||||
resolveIncludes(b.SyntaxDef)
|
||||
}
|
||||
|
||||
if b.SyntaxDef != nil {
|
||||
@@ -914,7 +1056,7 @@ func (b *Buffer) MergeCursors() {
|
||||
b.EventHandler.active = b.curCursor
|
||||
}
|
||||
|
||||
// UpdateCursors updates all the cursors indicies
|
||||
// UpdateCursors updates all the cursors indices
|
||||
func (b *Buffer) UpdateCursors() {
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.active = b.curCursor
|
||||
@@ -939,7 +1081,7 @@ func (b *Buffer) ClearCursors() {
|
||||
b.cursors = b.cursors[:1]
|
||||
b.UpdateCursors()
|
||||
b.curCursor = 0
|
||||
b.GetActiveCursor().ResetSelection()
|
||||
b.GetActiveCursor().Deselect(true)
|
||||
}
|
||||
|
||||
// MoveLinesUp moves the range of lines up one row
|
||||
@@ -990,34 +1132,14 @@ var BracePairs = [][2]rune{
|
||||
{'[', ']'},
|
||||
}
|
||||
|
||||
// FindMatchingBrace returns the location in the buffer of the matching bracket
|
||||
// It is given a brace type containing the open and closing character, (for example
|
||||
// '{' and '}') as well as the location to match from
|
||||
// TODO: maybe can be more efficient with utf8 package
|
||||
// returns the location of the matching brace
|
||||
// if the boolean returned is true then the original matching brace is one character left
|
||||
// of the starting location
|
||||
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
|
||||
curLine := []rune(string(b.LineBytes(start.Y)))
|
||||
startChar := ' '
|
||||
if start.X >= 0 && start.X < len(curLine) {
|
||||
startChar = curLine[start.X]
|
||||
}
|
||||
leftChar := ' '
|
||||
if start.X-1 >= 0 && start.X-1 < len(curLine) {
|
||||
leftChar = curLine[start.X-1]
|
||||
}
|
||||
func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc, bool) {
|
||||
var i int
|
||||
if startChar == braceType[0] || leftChar == braceType[0] {
|
||||
if char == braceType[0] {
|
||||
for y := start.Y; y < b.LinesNum(); y++ {
|
||||
l := []rune(string(b.LineBytes(y)))
|
||||
xInit := 0
|
||||
if y == start.Y {
|
||||
if startChar == braceType[0] {
|
||||
xInit = start.X
|
||||
} else {
|
||||
xInit = start.X - 1
|
||||
}
|
||||
xInit = start.X
|
||||
}
|
||||
for x := xInit; x < len(l); x++ {
|
||||
r := l[x]
|
||||
@@ -1026,42 +1148,76 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo
|
||||
} else if r == braceType[1] {
|
||||
i--
|
||||
if i == 0 {
|
||||
if startChar == braceType[0] {
|
||||
return Loc{x, y}, false, true
|
||||
}
|
||||
return Loc{x, y}, true, true
|
||||
return Loc{x, y}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if startChar == braceType[1] || leftChar == braceType[1] {
|
||||
} else if char == braceType[1] {
|
||||
for y := start.Y; y >= 0; y-- {
|
||||
l := []rune(string(b.lines[y].data))
|
||||
xInit := len(l) - 1
|
||||
if y == start.Y {
|
||||
if leftChar == braceType[1] {
|
||||
xInit = start.X - 1
|
||||
} else {
|
||||
xInit = start.X
|
||||
}
|
||||
xInit = start.X
|
||||
}
|
||||
for x := xInit; x >= 0; x-- {
|
||||
r := l[x]
|
||||
if r == braceType[0] {
|
||||
if r == braceType[1] {
|
||||
i++
|
||||
} else if r == braceType[0] {
|
||||
i--
|
||||
if i == 0 {
|
||||
if leftChar == braceType[1] {
|
||||
return Loc{x, y}, true, true
|
||||
}
|
||||
return Loc{x, y}, false, true
|
||||
return Loc{x, y}, true
|
||||
}
|
||||
} else if r == braceType[1] {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return start, true, false
|
||||
return start, false
|
||||
}
|
||||
|
||||
// If there is a brace character (for example '{' or ']') at the given start location,
|
||||
// FindMatchingBrace returns the location of the matching brace for it (for example '}'
|
||||
// or '['). The second returned value is true if there was no matching brace found
|
||||
// for given starting location but it was found for the location one character left
|
||||
// of it. The third returned value is true if the matching brace was found at all.
|
||||
func (b *Buffer) FindMatchingBrace(start Loc) (Loc, bool, bool) {
|
||||
// TODO: maybe can be more efficient with utf8 package
|
||||
curLine := []rune(string(b.LineBytes(start.Y)))
|
||||
|
||||
// first try to find matching brace for the given location (it has higher priority)
|
||||
if start.X >= 0 && start.X < len(curLine) {
|
||||
startChar := curLine[start.X]
|
||||
|
||||
for _, bp := range BracePairs {
|
||||
if startChar == bp[0] || startChar == bp[1] {
|
||||
mb, found := b.findMatchingBrace(bp, start, startChar)
|
||||
if found {
|
||||
return mb, false, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.Settings["matchbraceleft"].(bool) {
|
||||
// failed to find matching brace for the given location, so try to find matching
|
||||
// brace for the location one character left of it
|
||||
if start.X-1 >= 0 && start.X-1 < len(curLine) {
|
||||
leftChar := curLine[start.X-1]
|
||||
left := Loc{start.X - 1, start.Y}
|
||||
|
||||
for _, bp := range BracePairs {
|
||||
if leftChar == bp[0] || leftChar == bp[1] {
|
||||
mb, found := b.findMatchingBrace(bp, left, leftChar)
|
||||
if found {
|
||||
return mb, true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return start, false, false
|
||||
}
|
||||
|
||||
// Retab changes all tabs to spaces or vice versa
|
||||
@@ -1083,7 +1239,11 @@ func (b *Buffer) Retab() {
|
||||
}
|
||||
|
||||
l = bytes.TrimLeft(l, " \t")
|
||||
|
||||
b.Lock()
|
||||
b.lines[i].data = append(ws, l...)
|
||||
b.Unlock()
|
||||
|
||||
b.MarkModified(i, i)
|
||||
dirty = true
|
||||
}
|
||||
@@ -1126,7 +1286,7 @@ func (b *Buffer) Write(bytes []byte) (n int, err error) {
|
||||
return len(bytes), nil
|
||||
}
|
||||
|
||||
func (b *Buffer) updateDiffSync() {
|
||||
func (b *Buffer) updateDiff(synchronous bool) {
|
||||
b.diffLock.Lock()
|
||||
defer b.diffLock.Unlock()
|
||||
|
||||
@@ -1137,7 +1297,16 @@ func (b *Buffer) updateDiffSync() {
|
||||
}
|
||||
|
||||
differ := dmp.New()
|
||||
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
|
||||
|
||||
if !synchronous {
|
||||
b.Lock()
|
||||
}
|
||||
bytes := b.Bytes()
|
||||
if !synchronous {
|
||||
b.Unlock()
|
||||
}
|
||||
|
||||
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(bytes))
|
||||
diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
|
||||
lineN := 0
|
||||
|
||||
@@ -1166,13 +1335,9 @@ func (b *Buffer) updateDiffSync() {
|
||||
|
||||
// UpdateDiff computes the diff between the diff base and the buffer content.
|
||||
// The update may be performed synchronously or asynchronously.
|
||||
// UpdateDiff calls the supplied callback when the update is complete.
|
||||
// The argument passed to the callback is set to true if and only if
|
||||
// the update was performed synchronously.
|
||||
// If an asynchronous update is already pending when UpdateDiff is called,
|
||||
// UpdateDiff does not schedule another update, in which case the callback
|
||||
// is not called.
|
||||
func (b *Buffer) UpdateDiff(callback func(bool)) {
|
||||
// UpdateDiff does not schedule another update.
|
||||
func (b *Buffer) UpdateDiff() {
|
||||
if b.updateDiffTimer != nil {
|
||||
return
|
||||
}
|
||||
@@ -1183,20 +1348,18 @@ func (b *Buffer) UpdateDiff(callback func(bool)) {
|
||||
}
|
||||
|
||||
if lineCount < 1000 {
|
||||
b.updateDiffSync()
|
||||
callback(true)
|
||||
b.updateDiff(true)
|
||||
} else if lineCount < 30000 {
|
||||
b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
|
||||
b.updateDiffTimer = nil
|
||||
b.updateDiffSync()
|
||||
callback(false)
|
||||
b.updateDiff(false)
|
||||
screen.Redraw()
|
||||
})
|
||||
} else {
|
||||
// Don't compute diffs for very large files
|
||||
b.diffLock.Lock()
|
||||
b.diff = make(map[int]DiffStatus)
|
||||
b.diffLock.Unlock()
|
||||
callback(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1208,9 +1371,7 @@ func (b *Buffer) SetDiffBase(diffBase []byte) {
|
||||
} else {
|
||||
b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
|
||||
}
|
||||
b.UpdateDiff(func(synchronous bool) {
|
||||
screen.Redraw()
|
||||
})
|
||||
b.UpdateDiff()
|
||||
}
|
||||
|
||||
// DiffStatus returns the diff status for a line in the buffer
|
||||
|
||||
@@ -20,6 +20,7 @@ type operation struct {
|
||||
|
||||
func init() {
|
||||
ulua.L = lua.NewState()
|
||||
config.InitRuntimeFiles(false)
|
||||
config.InitGlobalSettings()
|
||||
config.GlobalSettings["backup"] = false
|
||||
config.GlobalSettings["fastdirty"] = true
|
||||
|
||||
@@ -30,6 +30,11 @@ type Cursor struct {
|
||||
// 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
|
||||
}
|
||||
@@ -38,6 +43,8 @@ func NewCursor(b *Buffer, l Loc) *Cursor {
|
||||
c := &Cursor{
|
||||
buf: b,
|
||||
Loc: l,
|
||||
|
||||
NewTrailingWsY: -1,
|
||||
}
|
||||
c.StoreVisualX()
|
||||
return c
|
||||
@@ -396,13 +403,26 @@ func (c *Cursor) SelectTo(loc Loc) {
|
||||
|
||||
// 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)) {
|
||||
c.Right()
|
||||
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)) {
|
||||
@@ -414,6 +434,10 @@ func (c *Cursor) WordRight() {
|
||||
|
||||
// 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 {
|
||||
@@ -421,6 +445,17 @@ func (c *Cursor) WordLeft() {
|
||||
}
|
||||
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 {
|
||||
@@ -431,6 +466,132 @@ func (c *Cursor) WordLeft() {
|
||||
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)
|
||||
|
||||
@@ -106,6 +106,10 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
|
||||
c.Relocate()
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
if useUndo {
|
||||
eh.updateTrailingWs(t)
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
@@ -249,11 +253,11 @@ func (eh *EventHandler) Execute(t *TextEvent) {
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
// Undo the first event in the undo stack
|
||||
func (eh *EventHandler) Undo() {
|
||||
// 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
|
||||
return false
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
@@ -262,15 +266,16 @@ func (eh *EventHandler) Undo() {
|
||||
for {
|
||||
t = eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
break
|
||||
}
|
||||
|
||||
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
||||
return
|
||||
break
|
||||
}
|
||||
|
||||
eh.UndoOneEvent()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UndoOneEvent undoes one event
|
||||
@@ -286,23 +291,20 @@ func (eh *EventHandler) UndoOneEvent() {
|
||||
eh.UndoTextEvent(t)
|
||||
|
||||
// Set the cursor in the right place
|
||||
teCursor := t.C
|
||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
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
|
||||
func (eh *EventHandler) Redo() {
|
||||
// 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
|
||||
return false
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
@@ -311,15 +313,16 @@ func (eh *EventHandler) Redo() {
|
||||
for {
|
||||
t = eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
break
|
||||
}
|
||||
|
||||
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
||||
return
|
||||
break
|
||||
}
|
||||
|
||||
eh.RedoOneEvent()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RedoOneEvent redoes one event
|
||||
@@ -329,12 +332,9 @@ func (eh *EventHandler) RedoOneEvent() {
|
||||
return
|
||||
}
|
||||
|
||||
teCursor := t.C
|
||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
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
|
||||
@@ -342,3 +342,58 @@ func (eh *EventHandler) RedoOneEvent() {
|
||||
|
||||
eh.UndoStack.Push(t)
|
||||
}
|
||||
|
||||
// updateTrailingWs updates the cursor's trailing whitespace status after a text event
|
||||
func (eh *EventHandler) updateTrailingWs(t *TextEvent) {
|
||||
if len(t.Deltas) != 1 {
|
||||
return
|
||||
}
|
||||
text := t.Deltas[0].Text
|
||||
start := t.Deltas[0].Start
|
||||
end := t.Deltas[0].End
|
||||
|
||||
c := eh.cursors[eh.active]
|
||||
isEol := func(loc Loc) bool {
|
||||
return loc.X == util.CharacterCount(eh.buf.LineBytes(loc.Y))
|
||||
}
|
||||
if t.EventType == TextEventInsert && c.Loc == end && isEol(end) {
|
||||
var addedTrailingWs bool
|
||||
addedAfterWs := false
|
||||
addedWsOnly := false
|
||||
if start.Y == end.Y {
|
||||
addedTrailingWs = util.HasTrailingWhitespace(text)
|
||||
addedWsOnly = util.IsBytesWhitespace(text)
|
||||
addedAfterWs = start.X > 0 && util.IsWhitespace(c.buf.RuneAt(Loc{start.X - 1, start.Y}))
|
||||
} else {
|
||||
lastnl := bytes.LastIndex(text, []byte{'\n'})
|
||||
addedTrailingWs = util.HasTrailingWhitespace(text[lastnl+1:])
|
||||
}
|
||||
|
||||
if addedTrailingWs && !(addedAfterWs && addedWsOnly) {
|
||||
c.NewTrailingWsY = c.Y
|
||||
} else if !addedTrailingWs {
|
||||
c.NewTrailingWsY = -1
|
||||
}
|
||||
} else if t.EventType == TextEventRemove && c.Loc == start && isEol(start) {
|
||||
removedAfterWs := util.HasTrailingWhitespace(eh.buf.LineBytes(start.Y))
|
||||
var removedWsOnly bool
|
||||
if start.Y == end.Y {
|
||||
removedWsOnly = util.IsBytesWhitespace(text)
|
||||
} else {
|
||||
firstnl := bytes.Index(text, []byte{'\n'})
|
||||
removedWsOnly = util.IsBytesWhitespace(text[:firstnl])
|
||||
}
|
||||
|
||||
if removedAfterWs && !removedWsOnly {
|
||||
c.NewTrailingWsY = c.Y
|
||||
} else if !removedAfterWs {
|
||||
c.NewTrailingWsY = -1
|
||||
}
|
||||
} else if c.NewTrailingWsY != -1 && start.Y != end.Y && c.Loc.GreaterThan(start) &&
|
||||
((t.EventType == TextEventInsert && c.Y == c.NewTrailingWsY+(end.Y-start.Y)) ||
|
||||
(t.EventType == TextEventRemove && c.Y == c.NewTrailingWsY-(end.Y-start.Y))) {
|
||||
// The cursor still has its new trailingws
|
||||
// but its line number was shifted by insert or remove of lines above
|
||||
c.NewTrailingWsY = c.Y
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,9 @@ type searchState struct {
|
||||
type Line struct {
|
||||
data []byte
|
||||
|
||||
state highlight.State
|
||||
match highlight.LineMatch
|
||||
rehighlight bool
|
||||
lock sync.Mutex
|
||||
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.
|
||||
@@ -75,6 +74,7 @@ type LineArray struct {
|
||||
lines []Line
|
||||
Endings FileFormat
|
||||
initsize uint64
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Append efficiently appends lines together
|
||||
@@ -147,20 +147,18 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
la.lines = Append(la.lines, Line{
|
||||
data: data,
|
||||
state: nil,
|
||||
match: nil,
|
||||
rehighlight: false,
|
||||
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,
|
||||
rehighlight: false,
|
||||
data: data[:dlen-1],
|
||||
state: nil,
|
||||
match: nil,
|
||||
})
|
||||
}
|
||||
n++
|
||||
@@ -190,22 +188,23 @@ func (la *LineArray) Bytes() []byte {
|
||||
// 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,
|
||||
rehighlight: false,
|
||||
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,
|
||||
rehighlight: false,
|
||||
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') {
|
||||
@@ -233,24 +232,26 @@ func (la *LineArray) insertByte(pos Loc, value byte) {
|
||||
|
||||
// joinLines joins the two lines a and b
|
||||
func (la *LineArray) joinLines(a, b int) {
|
||||
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
|
||||
la.lines[a].data = append(la.lines[a].data, la.lines[b].data...)
|
||||
la.deleteLine(b)
|
||||
}
|
||||
|
||||
// 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].data[pos.X:])
|
||||
la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...)
|
||||
la.lines[pos.Y+1].state = la.lines[pos.Y].state
|
||||
la.lines[pos.Y].state = nil
|
||||
la.lines[pos.Y].match = nil
|
||||
la.lines[pos.Y+1].match = nil
|
||||
la.lines[pos.Y].rehighlight = true
|
||||
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)
|
||||
@@ -327,11 +328,11 @@ func (la *LineArray) End() Loc {
|
||||
}
|
||||
|
||||
// LineBytes returns line n as an array of bytes
|
||||
func (la *LineArray) LineBytes(n int) []byte {
|
||||
if n >= len(la.lines) || n < 0 {
|
||||
func (la *LineArray) LineBytes(lineN int) []byte {
|
||||
if lineN >= len(la.lines) || lineN < 0 {
|
||||
return []byte{}
|
||||
}
|
||||
return la.lines[n].data
|
||||
return la.lines[lineN].data
|
||||
}
|
||||
|
||||
// State gets the highlight state for the given line number
|
||||
@@ -362,16 +363,14 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch {
|
||||
return la.lines[lineN].match
|
||||
}
|
||||
|
||||
func (la *LineArray) Rehighlight(lineN int) bool {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
return la.lines[lineN].rehighlight
|
||||
// Locks the whole LineArray
|
||||
func (la *LineArray) Lock() {
|
||||
la.lock.Lock()
|
||||
}
|
||||
|
||||
func (la *LineArray) SetRehighlight(lineN int, on bool) {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
la.lines[lineN].rehighlight = on
|
||||
// Unlocks the whole LineArray
|
||||
func (la *LineArray) Unlock() {
|
||||
la.lock.Unlock()
|
||||
}
|
||||
|
||||
// SearchMatch returns true if the location `pos` is within a match
|
||||
|
||||
@@ -54,7 +54,7 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error,
|
||||
screen.TempStart(screenb)
|
||||
return err
|
||||
}
|
||||
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,9 +93,19 @@ func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// AutoSave saves the buffer to its default path
|
||||
func (b *Buffer) AutoSave() error {
|
||||
// Doing full b.Modified() check every time would be costly, due to the hash
|
||||
// calculation. So use just isModified even if fastdirty is not set.
|
||||
if !b.isModified {
|
||||
return nil
|
||||
}
|
||||
return b.saveToFile(b.Path, false, true)
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
return b.saveToFile(filename, false)
|
||||
return b.saveToFile(filename, false, false)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
@@ -103,10 +113,10 @@ func (b *Buffer) SaveWithSudo() error {
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
return b.saveToFile(filename, true)
|
||||
return b.saveToFile(filename, true, false)
|
||||
}
|
||||
|
||||
func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
||||
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error {
|
||||
var err error
|
||||
if b.Type.Readonly {
|
||||
return errors.New("Cannot save readonly buffer")
|
||||
@@ -118,7 +128,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
||||
return errors.New("Save with sudo not supported on Windows")
|
||||
}
|
||||
|
||||
if b.Settings["rmtrailingws"].(bool) {
|
||||
if !autoSave && b.Settings["rmtrailingws"].(bool) {
|
||||
for i, l := range b.lines {
|
||||
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo
|
||||
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
||||
// and returns the number of replacements made and the number of runes
|
||||
// added or removed on the last line of the range
|
||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) {
|
||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) {
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
@@ -172,9 +172,13 @@ func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []b
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
||||
result := []byte{}
|
||||
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
|
||||
result = search.Expand(result, replace, in, submatches)
|
||||
var result []byte
|
||||
if captureGroups {
|
||||
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
|
||||
result = search.Expand(result, replace, in, submatches)
|
||||
}
|
||||
} else {
|
||||
result = replace
|
||||
}
|
||||
found++
|
||||
if i == end.Y {
|
||||
|
||||
@@ -1,36 +1,74 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"reflect"
|
||||
|
||||
"github.com/zyedidia/micro/v2/internal/config"
|
||||
"github.com/zyedidia/micro/v2/internal/screen"
|
||||
)
|
||||
|
||||
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
||||
func (b *Buffer) ReloadSettings(reloadFiletype bool) {
|
||||
settings := config.ParsedSettings()
|
||||
|
||||
if _, ok := b.LocalSettings["filetype"]; !ok && reloadFiletype {
|
||||
// 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()
|
||||
settings["filetype"] = b.Settings["filetype"]
|
||||
|
||||
config.InitLocalSettings(settings, b.Path)
|
||||
for k, v := range config.DefaultCommonSettings() {
|
||||
if k == "filetype" {
|
||||
// prevent recursion
|
||||
continue
|
||||
}
|
||||
if _, ok := config.VolatileSettings[k]; ok {
|
||||
// reload should not override volatile settings
|
||||
continue
|
||||
}
|
||||
if _, ok := b.LocalSettings[k]; ok {
|
||||
// reload should not override local settings
|
||||
continue
|
||||
}
|
||||
if _, ok := settings[k]; ok {
|
||||
b.DoSetOptionNative(k, settings[k])
|
||||
} else {
|
||||
b.DoSetOptionNative(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
|
||||
if reflect.DeepEqual(b.Settings[option], nativeValue) {
|
||||
return
|
||||
}
|
||||
|
||||
b.Settings[option] = nativeValue
|
||||
|
||||
if option == "fastdirty" {
|
||||
if !nativeValue.(bool) {
|
||||
if !b.Modified() {
|
||||
e := calcHash(b, &b.origHash)
|
||||
if e == ErrFileTooLarge {
|
||||
b.Settings["fastdirty"] = false
|
||||
if b.Size() > LargeFileThreshold {
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
if !b.isModified {
|
||||
calcHash(b, &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" {
|
||||
config.InitRuntimeFiles()
|
||||
err := config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
err = config.InitGlobalSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
config.InitLocalSettings(b.Settings, b.Path)
|
||||
b.UpdateRules()
|
||||
b.ReloadSettings(false)
|
||||
} else if option == "fileformat" {
|
||||
switch b.Settings["fileformat"].(string) {
|
||||
case "unix":
|
||||
@@ -74,11 +112,20 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.OptionCallback != nil {
|
||||
b.OptionCallback(option, nativeValue)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
||||
if err := config.OptionIsValid(option, nativeValue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.DoSetOptionNative(option, nativeValue)
|
||||
b.LocalSettings[option] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Autosave chan bool
|
||||
var autotime int
|
||||
|
||||
// lock for autosave
|
||||
var autolock sync.Mutex
|
||||
var autotime chan float64
|
||||
|
||||
func init() {
|
||||
Autosave = make(chan bool)
|
||||
autotime = make(chan float64)
|
||||
}
|
||||
|
||||
func SetAutoTime(a int) {
|
||||
autolock.Lock()
|
||||
autotime = a
|
||||
autolock.Unlock()
|
||||
}
|
||||
|
||||
func GetAutoTime() int {
|
||||
autolock.Lock()
|
||||
a := autotime
|
||||
autolock.Unlock()
|
||||
return a
|
||||
func SetAutoTime(a float64) {
|
||||
autotime <- a
|
||||
}
|
||||
|
||||
func StartAutoSave() {
|
||||
go func() {
|
||||
var a float64
|
||||
var t *time.Timer
|
||||
var elapsed <-chan time.Time
|
||||
for {
|
||||
autolock.Lock()
|
||||
a := autotime
|
||||
autolock.Unlock()
|
||||
if a < 1 {
|
||||
break
|
||||
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
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Duration(a) * time.Second)
|
||||
Autosave <- true
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -52,43 +52,55 @@ func InitColorscheme() error {
|
||||
Colorscheme = make(map[string]tcell.Style)
|
||||
DefStyle = tcell.StyleDefault
|
||||
|
||||
return LoadDefaultColorscheme()
|
||||
c, err := LoadDefaultColorscheme()
|
||||
if err == nil {
|
||||
Colorscheme = c
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
|
||||
func LoadDefaultColorscheme() error {
|
||||
return LoadColorscheme(GlobalSettings["colorscheme"].(string))
|
||||
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) error {
|
||||
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 errors.New(colorschemeName + " is not a valid colorscheme")
|
||||
return c, errors.New(colorschemeName + " is not a valid colorscheme")
|
||||
}
|
||||
if data, err := file.Data(); err != nil {
|
||||
return errors.New("Error loading colorscheme: " + err.Error())
|
||||
return c, errors.New("Error loading colorscheme: " + err.Error())
|
||||
} else {
|
||||
Colorscheme, err = ParseColorscheme(string(data))
|
||||
var err error
|
||||
c, err = ParseColorscheme(file.Name(), string(data), parsedColorschemes)
|
||||
if err != nil {
|
||||
return err
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
|
||||
// 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) (map[string]tcell.Style, error) {
|
||||
func ParseColorscheme(name string, text string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
|
||||
var err error
|
||||
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||
|
||||
colorParser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||
includeParser := regexp.MustCompile(`include\s+"(.*)"`)
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
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] == '#' {
|
||||
@@ -96,7 +108,30 @@ func ParseColorscheme(text string) (map[string]tcell.Style, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
matches := parser.FindSubmatch([]byte(line))
|
||||
matches := includeParser.FindSubmatch([]byte(line))
|
||||
if len(matches) == 2 {
|
||||
// support includes only in case parsedColorschemes are given
|
||||
if parsedColorschemes != nil {
|
||||
include := string(matches[1])
|
||||
for _, name := range *parsedColorschemes {
|
||||
// check for circular includes...
|
||||
if name == include {
|
||||
// ...and prevent them
|
||||
continue lineLoop
|
||||
}
|
||||
}
|
||||
includeScheme, err := LoadColorscheme(include, parsedColorschemes)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
for k, v := range includeScheme {
|
||||
c[k] = v
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
matches = colorParser.FindSubmatch([]byte(line))
|
||||
if len(matches) == 3 {
|
||||
link := string(matches[1])
|
||||
colors := string(matches[2])
|
||||
|
||||
@@ -65,7 +65,7 @@ color-link constant "#AE81FF,#282828"
|
||||
color-link constant.string "#E6DB74,#282828"
|
||||
color-link constant.string.char "#BDE6AD,#282828"`
|
||||
|
||||
c, err := ParseColorscheme(testColorscheme)
|
||||
c, err := ParseColorscheme("testColorscheme", testColorscheme, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
fg, bg, _ := c["comment"].Decompose()
|
||||
|
||||
@@ -40,6 +40,10 @@ var allFiles [][]RuntimeFile
|
||||
var realFiles [][]RuntimeFile
|
||||
|
||||
func init() {
|
||||
initRuntimeVars()
|
||||
}
|
||||
|
||||
func initRuntimeVars() {
|
||||
allFiles = make([][]RuntimeFile, NumTypes)
|
||||
realFiles = make([][]RuntimeFile, NumTypes)
|
||||
}
|
||||
@@ -129,9 +133,17 @@ func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assetLoop:
|
||||
for _, f := range files {
|
||||
if ok, _ := path.Match(pattern, f); ok {
|
||||
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
|
||||
af := assetFile(path.Join(directory, f))
|
||||
for _, rf := range realFiles[fileType] {
|
||||
if af.Name() == rf.Name() {
|
||||
continue assetLoop
|
||||
}
|
||||
}
|
||||
AddRuntimeFile(fileType, af)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,19 +170,30 @@ func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
|
||||
return realFiles[fileType]
|
||||
}
|
||||
|
||||
// InitRuntimeFiles initializes all assets file and the config directory
|
||||
func InitRuntimeFiles() {
|
||||
// InitRuntimeFiles initializes all assets files and the config directory.
|
||||
// If `user` is false, InitRuntimeFiles ignores the config directory and
|
||||
// initializes asset files only.
|
||||
func InitRuntimeFiles(user bool) {
|
||||
add := func(fileType RTFiletype, dir, pattern string) {
|
||||
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
|
||||
if user {
|
||||
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
|
||||
}
|
||||
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
|
||||
}
|
||||
|
||||
initRuntimeVars()
|
||||
|
||||
add(RTColorscheme, "colorschemes", "*.micro")
|
||||
add(RTSyntax, "syntax", "*.yaml")
|
||||
add(RTSyntaxHeader, "syntax", "*.hdr")
|
||||
add(RTHelp, "help", "*.md")
|
||||
}
|
||||
|
||||
// InitPlugins initializes the plugins
|
||||
func InitPlugins() {
|
||||
Plugins = Plugins[:0]
|
||||
initlua := filepath.Join(ConfigDir, "init.lua")
|
||||
|
||||
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
|
||||
p := new(Plugin)
|
||||
p.Name = "initlua"
|
||||
@@ -218,7 +241,15 @@ func InitRuntimeFiles() {
|
||||
|
||||
plugdir = filepath.Join("runtime", "plugins")
|
||||
if files, err := rt.AssetDir(plugdir); err == nil {
|
||||
outer:
|
||||
for _, d := range files {
|
||||
for _, p := range Plugins {
|
||||
if p.Name == d {
|
||||
log.Println(p.Name, "built-in plugin overridden by user-defined one")
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
if srcs, err := rt.AssetDir(filepath.Join(plugdir, d)); err == nil {
|
||||
p := new(Plugin)
|
||||
p.Name = d
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitRuntimeFiles()
|
||||
InitRuntimeFiles(false)
|
||||
}
|
||||
|
||||
func TestAddFile(t *testing.T) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -19,325 +20,84 @@ import (
|
||||
|
||||
type optionValidator func(string, interface{}) error
|
||||
|
||||
var (
|
||||
ErrInvalidOption = errors.New("Invalid option")
|
||||
ErrInvalidValue = errors.New("Invalid value")
|
||||
|
||||
// The options that the user can set
|
||||
GlobalSettings map[string]interface{}
|
||||
|
||||
// This is the raw parsed json
|
||||
parsedSettings map[string]interface{}
|
||||
settingsParseError bool
|
||||
|
||||
// ModifiedSettings is a map of settings which should be written to disk
|
||||
// because they have been modified by the user in this session
|
||||
ModifiedSettings map[string]bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
ModifiedSettings = make(map[string]bool)
|
||||
parsedSettings = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Options with validators
|
||||
// a list of settings that need option validators
|
||||
var optionValidators = map[string]optionValidator{
|
||||
"autosave": validateNonNegativeValue,
|
||||
"clipboard": validateClipboard,
|
||||
"tabsize": validatePositiveValue,
|
||||
"scrollmargin": validateNonNegativeValue,
|
||||
"scrollspeed": validateNonNegativeValue,
|
||||
"colorscheme": validateColorscheme,
|
||||
"colorcolumn": validateNonNegativeValue,
|
||||
"fileformat": validateLineEnding,
|
||||
"encoding": validateEncoding,
|
||||
"multiopen": validateMultiOpen,
|
||||
"reload": validateReload,
|
||||
"autosave": validateNonNegativeValue,
|
||||
"clipboard": validateChoice,
|
||||
"colorcolumn": validateNonNegativeValue,
|
||||
"colorscheme": validateColorscheme,
|
||||
"detectlimit": validateNonNegativeValue,
|
||||
"encoding": validateEncoding,
|
||||
"fileformat": validateChoice,
|
||||
"matchbracestyle": validateChoice,
|
||||
"multiopen": validateChoice,
|
||||
"reload": validateChoice,
|
||||
"scrollmargin": validateNonNegativeValue,
|
||||
"scrollspeed": validateNonNegativeValue,
|
||||
"tabsize": validatePositiveValue,
|
||||
}
|
||||
|
||||
func ReadSettings() error {
|
||||
filename := filepath.Join(ConfigDir, "settings.json")
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
settingsParseError = true
|
||||
return errors.New("Error reading settings.json file: " + err.Error())
|
||||
}
|
||||
if !strings.HasPrefix(string(input), "null") {
|
||||
// Unmarshal the input into the parsed map
|
||||
err = json5.Unmarshal(input, &parsedSettings)
|
||||
if err != nil {
|
||||
settingsParseError = true
|
||||
return errors.New("Error reading settings.json: " + err.Error())
|
||||
}
|
||||
|
||||
// check if autosave is a boolean and convert it to float if so
|
||||
if v, ok := parsedSettings["autosave"]; ok {
|
||||
s, ok := v.(bool)
|
||||
if ok {
|
||||
if s {
|
||||
parsedSettings["autosave"] = 8.0
|
||||
} else {
|
||||
parsedSettings["autosave"] = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
|
||||
var interfaceArr []interface{}
|
||||
switch option {
|
||||
case "pluginrepos", "pluginchannels":
|
||||
return value.AssignableTo(reflect.TypeOf(interfaceArr))
|
||||
default:
|
||||
return def.AssignableTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
// InitGlobalSettings initializes the options map and sets all options to their default values
|
||||
// Must be called after ReadSettings
|
||||
func InitGlobalSettings() error {
|
||||
var err error
|
||||
GlobalSettings = DefaultGlobalSettings()
|
||||
|
||||
for k, v := range parsedSettings {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
|
||||
err = fmt.Errorf("Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v), GlobalSettings[k], reflect.TypeOf(GlobalSettings[k]))
|
||||
continue
|
||||
}
|
||||
|
||||
GlobalSettings[k] = v
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// InitLocalSettings scans the json in settings.json and sets the options locally based
|
||||
// on whether the filetype or path matches ft or glob local settings
|
||||
// Must be called after ReadSettings
|
||||
func InitLocalSettings(settings map[string]interface{}, path string) error {
|
||||
var parseError error
|
||||
for k, v := range parsedSettings {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if strings.HasPrefix(k, "ft:") {
|
||||
if settings["filetype"].(string) == k[3:] {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
|
||||
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
|
||||
continue
|
||||
}
|
||||
settings[k1] = v1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g, err := glob.Compile(k)
|
||||
if err != nil {
|
||||
parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if g.MatchString(path) {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
|
||||
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
|
||||
continue
|
||||
}
|
||||
settings[k1] = v1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseError
|
||||
}
|
||||
|
||||
// WriteSettings writes the settings to the specified filename as JSON
|
||||
func WriteSettings(filename string) error {
|
||||
if settingsParseError {
|
||||
// Don't write settings if there was a parse error
|
||||
// because this will delete the settings.json if it
|
||||
// is invalid. Instead we should allow the user to fix
|
||||
// it manually.
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if _, e := os.Stat(ConfigDir); e == nil {
|
||||
defaults := DefaultGlobalSettings()
|
||||
|
||||
// remove any options froms parsedSettings that have since been marked as default
|
||||
for k, v := range parsedSettings {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
cur, okcur := GlobalSettings[k]
|
||||
if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
|
||||
delete(parsedSettings, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add any options to parsedSettings that have since been marked as non-default
|
||||
for k, v := range GlobalSettings {
|
||||
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
||||
if _, wr := ModifiedSettings[k]; wr {
|
||||
parsedSettings[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// OverwriteSettings writes the current settings to settings.json and
|
||||
// resets any user configuration of local settings present in settings.json
|
||||
func OverwriteSettings(filename string) error {
|
||||
settings := make(map[string]interface{})
|
||||
|
||||
var err error
|
||||
if _, e := os.Stat(ConfigDir); e == nil {
|
||||
defaults := DefaultGlobalSettings()
|
||||
for k, v := range GlobalSettings {
|
||||
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
||||
if _, wr := ModifiedSettings[k]; wr {
|
||||
settings[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(settings, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
|
||||
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||
name = pl + "." + name
|
||||
if _, ok := GlobalSettings[name]; !ok {
|
||||
defaultCommonSettings[name] = defaultvalue
|
||||
GlobalSettings[name] = defaultvalue
|
||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
||||
if err != nil {
|
||||
return errors.New("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
defaultCommonSettings[name] = defaultvalue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
|
||||
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||
return RegisterGlobalOption(pl+"."+name, defaultvalue)
|
||||
}
|
||||
|
||||
// RegisterCommonOption creates a new option
|
||||
func RegisterCommonOption(name string, defaultvalue interface{}) error {
|
||||
if v, ok := GlobalSettings[name]; !ok {
|
||||
defaultCommonSettings[name] = defaultvalue
|
||||
GlobalSettings[name] = defaultvalue
|
||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
||||
if err != nil {
|
||||
return errors.New("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
defaultCommonSettings[name] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterGlobalOption creates a new global-only option
|
||||
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
|
||||
if v, ok := GlobalSettings[name]; !ok {
|
||||
DefaultGlobalOnlySettings[name] = defaultvalue
|
||||
GlobalSettings[name] = defaultvalue
|
||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
||||
if err != nil {
|
||||
return errors.New("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
DefaultGlobalOnlySettings[name] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGlobalOption returns the global value of the given option
|
||||
func GetGlobalOption(name string) interface{} {
|
||||
return GlobalSettings[name]
|
||||
// a list of settings with pre-defined choices
|
||||
var OptionChoices = map[string][]string{
|
||||
"clipboard": {"internal", "external", "terminal"},
|
||||
"fileformat": {"unix", "dos"},
|
||||
"matchbracestyle": {"underline", "highlight"},
|
||||
"multiopen": {"tab", "hsplit", "vsplit"},
|
||||
"reload": {"prompt", "auto", "disabled"},
|
||||
}
|
||||
|
||||
// a list of settings that can be globally and locally modified and their
|
||||
// default values
|
||||
var defaultCommonSettings = map[string]interface{}{
|
||||
"autoindent": true,
|
||||
"autosu": false,
|
||||
"backup": true,
|
||||
"backupdir": "",
|
||||
"basename": false,
|
||||
"colorcolumn": float64(0),
|
||||
"cursorline": true,
|
||||
"diffgutter": false,
|
||||
"encoding": "utf-8",
|
||||
"eofnewline": true,
|
||||
"fastdirty": false,
|
||||
"fileformat": "unix",
|
||||
"filetype": "unknown",
|
||||
"hlsearch": false,
|
||||
"incsearch": true,
|
||||
"ignorecase": true,
|
||||
"indentchar": " ",
|
||||
"keepautoindent": false,
|
||||
"matchbrace": true,
|
||||
"mkparents": false,
|
||||
"permbackup": false,
|
||||
"readonly": false,
|
||||
"reload": "prompt",
|
||||
"rmtrailingws": false,
|
||||
"ruler": true,
|
||||
"relativeruler": false,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollbar": false,
|
||||
"scrollmargin": float64(3),
|
||||
"scrollspeed": float64(2),
|
||||
"smartpaste": true,
|
||||
"softwrap": false,
|
||||
"splitbottom": true,
|
||||
"splitright": true,
|
||||
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
|
||||
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabmovement": false,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
"useprimary": true,
|
||||
"wordwrap": false,
|
||||
}
|
||||
|
||||
func GetInfoBarOffset() int {
|
||||
offset := 0
|
||||
if GetGlobalOption("infobar").(bool) {
|
||||
offset++
|
||||
}
|
||||
if GetGlobalOption("keymenu").(bool) {
|
||||
offset += 2
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
// DefaultCommonSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultCommonSettings() map[string]interface{} {
|
||||
commonsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
commonsettings[k] = v
|
||||
}
|
||||
return commonsettings
|
||||
"autoindent": true,
|
||||
"autosu": false,
|
||||
"backup": true,
|
||||
"backupdir": "",
|
||||
"basename": false,
|
||||
"colorcolumn": float64(0),
|
||||
"cursorline": true,
|
||||
"detectlimit": float64(100),
|
||||
"diffgutter": false,
|
||||
"encoding": "utf-8",
|
||||
"eofnewline": true,
|
||||
"fastdirty": false,
|
||||
"fileformat": defaultFileFormat(),
|
||||
"filetype": "unknown",
|
||||
"hlsearch": false,
|
||||
"hltaberrors": false,
|
||||
"hltrailingws": false,
|
||||
"incsearch": true,
|
||||
"ignorecase": true,
|
||||
"indentchar": " ",
|
||||
"keepautoindent": false,
|
||||
"matchbrace": true,
|
||||
"matchbraceleft": true,
|
||||
"matchbracestyle": "underline",
|
||||
"mkparents": false,
|
||||
"permbackup": false,
|
||||
"readonly": false,
|
||||
"reload": "prompt",
|
||||
"rmtrailingws": false,
|
||||
"ruler": true,
|
||||
"relativeruler": false,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollbar": false,
|
||||
"scrollmargin": float64(3),
|
||||
"scrollspeed": float64(2),
|
||||
"smartpaste": true,
|
||||
"softwrap": false,
|
||||
"splitbottom": true,
|
||||
"splitright": true,
|
||||
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
|
||||
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabmovement": false,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
"useprimary": true,
|
||||
"wordwrap": false,
|
||||
}
|
||||
|
||||
// a list of settings that should only be globally modified and their
|
||||
@@ -371,21 +131,307 @@ var LocalSettings = []string{
|
||||
"readonly",
|
||||
}
|
||||
|
||||
// DefaultGlobalSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultGlobalSettings() map[string]interface{} {
|
||||
globalsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
globalsettings[k] = v
|
||||
}
|
||||
for k, v := range DefaultGlobalOnlySettings {
|
||||
globalsettings[k] = v
|
||||
}
|
||||
return globalsettings
|
||||
var (
|
||||
ErrInvalidOption = errors.New("Invalid option")
|
||||
ErrInvalidValue = errors.New("Invalid value")
|
||||
|
||||
// The options that the user can set
|
||||
GlobalSettings map[string]interface{}
|
||||
|
||||
// This is the raw parsed json
|
||||
parsedSettings map[string]interface{}
|
||||
settingsParseError bool
|
||||
|
||||
// ModifiedSettings is a map of settings which should be written to disk
|
||||
// because they have been modified by the user in this session
|
||||
ModifiedSettings map[string]bool
|
||||
|
||||
// VolatileSettings is a map of settings which should not be written to disk
|
||||
// because they have been temporarily set for this session only
|
||||
VolatileSettings map[string]bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
ModifiedSettings = make(map[string]bool)
|
||||
VolatileSettings = make(map[string]bool)
|
||||
}
|
||||
|
||||
// DefaultAllSettings returns a map of all settings and their
|
||||
// default values (both common and global settings)
|
||||
func validateParsedSettings() error {
|
||||
var err error
|
||||
defaults := DefaultAllSettings()
|
||||
for k, v := range parsedSettings {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if strings.HasPrefix(k, "ft:") {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
if _, ok := defaults[k1]; ok {
|
||||
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
|
||||
err = e
|
||||
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, e := glob.Compile(k); e != nil {
|
||||
err = errors.New("Error with glob setting " + k + ": " + e.Error())
|
||||
delete(parsedSettings, k)
|
||||
continue
|
||||
}
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
if _, ok := defaults[k1]; ok {
|
||||
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
|
||||
err = e
|
||||
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if k == "autosave" {
|
||||
// if autosave is a boolean convert it to float
|
||||
s, ok := v.(bool)
|
||||
if ok {
|
||||
if s {
|
||||
parsedSettings["autosave"] = 8.0
|
||||
} else {
|
||||
parsedSettings["autosave"] = 0.0
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := defaults[k]; ok {
|
||||
if e := verifySetting(k, v, defaults[k]); e != nil {
|
||||
err = e
|
||||
parsedSettings[k] = defaults[k]
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ReadSettings() error {
|
||||
parsedSettings = make(map[string]interface{})
|
||||
filename := filepath.Join(ConfigDir, "settings.json")
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
settingsParseError = true
|
||||
return errors.New("Error reading settings.json file: " + err.Error())
|
||||
}
|
||||
if !strings.HasPrefix(string(input), "null") {
|
||||
// Unmarshal the input into the parsed map
|
||||
err = json5.Unmarshal(input, &parsedSettings)
|
||||
if err != nil {
|
||||
settingsParseError = true
|
||||
return errors.New("Error reading settings.json: " + err.Error())
|
||||
}
|
||||
err = validateParsedSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParsedSettings() map[string]interface{} {
|
||||
s := make(map[string]interface{})
|
||||
for k, v := range parsedSettings {
|
||||
s[k] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func verifySetting(option string, value interface{}, def interface{}) error {
|
||||
var interfaceArr []interface{}
|
||||
valType := reflect.TypeOf(value)
|
||||
defType := reflect.TypeOf(def)
|
||||
assignable := false
|
||||
|
||||
switch option {
|
||||
case "pluginrepos", "pluginchannels":
|
||||
assignable = valType.AssignableTo(reflect.TypeOf(interfaceArr))
|
||||
default:
|
||||
assignable = defType.AssignableTo(valType)
|
||||
}
|
||||
if !assignable {
|
||||
return fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", option, valType, def, defType)
|
||||
}
|
||||
|
||||
if err := OptionIsValid(option, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitGlobalSettings initializes the options map and sets all options to their default values
|
||||
// Must be called after ReadSettings
|
||||
func InitGlobalSettings() error {
|
||||
var err error
|
||||
GlobalSettings = DefaultAllSettings()
|
||||
|
||||
for k, v := range parsedSettings {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
GlobalSettings[k] = v
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// InitLocalSettings scans the json in settings.json and sets the options locally based
|
||||
// on whether the filetype or path matches ft or glob local settings
|
||||
// Must be called after ReadSettings
|
||||
func InitLocalSettings(settings map[string]interface{}, path string) {
|
||||
for k, v := range parsedSettings {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if strings.HasPrefix(k, "ft:") {
|
||||
if settings["filetype"].(string) == k[3:] {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
settings[k1] = v1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g, _ := glob.Compile(k)
|
||||
if g.MatchString(path) {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
settings[k1] = v1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSettings writes the settings to the specified filename as JSON
|
||||
func WriteSettings(filename string) error {
|
||||
if settingsParseError {
|
||||
// Don't write settings if there was a parse error
|
||||
// because this will delete the settings.json if it
|
||||
// is invalid. Instead we should allow the user to fix
|
||||
// it manually.
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
if _, e := os.Stat(ConfigDir); e == nil {
|
||||
defaults := DefaultAllSettings()
|
||||
|
||||
// remove any options froms parsedSettings that have since been marked as default
|
||||
for k, v := range parsedSettings {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
cur, okcur := GlobalSettings[k]
|
||||
_, vol := VolatileSettings[k]
|
||||
if def, ok := defaults[k]; ok && okcur && !vol && reflect.DeepEqual(cur, def) {
|
||||
delete(parsedSettings, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add any options to parsedSettings that have since been marked as non-default
|
||||
for k, v := range GlobalSettings {
|
||||
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
||||
if _, wr := ModifiedSettings[k]; wr {
|
||||
parsedSettings[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// OverwriteSettings writes the current settings to settings.json and
|
||||
// resets any user configuration of local settings present in settings.json
|
||||
func OverwriteSettings(filename string) error {
|
||||
settings := make(map[string]interface{})
|
||||
|
||||
var err error
|
||||
if _, e := os.Stat(ConfigDir); e == nil {
|
||||
defaults := DefaultAllSettings()
|
||||
for k, v := range GlobalSettings {
|
||||
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
|
||||
if _, wr := ModifiedSettings[k]; wr {
|
||||
settings[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(settings, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
|
||||
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||
return RegisterCommonOption(pl+"."+name, defaultvalue)
|
||||
}
|
||||
|
||||
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
|
||||
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||
return RegisterGlobalOption(pl+"."+name, defaultvalue)
|
||||
}
|
||||
|
||||
// RegisterCommonOption creates a new option
|
||||
func RegisterCommonOption(name string, defaultvalue interface{}) error {
|
||||
if _, ok := GlobalSettings[name]; !ok {
|
||||
GlobalSettings[name] = defaultvalue
|
||||
}
|
||||
defaultCommonSettings[name] = defaultvalue
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterGlobalOption creates a new global-only option
|
||||
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
|
||||
if _, ok := GlobalSettings[name]; !ok {
|
||||
GlobalSettings[name] = defaultvalue
|
||||
}
|
||||
DefaultGlobalOnlySettings[name] = defaultvalue
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGlobalOption returns the global value of the given option
|
||||
func GetGlobalOption(name string) interface{} {
|
||||
return GlobalSettings[name]
|
||||
}
|
||||
|
||||
func defaultFileFormat() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "dos"
|
||||
}
|
||||
return "unix"
|
||||
}
|
||||
|
||||
func GetInfoBarOffset() int {
|
||||
offset := 0
|
||||
if GetGlobalOption("infobar").(bool) {
|
||||
offset++
|
||||
}
|
||||
if GetGlobalOption("keymenu").(bool) {
|
||||
offset += 2
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
// DefaultCommonSettings returns a map of all common buffer settings
|
||||
// and their default values
|
||||
func DefaultCommonSettings() map[string]interface{} {
|
||||
commonsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
commonsettings[k] = v
|
||||
}
|
||||
return commonsettings
|
||||
}
|
||||
|
||||
// DefaultAllSettings returns a map of all common buffer & global-only settings
|
||||
// and their default values
|
||||
func DefaultAllSettings() map[string]interface{} {
|
||||
allsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
@@ -410,18 +456,15 @@ func GetNativeValue(option string, realValue interface{}, value string) (interfa
|
||||
} else if kind == reflect.String {
|
||||
native = value
|
||||
} else if kind == reflect.Float64 {
|
||||
i, err := strconv.Atoi(value)
|
||||
f, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidValue
|
||||
}
|
||||
native = float64(i)
|
||||
native = f
|
||||
} else {
|
||||
return nil, ErrInvalidValue
|
||||
}
|
||||
|
||||
if err := OptionIsValid(option, native); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return native, nil
|
||||
}
|
||||
|
||||
@@ -437,13 +480,13 @@ func OptionIsValid(option string, value interface{}) error {
|
||||
// Option validators
|
||||
|
||||
func validatePositiveValue(option string, value interface{}) error {
|
||||
tabsize, ok := value.(float64)
|
||||
nativeValue, ok := value.(float64)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected numeric type for " + option)
|
||||
}
|
||||
|
||||
if tabsize < 1 {
|
||||
if nativeValue < 1 {
|
||||
return errors.New(option + " must be greater than 0")
|
||||
}
|
||||
|
||||
@@ -464,6 +507,26 @@ func validateNonNegativeValue(option string, value interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateChoice(option string, value interface{}) error {
|
||||
if choices, ok := OptionChoices[option]; ok {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.New("Expected string type for " + option)
|
||||
}
|
||||
|
||||
for _, v := range choices {
|
||||
if val == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
choicesStr := strings.Join(choices, ", ")
|
||||
return errors.New(option + " must be one of: " + choicesStr)
|
||||
}
|
||||
|
||||
return errors.New("Option has no pre-defined choices")
|
||||
}
|
||||
|
||||
func validateColorscheme(option string, value interface{}) error {
|
||||
colorscheme, ok := value.(string)
|
||||
|
||||
@@ -478,69 +541,7 @@ func validateColorscheme(option string, value interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateClipboard(option string, value interface{}) error {
|
||||
val, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for clipboard")
|
||||
}
|
||||
|
||||
switch val {
|
||||
case "internal", "external", "terminal":
|
||||
default:
|
||||
return errors.New(option + " must be 'internal', 'external', or 'terminal'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLineEnding(option string, value interface{}) error {
|
||||
endingType, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for file format")
|
||||
}
|
||||
|
||||
if endingType != "unix" && endingType != "dos" {
|
||||
return errors.New("File format must be either 'unix' or 'dos'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEncoding(option string, value interface{}) error {
|
||||
_, err := htmlindex.Get(value.(string))
|
||||
return err
|
||||
}
|
||||
|
||||
func validateMultiOpen(option string, value interface{}) error {
|
||||
val, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for multiopen")
|
||||
}
|
||||
|
||||
switch val {
|
||||
case "tab", "hsplit", "vsplit":
|
||||
default:
|
||||
return errors.New(option + " must be 'tab', 'hsplit', or 'vsplit'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateReload(option string, value interface{}) error {
|
||||
val, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for reload")
|
||||
}
|
||||
|
||||
switch val {
|
||||
case "prompt", "auto", "disabled":
|
||||
default:
|
||||
return errors.New(option + " must be 'prompt', 'auto' or 'disabled'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,6 +129,11 @@ func (w *BufWindow) updateDisplayInfo() {
|
||||
w.bufHeight--
|
||||
}
|
||||
|
||||
scrollbarWidth := 0
|
||||
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height && w.Width > 0 {
|
||||
scrollbarWidth = 1
|
||||
}
|
||||
|
||||
w.hasMessage = len(b.Messages) > 0
|
||||
|
||||
// We need to know the string length of the largest line number
|
||||
@@ -146,13 +151,13 @@ func (w *BufWindow) updateDisplayInfo() {
|
||||
w.gutterOffset += w.maxLineNumLength + 1
|
||||
}
|
||||
|
||||
prevBufWidth := w.bufWidth
|
||||
|
||||
w.bufWidth = w.Width - w.gutterOffset
|
||||
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
|
||||
w.bufWidth--
|
||||
if w.gutterOffset > w.Width-scrollbarWidth {
|
||||
w.gutterOffset = w.Width - scrollbarWidth
|
||||
}
|
||||
|
||||
prevBufWidth := w.bufWidth
|
||||
w.bufWidth = w.Width - w.gutterOffset - scrollbarWidth
|
||||
|
||||
if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) {
|
||||
for _, c := range w.Buf.GetCursors() {
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
@@ -277,13 +282,17 @@ func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
|
||||
break
|
||||
}
|
||||
}
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
||||
vloc.X++
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
||||
vloc.X++
|
||||
for i := 0; i < 2 && vloc.X < w.gutterOffset; i++ {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
||||
vloc.X++
|
||||
}
|
||||
}
|
||||
|
||||
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
|
||||
if vloc.X >= w.gutterOffset {
|
||||
return
|
||||
}
|
||||
|
||||
symbol := ' '
|
||||
styleName := ""
|
||||
|
||||
@@ -319,26 +328,28 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc
|
||||
} else {
|
||||
lineInt = bloc.Y - cursorLine
|
||||
}
|
||||
lineNum := strconv.Itoa(util.Abs(lineInt))
|
||||
lineNum := []rune(strconv.Itoa(util.Abs(lineInt)))
|
||||
|
||||
// Write the spaces before the line number if necessary
|
||||
for i := 0; i < w.maxLineNumLength-len(lineNum); i++ {
|
||||
for i := 0; i < w.maxLineNumLength-len(lineNum) && vloc.X < w.gutterOffset; i++ {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
vloc.X++
|
||||
}
|
||||
// Write the actual line number
|
||||
for _, ch := range lineNum {
|
||||
if softwrapped {
|
||||
for i := 0; i < len(lineNum) && vloc.X < w.gutterOffset; i++ {
|
||||
if softwrapped || (w.bufWidth == 0 && w.Buf.Settings["softwrap"] == true) {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
} else {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, lineNum[i], nil, lineNumStyle)
|
||||
}
|
||||
vloc.X++
|
||||
}
|
||||
|
||||
// Write the extra space
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
vloc.X++
|
||||
if vloc.X < w.gutterOffset {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
vloc.X++
|
||||
}
|
||||
}
|
||||
|
||||
// getStyle returns the highlight style for the given character position
|
||||
@@ -373,18 +384,7 @@ func (w *BufWindow) displayBuffer() {
|
||||
|
||||
if b.ModifiedThisFrame {
|
||||
if b.Settings["diffgutter"].(bool) {
|
||||
b.UpdateDiff(func(synchronous bool) {
|
||||
// If the diff was updated asynchronously, the outer call to
|
||||
// displayBuffer might already be completed and we need to
|
||||
// schedule a redraw in order to display the new diff.
|
||||
// Note that this cannot lead to an infinite recursion
|
||||
// because the modifications were cleared above so there won't
|
||||
// be another call to UpdateDiff when displayBuffer is called
|
||||
// during the redraw.
|
||||
if !synchronous {
|
||||
screen.Redraw()
|
||||
}
|
||||
})
|
||||
b.UpdateDiff()
|
||||
}
|
||||
b.ModifiedThisFrame = false
|
||||
}
|
||||
@@ -392,26 +392,20 @@ func (w *BufWindow) displayBuffer() {
|
||||
var matchingBraces []buffer.Loc
|
||||
// bracePairs is defined in buffer.go
|
||||
if b.Settings["matchbrace"].(bool) {
|
||||
for _, bp := range buffer.BracePairs {
|
||||
for _, c := range b.GetCursors() {
|
||||
if c.HasSelection() {
|
||||
continue
|
||||
}
|
||||
curX := c.X
|
||||
curLoc := c.Loc
|
||||
for _, c := range b.GetCursors() {
|
||||
if c.HasSelection() {
|
||||
continue
|
||||
}
|
||||
|
||||
r := c.RuneUnder(curX)
|
||||
rl := c.RuneUnder(curX - 1)
|
||||
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
|
||||
mb, left, found := b.FindMatchingBrace(bp, curLoc)
|
||||
if found {
|
||||
matchingBraces = append(matchingBraces, mb)
|
||||
if !left {
|
||||
matchingBraces = append(matchingBraces, curLoc)
|
||||
} else {
|
||||
matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
|
||||
}
|
||||
mb, left, found := b.FindMatchingBrace(c.Loc)
|
||||
if found {
|
||||
matchingBraces = append(matchingBraces, mb)
|
||||
if !left {
|
||||
if b.Settings["matchbracestyle"].(string) != "highlight" {
|
||||
matchingBraces = append(matchingBraces, c.Loc)
|
||||
}
|
||||
} else {
|
||||
matchingBraces = append(matchingBraces, c.Loc.Move(-1, b))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +476,12 @@ func (w *BufWindow) displayBuffer() {
|
||||
vloc.X = w.gutterOffset
|
||||
}
|
||||
|
||||
bline := b.LineBytes(bloc.Y)
|
||||
blineLen := util.CharacterCount(bline)
|
||||
|
||||
leadingwsEnd := len(util.GetLeadingWhitespace(bline))
|
||||
trailingwsStart := blineLen - util.CharacterCount(util.GetTrailingWhitespace(bline))
|
||||
|
||||
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
|
||||
if startStyle != nil {
|
||||
curStyle = *startStyle
|
||||
@@ -505,6 +505,37 @@ func (w *BufWindow) displayBuffer() {
|
||||
// over cursor-line and color-column
|
||||
dontOverrideBackground := origBg != defBg
|
||||
|
||||
if b.Settings["hltaberrors"].(bool) {
|
||||
if s, ok := config.Colorscheme["tab-error"]; ok {
|
||||
isTab := (r == '\t') || (r == ' ' && !showcursor)
|
||||
if (b.Settings["tabstospaces"].(bool) && isTab) ||
|
||||
(!b.Settings["tabstospaces"].(bool) && bloc.X < leadingwsEnd && r == ' ' && !isTab) {
|
||||
fg, _, _ := s.Decompose()
|
||||
style = style.Background(fg)
|
||||
dontOverrideBackground = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.Settings["hltrailingws"].(bool) {
|
||||
if s, ok := config.Colorscheme["trailingws"]; ok {
|
||||
if bloc.X >= trailingwsStart && bloc.X < blineLen {
|
||||
hl := true
|
||||
for _, c := range cursors {
|
||||
if c.NewTrailingWsY == bloc.Y {
|
||||
hl = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if hl {
|
||||
fg, _, _ := s.Decompose()
|
||||
style = style.Background(fg)
|
||||
dontOverrideBackground = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range cursors {
|
||||
if c.HasSelection() &&
|
||||
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
|
||||
@@ -557,7 +588,15 @@ func (w *BufWindow) displayBuffer() {
|
||||
|
||||
for _, mb := range matchingBraces {
|
||||
if mb.X == bloc.X && mb.Y == bloc.Y {
|
||||
style = style.Underline(true)
|
||||
if b.Settings["matchbracestyle"].(string) == "highlight" {
|
||||
if s, ok := config.Colorscheme["match-brace"]; ok {
|
||||
style = s
|
||||
} else {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
} else {
|
||||
style = style.Underline(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -609,7 +648,7 @@ func (w *BufWindow) displayBuffer() {
|
||||
wordwidth := 0
|
||||
|
||||
totalwidth := w.StartCol - nColsBeforeStart
|
||||
for len(line) > 0 {
|
||||
for len(line) > 0 && vloc.X < maxWidth {
|
||||
r, combc, size := util.DecodeCharacter(line)
|
||||
line = line[size:]
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ func (w *TermWindow) Display() {
|
||||
}
|
||||
if w.State.CursorVisible() && w.active {
|
||||
curx, cury := w.State.Cursor()
|
||||
screen.ShowCursor(curx+w.X, cury+w.Y)
|
||||
if curx < w.Width && cury < w.Height {
|
||||
screen.ShowCursor(curx+w.X, cury+w.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// The InfoBuf displays messages and other info at the bottom of the screen.
|
||||
// It is respresented as a buffer and a message with a style.
|
||||
// It is represented as a buffer and a message with a style.
|
||||
type InfoBuf struct {
|
||||
*buffer.Buffer
|
||||
|
||||
@@ -143,13 +143,12 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
|
||||
if i.PromptCallback != nil {
|
||||
if canceled {
|
||||
i.Replace(i.Start(), i.End(), "")
|
||||
i.PromptCallback("", true)
|
||||
h := i.History[i.PromptType]
|
||||
i.History[i.PromptType] = h[:len(h)-1]
|
||||
i.PromptCallback("", true)
|
||||
} else {
|
||||
resp := string(i.LineBytes(0))
|
||||
i.Replace(i.Start(), i.End(), "")
|
||||
i.PromptCallback(resp, false)
|
||||
h := i.History[i.PromptType]
|
||||
h[len(h)-1] = resp
|
||||
|
||||
@@ -160,6 +159,8 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i.PromptCallback(resp, false)
|
||||
}
|
||||
// i.PromptCallback = nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -27,7 +26,6 @@ import (
|
||||
)
|
||||
|
||||
var L *lua.LState
|
||||
var Lock sync.Mutex
|
||||
|
||||
// LoadFile loads a lua file
|
||||
func LoadFile(module string, file string, data []byte) error {
|
||||
@@ -523,21 +521,16 @@ func importErrors() *lua.LTable {
|
||||
func importTime() *lua.LTable {
|
||||
pkg := L.NewTable()
|
||||
|
||||
L.SetField(pkg, "After", luar.New(L, time.After))
|
||||
L.SetField(pkg, "Sleep", luar.New(L, time.Sleep))
|
||||
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
|
||||
L.SetField(pkg, "Since", luar.New(L, time.Since))
|
||||
L.SetField(pkg, "FixedZone", luar.New(L, time.FixedZone))
|
||||
L.SetField(pkg, "LoadLocation", luar.New(L, time.LoadLocation))
|
||||
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
|
||||
L.SetField(pkg, "Date", luar.New(L, time.Date))
|
||||
L.SetField(pkg, "Now", luar.New(L, time.Now))
|
||||
L.SetField(pkg, "Parse", luar.New(L, time.Parse))
|
||||
L.SetField(pkg, "ParseDuration", luar.New(L, time.ParseDuration))
|
||||
L.SetField(pkg, "ParseInLocation", luar.New(L, time.ParseInLocation))
|
||||
L.SetField(pkg, "Unix", luar.New(L, time.Unix))
|
||||
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
|
||||
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
|
||||
L.SetField(pkg, "Nanosecond", luar.New(L, time.Nanosecond))
|
||||
L.SetField(pkg, "Microsecond", luar.New(L, time.Microsecond))
|
||||
L.SetField(pkg, "Millisecond", luar.New(L, time.Millisecond))
|
||||
@@ -545,6 +538,15 @@ func importTime() *lua.LTable {
|
||||
L.SetField(pkg, "Minute", luar.New(L, time.Minute))
|
||||
L.SetField(pkg, "Hour", luar.New(L, time.Hour))
|
||||
|
||||
// TODO: these raw Go timer APIs don't provide any synchronization
|
||||
// with micro. Stop exposing them to lua once plugins switch to using
|
||||
// the safer micro.After() interface instead. See issue #3209
|
||||
L.SetField(pkg, "After", luar.New(L, time.After))
|
||||
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
|
||||
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
|
||||
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
|
||||
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ var Screen tcell.Screen
|
||||
// Events is the channel of tcell events
|
||||
var Events chan (tcell.Event)
|
||||
|
||||
// RestartCallback is called when the screen is restarted after it was
|
||||
// temporarily shut down
|
||||
var RestartCallback func()
|
||||
|
||||
// The lock is necessary since the screen is polled on a separate thread
|
||||
var lock sync.Mutex
|
||||
|
||||
@@ -134,6 +138,10 @@ func TempStart(screenWasNil bool) {
|
||||
if !screenWasNil {
|
||||
Init()
|
||||
Unlock()
|
||||
|
||||
if RestartCallback != nil {
|
||||
RestartCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/zyedidia/micro/v2/internal/screen"
|
||||
"github.com/zyedidia/micro/v2/internal/util"
|
||||
)
|
||||
|
||||
// ExecCommand executes a command using exec
|
||||
@@ -95,28 +96,30 @@ func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error
|
||||
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
|
||||
// micro is killed if the signal is ignored only on Windows, so it is
|
||||
// received
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Reset(os.Interrupt)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
err = cmd.Start()
|
||||
if err == nil {
|
||||
err = cmd.Wait()
|
||||
if wait {
|
||||
// This is just so we don't return right away and let the user press enter to return
|
||||
screen.TermMessage("")
|
||||
}
|
||||
}()
|
||||
|
||||
cmd.Start()
|
||||
err = cmd.Wait()
|
||||
} else {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
output := outputBytes.String()
|
||||
|
||||
if wait {
|
||||
// This is just so we don't return right away and let the user press enter to return
|
||||
screen.TermMessage("")
|
||||
}
|
||||
|
||||
// Start the screen back up
|
||||
screen.TempStart(screenb)
|
||||
|
||||
signal.Notify(util.Sigterm, os.Interrupt)
|
||||
signal.Stop(c)
|
||||
|
||||
return output, err
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/blang/semver"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
@@ -40,6 +41,8 @@ var (
|
||||
|
||||
// Stdout is a buffer that is written to stdout when micro closes
|
||||
Stdout *bytes.Buffer
|
||||
// Sigterm is a channel where micro exits when written
|
||||
Sigterm chan os.Signal
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -217,10 +220,68 @@ func FSize(f *os.File) int64 {
|
||||
return fi.Size()
|
||||
}
|
||||
|
||||
// IsWordChar returns whether or not the string is a 'word character'
|
||||
// Word characters are defined as numbers, letters, or '_'
|
||||
// IsWordChar returns whether or not a rune is a 'word character'
|
||||
// Word characters are defined as numbers, letters or sub-word delimiters
|
||||
func IsWordChar(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
|
||||
return IsAlphanumeric(r) || IsSubwordDelimiter(r)
|
||||
}
|
||||
|
||||
// IsNonWordChar returns whether or not a rune is not a 'word character'
|
||||
// Non word characters are defined as all characters not being numbers, letters or sub-word delimiters
|
||||
// See IsWordChar()
|
||||
func IsNonWordChar(r rune) bool {
|
||||
return !IsWordChar(r)
|
||||
}
|
||||
|
||||
// IsUpperWordChar returns whether or not a rune is an 'upper word character'
|
||||
// Upper word characters are defined as numbers, upper-case letters or sub-word delimiters
|
||||
func IsUpperWordChar(r rune) bool {
|
||||
return IsUpperAlphanumeric(r) || IsSubwordDelimiter(r)
|
||||
}
|
||||
|
||||
// IsLowerWordChar returns whether or not a rune is a 'lower word character'
|
||||
// Lower word characters are defined as numbers, lower-case letters or sub-word delimiters
|
||||
func IsLowerWordChar(r rune) bool {
|
||||
return IsLowerAlphanumeric(r) || IsSubwordDelimiter(r)
|
||||
}
|
||||
|
||||
// IsSubwordDelimiter returns whether or not a rune is a 'sub-word delimiter character'
|
||||
// i.e. is considered a part of the word and is used as a delimiter between sub-words of the word.
|
||||
// For now the only sub-word delimiter character is '_'.
|
||||
func IsSubwordDelimiter(r rune) bool {
|
||||
return r == '_'
|
||||
}
|
||||
|
||||
// IsAlphanumeric returns whether or not a rune is an 'alphanumeric character'
|
||||
// Alphanumeric characters are defined as numbers or letters
|
||||
func IsAlphanumeric(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
// IsUpperAlphanumeric returns whether or not a rune is an 'upper alphanumeric character'
|
||||
// Upper alphanumeric characters are defined as numbers or upper-case letters
|
||||
func IsUpperAlphanumeric(r rune) bool {
|
||||
return IsUpperLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
// IsLowerAlphanumeric returns whether or not a rune is a 'lower alphanumeric character'
|
||||
// Lower alphanumeric characters are defined as numbers or lower-case letters
|
||||
func IsLowerAlphanumeric(r rune) bool {
|
||||
return IsLowerLetter(r) || unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
// IsUpperLetter returns whether or not a rune is an 'upper letter character'
|
||||
// Upper letter characters are defined as upper-case letters
|
||||
func IsUpperLetter(r rune) bool {
|
||||
// unicode.IsUpper() returns true for letters only
|
||||
return unicode.IsUpper(r)
|
||||
}
|
||||
|
||||
// IsLowerLetter returns whether or not a rune is a 'lower letter character'
|
||||
// Lower letter characters are defined as lower-case letters
|
||||
func IsLowerLetter(r rune) bool {
|
||||
// unicode.IsLower() returns true for letters only
|
||||
return unicode.IsLower(r)
|
||||
}
|
||||
|
||||
// Spaces returns a string with n spaces
|
||||
@@ -315,7 +376,7 @@ func ReplaceHome(path string) (string, error) {
|
||||
// This is used for opening files like util.go:10:5 to specify a line and column
|
||||
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
|
||||
func GetPathAndCursorPosition(path string) (string, []string) {
|
||||
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
|
||||
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?$`)
|
||||
match := re.FindStringSubmatch(path)
|
||||
// no lines/columns were specified in the path, return just the path with no cursor location
|
||||
if len(match) == 0 {
|
||||
@@ -363,6 +424,28 @@ func GetLeadingWhitespace(b []byte) []byte {
|
||||
return ws
|
||||
}
|
||||
|
||||
// GetTrailingWhitespace returns the trailing whitespace of the given byte array
|
||||
func GetTrailingWhitespace(b []byte) []byte {
|
||||
ws := []byte{}
|
||||
for len(b) > 0 {
|
||||
r, size := utf8.DecodeLastRune(b)
|
||||
if IsWhitespace(r) {
|
||||
ws = append([]byte(string(r)), ws...)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
b = b[:len(b)-size]
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
// HasTrailingWhitespace returns true if the given byte array ends with a whitespace
|
||||
func HasTrailingWhitespace(b []byte) bool {
|
||||
r, _ := utf8.DecodeLastRune(b)
|
||||
return IsWhitespace(r)
|
||||
}
|
||||
|
||||
// IntOpt turns a float64 setting to an int
|
||||
func IntOpt(opt interface{}) int {
|
||||
return int(opt.(float64))
|
||||
@@ -422,14 +505,9 @@ func Clamp(val, min, max int) int {
|
||||
return val
|
||||
}
|
||||
|
||||
// IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
|
||||
func IsNonAlphaNumeric(c rune) bool {
|
||||
return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
|
||||
}
|
||||
|
||||
// IsAutocomplete returns whether a character should begin an autocompletion.
|
||||
func IsAutocomplete(c rune) bool {
|
||||
return c == '.' || !IsNonAlphaNumeric(c)
|
||||
return c == '.' || IsWordChar(c)
|
||||
}
|
||||
|
||||
// ParseSpecial replaces escaped ts with '\t'.
|
||||
|
||||
@@ -185,6 +185,10 @@ func (n *Node) hResizeSplit(i int, size int) bool {
|
||||
|
||||
// ResizeSplit resizes a certain split to a given size
|
||||
func (n *Node) ResizeSplit(size int) bool {
|
||||
// TODO: `size < 0` does not work for some reason
|
||||
if size <= 0 {
|
||||
return false
|
||||
}
|
||||
if len(n.parent.children) <= 1 {
|
||||
// cannot resize a lone node
|
||||
return false
|
||||
@@ -201,7 +205,7 @@ func (n *Node) ResizeSplit(size int) bool {
|
||||
return n.parent.hResizeSplit(ind, size)
|
||||
}
|
||||
|
||||
// Resize sets this node's size and resizes all children accordlingly
|
||||
// Resize sets this node's size and resizes all children accordingly
|
||||
func (n *Node) Resize(w, h int) {
|
||||
n.W, n.H = w, h
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package highlight
|
||||
|
||||
import "regexp"
|
||||
|
||||
// MatchFiletype will use the list of syntax definitions provided and the filename and first line of the file
|
||||
// to determine the filetype of the file
|
||||
// It will return the corresponding syntax definition for the filetype
|
||||
func MatchFiletype(ftdetect [2]*regexp.Regexp, filename string, firstLine []byte) bool {
|
||||
if ftdetect[0] != nil && ftdetect[0].MatchString(filename) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ftdetect[1] != nil {
|
||||
return ftdetect[1].Match(firstLine)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -67,9 +67,6 @@ func combineLineMatch(src, dst LineMatch) LineMatch {
|
||||
// A State represents the region at the end of a line
|
||||
type State *region
|
||||
|
||||
// EmptyDef is an empty definition.
|
||||
var EmptyDef = Def{nil, &rules{}}
|
||||
|
||||
// LineStates is an interface for a buffer-like object which can also store the states and matches for every line
|
||||
type LineStates interface {
|
||||
LineBytes(n int) []byte
|
||||
@@ -77,6 +74,8 @@ type LineStates interface {
|
||||
State(lineN int) State
|
||||
SetState(lineN int, s State)
|
||||
SetMatch(lineN int, m LineMatch)
|
||||
Lock()
|
||||
Unlock()
|
||||
}
|
||||
|
||||
// A Highlighter contains the information needed to highlight a string
|
||||
@@ -303,7 +302,13 @@ func (h *Highlighter) HighlightString(input string) []LineMatch {
|
||||
|
||||
// HighlightStates correctly sets all states for the buffer
|
||||
func (h *Highlighter) HighlightStates(input LineStates) {
|
||||
for i := 0; i < input.LinesNum(); i++ {
|
||||
for i := 0; ; i++ {
|
||||
input.Lock()
|
||||
if i >= input.LinesNum() {
|
||||
input.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
line := input.LineBytes(i)
|
||||
// highlights := make(LineMatch)
|
||||
|
||||
@@ -316,6 +321,7 @@ func (h *Highlighter) HighlightStates(input LineStates) {
|
||||
curState := h.lastRegion
|
||||
|
||||
input.SetState(i, curState)
|
||||
input.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +330,9 @@ func (h *Highlighter) HighlightStates(input LineStates) {
|
||||
// This assumes that all the states are set correctly
|
||||
func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
|
||||
for i := startline; i <= endline; i++ {
|
||||
input.Lock()
|
||||
if i >= input.LinesNum() {
|
||||
input.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -339,6 +347,7 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int)
|
||||
}
|
||||
|
||||
input.SetMatch(i, match)
|
||||
input.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,9 +359,19 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
|
||||
|
||||
h.lastRegion = nil
|
||||
if startline > 0 {
|
||||
h.lastRegion = input.State(startline - 1)
|
||||
input.Lock()
|
||||
if startline-1 < input.LinesNum() {
|
||||
h.lastRegion = input.State(startline - 1)
|
||||
}
|
||||
input.Unlock()
|
||||
}
|
||||
for i := startline; i < input.LinesNum(); i++ {
|
||||
for i := startline; ; i++ {
|
||||
input.Lock()
|
||||
if i >= input.LinesNum() {
|
||||
input.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
line := input.LineBytes(i)
|
||||
// highlights := make(LineMatch)
|
||||
|
||||
@@ -366,6 +385,7 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
|
||||
lastState := input.State(i)
|
||||
|
||||
input.SetState(i, curState)
|
||||
input.Unlock()
|
||||
|
||||
if curState == lastState {
|
||||
return i
|
||||
@@ -377,6 +397,9 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
|
||||
|
||||
// ReHighlightLine will rehighlight the state and match for a single line
|
||||
func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
|
||||
input.Lock()
|
||||
defer input.Unlock()
|
||||
|
||||
line := input.LineBytes(lineN)
|
||||
highlights := make(LineMatch)
|
||||
|
||||
|
||||
@@ -33,27 +33,28 @@ func (g Group) String() string {
|
||||
// Then it has the rules which define how to highlight the file
|
||||
type Def struct {
|
||||
*Header
|
||||
|
||||
rules *rules
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
FileType string
|
||||
FtDetect [2]*regexp.Regexp
|
||||
FileType string
|
||||
FileNameRegex *regexp.Regexp
|
||||
HeaderRegex *regexp.Regexp
|
||||
SignatureRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
type HeaderYaml struct {
|
||||
FileType string `yaml:"filetype"`
|
||||
Detect struct {
|
||||
FNameRgx string `yaml:"filename"`
|
||||
HeaderRgx string `yaml:"header"`
|
||||
FNameRegexStr string `yaml:"filename"`
|
||||
HeaderRegexStr string `yaml:"header"`
|
||||
SignatureRegexStr string `yaml:"signature"`
|
||||
} `yaml:"detect"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FileType string
|
||||
|
||||
yamlSrc map[interface{}]interface{}
|
||||
yamlSrc map[interface{}]interface{}
|
||||
}
|
||||
|
||||
// A Pattern is one simple syntax rule
|
||||
@@ -97,20 +98,24 @@ func init() {
|
||||
// A yaml file might take ~400us to parse while a header file only takes ~20us
|
||||
func MakeHeader(data []byte) (*Header, error) {
|
||||
lines := bytes.Split(data, []byte{'\n'})
|
||||
if len(lines) < 3 {
|
||||
if len(lines) < 4 {
|
||||
return nil, errors.New("Header file has incorrect format")
|
||||
}
|
||||
header := new(Header)
|
||||
var err error
|
||||
header.FileType = string(lines[0])
|
||||
fnameRgx := string(lines[1])
|
||||
headerRgx := string(lines[2])
|
||||
fnameRegexStr := string(lines[1])
|
||||
headerRegexStr := string(lines[2])
|
||||
signatureRegexStr := string(lines[3])
|
||||
|
||||
if fnameRgx != "" {
|
||||
header.FtDetect[0], err = regexp.Compile(fnameRgx)
|
||||
if fnameRegexStr != "" {
|
||||
header.FileNameRegex, err = regexp.Compile(fnameRegexStr)
|
||||
}
|
||||
if err == nil && headerRgx != "" {
|
||||
header.FtDetect[1], err = regexp.Compile(headerRgx)
|
||||
if err == nil && headerRegexStr != "" {
|
||||
header.HeaderRegex, err = regexp.Compile(headerRegexStr)
|
||||
}
|
||||
if err == nil && signatureRegexStr != "" {
|
||||
header.SignatureRegex, err = regexp.Compile(signatureRegexStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -132,11 +137,14 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
|
||||
header := new(Header)
|
||||
header.FileType = hdrYaml.FileType
|
||||
|
||||
if hdrYaml.Detect.FNameRgx != "" {
|
||||
header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx)
|
||||
if hdrYaml.Detect.FNameRegexStr != "" {
|
||||
header.FileNameRegex, err = regexp.Compile(hdrYaml.Detect.FNameRegexStr)
|
||||
}
|
||||
if err == nil && hdrYaml.Detect.HeaderRgx != "" {
|
||||
header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx)
|
||||
if err == nil && hdrYaml.Detect.HeaderRegexStr != "" {
|
||||
header.HeaderRegex, err = regexp.Compile(hdrYaml.Detect.HeaderRegexStr)
|
||||
}
|
||||
if err == nil && hdrYaml.Detect.SignatureRegexStr != "" {
|
||||
header.SignatureRegex, err = regexp.Compile(hdrYaml.Detect.SignatureRegexStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -146,6 +154,37 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// MatchFileName will check the given file name with the stored regex
|
||||
func (header *Header) MatchFileName(filename string) bool {
|
||||
if header.FileNameRegex != nil {
|
||||
return header.FileNameRegex.MatchString(filename)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (header *Header) MatchFileHeader(firstLine []byte) bool {
|
||||
if header.HeaderRegex != nil {
|
||||
return header.HeaderRegex.Match(firstLine)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasFileSignature checks the presence of a stored signature
|
||||
func (header *Header) HasFileSignature() bool {
|
||||
return header.SignatureRegex != nil
|
||||
}
|
||||
|
||||
// MatchFileSignature will check the given line with the stored regex
|
||||
func (header *Header) MatchFileSignature(line []byte) bool {
|
||||
if header.SignatureRegex != nil {
|
||||
return header.SignatureRegex.Match(line)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ParseFile(input []byte) (f *File, err error) {
|
||||
// This is just so if we have an error, we can exit cleanly and return the parse error to the user
|
||||
defer func() {
|
||||
@@ -170,11 +209,19 @@ func ParseFile(input []byte) (f *File, err error) {
|
||||
if k == "filetype" {
|
||||
filetype := v.(string)
|
||||
|
||||
if filetype == "" {
|
||||
return nil, errors.New("empty filetype")
|
||||
}
|
||||
|
||||
f.FileType = filetype
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if f.FileType == "" {
|
||||
return nil, errors.New("missing filetype")
|
||||
}
|
||||
|
||||
return f, err
|
||||
}
|
||||
|
||||
@@ -191,12 +238,12 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
rules := f.yamlSrc
|
||||
src := f.yamlSrc
|
||||
|
||||
s = new(Def)
|
||||
s.Header = header
|
||||
|
||||
for k, v := range rules {
|
||||
for k, v := range src {
|
||||
if k == "rules" {
|
||||
inputRules := v.([]interface{})
|
||||
|
||||
@@ -209,6 +256,11 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if s.rules == nil {
|
||||
// allow empty rules
|
||||
s.rules = new(rules)
|
||||
}
|
||||
|
||||
return s, err
|
||||
}
|
||||
|
||||
@@ -303,6 +355,10 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
|
||||
|
||||
switch object := val.(type) {
|
||||
case string:
|
||||
if object == "" {
|
||||
return nil, fmt.Errorf("Empty rule %s", k)
|
||||
}
|
||||
|
||||
if k == "include" {
|
||||
ru.includes = append(ru.includes, object)
|
||||
} else {
|
||||
@@ -356,30 +412,56 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
|
||||
r.group = groupNum
|
||||
r.parent = prevRegion
|
||||
|
||||
r.start, err = regexp.Compile(regionInfo["start"].(string))
|
||||
// start is mandatory
|
||||
if start, ok := regionInfo["start"]; ok {
|
||||
start := start.(string)
|
||||
if start == "" {
|
||||
return nil, fmt.Errorf("Empty start in %s", group)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
r.start, err = regexp.Compile(start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Missing start in %s", group)
|
||||
}
|
||||
|
||||
r.end, err = regexp.Compile(regionInfo["end"].(string))
|
||||
// end is mandatory
|
||||
if end, ok := regionInfo["end"]; ok {
|
||||
end := end.(string)
|
||||
if end == "" {
|
||||
return nil, fmt.Errorf("Empty end in %s", group)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
r.end, err = regexp.Compile(end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Missing end in %s", group)
|
||||
}
|
||||
|
||||
// skip is optional
|
||||
if _, ok := regionInfo["skip"]; ok {
|
||||
r.skip, err = regexp.Compile(regionInfo["skip"].(string))
|
||||
if skip, ok := regionInfo["skip"]; ok {
|
||||
skip := skip.(string)
|
||||
if skip == "" {
|
||||
return nil, fmt.Errorf("Empty skip in %s", group)
|
||||
}
|
||||
|
||||
r.skip, err = regexp.Compile(skip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// limit-color is optional
|
||||
if _, ok := regionInfo["limit-group"]; ok {
|
||||
groupStr := regionInfo["limit-group"].(string)
|
||||
if groupStr, ok := regionInfo["limit-group"]; ok {
|
||||
groupStr := groupStr.(string)
|
||||
if groupStr == "" {
|
||||
return nil, fmt.Errorf("Empty limit-group in %s", group)
|
||||
}
|
||||
|
||||
if _, ok := Groups[groupStr]; !ok {
|
||||
numGroups++
|
||||
Groups[groupStr] = numGroups
|
||||
|
||||
@@ -28,3 +28,6 @@ color-link color-column "#2D2F31"
|
||||
#No extended types (bool in C, etc.)
|
||||
#color-link type.extended "default"
|
||||
#Plain brackets
|
||||
color-link match-brace "#1D1F21,#62B1FE"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -26,3 +26,6 @@ color-link color-column "254"
|
||||
#No extended types (bool in C, &c.) and plain brackets
|
||||
color-link type.extended "241,231"
|
||||
color-link symbol.brackets "241,231"
|
||||
color-link match-brace "231,28"
|
||||
color-link tab-error "210"
|
||||
color-link trailingws "210"
|
||||
|
||||
@@ -42,3 +42,6 @@ color-link gutter-warning "red"
|
||||
color-link color-column "cyan"
|
||||
color-link underlined.url "underline blue, white"
|
||||
color-link divider "blue"
|
||||
color-link match-brace "black,cyan"
|
||||
color-link tab-error "brightred"
|
||||
color-link trailingws "brightred"
|
||||
|
||||
@@ -38,3 +38,6 @@ color-link color-column "#f26522"
|
||||
color-link constant.bool "bold #55ffff"
|
||||
color-link constant.bool.true "bold #85ff85"
|
||||
color-link constant.bool.false "bold #ff8585"
|
||||
color-link match-brace "#1e2124,#55ffff"
|
||||
color-link tab-error "#d75f5f"
|
||||
color-link trailingws "#d75f5f"
|
||||
|
||||
@@ -29,3 +29,6 @@ color-link color-column "#2C2C2C"
|
||||
color-link type.extended "default"
|
||||
#color-link symbol.brackets "default"
|
||||
color-link symbol.tag "#AE81FF,#242424"
|
||||
color-link match-brace "#242424,#7A9EC2"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -1,31 +1 @@
|
||||
color-link default "#F8F8F2,#282828"
|
||||
color-link comment "#75715E,#282828"
|
||||
color-link identifier "#66D9EF,#282828"
|
||||
color-link constant "#AE81FF,#282828"
|
||||
color-link constant.string "#E6DB74,#282828"
|
||||
color-link constant.string.char "#BDE6AD,#282828"
|
||||
color-link statement "#F92672,#282828"
|
||||
color-link symbol.operator "#F92671,#282828"
|
||||
color-link preproc "#CB4B16,#282828"
|
||||
color-link type "#66D9EF,#282828"
|
||||
color-link special "#A6E22E,#282828"
|
||||
color-link underlined "#D33682,#282828"
|
||||
color-link error "bold #CB4B16,#282828"
|
||||
color-link todo "bold #D33682,#282828"
|
||||
color-link hlsearch "#282828,#E6DB74"
|
||||
color-link statusline "#282828,#F8F8F2"
|
||||
color-link tabbar "#282828,#F8F8F2"
|
||||
color-link indent-char "#505050,#282828"
|
||||
color-link line-number "#AAAAAA,#323232"
|
||||
color-link current-line-number "#AAAAAA,#282828"
|
||||
color-link diff-added "#00AF00"
|
||||
color-link diff-modified "#FFAF00"
|
||||
color-link diff-deleted "#D70000"
|
||||
color-link gutter-error "#CB4B16,#282828"
|
||||
color-link gutter-warning "#E6DB74,#282828"
|
||||
color-link cursor-line "#323232"
|
||||
color-link color-column "#323232"
|
||||
#No extended types; Plain brackets.
|
||||
color-link type.extended "default"
|
||||
#color-link symbol.brackets "default"
|
||||
color-link symbol.tag "#AE81FF,#282828"
|
||||
include "monokai"
|
||||
|
||||
@@ -43,3 +43,7 @@ color-link cursor-line "#44475A,#F8F8F2"
|
||||
color-link color-column "#44475A"
|
||||
color-link type.extended "default"
|
||||
|
||||
color-link match-brace "#282A36,#FF79C6"
|
||||
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#001e28"
|
||||
color-link type.keyword "bold #5aaae6,#001e28"
|
||||
color-link type.extended "#ffffff,#001e28"
|
||||
color-link underlined "#608b4e,#001e28"
|
||||
color-link match-brace "#001e28,#5aaae6"
|
||||
color-link tab-error "#d75f5f"
|
||||
color-link trailingws "#d75f5f"
|
||||
|
||||
@@ -33,3 +33,6 @@ color-link type "bold #004080,#f0f0f0"
|
||||
color-link type.keyword "bold #780050,#f0f0f0"
|
||||
color-link type.extended "#000000,#f0f0f0"
|
||||
color-link underlined "#3f7f5f,#f0f0f0"
|
||||
color-link match-brace "#f0f0f0,#780050"
|
||||
color-link tab-error "#ff8787"
|
||||
color-link trailingws "#ff8787"
|
||||
|
||||
@@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#2d0023"
|
||||
color-link type.keyword "bold #5aaae6,#2d0023"
|
||||
color-link type.extended "#ffffff,#2d0023"
|
||||
color-link underlined "#886484,#2d0023"
|
||||
color-link match-brace "#2d0023,#5aaae6"
|
||||
color-link tab-error "#d75f5f"
|
||||
color-link trailingws "#d75f5f"
|
||||
|
||||
@@ -24,3 +24,6 @@ color-link diff-modified "yellow"
|
||||
color-link diff-deleted "red"
|
||||
color-link gutter-error ",red"
|
||||
color-link gutter-warning "red"
|
||||
color-link match-brace "black,cyan"
|
||||
color-link tab-error "brightred"
|
||||
color-link trailingws "brightred"
|
||||
|
||||
@@ -24,3 +24,6 @@ color-link gutter-warning "#EDB443,#11151C"
|
||||
color-link cursor-line "#091F2E"
|
||||
color-link color-column "#11151C"
|
||||
color-link symbol "#99D1CE,#0C1014"
|
||||
color-link match-brace "#0C1014,#D26937"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -24,3 +24,6 @@ color-link cursor-line "#3c3836"
|
||||
color-link color-column "#79740e"
|
||||
color-link statusline "#ebdbb2,#665c54"
|
||||
color-link tabbar "#ebdbb2,#665c54"
|
||||
color-link match-brace "#282828,#d3869b"
|
||||
color-link tab-error "#d75f5f"
|
||||
color-link trailingws "#d75f5f"
|
||||
|
||||
@@ -21,3 +21,6 @@ color-link cursor-line "237"
|
||||
color-link color-column "237"
|
||||
color-link statusline "223,237"
|
||||
color-link tabbar "223,237"
|
||||
color-link match-brace "235,72"
|
||||
color-link tab-error "167"
|
||||
color-link trailingws "167"
|
||||
|
||||
@@ -20,6 +20,7 @@ color-link identifier.macro "#FFCB6B,#263238"
|
||||
color-link indent-char "#505050,#263238"
|
||||
color-link line-number "#656866,#283942"
|
||||
color-link preproc "#C792EA,#263238"
|
||||
color-link scrollbar "#80DEEA,#283942"
|
||||
color-link special "#C792EA,#263238"
|
||||
color-link statement "#C792EA,#263238"
|
||||
color-link statusline "#80DEEA,#3b4d56"
|
||||
@@ -30,3 +31,6 @@ color-link tabbar "#80DEEA,#3b4d56"
|
||||
color-link todo "bold #C792EA,#263238"
|
||||
color-link type "#FFCB6B,#263238"
|
||||
color-link underlined "underline #EEFFFF,#263238"
|
||||
color-link match-brace "#263238,#C792EA"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -23,3 +23,6 @@ color-link gutter-error "#CB4B16"
|
||||
color-link gutter-warning "#E6DB74"
|
||||
color-link cursor-line "#323232"
|
||||
color-link color-column "#323232"
|
||||
color-link match-brace "#1D0000,#AE81FF"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -29,3 +29,6 @@ color-link color-column "#323232"
|
||||
color-link type.extended "default"
|
||||
#color-link symbol.brackets "default"
|
||||
color-link symbol.tag "#AE81FF,#282828"
|
||||
color-link match-brace "#282828,#AE81FF"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -34,3 +34,6 @@ color-link todo "#8B98AB"
|
||||
color-link type "#66D9EF"
|
||||
color-link type.keyword "#C678DD"
|
||||
color-link underlined "#8996A8"
|
||||
color-link match-brace "#21252C,#C678DD"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -28,6 +28,10 @@ color-link tabbar "bold #b1b1b1,#232323"
|
||||
color-link cursor-line "#353535"
|
||||
color-link color-column "#353535"
|
||||
color-link space "underline #e6e1dc,#2b2b2b"
|
||||
color-link tab-error "#d75f5f"
|
||||
color-link trailingws "#d75f5f"
|
||||
|
||||
#the Python syntax definition are wrong. This is not how you should do decorators!
|
||||
color-link brightgreen "#edb753,#2b2b2b"
|
||||
|
||||
color-link match-brace "#2b2b2b,#a5c261"
|
||||
|
||||
@@ -10,6 +10,7 @@ color-link ignore "default"
|
||||
color-link error ",brightred"
|
||||
color-link todo ",brightyellow"
|
||||
color-link hlsearch "black,yellow"
|
||||
color-link statusline "black,white"
|
||||
color-link indent-char "black"
|
||||
color-link line-number "yellow"
|
||||
color-link current-line-number "red"
|
||||
@@ -27,3 +28,6 @@ color-link type.extended "default"
|
||||
color-link symbol.brackets "default"
|
||||
#Color shebangs the comment color
|
||||
color-link preproc.shebang "comment"
|
||||
color-link match-brace ",magenta"
|
||||
color-link tab-error "brightred"
|
||||
color-link trailingws "brightred"
|
||||
|
||||
@@ -26,3 +26,6 @@ color-link cursor-line "#003541"
|
||||
color-link color-column "#003541"
|
||||
color-link type.extended "#839496,#002833"
|
||||
color-link symbol.brackets "#839496,#002833"
|
||||
color-link match-brace "#002833,#268BD2"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -25,3 +25,6 @@ color-link cursor-line "black"
|
||||
color-link color-column "black"
|
||||
color-link type.extended "default"
|
||||
color-link symbol.brackets "default"
|
||||
color-link match-brace ",blue"
|
||||
color-link tab-error "brightred"
|
||||
color-link trailingws "brightred"
|
||||
|
||||
@@ -24,3 +24,6 @@ color-link gutter-warning "88"
|
||||
color-link cursor-line "229"
|
||||
#color-link color-column "196"
|
||||
color-link current-line-number "246"
|
||||
color-line match-brace "230,22"
|
||||
color-link tab-error "210"
|
||||
color-link trailingws "210"
|
||||
|
||||
@@ -35,3 +35,6 @@ color-link todo "#8B98AB"
|
||||
color-link type "#F9EE98"
|
||||
color-link type.keyword "#CDA869"
|
||||
color-link underlined "#8996A8"
|
||||
color-link match-brace "#141414,#E0C589"
|
||||
color-link tab-error "#D75F5F"
|
||||
color-link trailingws "#D75F5F"
|
||||
|
||||
@@ -25,3 +25,6 @@ color-link gutter-warning "174,237"
|
||||
color-link cursor-line "238"
|
||||
color-link color-column "238"
|
||||
color-link current-line-number "188,237"
|
||||
color-link match-brace "237,223"
|
||||
color-link tab-error "167"
|
||||
color-link trailingws "167"
|
||||
|
||||
@@ -8,7 +8,7 @@ This help page aims to cover two aspects of micro's syntax highlighting engine:
|
||||
|
||||
## Colorschemes
|
||||
|
||||
To change your colorscheme, press Ctrl-e in micro to bring up the command
|
||||
To change your colorscheme, press `Ctrl-e` in micro to bring up the command
|
||||
prompt, and type:
|
||||
|
||||
```
|
||||
@@ -93,7 +93,7 @@ and set this variable yourself.
|
||||
* `solarized-tc`: this is the solarized colorscheme for true color.
|
||||
* `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme.
|
||||
* `cmc-tc`: A true colour variant of the cmc theme. It requires true color to
|
||||
look its best. Use cmc-16 if your terminal doesn't support true color.
|
||||
look its best. Use cmc-16 if your terminal doesn't support true color.
|
||||
* `gruvbox-tc`: The true color version of the gruvbox colorscheme
|
||||
* `material-tc`: Colorscheme based off of Google's Material Design palette
|
||||
|
||||
@@ -106,7 +106,7 @@ be found
|
||||
Custom colorschemes should be placed in the `~/.config/micro/colorschemes`
|
||||
directory.
|
||||
|
||||
A number of custom directives are placed in a `.micro` file. Colorschemes are
|
||||
A number of custom directives are placed in a `.micro` file. Colorschemes are
|
||||
typically only 18-30 lines in total.
|
||||
|
||||
To create the colorscheme you need to link highlight groups with
|
||||
@@ -152,7 +152,7 @@ Then you can use the terminals 256 colors by using their numbers 1-256 (numbers
|
||||
|
||||
If the user's terminal supports true color, then you can also specify colors
|
||||
exactly using their hex codes. If the terminal is not true color but micro is
|
||||
told to use a true color colorscheme it will attempt to map the colors to the
|
||||
told to use a true color colorscheme it will attempt to map the colors to the
|
||||
available 256 colors.
|
||||
|
||||
Generally colorschemes which require true color terminals to look good are
|
||||
@@ -194,6 +194,10 @@ Here is a list of the colorscheme groups that you can use:
|
||||
* divider (Color of the divider between vertical splits)
|
||||
* message (Color of messages in the bottom line of the screen)
|
||||
* error-message (Color of error messages in the bottom line of the screen)
|
||||
* match-brace (Color of matching brackets when `matchbracestyle` is set to `highlight`)
|
||||
* hlsearch (Color of highlighted search results when `hlsearch` is enabled)
|
||||
* tab-error (Color of tab vs space errors when `hltaberrors` is enabled)
|
||||
* trailingws (Color of trailing whitespaces when `hltrailingws` is enabled)
|
||||
|
||||
Colorschemes must be placed in the `~/.config/micro/colorschemes` directory to
|
||||
be used.
|
||||
@@ -210,9 +214,9 @@ safe and recommended to use subgroups in your custom syntax files.
|
||||
For example if `constant.string` is found in your colorscheme, micro will us
|
||||
that for highlighting strings. If it's not found, it will use constant instead.
|
||||
Micro tries to match the largest set of groups it can find in the colorscheme
|
||||
definitions, so if, for examle `constant.bool.true` is found then micro will
|
||||
definitions, so if, for example `constant.bool.true` is found then micro will
|
||||
use that. If `constant.bool.true` is not found but `constant.bool` is found
|
||||
micro will use `constant.bool`. If not, it uses `constant`.
|
||||
micro will use `constant.bool`. If not, it uses `constant`.
|
||||
|
||||
Here's a list of subgroups used in micro's built-in syntax files.
|
||||
|
||||
@@ -220,10 +224,10 @@ Here's a list of subgroups used in micro's built-in syntax files.
|
||||
* constant.bool
|
||||
* constant.bool.true
|
||||
* constant.bool.false
|
||||
* constant.number
|
||||
* constant.number
|
||||
* constant.specialChar
|
||||
* constant.string
|
||||
* constant.string.url
|
||||
* constant.string.url
|
||||
* identifier.class (Also used for functions)
|
||||
* identifier.macro
|
||||
* identifier.var
|
||||
@@ -236,6 +240,12 @@ Here's a list of subgroups used in micro's built-in syntax files.
|
||||
|
||||
In the future, plugins may also be able to use color groups for styling.
|
||||
|
||||
---
|
||||
|
||||
Last but not least it's even possible to use `include` followed by the
|
||||
colorscheme name as string to include a different colorscheme within a new one.
|
||||
Additionally the groups can then be extended or overwritten. The `default.micro`
|
||||
theme can be seen as an example, which links to the chosen default colorscheme.
|
||||
|
||||
## Syntax files
|
||||
|
||||
@@ -244,7 +254,7 @@ languages.
|
||||
|
||||
Micro's builtin syntax highlighting tries very hard to be sane, sensible and
|
||||
provide ample coverage of the meaningful elements of a language. Micro has
|
||||
syntax files built in for over 100 languages now! However, there may be
|
||||
syntax files built in for over 100 languages now! However, there may be
|
||||
situations where you find Micro's highlighting to be insufficient or not to
|
||||
your liking. The good news is that you can create your own syntax files, and
|
||||
place them in `~/.config/micro/syntax` and Micro will use those instead.
|
||||
@@ -267,8 +277,9 @@ detect:
|
||||
```
|
||||
|
||||
Micro will match this regex against a given filename to detect the filetype.
|
||||
You may also provide an optional `header` regex that will check the first line
|
||||
of the file. For example:
|
||||
|
||||
In addition to the `filename` regex (or even instead of it) you can provide
|
||||
a `header` regex that will check the first line of the file. For example:
|
||||
|
||||
```
|
||||
detect:
|
||||
@@ -276,6 +287,32 @@ detect:
|
||||
header: "%YAML"
|
||||
```
|
||||
|
||||
This is useful in cases when the given file name is not sufficient to determine
|
||||
the filetype, e.g. with the above example, if a YAML file has no `.yaml`
|
||||
extension but may contain a `%YAML` directive in its first line.
|
||||
|
||||
`filename` takes precedence over `header`, i.e. if there is a syntax file that
|
||||
matches the file with a filetype by the `filename` and another syntax file that
|
||||
matches the same file with another filetype by the `header`, the first filetype
|
||||
will be used.
|
||||
|
||||
Finally, in addition to `filename` and/or `header` (but not instead of them)
|
||||
you may also provide an optional `signature` regex which is useful for resolving
|
||||
ambiguities when there are multiple syntax files matching the same file with
|
||||
different filetypes. If a `signature` regex is given, micro will match a certain
|
||||
amount of first lines in the file (this amount is determined by the `detectlimit`
|
||||
option) against this regex, and if any of the lines match, this syntax file's
|
||||
filetype will be preferred over other matching filetypes.
|
||||
|
||||
For example, to distinguish C++ header files from C and Objective-C header files
|
||||
that have the same `.h` extension:
|
||||
|
||||
```
|
||||
detect:
|
||||
filename: "\\.c(c|pp|xx)$|\\.h(h|pp|xx)?$"
|
||||
signature: "namespace|template|public|protected|private"
|
||||
```
|
||||
|
||||
### Syntax rules
|
||||
|
||||
Next you must provide the syntax highlighting rules. There are two types of
|
||||
@@ -356,15 +393,28 @@ example, the following is possible for html:
|
||||
- include: "css"
|
||||
```
|
||||
|
||||
## Syntax file headers
|
||||
Note that nested include (i.e. including syntax files that include other syntax
|
||||
files) is not supported yet.
|
||||
|
||||
Syntax file headers are an optimization and it is likely you do not need to
|
||||
worry about them.
|
||||
### Default syntax highlighting
|
||||
|
||||
Syntax file headers are files that contain only the filetype and the detection
|
||||
regular expressions for a given syntax file. They have a `.hdr` suffix and are
|
||||
used by default only for the pre-installed syntax files. Header files allow
|
||||
micro to parse the syntax files much faster when checking the filetype of a
|
||||
certain file. Custom syntax files may provide header files in
|
||||
`~/.config/micro/syntax` as well but it is not necessary (only do this if you
|
||||
have many (100+) custom syntax files and want to improve performance).
|
||||
If micro cannot detect the filetype of the file, it falls back to using the
|
||||
default syntax highlighting for it, which highlights just the bare minimum:
|
||||
email addresses, URLs etc.
|
||||
|
||||
Just like in other cases, you can override the default highlighting by adding
|
||||
your own custom `default.yaml` file to `~/.config/micro/syntax`.
|
||||
|
||||
For example, if you work with various config files that use the `#` sign to mark
|
||||
the beginning of a comment, you can use the following custom `default.yaml` to
|
||||
highlight those comments by default:
|
||||
|
||||
```
|
||||
filetype: unknown
|
||||
|
||||
detect:
|
||||
filename: ""
|
||||
|
||||
rules:
|
||||
- comment: "(^|\\s)#.*$"
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Command bar
|
||||
|
||||
The command bar is opened by pressing Ctrl-e. It is a single-line buffer,
|
||||
The command bar is opened by pressing `Ctrl-e`. It is a single-line buffer,
|
||||
meaning that all keybindings from a normal buffer are supported (as well
|
||||
as mouse and selection).
|
||||
|
||||
@@ -21,32 +21,43 @@ quotes here but these are not necessary when entering the command in micro.
|
||||
This command will modify `bindings.json` and overwrite any bindings to
|
||||
`key` that already exist.
|
||||
|
||||
* `help 'topic'?`: opens the corresponding help topic. If no topic is provided
|
||||
* `help ['topic']`: opens the corresponding help topic. If no topic is provided
|
||||
opens the default help screen. Help topics are stored as `.md` files in the
|
||||
`runtime/help` directory of the source tree, which is embedded in the final
|
||||
binary.
|
||||
|
||||
* `save 'filename'?`: saves the current buffer. If the file is provided it
|
||||
* `save ['filename']`: saves the current buffer. If the file is provided it
|
||||
will 'save as' the filename.
|
||||
|
||||
* `quit`: quits micro.
|
||||
|
||||
* `goto 'line'`: jumps to the given line number. A negative number can be
|
||||
passed to jump inward from the end of the file; for example, -5 jumps
|
||||
to the 5th-last line in the file.
|
||||
* `goto 'line[:col]'`: goes to the given absolute line (and optional column)
|
||||
number.
|
||||
A negative number can be passed to go inward from the end of the file.
|
||||
Example: -5 goes to the 5th-last line in the file.
|
||||
|
||||
* `replace 'search' 'value' 'flags'?`: This will replace `search` with `value`.
|
||||
* `jump 'line[:col]'`: goes to the given relative number from the current
|
||||
line (and optional absolute column) number.
|
||||
Example: -5 jumps 5 lines up in the file, while (+)3 jumps 3 lines down.
|
||||
|
||||
* `replace 'search' 'value' ['flags']`: This will replace `search` with `value`.
|
||||
The `flags` are optional. Possible flags are:
|
||||
* `-a`: Replace all occurrences at once
|
||||
* `-l`: Do a literal search instead of a regex search
|
||||
|
||||
Note that `search` must be a valid regex (unless `-l` is passed). If one
|
||||
Note that `search` must be a valid regex (unless `-l` is passed). If one
|
||||
of the arguments does not have any spaces in it, you may omit the quotes.
|
||||
|
||||
In case the search is done non-literal (without `-l`), the 'value'
|
||||
is interpreted as a template:
|
||||
* `$3` or `${3}` substitutes the submatch of the 3rd (capturing group)
|
||||
* `$foo` or `${foo}` substitutes the submatch of the (?P<foo>named group)
|
||||
* You have to write `$$` to substitute a literal dollar.
|
||||
|
||||
* `replaceall 'search' 'value'`: this will replace all occurrences of `search`
|
||||
with `value` without user confirmation.
|
||||
|
||||
See `replace` command for more information.
|
||||
See `replace` command for more information.
|
||||
|
||||
* `set 'option' 'value'`: sets the option to value. See the `options` help
|
||||
topic for a list of options you can set. This will modify your
|
||||
@@ -57,18 +68,18 @@ quotes here but these are not necessary when entering the command in micro.
|
||||
|
||||
* `show 'option'`: shows the current value of the given option.
|
||||
|
||||
* `run 'sh-command'`: runs the given shell command in the background. The
|
||||
* `run 'sh-command'`: runs the given shell command in the background. The
|
||||
command's output will be displayed in one line when it finishes running.
|
||||
|
||||
* `vsplit 'filename'`: opens a vertical split with `filename`. If no filename
|
||||
* `vsplit ['filename']`: opens a vertical split with `filename`. If no filename
|
||||
is provided, a vertical split is opened with an empty buffer.
|
||||
|
||||
* `hsplit 'filename'`: same as `vsplit` but opens a horizontal split instead
|
||||
* `hsplit ['filename']`: same as `vsplit` but opens a horizontal split instead
|
||||
of a vertical split.
|
||||
|
||||
* `tab 'filename'`: opens the given file in a new tab.
|
||||
* `tab ['filename']`: opens the given file in a new tab.
|
||||
|
||||
* `tabmove '[-+]?n'`: Moves the active tab to another slot. `n` is an integer.
|
||||
* `tabmove '[-+]n'`: Moves the active tab to another slot. `n` is an integer.
|
||||
If `n` is prefixed with `-` or `+`, then it represents a relative position
|
||||
(e.g. `tabmove +2` moves the tab to the right by `2`). If `n` has no prefix,
|
||||
it represents an absolute position (e.g. `tabmove 2` moves the tab to slot `2`).
|
||||
@@ -89,14 +100,17 @@ quotes here but these are not necessary when entering the command in micro.
|
||||
|
||||
* `plugin remove 'pl'`: remove a plugin.
|
||||
|
||||
* `plugin update 'pl'`: update a plugin (if no arguments are provided
|
||||
* `plugin update ['pl']`: update a plugin (if no arguments are provided
|
||||
updates all plugins).
|
||||
|
||||
* `plugin search 'pl'`: search available plugins for a keyword.
|
||||
|
||||
* `plugin available`: show available plugins that can be installed.
|
||||
|
||||
* `reload`: reloads all runtime files.
|
||||
* `reload`: reloads all runtime files (settings, keybindings, syntax files,
|
||||
colorschemes, plugins). All plugins will be unloaded by running their
|
||||
`deinit()` function (if it exists), and then loaded again by calling the
|
||||
`preinit()`, `init()` and `postinit()` functions (if they exist).
|
||||
|
||||
* `cd 'path'`: Change the working directory to the given `path`.
|
||||
|
||||
@@ -104,6 +118,8 @@ quotes here but these are not necessary when entering the command in micro.
|
||||
|
||||
* `open 'filename'`: Open a file in the current buffer.
|
||||
|
||||
* `reopen`: Reopens the current file from disk.
|
||||
|
||||
* `reset 'option'`: resets the given option to its default value
|
||||
|
||||
* `retab`: Replaces all leading tabs with spaces or leading spaces with tabs
|
||||
@@ -114,10 +130,10 @@ quotes here but these are not necessary when entering the command in micro.
|
||||
the terminal and helps you see which bindings aren't possible and why. This
|
||||
is most useful for debugging keybindings.
|
||||
|
||||
* `showkey`: Show the action(s) bound to a given key. For example
|
||||
* `showkey 'key'`: Show the action(s) bound to a given key. For example
|
||||
running `> showkey Ctrl-c` will display `Copy`.
|
||||
|
||||
* `term exec?`: Open a terminal emulator running the given executable. If no
|
||||
* `term ['exec']`: Open a terminal emulator running the given executable. If no
|
||||
executable is given, this will open the default shell in the terminal
|
||||
emulator.
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ is limited support among terminal emulators for the terminal clipboard
|
||||
(which uses the OSC 52 protocol to communicate clipboard contents).
|
||||
Here is a list of terminal emulators and their status:
|
||||
|
||||
* Kitty: supported, but only writing is enabled by default. To enable
|
||||
reading, add `read-primary` and `read-clipboard` to the
|
||||
`clipboard_control` option.
|
||||
* `Kitty`: supported, but only writing is enabled by default. To enable
|
||||
reading, add `read-primary` and `read-clipboard` to the
|
||||
`clipboard_control` option.
|
||||
|
||||
* iTerm2: only copying (writing to clipboard) is supported. Must be enabled in
|
||||
`Preferences->General-> Selection->Applications in terminal may access clipboard`.
|
||||
You can use Command-v to paste.
|
||||
* `iTerm2`: only copying (writing to clipboard) is supported. Must be enabled in
|
||||
`Preferences->General-> Selection->Applications in terminal may access clipboard`.
|
||||
You can use `Command-v` to paste.
|
||||
|
||||
* `st`: supported.
|
||||
|
||||
@@ -31,7 +31,8 @@ Here is a list of terminal emulators and their status:
|
||||
|
||||
* `gnome-terminal`: does not support OSC 52.
|
||||
|
||||
* `alacritty`: supported.
|
||||
* `alacritty`: supported. Since 0.13.0, reading has been disabled by default.
|
||||
To reenable it, set the `terminal.osc52` option to `CopyPaste`.
|
||||
|
||||
* `foot`: supported.
|
||||
|
||||
@@ -48,12 +49,12 @@ supports OSC 52.
|
||||
|
||||
The recommended method of pasting is the following:
|
||||
|
||||
* If you are not working over SSH, use the micro keybinding (Ctrl-v
|
||||
* If you are not working over SSH, use the micro keybinding (`Ctrl-v`
|
||||
by default) to perform pastes. If on Linux, install `xclip` or
|
||||
`xsel` beforehand.
|
||||
|
||||
* If you are working over SSH, use the terminal keybinding
|
||||
(Ctrl-Shift-v or Command-v) to perform pastes. If your terminal
|
||||
(`Ctrl-Shift-v` or `Command-v`) to perform pastes. If your terminal
|
||||
does not support bracketed paste, when performing a paste first
|
||||
enable the `paste` option, and when finished disable the option.
|
||||
|
||||
@@ -62,8 +63,8 @@ The recommended method of pasting is the following:
|
||||
Micro is an application that runs within the terminal. This means
|
||||
that the terminal sends micro events, such as key events, mouse
|
||||
events, resize events, and paste events. Micro's default keybinding
|
||||
for paste is Ctrl-v. This means that when micro receives the key
|
||||
event saying Ctrl-v has been pressed from the terminal, it will
|
||||
for paste is `Ctrl-v`. This means that when micro receives the key
|
||||
event saying `Ctrl-v` has been pressed from the terminal, it will
|
||||
attempt to access the system clipboard and effect a paste. The
|
||||
system clipboard will be accessed through `pbpaste` on MacOS
|
||||
(installed by default), `xclip` or `xsel` on Linux (these
|
||||
@@ -76,8 +77,8 @@ For certain keypresses, the terminal will not send an event to
|
||||
micro and will instead do something itself. In this document,
|
||||
such keypresses will be called "terminal keybindings." Often
|
||||
there will be a terminal keybinding for pasting and copying. On
|
||||
MacOS these are Command-v and Command-c and on Linux Ctrl-Shift-v
|
||||
and Ctrl-Shift-c. When the terminal keybinding for paste is
|
||||
MacOS these are Command-v and Command-c and on Linux `Ctrl-Shift-v`
|
||||
and `Ctrl-Shift-c`. When the terminal keybinding for paste is
|
||||
executed, your terminal will access the system clipboard, and send
|
||||
micro either a paste event or a list of key events (one key for each
|
||||
character in the paste), depending on whether or not your terminal
|
||||
@@ -89,7 +90,7 @@ sends a list of key events, this can cause issues because micro
|
||||
will think you manually entered each character and may add closing
|
||||
brackets or automatic indentation, which will mess up the pasted
|
||||
text. To avoid this, you can temporarily enable the `paste` option
|
||||
while you perform the paste. When paste option is on, micro will
|
||||
while you perform the paste. When paste option is on, micro will
|
||||
aggregate lists of multiple key events into larger paste events.
|
||||
It is a good idea to disable the `paste` option during normal use
|
||||
as occasionally if you are typing quickly, the terminal will send
|
||||
@@ -100,7 +101,7 @@ entered.
|
||||
|
||||
When working over SSH, micro is running on the remote machine and
|
||||
your terminal is running on your local machine. Therefore if you
|
||||
would like to paste, using Ctrl-v (micro's keybinding) will not
|
||||
would like to paste, using `Ctrl-v` (micro's keybinding) will not
|
||||
work because when micro attempts to access the system clipboard,
|
||||
it will access the remote machine's clipboard rather than the local
|
||||
machine's clipboard. On the other hand, the terminal keybinding
|
||||
@@ -113,12 +114,12 @@ the network as a paste event, which is what you want.
|
||||
|
||||
The recommended method of copying is the following:
|
||||
|
||||
* If you are not working over SSH, use the micro keybinding (Ctrl-c by
|
||||
* If you are not working over SSH, use the micro keybinding (`Ctrl-c` by
|
||||
default) to perform copies. If on Linux, install `xclip` or `xsel`
|
||||
beforehand.
|
||||
|
||||
* If you are working over SSH, use the terminal keybinding
|
||||
(Ctrl-Shift-c or Command-c) to perform copies. You must first disable
|
||||
(`Ctrl-Shift-c` or `Command-c`) to perform copies. You must first disable
|
||||
the `mouse` option to perform a terminal selection, and you may wish
|
||||
to disable line numbers and diff indicators (`ruler` and `diffgutter`
|
||||
options) and close other splits. This method will only be able to copy
|
||||
@@ -129,14 +130,14 @@ Copying follows a similar discussion to the one above about pasting.
|
||||
The primary difference is before performing a copy, the application
|
||||
doing the copy must be told what text needs to be copied.
|
||||
|
||||
Micro has a keybinding (Ctrl-c) for copying and will access the system
|
||||
Micro has a keybinding (`Ctrl-c`) for copying and will access the system
|
||||
clipboard to perform the copy. The text that micro will copy into is
|
||||
the text that is currently selected in micro (usually such text is
|
||||
displayed with a white background). When the `mouse` option is enabled,
|
||||
the mouse can be used to select text, as well as other keybindings,
|
||||
such as ShiftLeft, etc...
|
||||
|
||||
The terminal also has a keybinding (Ctrl-Shift-c or Command-c) to perform
|
||||
The terminal also has a keybinding (`Ctrl-Shift-c` or `Command-c`) to perform
|
||||
a copy, and the text that it copies is the text selected by the terminal's
|
||||
selection (*not* micro's selection). To select text with the terminal
|
||||
selection, micro's mouse support must first be disabled by turning the
|
||||
|
||||
@@ -52,9 +52,9 @@ can change it!
|
||||
| Ctrl-n | Find next instance of current search |
|
||||
| Ctrl-p | Find previous instance of current search |
|
||||
|
||||
Note: Ctrl-n and Ctrl-p should be used from the main buffer, not from inside
|
||||
the search prompt. After Ctrl-f, press enter to complete the search and then
|
||||
you can use Ctrl-n and Ctrl-p to cycle through matches.
|
||||
Note: `Ctrl-n` and `Ctrl-p` should be used from the main buffer, not from inside
|
||||
the search prompt. After `Ctrl-f`, press enter to complete the search and then
|
||||
you can use `Ctrl-n` and `Ctrl-p` to cycle through matches.
|
||||
|
||||
### File Operations
|
||||
|
||||
@@ -129,7 +129,7 @@ you can use Ctrl-n and Ctrl-p to cycle through matches.
|
||||
|
||||
### Function keys.
|
||||
|
||||
Warning! The function keys may not work in all terminals!
|
||||
Warning! The function keys may not work in all terminals!
|
||||
|
||||
| Key | Description of function |
|
||||
|------ |-------------------------- |
|
||||
|
||||
@@ -4,9 +4,9 @@ Micro is an easy to use, intuitive, text editor that takes advantage of the
|
||||
full capabilities of modern terminals.
|
||||
|
||||
Micro can be controlled by commands entered on the command bar, or with
|
||||
keybindings. To open the command bar, press Ctrl-e: the `>` prompt will
|
||||
keybindings. To open the command bar, press `Ctrl-e`: the `>` prompt will
|
||||
display. From now on, when the documentation shows a command to run (such as
|
||||
`> help`), press Ctrl-e and type the command followed by enter.
|
||||
`> help`), press `Ctrl-e` and type the command followed by enter.
|
||||
|
||||
For a list of the default keybindings, run `> help defaultkeys`.
|
||||
For more information on keybindings, see `> help keybindings`.
|
||||
@@ -14,7 +14,7 @@ To toggle a short list of important keybindings, press Alt-g.
|
||||
|
||||
## Quick-start
|
||||
|
||||
To quit, press Ctrl-q. Save by pressing Ctrl-s. Press Ctrl-e, as previously
|
||||
To quit, press `Ctrl-q`. Save by pressing `Ctrl-s`. Press `Ctrl-e`, as previously
|
||||
mentioned, to start typing commands. To see which commands are available, at the
|
||||
prompt, press tab, or view the help topic with `> help commands`.
|
||||
|
||||
@@ -26,31 +26,31 @@ If the colorscheme doesn't look good, you can change it with
|
||||
or see more information about colorschemes and syntax highlighting with `> help
|
||||
colors`.
|
||||
|
||||
Press Ctrl-w to move between splits, and type `> vsplit filename` or
|
||||
Press `Ctrl-w` to move between splits, and type `> vsplit filename` or
|
||||
`> hsplit filename` to open a new split.
|
||||
|
||||
## Accessing more help
|
||||
|
||||
Micro has a built-in help system which can be accessed with the `> help` command.
|
||||
|
||||
To view help for the various available topics, press Ctrl-e to access command
|
||||
To view help for the various available topics, press `Ctrl-e` to access command
|
||||
mode and type in `> help` followed by a topic. Typing just `> help` will open
|
||||
this page.
|
||||
|
||||
Here are the available help topics:
|
||||
|
||||
* tutorial: A brief tutorial which gives an overview of all the other help
|
||||
topics
|
||||
* keybindings: Gives a full list of the default keybindings as well as how to
|
||||
rebind them
|
||||
* defaultkeys: Gives a more straight-forward list of the hotkey commands and
|
||||
what they do
|
||||
* commands: Gives a list of all the commands and what they do
|
||||
* options: Gives a list of all the options you can customize
|
||||
* plugins: Explains how micro's plugin system works and how to create your own
|
||||
plugins
|
||||
* colors: Explains micro's colorscheme and syntax highlighting engine and how
|
||||
to create your own colorschemes or add new languages to the engine
|
||||
* `tutorial`: A brief tutorial which gives an overview of all the other help
|
||||
topics
|
||||
* `keybindings`: Gives a full list of the default keybindings as well as how to
|
||||
rebind them
|
||||
* `defaultkeys`: Gives a more straight-forward list of the hotkey commands and
|
||||
what they do
|
||||
* `commands`: Gives a list of all the commands and what they do
|
||||
* `options`: Gives a list of all the options you can customize
|
||||
* `plugins`: Explains how micro's plugin system works and how to create your own
|
||||
plugins
|
||||
* `colors`: Explains micro's colorscheme and syntax highlighting engine and how
|
||||
to create your own colorschemes or add new languages to the engine
|
||||
|
||||
For example, to open the help page on plugins you would run `> help plugins`.
|
||||
|
||||
|
||||
@@ -36,15 +36,15 @@ following in the `bindings.json` file.
|
||||
```
|
||||
|
||||
**Note:** The syntax `<Modifier><key>` is equivalent to `<Modifier>-<key>`. In
|
||||
addition, Ctrl-Shift bindings are not supported by terminals, and are the same
|
||||
as simply Ctrl bindings. This means that `CtrlG`, `Ctrl-G`, and `Ctrl-g` all
|
||||
mean the same thing. However, for Alt this is not the case: `AltG` and `Alt-G`
|
||||
addition, `Ctrl-Shift` bindings are not supported by terminals, and are the same
|
||||
as simply `Ctrl` bindings. This means that `CtrlG`, `Ctrl-G`, and `Ctrl-g` all
|
||||
mean the same thing. However, for `Alt` this is not the case: `AltG` and `Alt-G`
|
||||
mean `Alt-Shift-g`, while `Alt-g` does not require the Shift modifier.
|
||||
|
||||
In addition to editing your `~/.config/micro/bindings.json`, you can run
|
||||
`>bind <keycombo> <action>` For a list of bindable actions, see below.
|
||||
|
||||
You can also chain commands when rebinding. For example, if you want Alt-s to
|
||||
You can also chain commands when rebinding. For example, if you want `Alt-s` to
|
||||
save and quit you can bind it like so:
|
||||
|
||||
```json
|
||||
@@ -178,12 +178,18 @@ SelectToStartOfText
|
||||
SelectToStartOfTextToggle
|
||||
WordRight
|
||||
WordLeft
|
||||
SubWordRight
|
||||
SubWordLeft
|
||||
SelectWordRight
|
||||
SelectWordLeft
|
||||
SelectSubWordRight
|
||||
SelectSubWordLeft
|
||||
MoveLinesUp
|
||||
MoveLinesDown
|
||||
DeleteWordRight
|
||||
DeleteWordLeft
|
||||
DeleteSubWordRight
|
||||
DeleteSubWordLeft
|
||||
SelectLine
|
||||
SelectToStartOfLine
|
||||
SelectToEndOfLine
|
||||
@@ -231,10 +237,14 @@ StartOfText
|
||||
StartOfTextToggle
|
||||
ParagraphPrevious
|
||||
ParagraphNext
|
||||
SelectToParagraphPrevious
|
||||
SelectToParagraphNext
|
||||
ToggleHelp
|
||||
ToggleDiffGutter
|
||||
ToggleRuler
|
||||
JumpLine
|
||||
ResetSearch
|
||||
ClearInfo
|
||||
ClearStatus
|
||||
ShellMode
|
||||
CommandMode
|
||||
@@ -409,8 +419,14 @@ mouse actions)
|
||||
|
||||
```
|
||||
MouseLeft
|
||||
MouseLeftDrag
|
||||
MouseLeftRelease
|
||||
MouseMiddle
|
||||
MouseMiddleDrag
|
||||
MouseMiddleRelease
|
||||
MouseRight
|
||||
MouseRightDrag
|
||||
MouseRightRelease
|
||||
MouseWheelUp
|
||||
MouseWheelDown
|
||||
MouseWheelLeft
|
||||
@@ -496,6 +512,8 @@ conventions for text editing defaults.
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab",
|
||||
"CtrlPageDown": "NextTab",
|
||||
"ShiftPageUp": "SelectPageUp",
|
||||
"ShiftPageDown": "SelectPageDown",
|
||||
"Ctrl-g": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"Ctrl-r": "ToggleRuler",
|
||||
@@ -524,11 +542,13 @@ conventions for text editing defaults.
|
||||
"Esc": "Escape",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
// Multi-cursor bindings
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
@@ -634,10 +654,12 @@ are given below:
|
||||
"Esc": "AbortCommand",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary"
|
||||
"MouseWheelUp": "HistoryUp",
|
||||
"MouseWheelDown": "HistoryDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseLeftDrag": "MouseDrag",
|
||||
"MouseLeftRelease": "MouseRelease",
|
||||
"MouseMiddle": "PastePrimary"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -11,10 +11,10 @@ if you have set either of the above environment variables).
|
||||
|
||||
Here are the available options:
|
||||
|
||||
* `autoindent`: when creating a new line, use the same indentation as the
|
||||
* `autoindent`: when creating a new line, use the same indentation as the
|
||||
previous line.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `autosave`: automatically save the buffer every n seconds, where n is the
|
||||
value of the autosave option. Also when quitting on a modified buffer, micro
|
||||
@@ -77,32 +77,39 @@ Here are the available options:
|
||||
specified column. This is useful if you want column 80 to be highlighted
|
||||
special for example.
|
||||
|
||||
default value: `0`
|
||||
default value: `0`
|
||||
|
||||
* `colorscheme`: loads the colorscheme stored in
|
||||
* `colorscheme`: loads the colorscheme stored in
|
||||
$(configDir)/colorschemes/`option`.micro, This setting is `global only`.
|
||||
|
||||
default value: `default`
|
||||
default value: `default`
|
||||
|
||||
Note that the default colorschemes (default, solarized, and solarized-tc)
|
||||
are not located in configDir, because they are embedded in the micro
|
||||
binary.
|
||||
Note that the default colorschemes (default, solarized, and solarized-tc)
|
||||
are not located in configDir, because they are embedded in the micro
|
||||
binary.
|
||||
|
||||
The colorscheme can be selected from all the files in the
|
||||
~/.config/micro/colorschemes/ directory. Micro comes by default with
|
||||
three colorschemes:
|
||||
The colorscheme can be selected from all the files in the
|
||||
~/.config/micro/colorschemes/ directory. Micro comes by default with
|
||||
three colorschemes:
|
||||
|
||||
You can read more about micro's colorschemes in the `colors` help topic
|
||||
(`help colors`).
|
||||
You can read more about micro's colorschemes in the `colors` help topic
|
||||
(`help colors`).
|
||||
|
||||
* `cursorline`: highlight the line that the cursor is on in a different color
|
||||
(the color is defined by the colorscheme you are using).
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `detectlimit`: if this is not set to 0, it will limit the amount of first
|
||||
lines in a file that are matched to determine the filetype.
|
||||
A higher limit means better accuracy of guessing the filetype, but also
|
||||
taking more time.
|
||||
|
||||
default value: `100`
|
||||
|
||||
* `diffgutter`: display diff indicators before lines.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `divchars`: specifies the "divider" characters used for the dividing line
|
||||
between vertical/horizontal splits. The first character is for vertical
|
||||
@@ -127,11 +134,11 @@ Here are the available options:
|
||||
* `eofnewline`: micro will automatically add a newline to the end of the
|
||||
file if one does not exist.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `fakecursor`: forces micro to render the cursor using terminal colors rather
|
||||
than the actual terminal cursor. This is useful when the terminal's cursor is
|
||||
slow or otherwise unavailable/undesirable to use.
|
||||
than the actual terminal cursor. This is useful when the terminal's cursor is
|
||||
slow or otherwise unavailable/undesirable to use.
|
||||
|
||||
default value: `false`
|
||||
|
||||
@@ -144,7 +151,7 @@ Here are the available options:
|
||||
intensive. This option will be automatically disabled if the file size
|
||||
exceeds 50KB.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `fileformat`: this determines what kind of line endings micro will use for
|
||||
the file. Unix line endings are just `\n` (linefeed) whereas dos line
|
||||
@@ -157,12 +164,12 @@ Here are the available options:
|
||||
an effect if the file is empty/newly created, because otherwise the fileformat
|
||||
will be automatically detected from the existing line endings.
|
||||
|
||||
default value: `unix`
|
||||
default value: `unix` on Unix systems, `dos` on Windows
|
||||
|
||||
* `filetype`: sets the filetype for the current buffer. Set this option to
|
||||
`off` to completely disable filetype detection.
|
||||
`off` to completely disable filetype detection.
|
||||
|
||||
default value: `unknown`. This will be automatically overridden depending
|
||||
default value: `unknown`. This will be automatically overridden depending
|
||||
on the file you open.
|
||||
|
||||
* `hlsearch`: highlight all instances of the searched text after a successful
|
||||
@@ -172,49 +179,82 @@ Here are the available options:
|
||||
change the `hlsearch` setting. As long as `hlsearch` is set to true, the next
|
||||
search will have the highlighting turned on again.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `hltaberrors`: highlight tabs when spaces are expected, and spaces when tabs
|
||||
are expected. More precisely: if `tabstospaces` option is on, highlight
|
||||
all tab characters; if `tabstospaces` is off, highlight space characters
|
||||
in the initial indent part of the line.
|
||||
|
||||
default value: `false`
|
||||
|
||||
* `hltrailingws`: highlight trailing whitespaces at ends of lines. Note that
|
||||
it doesn't highlight newly added trailing whitespaces that naturally occur
|
||||
while typing text. It highlights only nasty forgotten trailing whitespaces.
|
||||
|
||||
default value: `false`
|
||||
|
||||
* `incsearch`: enable incremental search in "Find" prompt (matching as you type).
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `ignorecase`: perform case-insensitive searches.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `indentchar`: sets the indentation character. This will not be inserted into
|
||||
files; it is only a visual indicator that whitespace is present. If set to a
|
||||
printing character, it functions as a subset of the "show invisibles"
|
||||
setting available in many other text editors. The color of this character is
|
||||
determined by the `indent-char` field in the current theme rather than the
|
||||
default text color.
|
||||
files; it is only a visual indicator that whitespace is present. If set to a
|
||||
printing character, it functions as a subset of the "show invisibles"
|
||||
setting available in many other text editors. The color of this character is
|
||||
determined by the `indent-char` field in the current theme rather than the
|
||||
default text color.
|
||||
|
||||
default value: ` ` (space)
|
||||
default value: ` ` (space)
|
||||
|
||||
* `infobar`: enables the line at the bottom of the editor where messages are
|
||||
printed. This option is `global only`.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `keepautoindent`: when using autoindent, whitespace is added for you. This
|
||||
option determines if when you move to the next line without any insertions
|
||||
the whitespace that was added should be deleted to remove trailing
|
||||
whitespace. By default, the autoindent whitespace is deleted if the line
|
||||
whitespace. By default, the autoindent whitespace is deleted if the line
|
||||
was left empty.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `keymenu`: display the nano-style key menu at the bottom of the screen. Note
|
||||
that ToggleKeyMenu is bound to `Alt-g` by default and this is displayed in
|
||||
the statusline. To disable the key binding, bind `Alt-g` to `None`.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `matchbrace`: underline matching braces for '()', '{}', '[]' when the cursor
|
||||
is on a brace character.
|
||||
* `matchbrace`: show matching braces for '()', '{}', '[]' when the cursor
|
||||
is on a brace character or (if `matchbraceleft` is enabled) next to it.
|
||||
|
||||
default value: `true`
|
||||
|
||||
* `matchbraceleft`: simulate I-beam cursor behavior (cursor located not on a
|
||||
character but "between" characters): when showing matching braces, if there
|
||||
is no brace character directly under the cursor, match the brace character
|
||||
to the left of the cursor instead. Also when jumping to the matching brace,
|
||||
move the cursor either to the matching brace character or to the character
|
||||
next to it, depending on whether the initial cursor position was on the
|
||||
brace character or next to it (i.e. "inside" or "outside" the braces).
|
||||
With `matchbraceleft` disabled, micro will only match the brace directly
|
||||
under the cursor and will only jump to precisely to the matching brace.
|
||||
|
||||
default value: `true`
|
||||
|
||||
* `matchbracestyle`: whether to underline or highlight matching braces when
|
||||
`matchbrace` is enabled. The color of highlight is determined by the `match-brace`
|
||||
field in the current theme. Possible values:
|
||||
* `underline`: underline matching braces.
|
||||
* `highlight`: use `match-brace` style from the current theme.
|
||||
|
||||
default value: `underline`
|
||||
|
||||
* `mkparents`: if a file is opened on a path that does not exist, the file
|
||||
cannot be saved because the parent directories don't exist. This option lets
|
||||
micro automatically create the parent directories in such a situation.
|
||||
@@ -227,7 +267,7 @@ Here are the available options:
|
||||
example, because the terminal has access to the local clipboard and micro
|
||||
does not).
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `multiopen`: specifies how to layout multiple files opened at startup.
|
||||
Most useful as a command-line option, like `-multiopen vsplit`. Possible
|
||||
@@ -236,11 +276,11 @@ Here are the available options:
|
||||
* `vsplit`: open files side-by-side.
|
||||
* `hsplit`: open files stacked top to bottom.
|
||||
|
||||
default value: `tab`
|
||||
default value: `tab`
|
||||
|
||||
* `paste`: treat characters sent from the terminal in a single chunk as a paste
|
||||
event rather than a series of manual key presses. If you are pasting using
|
||||
the terminal keybinding (not Ctrl-v, which is micro's default paste
|
||||
the terminal keybinding (not `Ctrl-v`, which is micro's default paste
|
||||
keybinding) then it is a good idea to enable this option during the paste
|
||||
and disable once the paste is over. See `> help copypaste` for details about
|
||||
copying and pasting in a terminal environment.
|
||||
@@ -287,25 +327,28 @@ Here are the available options:
|
||||
default value: `prompt`
|
||||
|
||||
* `rmtrailingws`: micro will automatically trim trailing whitespaces at ends of
|
||||
lines. Note: This setting overrides `keepautoindent`
|
||||
lines.
|
||||
Note: This setting overrides `keepautoindent` and isn't used at timed `autosave`
|
||||
or forced `autosave` in case the buffer didn't change. A manual save will
|
||||
involve the action regardless if the buffer has been changed or not.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `ruler`: display line numbers.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `relativeruler`: make line numbers display relatively. If set to true, all
|
||||
lines except for the line that the cursor is located will display the distance
|
||||
from the cursor's line.
|
||||
from the cursor's line.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `savecursor`: remember where the cursor was last time the file was opened and
|
||||
put it there when you open the file again. Information is saved to
|
||||
`~/.config/micro/buffers/`
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `savehistory`: remember command history between closing and re-opening
|
||||
micro. Information is saved to `~/.config/micro/buffers/history`.
|
||||
@@ -316,7 +359,7 @@ Here are the available options:
|
||||
so if you close and reopen a file, you can keep undoing. Information is
|
||||
saved to `~/.config/micro/buffers/`.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `scrollbar`: display a scroll bar
|
||||
|
||||
@@ -329,31 +372,31 @@ Here are the available options:
|
||||
* `scrollmargin`: margin at which the view starts scrolling when the cursor
|
||||
approaches the edge of the view.
|
||||
|
||||
default value: `3`
|
||||
default value: `3`
|
||||
|
||||
* `scrollspeed`: amount of lines to scroll for one scroll event.
|
||||
|
||||
default value: `2`
|
||||
default value: `2`
|
||||
|
||||
* `smartpaste`: add leading whitespace when pasting multiple lines.
|
||||
This will attempt to preserve the current indentation level when pasting an
|
||||
unindented block.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `softwrap`: wrap lines that are too long to fit on the screen.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `splitbottom`: when a horizontal split is created, create it below the
|
||||
current split.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `splitright`: when a vertical split is created, create it to the right of the
|
||||
current split.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `statusformatl`: format string definition for the left-justified part of the
|
||||
statusline. Special directives should be placed inside `$()`. Special
|
||||
@@ -372,36 +415,36 @@ Here are the available options:
|
||||
|
||||
* `statusline`: display the status line at the bottom of the screen.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `sucmd`: specifies the super user command. On most systems this is "sudo" but
|
||||
on BSD it can be "doas." This option can be customized and is only used when
|
||||
saving with su.
|
||||
|
||||
default value: `sudo`
|
||||
default value: `sudo`
|
||||
|
||||
* `syntax`: enables syntax highlighting.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `tabmovement`: navigate spaces at the beginning of lines as if they are tabs
|
||||
(e.g. move over 4 spaces at once). This option only does anything if
|
||||
`tabstospaces` is on.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `tabhighlight`: inverts the tab characters' (filename, save indicator, etc)
|
||||
colors with respect to the tab bar.
|
||||
colors with respect to the tab bar.
|
||||
|
||||
default value: false
|
||||
default value: false
|
||||
|
||||
* `tabreverse`: reverses the tab bar colors when active.
|
||||
|
||||
default value: true
|
||||
default value: true
|
||||
|
||||
* `tabsize`: the size in spaces that a tab character should be displayed with.
|
||||
|
||||
default value: `4`
|
||||
default value: `4`
|
||||
|
||||
* `tabstospaces`: use spaces instead of tabs. Note: This option will be
|
||||
overridden by [the `ftoptions` plugin](https://github.com/zyedidia/micro/blob/master/runtime/plugins/ftoptions/ftoptions.lua)
|
||||
@@ -409,25 +452,25 @@ Here are the available options:
|
||||
your config. See [issue #2213](https://github.com/zyedidia/micro/issues/2213)
|
||||
for more details.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `useprimary` (only useful on unix): defines whether or not micro will use the
|
||||
primary clipboard to copy selections in the background. This does not affect
|
||||
the normal clipboard using Ctrl-c and Ctrl-v.
|
||||
the normal clipboard using `Ctrl-c` and `Ctrl-v`.
|
||||
|
||||
default value: `true`
|
||||
default value: `true`
|
||||
|
||||
* `wordwrap`: wrap long lines by words, i.e. break at spaces. This option
|
||||
only does anything if `softwrap` is on.
|
||||
|
||||
default value: `false`
|
||||
default value: `false`
|
||||
|
||||
* `xterm`: micro will assume that the terminal it is running in conforms to
|
||||
`xterm-256color` regardless of what the `$TERM` variable actually contains.
|
||||
Enabling this option may cause unwanted effects if your terminal in fact
|
||||
does not conform to the `xterm-256color` standard.
|
||||
|
||||
Default value: `false`
|
||||
default value: `false`
|
||||
|
||||
---
|
||||
|
||||
@@ -450,7 +493,7 @@ or disable them:
|
||||
recent Git commit rather than the diff since opening the file.
|
||||
|
||||
Any option you set in the editor will be saved to the file
|
||||
~/.config/micro/settings.json so, in effect, your configuration file will be
|
||||
~/.config/micro/settings.json so, in effect, your configuration file will be
|
||||
created for you. If you'd like to take your configuration with you to another
|
||||
machine, simply copy the settings.json to the other machine.
|
||||
|
||||
@@ -495,6 +538,8 @@ so that you can see what the formatting should look like.
|
||||
"linter": true,
|
||||
"literate": true,
|
||||
"matchbrace": true,
|
||||
"matchbraceleft": true,
|
||||
"matchbracestyle": "underline",
|
||||
"mkparents": false,
|
||||
"mouse": true,
|
||||
"parsecursor": false,
|
||||
@@ -551,14 +596,14 @@ all files except Go files, and `tabsize` 4 for all files except Ruby files:
|
||||
|
||||
```json
|
||||
{
|
||||
"ft:go": {
|
||||
"tabstospaces": false
|
||||
},
|
||||
"ft:ruby": {
|
||||
"tabsize": 2
|
||||
},
|
||||
"tabstospaces": true,
|
||||
"tabsize": 4
|
||||
"ft:go": {
|
||||
"tabstospaces": false
|
||||
},
|
||||
"ft:ruby": {
|
||||
"tabsize": 2
|
||||
},
|
||||
"tabstospaces": true,
|
||||
"tabsize": 4
|
||||
}
|
||||
```
|
||||
|
||||
@@ -566,13 +611,13 @@ Or similarly you can match with globs:
|
||||
|
||||
```json
|
||||
{
|
||||
"*.go": {
|
||||
"tabstospaces": false
|
||||
},
|
||||
"*.rb": {
|
||||
"tabsize": 2
|
||||
},
|
||||
"tabstospaces": true,
|
||||
"tabsize": 4
|
||||
"*.go": {
|
||||
"tabstospaces": false
|
||||
},
|
||||
"*.rb": {
|
||||
"tabsize": 2
|
||||
},
|
||||
"tabstospaces": true,
|
||||
"tabsize": 4
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# Plugins
|
||||
|
||||
This help topic is about creating plugins. If you need help installing or
|
||||
managing plugins, look for `plugin` commands in `help commands`. If you want to
|
||||
enable or disable a plugin, look for `Plugin options` in `help options`.
|
||||
|
||||
Micro supports creating plugins with a simple Lua system. Plugins are
|
||||
folders containing Lua files and possibly other source files placed
|
||||
in `~/.config/micro/plug`. The plugin directory (within `plug`) should
|
||||
contain at least one Lua file and a `repo.json` file. The `repo.json` file
|
||||
provides additional information such as the name of the plugin, the
|
||||
plugin's website, dependencies, etc... [Here is an example `repo.json` file](https://github.com/micro-editor/updated-plugins/blob/master/go-plugin/repo.json)
|
||||
plugin's website, dependencies, etc.
|
||||
[Here is an example `repo.json` file](https://github.com/micro-editor/updated-plugins/blob/master/go-plugin/repo.json)
|
||||
from the go plugin, which has the following file structure:
|
||||
|
||||
```
|
||||
@@ -18,9 +23,9 @@ from the go plugin, which has the following file structure:
|
||||
|
||||
The `go.lua` file contains the main code for the plugin, though the
|
||||
code may be distributed across multiple Lua files. The `repo.json`
|
||||
file contains information about the plugin such as the website,
|
||||
file contains information about the plugin, such as the website,
|
||||
description, version, and any requirements. Plugins may also
|
||||
have additional files which can be added to micro's runtime files,
|
||||
have additional files that can be added to micro's runtime files,
|
||||
of which there are 5 types:
|
||||
|
||||
* Colorschemes
|
||||
@@ -30,17 +35,16 @@ of which there are 5 types:
|
||||
* Syntax header files
|
||||
|
||||
In most cases, a plugin will want to add help files, but in certain
|
||||
cases a plugin may also want to add colorschemes or syntax files. It
|
||||
is unlikely for a plugin to need to add plugin files at runtime or
|
||||
syntax header files. No directory structure is enforced but keeping
|
||||
runtime files in their own directories is good practice.
|
||||
cases a plugin may also want to add colorschemes or syntax files.
|
||||
No directory structure is enforced, but keeping runtime files in their
|
||||
own directories is good practice.
|
||||
|
||||
## Lua callbacks
|
||||
|
||||
Plugins use Lua but also have access to many functions both from micro
|
||||
and from the Go standard library. Many callbacks are also defined which
|
||||
are called when certain events happen. Here is the list of callbacks
|
||||
which micro defines:
|
||||
Plugins use Lua but also have access to many functions, both from micro
|
||||
and from the Go standard library. Plugins can also define functions that micro
|
||||
will call when certain events happen. Here is the list of callbacks
|
||||
that micro defines:
|
||||
|
||||
* `init()`: this function should be used for your plugin initialization.
|
||||
This function is called after buffers have been initialized.
|
||||
@@ -48,7 +52,10 @@ which micro defines:
|
||||
* `preinit()`: initialization function called before buffers have been
|
||||
initialized.
|
||||
|
||||
* `postinit()`: initialization function called after `init()`.
|
||||
* `postinit()`: initialization function called after the `init()` function of
|
||||
all plugins has been called.
|
||||
|
||||
* `deinit()`: cleanup function called when your plugin is unloaded or reloaded.
|
||||
|
||||
* `onSetActive(bufpane)`: runs when changing the currently active panel.
|
||||
|
||||
@@ -58,6 +65,8 @@ which micro defines:
|
||||
* `onBufPaneOpen(bufpane)`: runs when a bufpane is opened. The input
|
||||
contains the bufpane object.
|
||||
|
||||
* `onSetActive(bufpane)`: runs when changing the currently active bufpane.
|
||||
|
||||
* `onAction(bufpane)`: runs when `Action` is triggered by the user, where
|
||||
`Action` is a bindable action (see `> help keybindings`). A bufpane
|
||||
is passed as input and the function should return a boolean defining
|
||||
@@ -67,11 +76,15 @@ which micro defines:
|
||||
by the user. Returns a boolean which defines whether the action should
|
||||
be canceled.
|
||||
|
||||
* `onRune(rune)`: runs when the composed rune has been inserted
|
||||
* `onRune(bufpane, rune)`: runs when the composed rune has been inserted
|
||||
|
||||
* `preRune(rune)`: runs before the composed rune will be inserted
|
||||
* `preRune(bufpane, rune)`: runs before the composed rune will be inserted
|
||||
|
||||
For example a function which is run every time the user saves the buffer
|
||||
* `onAnyEvent()`: runs when literally anything happens. It is useful for
|
||||
detecting various changes of micro's state that cannot be detected
|
||||
using other callbacks.
|
||||
|
||||
For example, a function that is run every time the user saves the buffer
|
||||
would be:
|
||||
|
||||
```lua
|
||||
@@ -82,7 +95,7 @@ end
|
||||
```
|
||||
|
||||
The `bp` variable is a reference to the bufpane the action is being executed
|
||||
within. This is almost always the current bufpane.
|
||||
within. This is almost always the current bufpane.
|
||||
|
||||
All available actions are listed in the keybindings section of the help.
|
||||
|
||||
@@ -91,8 +104,8 @@ should be relocated to the cursor or not after the action is complete.
|
||||
|
||||
## Accessing micro functions
|
||||
|
||||
Some of micro's internal information is exposed in the form of packages which
|
||||
can be imported by Lua plugins. A package can be imported in Lua and a value
|
||||
Some of micro's internal information is exposed in the form of packages, which
|
||||
can be imported by Lua plugins. A package can be imported in Lua, and a value
|
||||
within it can be accessed using the following syntax:
|
||||
|
||||
```lua
|
||||
@@ -100,7 +113,7 @@ local micro = import("micro")
|
||||
micro.Log("Hello")
|
||||
```
|
||||
|
||||
The packages and functions are listed below (in Go type signatures):
|
||||
The packages and their contents are listed below (in Go type signatures):
|
||||
|
||||
* `micro`
|
||||
- `TermMessage(msg interface{}...)`: temporarily close micro and print a
|
||||
@@ -109,7 +122,7 @@ The packages and functions are listed below (in Go type signatures):
|
||||
- `TermError(filename string, lineNum int, err string)`: temporarily close
|
||||
micro and print an error formatted as `filename, lineNum: err`.
|
||||
|
||||
- `InfoBar()`: return the infobar BufPane object.
|
||||
- `InfoBar() *InfoPane`: return the infobar BufPane object.
|
||||
|
||||
- `Log(msg interface{}...)`: write a message to `log.txt` (requires
|
||||
`-debug` flag, or binary built with `build-dbg`).
|
||||
@@ -121,128 +134,158 @@ The packages and functions are listed below (in Go type signatures):
|
||||
current pane is not a BufPane.
|
||||
|
||||
- `CurTab() *Tab`: returns the current tab.
|
||||
|
||||
- `Tabs() *TabList`: returns the global tab list.
|
||||
|
||||
- `After(t time.Duration, f func())`: run function `f` in the background
|
||||
after time `t` elapses. See https://pkg.go.dev/time#Duration for the
|
||||
usage of `time.Duration`.
|
||||
|
||||
Relevant links:
|
||||
[Time](https://pkg.go.dev/time#Duration)
|
||||
[BufPane](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/action#BufPane)
|
||||
[InfoPane](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/action#InfoPane)
|
||||
[Tab](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/action#Tab)
|
||||
[TabList](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/action#TabList)
|
||||
[interface{} / any](https://go.dev/tour/methods/14)
|
||||
|
||||
* `micro/config`
|
||||
- `MakeCommand(name string, action func(bp *BufPane, args[]string),
|
||||
- `MakeCommand(name string, action func(bp *BufPane, args[]string),
|
||||
completer buffer.Completer)`:
|
||||
create a command with the given name, and lua callback function when
|
||||
the command is run. A completer may also be given to specify how
|
||||
autocompletion should work with the custom command.
|
||||
autocompletion should work with the custom command. Any lua function
|
||||
that takes a Buffer argument and returns a pair of string arrays is a
|
||||
valid completer, as are the built in completers below:
|
||||
|
||||
- `FileComplete`: autocomplete using files in the current directory
|
||||
- `HelpComplete`: autocomplete using names of help documents
|
||||
- `OptionComplete`: autocomplete using names of options
|
||||
- `OptionValueComplete`: autocomplete using names of options, and valid
|
||||
- `FileComplete`: autocomplete using files in the current directory
|
||||
- `HelpComplete`: autocomplete using names of help documents
|
||||
- `OptionComplete`: autocomplete using names of options
|
||||
- `OptionValueComplete`: autocomplete using names of options, and valid
|
||||
values afterwards
|
||||
- `NoComplete`: no autocompletion suggestions
|
||||
- `NoComplete`: no autocompletion suggestions
|
||||
|
||||
- `TryBindKey(k, v string, overwrite bool) (bool, error)`: bind the key
|
||||
- `TryBindKey(k, v string, overwrite bool) (bool, error)`: bind the key
|
||||
`k` to the string `v` in the `bindings.json` file. If `overwrite` is
|
||||
true, this will overwrite any existing binding to key `k`. Returns true
|
||||
if the binding was made, and a possible error (for example writing to
|
||||
`bindings.json` can cause an error).
|
||||
|
||||
- `Reload()`: reload configuration files.
|
||||
- `Reload()`: reload configuration files.
|
||||
|
||||
- `AddRuntimeFileFromMemory(filetype RTFiletype, filename, data string)`:
|
||||
- `AddRuntimeFileFromMemory(filetype RTFiletype, filename, data string)`:
|
||||
add a runtime file to the `filetype` runtime filetype, with name
|
||||
`filename` and data `data`.
|
||||
|
||||
- `AddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype,
|
||||
- `AddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype,
|
||||
directory, pattern string)`:
|
||||
add runtime files for the given plugin with the given RTFiletype from
|
||||
a directory within the plugin root. Only adds files that match the
|
||||
pattern using Go's `filepath.Match`
|
||||
|
||||
- `AddRuntimeFile(plugin string, filetype RTFiletype, filepath string)`:
|
||||
- `AddRuntimeFile(plugin string, filetype RTFiletype, filepath string)`:
|
||||
add a given file inside the plugin root directory as a runtime file
|
||||
to the given RTFiletype category.
|
||||
|
||||
- `ListRuntimeFiles(fileType RTFiletype) []string`: returns a list of
|
||||
- `ListRuntimeFiles(fileType RTFiletype) []string`: returns a list of
|
||||
names of runtime files of the given type.
|
||||
|
||||
- `ReadRuntimeFile(fileType RTFiletype, name string) string`: returns the
|
||||
- `ReadRuntimeFile(fileType RTFiletype, name string) string`: returns the
|
||||
contents of a given runtime file.
|
||||
|
||||
- `NewRTFiletype() int`: creates a new RTFiletype, and returns its value.
|
||||
- `NewRTFiletype() int`: creates a new RTFiletype, and returns its value.
|
||||
|
||||
- `RTColorscheme`: runtime files for colorschemes.
|
||||
- `RTSyntax`: runtime files for syntax files.
|
||||
- `RTHelp`: runtime files for help documents.
|
||||
- `RTPlugin`: runtime files for plugin source code.
|
||||
- `RTColorscheme`: runtime files for colorschemes.
|
||||
- `RTSyntax`: runtime files for syntax files.
|
||||
- `RTHelp`: runtime files for help documents.
|
||||
- `RTPlugin`: runtime files for plugin source code.
|
||||
|
||||
- `RegisterCommonOption(pl string, name string, defaultvalue interface{})`:
|
||||
registers a new option with for the given plugin. The name of the
|
||||
- `RegisterCommonOption(pl string, name string, defaultvalue interface{})`:
|
||||
registers a new option for the given plugin. The name of the
|
||||
option will be `pl.name`, and will have the given default value. Since
|
||||
this registers a common option, the option will be modifiable on a
|
||||
per-buffer basis, while also having a global value (in the
|
||||
GlobalSettings map).
|
||||
|
||||
- `RegisterGlobalOption(pl string, name string, defaultvalue interface{})`:
|
||||
same as `RegisterCommonOption` but the option cannot be modified
|
||||
- `RegisterGlobalOption(pl string, name string, defaultvalue interface{})`:
|
||||
same as `RegisterCommonOption`, but the option cannot be modified
|
||||
locally to each buffer.
|
||||
|
||||
- `GetGlobalOption(name string) interface{}`: returns the value of a
|
||||
- `GetGlobalOption(name string) interface{}`: returns the value of a
|
||||
given plugin in the `GlobalSettings` map.
|
||||
|
||||
- `SetGlobalOption(option, value string) error`: sets an option to a
|
||||
given value. Same as using the `> set` command. This will parse the
|
||||
value to the actual value type.
|
||||
- `SetGlobalOption(option, value string) error`: sets an option to a
|
||||
given value. Same as using the `> set` command. This will try to convert
|
||||
the value into the proper type for the option. Can return an error if the
|
||||
option name is not valid, or the value can not be converted.
|
||||
|
||||
- `SetGlobalOptionNative(option string, value interface{}) error`: sets
|
||||
- `SetGlobalOptionNative(option string, value interface{}) error`: sets
|
||||
an option to a given value, where the type of value is the actual
|
||||
type of the value internally.
|
||||
type of the value internally. Can return an error if the provided value
|
||||
is not valid for the given option.
|
||||
|
||||
- `ConfigDir`: the path to micro's currently active config directory.
|
||||
|
||||
Relevant links:
|
||||
[Buffer](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/buffer#Buffer)
|
||||
[buffer.Completer](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/buffer#Completer)
|
||||
[Error](https://pkg.go.dev/builtin#error)
|
||||
[interface{} / any](https://go.dev/tour/methods/14)
|
||||
[filepath.Match](https://pkg.go.dev/path/filepath#Match)
|
||||
|
||||
* `micro/shell`
|
||||
- `ExecCommand(name string, arg ...string) (string, error)`: runs an
|
||||
- `ExecCommand(name string, arg ...string) (string, error)`: runs an
|
||||
executable with the given arguments, and pipes the output (stderr
|
||||
and stdout) of the executable to an internal buffer, which is
|
||||
returned as a string, along with a possible error.
|
||||
|
||||
- `RunCommand(input string) (string, error)`: same as `ExecCommand`,
|
||||
- `RunCommand(input string) (string, error)`: same as `ExecCommand`,
|
||||
except this uses micro's argument parser to parse the arguments from
|
||||
the input. For example `cat 'hello world.txt' file.txt`, will pass
|
||||
the input. For example, `cat 'hello world.txt' file.txt`, will pass
|
||||
two arguments in the `ExecCommand` argument list (quoting arguments
|
||||
will preserve spaces).
|
||||
|
||||
- `RunBackgroundShell(input string) (func() string, error)`: returns a
|
||||
- `RunBackgroundShell(input string) (func() string, error)`: returns a
|
||||
function that will run the given shell command and return its output.
|
||||
|
||||
- `RunInteractiveShell(input string, wait bool, getOutput bool)
|
||||
- `RunInteractiveShell(input string, wait bool, getOutput bool)
|
||||
(string, error)`:
|
||||
temporarily closes micro and runs the given command in the terminal.
|
||||
If `wait` is true, micro will wait for the user to press enter before
|
||||
returning to text editing. If `getOutput` is true, micro redirect
|
||||
returning to text editing. If `getOutput` is true, micro will redirect
|
||||
stdout from the command to the returned string.
|
||||
|
||||
- `JobStart(cmd string, onStdout, onStderr,
|
||||
- `JobStart(cmd string, onStdout, onStderr,
|
||||
onExit func(string, []interface{}), userargs ...interface{})
|
||||
*exec.Cmd`:
|
||||
Starts a background job by running the shell on the given command
|
||||
(using `sh -c`). Three callbacks can be provided which will be called
|
||||
when the command generates stdout, stderr, or exits. The userargs will
|
||||
be passed to the callbacks, along with the output as the first
|
||||
argument of the callback.
|
||||
argument of the callback. Returns the started command.
|
||||
|
||||
- `JobSpawn(cmd string, cmdArgs []string, onStdout, onStderr,
|
||||
- `JobSpawn(cmd string, cmdArgs []string, onStdout, onStderr,
|
||||
onExit func(string, []interface{}), userargs ...interface{})
|
||||
*exec.Cmd`:
|
||||
same as `JobStart`, except doesn't run the command through the shell
|
||||
and instead takes as inputs the list of arguments.
|
||||
and instead takes as inputs the list of arguments. Returns the started
|
||||
command.
|
||||
|
||||
- `JobStop(cmd *exec.Cmd)`: kills a job.
|
||||
- `JobSend(cmd *exec.Cmd, data string)`: sends some data to a job's stdin.
|
||||
- `JobStop(cmd *exec.Cmd)`: kills a job.
|
||||
- `JobSend(cmd *exec.Cmd, data string)`: sends some data to a job's stdin.
|
||||
|
||||
- `RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool,
|
||||
- `RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool,
|
||||
callback func(out string, userargs []interface{}),
|
||||
userargs []interface{}) error`:
|
||||
starts a terminal emulator from a given BufPane with the input command.
|
||||
If `wait` is true it will wait for the user to exit by pressing enter
|
||||
once the executable has terminated and 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
|
||||
If `wait` is true, it will wait for the user to exit by pressing enter
|
||||
once the executable has terminated, and 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. This function returns an error on systems
|
||||
where the terminal emulator is not supported.
|
||||
|
||||
- `TermEmuSupported`: true on systems where the terminal emulator is
|
||||
- `TermEmuSupported`: true on systems where the terminal emulator is
|
||||
supported and false otherwise. Supported systems:
|
||||
* Linux
|
||||
* MacOS
|
||||
@@ -250,15 +293,20 @@ The packages and functions are listed below (in Go type signatures):
|
||||
* OpenBSD
|
||||
* FreeBSD
|
||||
|
||||
Relevant links:
|
||||
[Cmd](https://pkg.go.dev/os/exec#Cmd)
|
||||
[BufPane](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/action#BufPane)
|
||||
[Error](https://pkg.go.dev/builtin#error)
|
||||
|
||||
* `micro/buffer`
|
||||
- `NewMessage(owner string, msg string, start, end, Loc, kind MsgType)
|
||||
*Message`:
|
||||
creates a new message with an owner over a range given by the start
|
||||
creates a new message with an owner over a range defined by the start
|
||||
and end locations.
|
||||
|
||||
- `NewMessageAtLine(owner string, msg string, line int, kindMsgType)
|
||||
*Message`:
|
||||
creates a new message with owner, type and message at a given line.
|
||||
creates a new message with owner, type, and text at a given line.
|
||||
|
||||
- `MTInfo`: info message.
|
||||
- `MTWarning`: warning message.
|
||||
@@ -268,7 +316,9 @@ The packages and functions are listed below (in Go type signatures):
|
||||
- `SLoc(line, row int) display.SLoc`: creates a new scrolling location struct.
|
||||
|
||||
- `BTDefault`: default buffer type.
|
||||
- `BTHelp`: help buffer type.
|
||||
- `BTLog`: log buffer type.
|
||||
- `BTScratch`: scratch buffer type (cannot be saved).
|
||||
- `BTRaw`: raw buffer type.
|
||||
- `BTInfo`: info buffer type.
|
||||
|
||||
@@ -276,13 +326,22 @@ The packages and functions are listed below (in Go type signatures):
|
||||
given text at a certain path.
|
||||
|
||||
- `NewBufferFromFile(path string) (*Buffer, error)`: creates a new
|
||||
buffer by reading from disk at the given path.
|
||||
buffer by reading the file at the given path from disk. Returns an error
|
||||
if the read operation fails (for example, due to the file not existing).
|
||||
|
||||
- `ByteOffset(pos Loc, buf *Buffer) int`: returns the byte index of the
|
||||
given position in a buffer.
|
||||
|
||||
- `Log(s string)`: writes a string to the log buffer.
|
||||
- `LogBuf() *Buffer`: returns the log buffer.
|
||||
|
||||
Relevant links:
|
||||
[Message](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/buffer#Message)
|
||||
[Loc](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/buffer#Loc)
|
||||
[display.SLoc](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/display#SLoc)
|
||||
[Buffer](https://pkg.go.dev/github.com/zyedidia/micro/v2/internal/buffer#Buffer)
|
||||
[Error](https://pkg.go.dev/builtin#error)
|
||||
|
||||
* `micro/util`
|
||||
- `RuneAt(str string, idx int) string`: returns the utf8 rune at a
|
||||
given index within a string.
|
||||
@@ -293,17 +352,26 @@ The packages and functions are listed below (in Go type signatures):
|
||||
- `String(b []byte) string`: converts a byte array to a string.
|
||||
- `RuneStr(r rune) string`: converts a rune to a string.
|
||||
- `Unzip(src, dest string) error`: unzips a file to given folder.
|
||||
- `HttpRequest(method string, url string, headers []string) (http.Response, error)`: makes a http request.
|
||||
- `Version`: micro's version number or commit hash
|
||||
- `SemVersion`: micro's semantic version
|
||||
- `HttpRequest(method string, url string, headers []string)
|
||||
(http.Response, error)`: makes a http request.
|
||||
- `CharacterCountInString(str string) int`: returns the number of
|
||||
characters in a string
|
||||
- `RuneStr(r rune) string`: converts a rune to a string.
|
||||
|
||||
This may seem like a small list of available functions but some of the objects
|
||||
Relevant links:
|
||||
[Rune](https://pkg.go.dev/builtin#rune)
|
||||
|
||||
This may seem like a small list of available functions, but some of the objects
|
||||
returned by the functions have many methods. The Lua plugin may access any
|
||||
public methods of an object returned by any of the functions above.
|
||||
Unfortunately it is not possible to list all the available functions on this
|
||||
Unfortunately, it is not possible to list all the available functions on this
|
||||
page. Please go to the internal documentation at
|
||||
https://pkg.go.dev/github.com/zyedidia/micro/v2/internal to see the full list
|
||||
of available methods. Note that only methods of types that are available to
|
||||
plugins via the functions above can be called from a plugin. For an even more
|
||||
detailed reference see the source code on Github.
|
||||
detailed reference, see the source code on Github.
|
||||
|
||||
For example, with a BufPane object called `bp`, you could call the `Save`
|
||||
function in Lua with `bp:Save()`.
|
||||
@@ -326,7 +394,7 @@ micro.InfoBar():Message()
|
||||
It is possible for your lua code to access many of the functions in the Go
|
||||
standard library.
|
||||
|
||||
Simply import the package you'd like and then you can use it. For example:
|
||||
Simply import the package you'd like, and then you can use it. For example:
|
||||
|
||||
```lua
|
||||
local ioutil = import("io/ioutil")
|
||||
@@ -349,38 +417,37 @@ end
|
||||
|
||||
Here are the packages from the Go standard library that you can access.
|
||||
Nearly all functions from these packages are supported. For an exact
|
||||
list of which functions are supported you can look through `lua.go`
|
||||
list of functions that are supported, you can look through `lua.go`
|
||||
(which should be easy to understand).
|
||||
|
||||
```
|
||||
fmt
|
||||
io
|
||||
io/ioutil
|
||||
net
|
||||
math
|
||||
math/rand
|
||||
os
|
||||
runtime
|
||||
path
|
||||
filepath
|
||||
strings
|
||||
regexp
|
||||
errors
|
||||
time
|
||||
archive/zip
|
||||
net/http
|
||||
```
|
||||
* [fmt](https://pkg.go.dev/fmt)
|
||||
* [io](https://pkg.go.dev/io)
|
||||
* [io/ioutil](https://pkg.go.dev/io/ioutil)
|
||||
* [net](https://pkg.go.dev/net)
|
||||
* [math](https://pkg.go.dev/math)
|
||||
* [math/rand](https://pkg.go.dev/math/rand)
|
||||
* [os](https://pkg.go.dev/os)
|
||||
* [runtime](https://pkg.go.dev/runtime)
|
||||
* [path](https://pkg.go.dev/path)
|
||||
* [filepath](https://pkg.go.dev/filepath)
|
||||
* [strings](https://pkg.go.dev/strings)
|
||||
* [regexp](https://pkg.go.dev/regexp)
|
||||
* [errors](https://pkg.go.dev/errors)
|
||||
* [time](https://pkg.go.dev/time)
|
||||
* [unicode/utf8](https://pkg.go.dev/unicode/utf8)
|
||||
* [archive/zip](https://pkg.go.dev/archive/zip)
|
||||
* [net/http](https://pkg.go.dev/net/http)
|
||||
|
||||
For documentation for each of these functions, see the Go standard
|
||||
library documentation at https://golang.org/pkg/ (for the packages
|
||||
exposed to micro plugins). The Lua standard library is also available
|
||||
to plugins though it is rather small.
|
||||
The following functions from the go-humanize package are also available:
|
||||
|
||||
The following functions are also available from the go-humanize package:
|
||||
* `humanize`:
|
||||
- `Bytes(s uint64) string`: produces a human readable representation of
|
||||
an SI size.
|
||||
- `Ordinal(x int) string`: gives you the input number in a rank/ordinal
|
||||
format.
|
||||
|
||||
The `humanize` package exposes:
|
||||
* `Bytes`
|
||||
* `Ordinal`
|
||||
[The Lua standard library](https://www.lua.org/manual/5.1/manual.html#5) is also
|
||||
available to plugins, though it is rather small.
|
||||
|
||||
## Adding help files, syntax files, or colorschemes in your plugin
|
||||
|
||||
@@ -388,7 +455,7 @@ You can use the `AddRuntimeFile(name string, type config.RTFiletype,
|
||||
path string)`
|
||||
function to add various kinds of files to your plugin. For example, if you'd
|
||||
like to add a help topic to your plugin called `test`, you would create a
|
||||
`test.md` file, and call the function:
|
||||
`test.md` file and call the function:
|
||||
|
||||
```lua
|
||||
config = import("micro/config")
|
||||
@@ -396,7 +463,7 @@ config.AddRuntimeFile("test", config.RTHelp, "test.md")
|
||||
```
|
||||
|
||||
Use `AddRuntimeFilesFromDirectory(name, type, dir, pattern)` to add a number of
|
||||
files to the runtime. To read the content of a runtime file use
|
||||
files to the runtime. To read the content of a runtime file, use
|
||||
`ReadRuntimeFile(fileType, name string)` or `ListRuntimeFiles(fileType string)`
|
||||
for all runtime files. In addition, there is `AddRuntimeFileFromMemory` which
|
||||
adds a runtime file based on a string that may have been constructed at
|
||||
@@ -404,11 +471,12 @@ runtime.
|
||||
|
||||
## Default plugins
|
||||
|
||||
There are 6 default plugins that come pre-installed with micro. These are
|
||||
The following plugins come pre-installed with micro:
|
||||
|
||||
* `autoclose`: automatically closes brackets, quotes, etc...
|
||||
* `comment`: provides automatic commenting for a number of languages
|
||||
* `ftoptions`: alters some default options (notably indentation) depending on the filetype
|
||||
* `ftoptions`: alters some default options (notably indentation) depending on
|
||||
the filetype
|
||||
* `linter`: provides extensible linting for many languages
|
||||
* `literate`: provides advanced syntax highlighting for the Literate
|
||||
programming tool.
|
||||
@@ -426,7 +494,7 @@ your own plugins.
|
||||
|
||||
## Plugin Manager
|
||||
|
||||
Micro also has a built in plugin manager which you can invoke with the
|
||||
Micro also has a built in plugin manager, which you can invoke with the
|
||||
`> plugin ...` command, or in the shell with `micro -plugin ...`.
|
||||
|
||||
For the valid commands you can use, see the `commands` help topic.
|
||||
@@ -439,7 +507,7 @@ directly link third-party plugins to allow installation through the plugin
|
||||
manager with the `pluginrepos` option.
|
||||
|
||||
If you'd like to publish a plugin you've made as an official plugin, you should
|
||||
upload your plugin online (to Github preferably) and add a `repo.json` file.
|
||||
upload your plugin online (preferably to Github) and add a `repo.json` file.
|
||||
This file will contain the metadata for your plugin. Here is an example:
|
||||
|
||||
```json
|
||||
@@ -460,9 +528,9 @@ This file will contain the metadata for your plugin. Here is an example:
|
||||
}]
|
||||
```
|
||||
|
||||
Then open a pull request at github.com/micro-editor/plugin-channel adding a
|
||||
Then open a pull request at github.com/micro-editor/plugin-channel, adding a
|
||||
link to the raw `repo.json` that is in your plugin repository.
|
||||
|
||||
To make updating the plugin work, the first line of your plugins lua code
|
||||
To make updating the plugin work, the first line of your plugin's lua code
|
||||
should contain the version of the plugin. (Like this: `VERSION = "1.0.0"`)
|
||||
Please make sure to use [semver](http://semver.org/) for versioning.
|
||||
|
||||
@@ -50,11 +50,11 @@ function preInsertNewline(bp)
|
||||
for i = 1, #autoNewlinePairs do
|
||||
if curRune == charAt(autoNewlinePairs[i], 1) then
|
||||
if nextRune == charAt(autoNewlinePairs[i], 2) then
|
||||
bp:InsertNewline()
|
||||
bp:InsertTab()
|
||||
bp.Buf:Insert(-bp.Cursor.Loc, "\n" .. ws)
|
||||
bp:StartOfLine()
|
||||
bp:CursorLeft()
|
||||
bp:InsertNewline()
|
||||
bp:InsertTab()
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,6 +28,7 @@ ft["ini"] = "; %s"
|
||||
ft["java"] = "// %s"
|
||||
ft["javascript"] = "// %s"
|
||||
ft["jinja2"] = "{# %s #}"
|
||||
ft["json"] = "// %s"
|
||||
ft["julia"] = "# %s"
|
||||
ft["kotlin"] = "// %s"
|
||||
ft["lua"] = "-- %s"
|
||||
@@ -65,9 +66,9 @@ local last_ft
|
||||
function updateCommentType(buf)
|
||||
if buf.Settings["commenttype"] == nil or (last_ft ~= buf.Settings["filetype"] and last_ft ~= nil) then
|
||||
if ft[buf.Settings["filetype"]] ~= nil then
|
||||
buf.Settings["commenttype"] = ft[buf.Settings["filetype"]]
|
||||
buf:SetOptionNative("commenttype", ft[buf.Settings["filetype"]])
|
||||
else
|
||||
buf.Settings["commenttype"] = "# %s"
|
||||
buf:SetOptionNative("commenttype", "# %s")
|
||||
end
|
||||
|
||||
last_ft = buf.Settings["filetype"]
|
||||
|
||||
@@ -8,7 +8,7 @@ file:
|
||||
|
||||
```json
|
||||
{
|
||||
"Alt-g": "comment.comment"
|
||||
"Alt-g": "lua:comment.comment"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -47,6 +47,7 @@ but it is only available for certain filetypes:
|
||||
* java: `// %s`
|
||||
* javascript: `// %s`
|
||||
* jinja2: `{# %s #}`
|
||||
* json: `// %s`
|
||||
* julia: `# %s`
|
||||
* kotlin: `// %s`
|
||||
* lua: `-- %s`
|
||||
|
||||
@@ -5,7 +5,6 @@ filetype: powershell
|
||||
|
||||
detect:
|
||||
filename: "\\.ps(1|m1|d1)$"
|
||||
#header: ""
|
||||
|
||||
rules:
|
||||
# - comment.block: # Block Comment
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Here are micro's syntax files.
|
||||
|
||||
Each yaml file specifies how to detect the filetype based on file extension or headers (first line of the file).
|
||||
Each yaml file specifies how to detect the filetype based on file extension or header (first line of the line).
|
||||
In addition, a signature can be provided to help resolving ambiguities when multiple matching filetypes are detected.
|
||||
Then there are patterns and regions linked to highlight groups which tell micro how to highlight that filetype.
|
||||
|
||||
Making your own syntax files is very simple. I recommend you check the file after you are finished with the
|
||||
@@ -21,7 +22,7 @@ syntax files that you would like to convert to the new filetype, you can use the
|
||||
$ go run syntax_converter.go c.micro > c.yaml
|
||||
```
|
||||
|
||||
Most the the syntax files here have been converted using that tool.
|
||||
Most of the syntax files here have been converted using that tool.
|
||||
|
||||
Note that the tool isn't perfect and though it is unlikely, you may run into some small issues that you will have to fix manually
|
||||
(about 4 files from this directory had issues after being converted).
|
||||
|
||||
@@ -3,7 +3,7 @@ filetype: ada
|
||||
detect:
|
||||
filename: "(\\.ads$|\\.adb$|\\.ada$)"
|
||||
|
||||
rules:
|
||||
rules:
|
||||
# Operators
|
||||
- symbol.operator: ([.:;,+*|=!?\\%]|<|>|/|-|&)
|
||||
- symbol.brackets: "[(){}]|\\[|\\]"
|
||||
@@ -18,11 +18,11 @@ rules:
|
||||
# Constant
|
||||
- constant.bool: \b(TRUE|FALSE)
|
||||
- constant.number: ([0-9]+)
|
||||
|
||||
|
||||
# Storage Types
|
||||
- type.storage: \b(INTEGER|NATURAL|POSITIVE|FLOAT|CHARACTER|STRING)\b
|
||||
- type.storage: \b(LONG_INTEGER|SHORT_INTEGER|LONG_FLOAT|SHORT_FLOAT)\b
|
||||
|
||||
|
||||
#Character
|
||||
- constant.string.char: \'.\'
|
||||
|
||||
@@ -36,9 +36,8 @@ rules:
|
||||
- constant.interpolation: \\\([[:graph:]]*\)
|
||||
- constant.unicode: \\u\{[[:xdigit:]]+}
|
||||
|
||||
|
||||
# Line Comment
|
||||
- comment.line: "--.*"
|
||||
|
||||
|
||||
# Todo
|
||||
- todo: "(TODO|XXX|FIXME):?"
|
||||
|
||||
@@ -4,9 +4,9 @@ detect:
|
||||
filename: "\\.?ino$"
|
||||
|
||||
rules:
|
||||
- identifier: "\\b[A-Z_][0-9A-Z_]+\\b"
|
||||
- identifier: "\\b[A-Z_][0-9A-Z_]+\\b"
|
||||
|
||||
##
|
||||
## Sized (u)int types
|
||||
- type: "\\b((s?size)|((u_?)?int(8|16|32|64|ptr)))_t\\b"
|
||||
|
||||
## Constants
|
||||
@@ -33,7 +33,7 @@ rules:
|
||||
- type: "\\b(boolean|byte|char|float|int|long|word)\\b"
|
||||
|
||||
## Control Structions
|
||||
- statement: "\\b(case|class|default|do|double|else|false|for|if|new|null|private|protected|public|short|signed|static|String|switch|this|throw|try|true|unsigned|void|while)\\b"
|
||||
- statement: "\\b(case|class|default|do|double|else|false|for|if|new|null|private|protected|public|short|signed|static|String|switch|this|throw|try|true|unsigned|void|while)\\b"
|
||||
- statement: "\\b(goto|continue|break|return)\\b"
|
||||
|
||||
## Math
|
||||
@@ -66,7 +66,7 @@ rules:
|
||||
## Structure
|
||||
- identifier: "\\b(setup|loop)\\b"
|
||||
|
||||
##
|
||||
##
|
||||
- statement: "^[[:space:]]*#[[:space:]]*(define|include(_next)?|(un|ifn?)def|endif|el(if|se)|if|warning|error|pragma)"
|
||||
|
||||
## GCC builtins
|
||||
|
||||
@@ -28,7 +28,7 @@ rules:
|
||||
# Paragraph Title
|
||||
- statement: "^\\..*$"
|
||||
|
||||
# source
|
||||
# source
|
||||
- identifier: "^\\[(source,.+|NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\]"
|
||||
|
||||
# Other markup
|
||||
|
||||
@@ -2,7 +2,6 @@ filetype: batch
|
||||
|
||||
detect:
|
||||
filename: "(\\.bat$|\\.cmd$)"
|
||||
# header: ""
|
||||
|
||||
rules:
|
||||
# Numbers
|
||||
|
||||
@@ -5,18 +5,22 @@ detect:
|
||||
|
||||
rules:
|
||||
- identifier: "\\b[A-Z_][0-9A-Z_]+\\b"
|
||||
- type: "\\b(float|double|bool|char|int|short|long|enum|void|struct|union|typedef|(un)?signed|inline)\\b"
|
||||
- type: "\\b((s?size)|((u_?)?int(8|16|32|64|ptr)))_t\\b"
|
||||
- type: "\\b(_Atomic|_BitInt|float|double|_Decimal32|_Decimal64|_Decimal128|_Complex|complex|_Imaginary|imaginary|_Bool|bool|char|int|short|long|enum|void|struct|union|typedef|typeof|typeof_unqual|(un)?signed|inline|_Noreturn)\\b"
|
||||
- type: "\\b((s?size)|((u_?)?int(8|16|32|64|ptr))|char(8|16|32)|wchar)_t\\b"
|
||||
# GCC float/decimal/fixed types
|
||||
- type: "\\b(_Float16|__fp16|_Float32|_Float32x|_Float64|_Float64x|__float80|_Float128|_Float128x|__float128|__ibm128|__int128|_Fract|_Sat|_Accum)\\b"
|
||||
- type: "\\b[a-z_][0-9a-z_]+(_t|_T)\\b"
|
||||
- statement: "\\b(auto|volatile|register|restrict|static|const|extern)\\b"
|
||||
- statement: "\\b(for|if|while|do|else|case|default|switch)\\b"
|
||||
- statement: "\\b(auto|volatile|register|restrict|_Alignas|alignas|_Alignof|alignof|static|const|constexpr|extern|_Thread_local|thread_local)\\b"
|
||||
- statement: "\\b(for|if|while|do|else|case|default|switch|_Generic|_Static_assert|static_assert)\\b"
|
||||
- statement: "\\b(goto|continue|break|return)\\b"
|
||||
- preproc: "^[[:space:]]*#[[:space:]]*(define|pragma|include|(un|ifn?)def|endif|el(if|se)|if|warning|error)"
|
||||
- statement: "\\b(asm|fortran)\\b"
|
||||
- preproc: "^[[:space:]]*#[[:space:]]*(define|embed|pragma|include|(un|ifn?)def|endif|el(if|ifdef|ifndef|se)|if|line|warning|error|__has_include|__has_embed|__has_c_attribute)"
|
||||
- preproc: "^[[:space:]]*_Pragma\\b"
|
||||
# GCC builtins
|
||||
- statement: "__attribute__[[:space:]]*\\(\\([^)]*\\)\\)"
|
||||
- statement: "__(aligned|asm|builtin|hidden|inline|packed|restrict|section|typeof|weak)__"
|
||||
- statement: "__(aligned|asm|builtin|extension|hidden|inline|packed|restrict|section|typeof|weak)__"
|
||||
# Operator Color
|
||||
- symbol.operator: "[-+*/%=<>.:;,~&|^!?]|\\b(sizeof)\\b"
|
||||
- symbol.operator: "[-+*/%=<>.:;,~&|^!?]|\\b(offsetof|sizeof)\\b"
|
||||
- symbol.brackets: "[(){}]|\\[|\\]"
|
||||
# Integer Constants
|
||||
- constant.number: "(\\b([1-9][0-9]*|0[0-7]*|0[Xx][0-9A-Fa-f]+|0[Bb][01]+)([Uu][Ll]?[Ll]?|[Ll][Ll]?[Uu]?)?\\b)"
|
||||
|
||||
7
runtime/syntax/cake.yaml
Normal file
7
runtime/syntax/cake.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
filetype: cake
|
||||
detect:
|
||||
filename: "\\.cake$"
|
||||
|
||||
rules:
|
||||
- include: "csharp"
|
||||
- preproc: "^[[:space:]]*#(addin|break|l(oad)?|module|r(eference)?|tool)"
|
||||
@@ -36,7 +36,7 @@ rules:
|
||||
skip: "\\\\."
|
||||
rules:
|
||||
- constant.specialChar: "\\\\."
|
||||
|
||||
|
||||
- constant.string:
|
||||
start: "'"
|
||||
end: "'"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
filetype: c++
|
||||
|
||||
detect:
|
||||
filename: "(\\.c(c|pp|xx)$|\\.h(h|pp|xx)$|\\.ii?$|\\.(def)$)"
|
||||
filename: "(\\.c(c|pp|xx)$|\\.h(h|pp|xx)?$|\\.ii?$|\\.(def)$)"
|
||||
signature: "namespace|template|public|protected|private"
|
||||
|
||||
rules:
|
||||
- identifier: "\\b[A-Z_][0-9A-Z_]*\\b"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
filetype: crontab
|
||||
|
||||
detect:
|
||||
filename: "crontab$"
|
||||
filename: "crontab$|/tmp/crontab\\.\\w+$"
|
||||
header: "^#.*?/etc/crontab"
|
||||
|
||||
rules:
|
||||
@@ -16,7 +16,7 @@ rules:
|
||||
|
||||
- statement: "^([\\*0-9,\\-\\/]+)\\s+([\\*0-9,\\-\\/]+)\\s+([\\*0-9,\\-\\/]+)\\s+(([\\*0-9,\\-\\/]+)|(\\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\\b))\\s+(([\\*0-9,\\-\\/]+)|(\\b(sun|mon|tue|wed|thu|fri|sat)\\b))\\s+(.*)$\\n?"
|
||||
- constant: "^([\\*0-9,\\-\\/]+)\\s+([\\*0-9,\\-\\/]+)\\s+([\\*0-9,\\-\\/]+)\\s+(([\\*0-9,\\-\\/]+)|(\\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\\b))\\s+(([\\*0-9,\\-\\/]+)|(\\b(sun|mon|tue|wed|thu|fri|sat)\\b))"
|
||||
|
||||
|
||||
# Shell Values
|
||||
- type: "^[A-Z]+\\="
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ rules:
|
||||
# Annotation
|
||||
- identifier.var: "@[A-Za-z]+"
|
||||
|
||||
- preproc: "^[[:space:]]*#[[:space:]]*(define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)"
|
||||
- identifier: "([A-Za-z0-9_]*[[:space:]]*[()])"
|
||||
- type: "\\b(bool|byte|sbyte|char|decimal|double|float|IntPtr|int|uint|long|ulong|object|short|ushort|string|base|this|var|void)\\b"
|
||||
- statement: "\\b(alias|as|case|catch|checked|default|do|dynamic|else|finally|fixed|for|foreach|goto|if|is|lock|new|null|return|switch|throw|try|unchecked|while)\\b"
|
||||
- statement: "\\b(abstract|async|class|const|delegate|enum|event|explicit|extern|get|implicit|in|internal|interface|namespace|operator|out|override|params|partial|private|protected|public|readonly|ref|sealed|set|sizeof|stackalloc|static|struct|typeof|unsafe|using|value|virtual|volatile|yield)\\b"
|
||||
- type: "\\b(bool|byte|sbyte|char|decimal|double|float|IntPtr|int|uint|long|ulong|managed|unmanaged|nint|nuint|object|short|ushort|string|base|this|var|void)\\b"
|
||||
- statement: "\\b(alias|as|case|catch|checked|default|do|dynamic|else|finally|fixed|for|foreach|goto|if|is|lock|new|null|return|switch|throw|try|unchecked|when|while|with)\\b"
|
||||
- statement: "\\b(abstract|add|and|args|async|await|class|const|delegate|enum|event|explicit|extern|file|get|global|implicit|in|init|internal|interface|nameof|namespace|not|notnull|operator|or|out|override|params|partial|private|protected|public|readonly|record|ref|remove|required|scoped|sealed|set|sizeof|stackalloc|static|struct|typeof|unsafe|using|value|virtual|volatile|yield)\\b"
|
||||
# LINQ-only keywords (ones that cannot be used outside of a LINQ query - lots others can)
|
||||
- statement: "\\b(from|where|select|group|info|orderby|join|let|in|on|equals|by|ascending|descending)\\b"
|
||||
- special: "\\b(break|continue)\\b"
|
||||
|
||||
@@ -41,4 +41,4 @@ rules:
|
||||
end: "\\*\\/"
|
||||
rules:
|
||||
- todo: "(TODO|XXX|FIXME):?"
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ filetype: csharp-script
|
||||
detect:
|
||||
filename: "\\.csx$"
|
||||
header: "^#!.*/(env +)?dotnet-script( |$)"
|
||||
|
||||
|
||||
rules:
|
||||
- include: "csharp"
|
||||
- preproc: "\\B(\\#!|\\#[r|load|]+\\b)"
|
||||
|
||||
@@ -51,11 +51,11 @@ rules:
|
||||
- constant: "\\b(__DATE__|__EOF__|__TIME__|__TIMESTAMP__|__VENDOR__|__VERSION__)\\b"
|
||||
# String literals
|
||||
# DoubleQuotedString
|
||||
- constant.string:
|
||||
- constant.string:
|
||||
start: "\""
|
||||
end: "\""
|
||||
skip: "\\\\."
|
||||
rules:
|
||||
rules:
|
||||
- constant.specialChar: "\\\\."
|
||||
# WysiwygString
|
||||
- constant.string:
|
||||
@@ -93,7 +93,7 @@ rules:
|
||||
- constant.string:
|
||||
start: "q\"<"
|
||||
end: "q\">"
|
||||
rules:
|
||||
rules:
|
||||
- constant.specialChar: "\\\\."
|
||||
- constant.string:
|
||||
start: "q\"[^({[<\"][^\"]*$"
|
||||
@@ -106,7 +106,7 @@ rules:
|
||||
rules:
|
||||
- constant.specialChar: "\\\\."
|
||||
# Comments
|
||||
- comment:
|
||||
- comment:
|
||||
start: "//"
|
||||
end: "$"
|
||||
rules: []
|
||||
|
||||
10
runtime/syntax/default.yaml
Normal file
10
runtime/syntax/default.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
filetype: unknown
|
||||
|
||||
detect:
|
||||
filename: ""
|
||||
|
||||
rules:
|
||||
# Mails
|
||||
- special: "[[:alnum:].%_+-]+@[[:alnum:].-]+"
|
||||
# URLs
|
||||
- identifier: "(https?|ftp|ssh)://\\S*[^])>\\s,.]"
|
||||
@@ -14,7 +14,7 @@ rules:
|
||||
skip: "\\\\."
|
||||
rules:
|
||||
- constant.specialChar: "\\\\."
|
||||
- constant.string:
|
||||
- constant.string:
|
||||
start: "\""
|
||||
end: "\""
|
||||
skip: "\\\\."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user