Compare commits

..

1 Commits
v2.0.4 ... dep

Author SHA1 Message Date
Zachary Yedidia
bd8776cdd0 Use dep for dependency management 2017-06-10 11:49:41 -04:00
271 changed files with 17220 additions and 30161 deletions

View File

@@ -1,7 +0,0 @@
# See http://editorconfig.org
# In Go files we indent with tabs but still
# set indent_size to control the GitHub web viewer.
[*.go]
indent_size=4

View File

@@ -2,7 +2,7 @@
## Specifications
<!-- You can use `micro -version` to get the commit hash. -->
You can use `micro -version` to get the commit hash.
Commit hash:
OS:

14
.gitignore vendored
View File

@@ -1,5 +1,3 @@
.DS_Store
micro
!cmd/micro
binaries/
@@ -7,14 +5,4 @@ tmp.sh
test/
.idea/
packages/
todo.txt
test.txt
log.txt
*.old
benchmark_results*
tools/build-version
tools/build-date
tools/info-plist
tools/bindata
tools/vscode-tests/
*.hdr
cmd/micro/vendor

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "tools/go-bindata"]
path = tools/go-bindata
url = https://github.com/zyedidia/go-bindata

View File

@@ -1,9 +1,2 @@
language: go
go:
- "1.11.x"
os:
- linux
- osx
script:
- env GO111MODULE=on make build
- env GO111MODULE=on make test
script: make test

View File

@@ -1,6 +1,6 @@
MIT License
Micro is licensed under the MIT "Expat" License:
Copyright (c) 2016-2020: Zachary Yedidia, et al.
Copyright (c) 2016: Zachary Yedidia, et al.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -1,4 +1,4 @@
Third party libraries directly used by micro and their licenses
Third party libraries
================
github.com/golang/go/LICENSE
@@ -58,6 +58,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
github.com/gdamore/encoding/LICENSE
================
@@ -394,9 +395,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
github.com/atotto/clipboard/LICENSE
================
github.com/zyedidia/clipboard/LICENSE (fork)
github.com/zyedidia/clipboard/LICENSE
================
Copyright (c) 2013 Ato Araki. All rights reserved.
@@ -428,9 +427,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
github.com/gdamore/tcell/LICENSE
================
github.com/zyedidia/tcell/LICENSE (fork)
github.com/zyedidia/tcell/LICENSE
================
@@ -637,6 +634,70 @@ github.com/zyedidia/tcell/LICENSE (fork)
limitations under the License.
golang.org/x/net/LICENSE
================
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
golang.org/x/sys/LICENSE
================
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
golang.org/x/text/LICENSE
================
@@ -1045,262 +1106,3 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
github.com/flynn/json5/LICENSE
================
github.com/zyedidia/json5/LICENSE (fork)
================
Decoder code based on package encoding/json from the Go language.
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Test data based on the parse cases from https://github.com/json5/json5
Copyright (c) 2012-2016 Aseem Kishore, and others.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/james4k/terminal/LICENSE
================
github.com/zyedidia/terminal/LICENSE (fork)
================
Copyright (C) 2013 James Gray
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without liitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and thismssion notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
github.com/kr/pty/License
================
github.com/zyedidia/pty/License (fork)
================
Copyright (c) 2011 Keith Rarick
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/npat-efault/poller/LICENSE.txt
================
github.com/zyedidia/poller/LICENSE.txt (fork)
================
Copyright (c) 2014, Nick Patavalis (npat@efault.net)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
github.com/zyedidia/glob
================
Glob is licensed under the MIT "Expat" License:
Copyright (c) 2016: Zachary Yedidia.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/dustin/go-humanize/LICENSE
================
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<http://www.opensource.org/licenses/mit-license.php>
gopkg.in/yaml.v2/LICENSE
================
Copyright 2011-2016 Canonical Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
github.com/kballard/go-shellquote/LICENSE
===============
Copyright (C) 2014 Kevin Ballard
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/stretchr/testify
=================
MIT License
Copyright (c) 2012-2018 Mat Ryer and Tyler Bunnell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,85 +1,54 @@
.PHONY: runtime
VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
VERSION := $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
go run tools/build-version.go)
HASH = $(shell git rev-parse --short HEAD)
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
HASH := $(shell git rev-parse --short HEAD)
DATE := $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
go run tools/build-date.go)
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH))
ADDITIONAL_GO_LINKER_FLAGS := $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(VERSION)")
GOBIN ?= $(shell go env GOPATH)/bin
GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)'
DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON
VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/'
# Builds micro after checking dependencies but without updating the runtime
build:
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-dbg:
go build -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
build-tags: fetch-tags
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build: deps
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Builds micro after building the runtime and checking dependencies
build-all: runtime build
# Builds micro without checking for dependencies
build-quick:
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Same as 'build' but installs to $GOBIN afterward
install:
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
install: deps
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Same as 'build-all' but installs to $GOBIN afterward
install-all: runtime install
# Same as 'build-quick' but installs to $GOBIN afterward
install-quick:
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
fetch-tags:
git fetch --tags
# Checks for dependencies
deps:
go get -d ./cmd/micro
update:
git pull
go get -u -d ./cmd/micro
# Builds the runtime
runtime:
git submodule update --init
go run runtime/syntax/make_headers.go runtime/syntax
go build -o tools/bindata ./tools/go-bindata
tools/bindata -pkg config -nomemcopy -nometadata -o runtime.go runtime/...
mv runtime.go internal/config
gofmt -w internal/config/runtime.go
testgen:
mkdir -p tools/vscode-tests
cd tools/vscode-tests && \
curl --remote-name-all $(VSCODE_TESTS_BASE_URL){editableTextModelAuto,editableTextModel,model.line}.test.ts
tsc tools/vscode-tests/*.ts > /dev/null; true
go run tools/testgen.go tools/vscode-tests/*.js > buffer_generated_test.go
mv buffer_generated_test.go internal/buffer
gofmt -w internal/buffer/buffer_generated_test.go
go get -u github.com/jteeuwen/go-bindata/...
$(GOBIN)/go-bindata -nometadata -o runtime.go runtime/...
mv runtime.go cmd/micro
test:
go test ./internal/...
bench:
for i in 1 2 3; do \
go test -bench=. ./internal/...; \
done > benchmark_results
benchstat benchmark_results
bench-baseline:
for i in 1 2 3; do \
go test -bench=. ./internal/...; \
done > benchmark_results_baseline
bench-compare:
for i in 1 2 3; do \
go test -bench=. ./internal/...; \
done > benchmark_results
benchstat benchmark_results_baseline benchmark_results
go get -d ./cmd/micro
go test ./cmd/micro
clean:
rm -f micro

235
README.md
View File

@@ -1,76 +1,56 @@
<img alt="micro logo" src="./assets/micro-logo.svg" width="500px"/>
# ![Micro](./assets/logo.png)
[![Build Status](https://travis-ci.org/zyedidia/micro.svg?branch=master)](https://travis-ci.org/zyedidia/micro)
[![Go Report Card](https://goreportcard.com/badge/github.com/zyedidia/micro)](https://goreportcard.com/report/github.com/zyedidia/micro)
[![Release](https://img.shields.io/github/release/zyedidia/micro.svg?label=Release)](https://github.com/zyedidia/micro/releases)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/zyedidia/micro/blob/master/LICENSE)
[![Join the chat at https://gitter.im/zyedidia/micro](https://badges.gitter.im/zyedidia/micro.svg)](https://gitter.im/zyedidia/micro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Snap Status](https://build.snapcraft.io/badge/zyedidia/micro.svg)](https://build.snapcraft.io/user/zyedidia/micro)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/zyedidia/micro/blob/master/LICENSE)
**micro** is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the capabilities
of modern terminals. It comes as a single, batteries-included, static binary with no dependencies; you can download and use it right now!
Micro is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the full capabilities
of modern terminals. It comes as one single, batteries-included, static binary with no dependencies, and you can download and use it right now.
As its name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use.
It strives to be enjoyable as a full-time editor for people who prefer to work in a terminal, or those who regularly edit files over SSH.
As the name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use in a pinch, but micro also aims to be
enjoyable to use full time, whether you work in the terminal because you prefer it (like me), or because you need to (over ssh).
Here is a picture of micro editing its source code.
![Screenshot](./assets/micro-solarized.png)
To see more screenshots of micro, showcasing some of the default color schemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
To see more screenshots of micro, showcasing all of the default colorschemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
You can also check out the website for Micro at https://micro-editor.github.io.
## Table of Contents
# Features
- [Features](#features)
- [Installation](#installation)
- [Prebuilt binaries](#prebuilt-binaries)
- [Package Managers](#package-managers)
- [Building from source](#building-from-source)
- [Fully static binary](#fully-static-binary)
- [macOS terminal](#macos-terminal)
- [Linux clipboard support](#linux-clipboard-support)
- [Colors and syntax highlighting](#colors-and-syntax-highlighting)
- [Plan9, Cygwin, Mingw](#plan9-cygwin-mingw)
- [Usage](#usage)
- [Documentation and Help](#documentation-and-help)
- [Contributing](#contributing)
* Easy to use and to install
* No dependencies or external files are needed -- just the binary you can download further down the page
* Common keybindings (ctrl-s, ctrl-c, ctrl-v, ctrl-z...)
* Keybindings can be rebound to your liking
* Sane defaults
* You shouldn't have to configure much out of the box (and it is extremely easy to configure)
* Splits and tabs
* Extremely good mouse support
* This means mouse dragging to create a selection, double click to select by word, and triple click to select by line
* Cross platform (It should work on all the platforms Go runs on)
* Note that while Windows is supported, there are still some bugs that need to be worked out
* Plugin system (plugins are written in Lua)
* Micro has a built-in plugin manager to automatically install, remove, and update all your plugins
* Persistent undo
* Automatic linting and error notifications
* Syntax highlighting (for over [90 languages](runtime/syntax)!)
* Colorscheme support
* By default, micro comes with 16, 256, and true color themes.
* True color support (set the `MICRO_TRUECOLOR` env variable to 1 to enable it)
* Snippets
* The snippet plugin can be installed with `> plugin install snippets`
* Copy and paste with the system clipboard
* Small and simple
* Easily configurable
* Macros
* Common editor things such as undo/redo, line numbers, Unicode support, softwrap...
- - -
Although not yet implemented, I hope to add more features such as autocompletion ([#174](https://github.com/zyedidia/micro/issues/174)), and multiple cursors ([#5](https://github.com/zyedidia/micro/issues/5)) in the future.
## Features
- Easy to use and install.
- No dependencies or external files are needed — just the binary you can download further down the page.
- Multiple cursors.
- Common keybindings (<kbd>Ctrl+S</kbd>, <kbd>Ctrl+C</kbd>, <kbd>Ctrl+V</kbd>, <kbd>Ctrl+Z</kbd>, …).
- Keybindings can be rebound to your liking.
- Sane defaults.
- You shouldn't have to configure much out of the box (and it is extremely easy to configure).
- Splits and tabs.
- nano-like menu to help you remember the keybindings.
- Extremely good mouse support.
- This means mouse dragging to create a selection, double click to select by word, and triple click to select by line.
- Cross-platform (it should work on all the platforms Go runs on).
- Note that while Windows is supported Mingw/Cygwin is not (see below)
- Plugin system (plugins are written in Lua).
- micro has a built-in plugin manager to automatically install, remove, and update plugins.
- Built-in diff gutter
- Simple autocompletion
- Persistent undo.
- Automatic linting and error notifications
- Syntax highlighting for over [130 languages](runtime/syntax).
- Color scheme support.
- By default, micro comes with 16, 256, and true color themes.
- True color support (set the `MICRO_TRUECOLOR` environment variable to 1 to enable it).
- Copy and paste with the system clipboard.
- Small and simple.
- Easily configurable.
- Macros.
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
## Installation
# Installation
To install micro, you can download a [prebuilt binary](https://github.com/zyedidia/micro/releases), or you can build it from source.
@@ -85,21 +65,9 @@ Download the binary from the [releases](https://github.com/zyedidia/micro/releas
On that page you'll see the nightly release, which contains binaries for micro which are built every night,
and you'll see all the stable releases with the corresponding binaries.
Running `micro -version` will give you the version information.
If you'd like to see more information after installing micro, run `micro -version`.
### Installation script
There is a script which can install micro for you by downloading the latest prebuilt binary. You can find it at <https://getmic.ro>.
You can easily install micro by running
```bash
curl https://getmic.ro | bash
```
The script will install the micro binary to the current directory. See its [GitHub repository](https://github.com/benweissmann/getmic.ro) for more information.
### Package managers
### Package Managers
You can install micro using Homebrew on Mac:
@@ -107,118 +75,77 @@ You can install micro using Homebrew on Mac:
brew install micro
```
**Note for Mac:** All micro keybindings use the control or alt (option) key, not the command
key. By default, macOS terminals do not forward alt key events. To fix this, please see
the section on [macOS terminals](https://github.com/zyedidia/micro#macos-terminal) further below.
On Windows, you can install micro through Chocolatey:
```
choco install micro
```
On Linux, you can install micro through [snap](https://snapcraft.io/docs/core/install)
```
snap install micro --classic
snap install micro --beta
```
On Debian `unstable | testing | buster-backports` and Ubuntu `focal` (20.04), micro is available
via `apt`:
```
sudo apt install micro
```
**Note for Linux:** for interfacing with the local system clipboard, `xclip` or `xsel`
must be installed. Please see the section on [Linux clipboard support](https://github.com/zyedidia/micro#linux-clipboard-support)
further below.
Micro is also available through other package managers on Linux such as AUR, Nix, and package managers
for other operating systems:
* Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop)
* `choco install micro`
* `scoop install micro`
* OpenBSD: Available in the ports tree and also available as a binary package
* `pkd_add -v micro`
* Arch Linux, CRUX, Termux for Android
* See details in the [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro)
### Building from source
If your operating system does not have a binary release, but does run Go, you can build from source.
Make sure that you have Go version 1.11 or greater and Go modules are enabled.
Make sure that you have Go version 1.5 or greater (Go 1.4 will work if your version supports CGO) and that your `GOPATH` env variable is set (I recommand setting it to `~/go` if you don't have one).
```
git clone https://github.com/zyedidia/micro
cd micro
make build
sudo mv micro /usr/local/bin # optional
go get -d github.com/zyedidia/micro/...
cd $GOPATH/src/github.com/zyedidia/micro
make install
```
The binary will be placed in the current directory and can be moved to
anywhere you like (for example `/usr/local/bin`).
Please make sure that when you are working with micro's code, you are working on your `GOPATH`.
The command `make install` will install the binary to `$GOPATH/bin` or `$GOBIN`.
The binary will then be installed to `$GOPATH/bin` (or your `$GOBIN`).
You can install directly with `go get` (`go get github.com/zyedidia/micro/cmd/micro`) but this isn't
recommended because it doesn't build micro with version information (necessary for the plugin manager),
and doesn't disable debug mode.
You can install directly with `go get` (`go get -u github.com/zyedidia/micro/cmd/micro`) but this isn't recommended because it doesn't build micro with version information which is useful for the plugin manager.
### Fully static binary
### MacOS terminal
By default, the micro binary will dynamically link with core system libraries (this is generally
recommended for security and portability). However, there is a fully static prebuilt binary that
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
```
CGO_ENABLED=0 make build
```
### macOS terminal
If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Load Preset`. The newest versions also support true color.
If you still insist on using the default Mac terminal, be sure to set `Use Option key as Meta key` under
`Preferences->Profiles->Keyboard` to use <kbd>option</kbd> as <kbd>alt</kbd>.
If you are using MacOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default Mac terminal. The iTerm2 terminal has much better mouse support as well as better handling of key events. The newest versions also support true color.
### Linux clipboard support
On Linux, clipboard support requires:
On Linux, clipboard support requires the 'xclip' or 'xsel' commands to be installed.
- On X11, the `xclip` or `xsel` commands (for Ubuntu: `sudo apt install xclip`)
- On Wayland, the `wl-clipboard` command
For Ubuntu:
If you don't have these commands, micro will use an internal clipboard for copy and paste, but it won't work with external applications.
```sh
sudo apt-get install xclip
```
If you don't have xclip or xsel, micro will use an internal clipboard for copy and paste, but it won't work with external applications.
### Colors and syntax highlighting
If you open micro and it doesn't seem like syntax highlighting is working, this is probably because
you are using a terminal which does not support 256 color mode. Try changing the color scheme to `simple`
by pressing <kbd>Ctrl+E</kbd> in micro and typing `set colorscheme simple`.
you are using a terminal which does not support 256 color. Try changing the colorscheme to `simple`
by pressing CtrlE in micro and typing `set colorscheme simple`.
If you are using the default Ubuntu terminal, to enable 256 make sure your `TERM` variable is set
to `xterm-256color`.
Many of the Windows terminals don't support more than 16 colors, which means
that micro's default color scheme won't look very good. You can either set
the color scheme to `simple`, or download and configure a better terminal emulator
than the Windows default.
that micro's default colorscheme won't look very good. You can either set
the colorscheme to `simple`, or download a better terminal emulator, like
mintty.
### Cygwin, Mingw, Plan9
### Plan9, NaCl, Cygwin
Cygwin, Mingw, and Plan9 are unfortunately not officially supported. In Cygwin and Mingw, micro will often work when run using
the `winpty` utility:
```
winpty micro.exe ...
```
Micro uses the amazing [tcell library](https://github.com/gdamore/tcell), but this
Please note that micro uses the amazing [tcell library](https://github.com/gdamore/tcell), but this
means that micro is restricted to the platforms tcell supports. As a result, micro does not support
Plan9, and Cygwin (although this may change in the future). Micro also doesn't support NaCl (which is deprecated anyway).
Plan9, NaCl, and Cygwin (although this may change in the future).
## Usage
# Usage
Once you have built the editor, start it by running `micro path/to/file.txt` or `micro` to open an empty buffer.
Once you have built the editor, simply start it by running `micro path/to/file.txt` or simply `micro` to open an empty buffer.
micro also supports creating buffers from `stdin`:
Micro also supports creating buffers from `stdin`:
```sh
ifconfig | micro
@@ -230,22 +157,22 @@ You can also use the mouse to manipulate the text. Simply clicking and dragging
will select text. You can also double click to enable word selection, and triple
click to enable line selection.
## Documentation and Help
# Documentation and Help
micro has a built-in help system which you can access by pressing <kbd>Ctrl+E</kbd> and typing `help`. Additionally, you can
Micro has a built-in help system which you can access by pressing `Ctrl-E` and typing `help`. Additionally, you can
view the help files here:
- [main help](https://github.com/zyedidia/micro/tree/master/runtime/help/help.md)
- [keybindings](https://github.com/zyedidia/micro/tree/master/runtime/help/keybindings.md)
- [commands](https://github.com/zyedidia/micro/tree/master/runtime/help/commands.md)
- [colors](https://github.com/zyedidia/micro/tree/master/runtime/help/colors.md)
- [options](https://github.com/zyedidia/micro/tree/master/runtime/help/options.md)
- [plugins](https://github.com/zyedidia/micro/tree/master/runtime/help/plugins.md)
* [main help](https://github.com/zyedidia/micro/tree/master/runtime/help/help.md)
* [keybindings](https://github.com/zyedidia/micro/tree/master/runtime/help/keybindings.md)
* [commands](https://github.com/zyedidia/micro/tree/master/runtime/help/commands.md)
* [colors](https://github.com/zyedidia/micro/tree/master/runtime/help/colors.md)
* [options](https://github.com/zyedidia/micro/tree/master/runtime/help/options.md)
* [plugins](https://github.com/zyedidia/micro/tree/master/runtime/help/plugins.md)
I also recommend reading the [tutorial](https://github.com/zyedidia/micro/tree/master/runtime/help/tutorial.md) for
a brief introduction to the more powerful configuration features micro offers.
## Contributing
# Contributing
If you find any bugs, please report them! I am also happy to accept pull requests from anyone.

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 299.89999 103.2"
enable-background="new 0 0 960 560"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="micro-logo.svg"
width="299.89999"
height="103.2"><metadata
id="metadata21"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs19" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1237"
inkscape:window-height="867"
id="namedview17"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.1416667"
inkscape:cx="75.655934"
inkscape:cy="-4"
inkscape:window-x="1097"
inkscape:window-y="185"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><g
id="g3"
transform="translate(-178,-172.8)"><path
d="m 306.8,213.8 0,-2.6 c 1.6,-0.1 2.9,-0.4 4.1,-0.8 1.2,-0.4 2.5,-1 4,-1.8 l 2.3,0 0,5.2 c 2.4,-1.9 4.2,-3.1 5.5,-3.8 2,-1 4,-1.5 5.8,-1.5 1.3,0 2.5,0.2 3.7,0.7 1.2,0.5 2.2,1 2.9,1.7 0.7,0.7 1.4,1.6 1.9,2.8 2.2,-1.9 4.2,-3.3 6,-4 1.9,-0.8 3.7,-1.2 5.6,-1.2 1.8,0 3.4,0.4 4.8,1.1 1.4,0.8 2.4,1.7 3,2.8 0.6,1.1 0.9,2.8 0.9,5 l 0,14.4 c 0,1.5 0,2.4 0.1,2.6 0.1,0.4 0.3,0.8 0.7,1.1 0.3,0.4 0.7,0.6 1.2,0.7 0.4,0.1 1.2,0.2 2.4,0.2 l 1,0 0,2.6 -15.5,0 0,-2.6 c 1.8,0 2.9,-0.1 3.5,-0.4 0.5,-0.2 0.9,-0.6 1.2,-1.2 0.3,-0.6 0.4,-1.6 0.4,-3.2 l 0,-13.7 c 0,-1.7 -0.2,-2.9 -0.5,-3.6 -0.3,-0.7 -0.9,-1.2 -1.7,-1.7 -0.8,-0.4 -1.8,-0.7 -3,-0.7 -1.5,0 -3,0.4 -4.6,1.2 -2.2,1.1 -3.9,2.3 -5.1,3.6 l 0,14.8 c 0,1.4 0.1,2.4 0.2,2.8 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.5 1.1,0.6 0.4,0.1 1.5,0.2 3.1,0.2 l 0,2.6 -15.3,0 0,-2.6 0.9,0 c 1.2,0 2.1,-0.1 2.6,-0.4 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.2,-0.5 0.3,-1.5 0.3,-2.9 l 0,-13.2 c 0,-1.9 -0.2,-3.3 -0.5,-3.9 -0.3,-0.7 -0.9,-1.3 -1.7,-1.7 -0.8,-0.5 -1.8,-0.7 -3,-0.7 -1.3,0 -2.7,0.3 -4.1,1 -2,1 -3.9,2.2 -5.6,3.8 l 0,15.9 c 0,1 0.1,1.6 0.4,2.1 0.3,0.4 0.7,0.8 1.2,1.1 0.6,0.3 1.3,0.4 2.3,0.4 l 1.1,0 0,2.6 -15.6,0 0,-2.6 0.8,0 c 1.4,0 2.4,-0.1 2.8,-0.3 0.7,-0.3 1.1,-0.8 1.4,-1.5 0.2,-0.4 0.2,-1.3 0.2,-2.9 l 0,-18.1 -5.1,0 z"
id="path5"
inkscape:connector-curvature="0" /><path
d="m 366.4,213.7 0,-2.6 c 1.7,-0.2 3.2,-0.5 4.3,-0.9 1.2,-0.4 2.5,-1 4,-1.7 l 2.3,0 0,24.9 c 0,0.9 0.1,1.5 0.4,2 0.2,0.4 0.6,0.8 1,0.9 0.4,0.2 1.3,0.3 2.4,0.3 l 1.5,0 0,2.6 -15.9,0 0,-2.6 1.3,0 c 1.4,0 2.3,-0.1 2.8,-0.4 0.5,-0.2 0.8,-0.6 1,-1.1 0.2,-0.5 0.3,-1.5 0.3,-3.2 l 0,-18.3 -5.4,0 z m 7.9,-19.2 c 1,0 1.8,0.3 2.5,1 0.7,0.7 1.1,1.5 1.1,2.5 0,1 -0.4,1.8 -1.1,2.5 -0.7,0.7 -1.6,1.1 -2.5,1.1 -1,0 -1.8,-0.4 -2.5,-1.1 -0.7,-0.7 -1.1,-1.6 -1.1,-2.5 0,-1 0.4,-1.8 1.1,-2.5 0.6,-0.6 1.5,-1 2.5,-1 z"
id="path7"
inkscape:connector-curvature="0" /><path
d="m 413.1,230.6 2,1.6 c -3.9,5.2 -8.6,7.8 -14,7.8 -4.2,0 -7.8,-1.5 -10.7,-4.5 -2.9,-3 -4.4,-6.8 -4.4,-11.3 0,-3 0.7,-5.7 2,-8.1 1.3,-2.4 3.2,-4.2 5.6,-5.6 2.4,-1.3 5.2,-2 8.3,-2 3.6,0 6.5,0.9 8.9,2.6 2.4,1.7 3.6,3.5 3.6,5.3 0,1 -0.3,1.7 -0.8,2.2 -0.5,0.5 -1.2,0.8 -1.9,0.8 -0.4,0 -0.7,-0.1 -1.1,-0.3 -0.4,-0.2 -0.7,-0.5 -1.1,-0.9 -0.2,-0.2 -0.5,-0.8 -0.9,-1.7 -0.6,-1.2 -1,-2 -1.3,-2.4 -0.6,-0.8 -1.4,-1.5 -2.4,-2 -0.9,-0.5 -2,-0.7 -3.1,-0.7 -1.8,0 -3.4,0.5 -4.9,1.5 -1.5,1 -2.7,2.4 -3.6,4.3 -0.9,1.9 -1.3,4.2 -1.3,6.8 0,4.1 1.1,7.3 3.3,9.7 1.9,2.1 4.1,3.1 6.7,3.1 1.2,0 2.4,-0.2 3.6,-0.6 1.2,-0.4 2.4,-1 3.5,-1.8 0.9,-0.6 2.2,-1.8 4,-3.8 z"
id="path9"
inkscape:connector-curvature="0" /><path
d="m 418.7,213.7 0,-2.6 c 1.5,-0.1 2.8,-0.4 4,-0.8 1.2,-0.4 2.5,-1 4,-1.9 l 2.3,0 0,5.9 c 1.5,-1.8 3.2,-3.2 5.1,-4.3 1.9,-1.1 3.7,-1.6 5.2,-1.6 1.5,0 2.7,0.4 3.6,1.1 0.9,0.7 1.3,1.6 1.3,2.6 0,0.7 -0.3,1.4 -0.9,2 -0.6,0.6 -1.3,0.9 -2.1,0.9 -0.4,0 -0.7,-0.1 -1,-0.2 -0.3,-0.1 -0.7,-0.3 -1.2,-0.7 -1.1,-0.7 -2.1,-1.1 -2.9,-1.1 -1,0 -2.2,0.4 -3.4,1.3 -1.6,1.1 -2.8,2.2 -3.7,3.3 l 0,14.4 c 0,1.2 0.1,2.1 0.2,2.5 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.6 1.1,0.7 0.5,0.1 1.3,0.2 2.4,0.2 l 1,0 0,2.6 -16,0 0,-2.6 1.3,0 c 1.3,0 2.1,-0.1 2.5,-0.3 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.3,-0.5 0.4,-1.5 0.4,-3 l 0,-18.3 -5.1,0 z"
id="path11"
inkscape:connector-curvature="0" /><path
d="m 462.8,208.5 c 3,0 5.7,0.6 7.9,1.9 2.2,1.3 4,3.1 5.3,5.5 1.3,2.4 1.9,5.2 1.9,8.3 0,3.1 -0.7,5.9 -2,8.3 -1.3,2.4 -3.1,4.3 -5.4,5.5 -2.3,1.3 -5,1.9 -8.1,1.9 -5,0 -8.8,-1.6 -11.3,-4.7 -2.5,-3.1 -3.8,-6.8 -3.8,-11 0,-3.1 0.7,-5.8 2,-8.2 1.3,-2.4 3.1,-4.2 5.5,-5.6 2.4,-1.2 5.1,-1.9 8,-1.9 z m -0.2,3 c -2.4,0 -4.4,0.9 -6,2.8 -2.1,2.3 -3.1,5.7 -3.1,10.1 0,4.3 0.9,7.5 2.6,9.7 1.6,2 3.8,3 6.5,3 1.8,0 3.3,-0.5 4.7,-1.4 1.4,-0.9 2.5,-2.4 3.3,-4.5 0.8,-2 1.3,-4.4 1.3,-7.2 0,-2.7 -0.5,-5.1 -1.4,-7.2 -0.7,-1.7 -1.8,-3 -3.2,-4 -1.3,-0.8 -2.9,-1.3 -4.7,-1.3 z"
id="path13"
inkscape:connector-curvature="0" /></g><path
d="M 51.6,0 C 23.1,0 0,23.1 0,51.6 c 0,28.5 23.1,51.6 51.6,51.6 28.5,0 51.6,-23.1 51.6,-51.6 C 103.2,23.1 80.1,0 51.6,0 Z m 24.5,58.6 c -0.5,2 -1.3,3.6 -2.4,4.9 -1,1.3 -2,2.1 -3.1,2.5 -1.1,0.4 -2.2,0.6 -3.4,0.6 -1.2,0 -2.2,-0.2 -3,-0.7 C 63.4,65.5 62.8,64.8 62.3,64 61.8,63.2 61.5,62.2 61.3,61.1 61.1,60 61,58.8 61,57.5 c 0,-0.5 0,-1 0.1,-1.7 0.1,-0.7 0.2,-1.6 0.3,-1.6 l -0.2,0 c -1.6,4 -3.8,6.9 -6.6,9.2 -2.8,2.3 -5.9,3.4 -9.3,3.4 -2.3,0 -4.2,-0.9 -5.5,-2.6 -1.4,-1.7 -2.1,-4.3 -2.1,-7.7 0,-0.5 0,-1 0.1,-1.6 0.1,-0.5 0.1,-0.7 0.2,-1.7 l -0.7,0 c -0.9,2 -1.7,4.8 -2.3,7.3 -0.6,2.5 -1.1,4.8 -1.4,6.9 -0.4,2.1 -0.6,4 -0.8,5.6 -0.2,1.6 -0.3,2.7 -0.4,3.3 0.1,0.5 0.2,1 0.3,1.6 0.2,0.6 0.3,1.2 0.5,1.7 0.2,0.5 0.3,1.1 0.4,1.6 0.1,0.5 0.2,0.9 0.2,1.2 0,1.4 -0.3,2.5 -0.9,3.2 -0.6,0.7 -1.3,1.1 -2,1.1 -0.9,0 -1.7,-0.3 -2.3,-0.8 -0.7,-0.6 -1,-1.5 -1,-2.7 0,-1.7 0.3,-3.9 0.9,-6.5 0.6,-2.6 1.5,-5.9 2.6,-9.8 0.6,-1.8 1.1,-3.6 1.7,-5.4 0.6,-1.8 1.1,-3.5 1.6,-5 0.5,-1.5 0.9,-2.9 1.3,-4.1 0.4,-1.2 0.6,-2.1 0.7,-2.8 0.1,-0.3 0.2,-1 0.3,-2 0.1,-1 0.2,-2.1 0.4,-3.4 0.2,-1.3 0.3,-2.7 0.5,-4.1 0.2,-1.5 0.4,-2.8 0.5,-4 0.2,-0.9 0.3,-1.9 0.5,-3 0.2,-1.1 0.5,-2.2 0.9,-3.1 0.4,-1 1,-1.8 1.7,-2.5 0.7,-0.7 1.6,-1 2.7,-1 1.2,0 2,0.4 2.4,1.1 0.4,0.7 0.6,1.6 0.5,2.6 -0.1,1 -0.2,2.1 -0.5,3.2 -0.3,1.1 -0.6,2.1 -0.9,2.9 -0.8,2.5 -1.6,4.8 -2.5,6.7 -0.9,1.9 -1.7,4 -2.4,6.2 -0.6,1.5 -0.8,2.9 -0.8,4.1 0,2.2 0.7,3.8 2,5 1.4,1.2 3,1.7 4.9,1.7 1.5,0 3,-0.5 4.4,-1.6 1.4,-1.1 2.7,-2.4 3.9,-3.9 1.2,-1.5 2.2,-3.1 3,-4.9 0.8,-1.7 1.4,-3.3 1.8,-4.6 0.1,-0.2 0.2,-0.6 0.3,-1.4 0.2,-0.8 0.3,-1.7 0.5,-2.7 0.2,-1 0.4,-2 0.6,-3.1 0.2,-1.1 0.4,-2 0.5,-2.7 0.2,-0.8 0.3,-1.6 0.5,-2.6 0.2,-1 0.5,-1.9 0.9,-2.8 0.4,-0.9 1,-1.6 1.6,-2.2 0.7,-0.6 1.5,-0.9 2.6,-0.9 1.3,0 2.1,0.4 2.6,1.1 0.4,0.7 0.6,1.6 0.6,2.6 -0.1,1 -0.2,2 -0.5,3 -0.3,1 -0.5,1.8 -0.7,2.4 -0.8,2.5 -1.6,4.7 -2.4,6.7 -0.8,2 -1.5,3.8 -2.2,5.2 -0.6,1.5 -1.1,2.6 -1.5,3.5 -0.4,0.9 -0.6,1.5 -0.6,1.8 0,2.6 0.6,4.5 1.7,5.6 1.1,1.1 2.3,1.7 3.6,1.7 2.2,0 3.9,-0.7 5.2,-2 1.3,-1.4 2.3,-3.9 2.9,-6.9 l 0.8,0 c 0.2,2.9 -0.1,5.3 -0.6,7.3 z"
id="path15"
inkscape:connector-curvature="0"
style="fill:#2e3192" /></svg>

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 289 KiB

View File

@@ -1,123 +0,0 @@
.TH micro 1 "2020-02-10"
.SH NAME
micro \- A modern and intuitive terminal-based text editor
.SH SYNOPSIS
.B micro
.RB [OPTIONS]
[FILE]\&...
.SH DESCRIPTION
Micro is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the full capabilities
of modern terminals. It comes as one single, batteries-included, static binary with no dependencies.
As the name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use in a pinch, but micro also aims to be
enjoyable to use full time, whether you work in the terminal because you prefer it (like me), or because you need to (over ssh).
.SH OPTIONS
.PP
\-clean
.RS 4
Cleans the configuration directory
.RE
.PP
\-config-dir dir
.RS 4
Specify a custom location for the configuration directory
.RE
.PP
[FILE]:LINE:COL
.RS 4
Specify a line and column to start the cursor at when opening a buffer
.RE
.PP
\-options
.RS 4
Show all option help
.RE
.PP
\-debug
.RS 4
Enable debug mode (enables logging to ./log.txt)
.RE
.PP
\-version
.RS 4
Show the version number and information
.RE
Micro's plugin's can be managed at the command line with the following commands.
.RS 4
.PP
\-plugin remove [PLUGIN]...
.RS 4
Remove plugin(s)
.RE
.PP
\-plugin update [PLUGIN]...
.RS 4
Update plugin(s) (if no argument is given, updates all plugins)
.RE
.PP
\-plugin search [PLUGIN]...
.RS 4
Search for a plugin
.RE
.PP
\-plugin list
.RS 4
List installed plugins
.RE
.PP
\-plugin available
.RS 4
List available plugins
.RE
.RE
Micro's options can also be set via command line arguments for quick
adjustments. For real configuration, please use the settings.json
file (see 'help options').
.RS 4
.PP
\-option value
.RS 4
Set `option` to `value` for this session
For example: `micro -syntax off file.c`
.RE
.SH CONFIGURATION
Micro uses $MICRO_CONFIG_HOME as the configuration directory.
If this environment variable is not set, it uses $XDG_CONFIG_HOME/micro instead.
If that environment variable is not set, it uses ~/.config/micro as the configuration directory.
In the documentation, we use ~/.config/micro to refer to the configuration directory
(even if it may in fact be somewhere else if you have set either of the above environment variables).
.SH NOTICE
This manpage is intended only to serve as a quick guide to the invocation of
micro and is not intended to replace the full documentation included with micro
which can be accessed from within micro. Micro tells you what key combination to
press to get help in the lower right.
.SH BUGS
A comprehensive list of bugs will not be listed in this manpage. See the Github
page at \fBhttps://github.com/zyedidia/micro/issues\fP for a list of known bugs
and to report any newly encountered bugs you may find. We strive to correct
bugs as swiftly as possible.
.SH COPYRIGHT
Copyright \(co 2020 Zachary Yedidia, et al.
See /usr/share/doc/micro/LICENSE and /usr/share/doc/micro/AUTHORS for more information.

View File

@@ -1,15 +0,0 @@
[Desktop Entry]
Name=Micro
GenericName=Text Editor
Comment=Edit text files in a terminal
Icon=micro
Type=Application
Categories=terminal;TextEditor;
Keywords=text;editor;syntax;terminal;
Exec=micro %U
StartupNotify=false
Terminal=true
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;

123
cmd/micro/Gopkg.lock generated Normal file
View File

@@ -0,0 +1,123 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/blang/semver"
packages = ["."]
revision = "4a1e882c79dcf4ec00d2e29fac74b9c8938d5052"
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "259d2a102b871d17f30e3cd9881a642961a1e486"
[[projects]]
branch = "master"
name = "github.com/gdamore/encoding"
packages = ["."]
revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
[[projects]]
branch = "master"
name = "github.com/go-errors/errors"
packages = ["."]
revision = "8fa88b06e5974e97fbf9899a7f86a344bfd1f105"
[[projects]]
branch = "master"
name = "github.com/lucasb-eyer/go-colorful"
packages = ["."]
revision = "9c2852a141bf4711e4276f8f119c90d0f20a556c"
[[projects]]
branch = "master"
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
[[projects]]
branch = "master"
name = "github.com/mattn/go-runewidth"
packages = ["."]
revision = "97311d9f7767e3d6f422ea06661bc2c7a19e8a5d"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
[[projects]]
branch = "master"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
revision = "feef008d51ad2b3778f85d387ccf91735543008d"
[[projects]]
branch = "master"
name = "github.com/yuin/gopher-lua"
packages = [".","ast","parse","pm"]
revision = "b402f3114ec730d8bddb074a6c137309f561aa78"
[[projects]]
branch = "master"
name = "github.com/zyedidia/clipboard"
packages = ["."]
revision = "adacf416cec40266b051e7bc096c52951f2725e9"
[[projects]]
branch = "master"
name = "github.com/zyedidia/glob"
packages = ["."]
revision = "72567a468b2481490f359cdfb015231389e0bf9d"
[[projects]]
branch = "master"
name = "github.com/zyedidia/json5"
packages = ["encoding/json5"]
revision = "2518f8beebde6814f2d30d566260480d2ded2f76"
[[projects]]
branch = "master"
name = "github.com/zyedidia/tcell"
packages = [".","encoding"]
revision = "7095cc1c7f4173ae48314d80878e9985a0658889"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context"]
revision = "007e530097ad7f954752df63046b4036f98ba6a6"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "d4feaf1a7e61e1d9e79e6c4e76c6349e9cab0a03"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = ["encoding","encoding/charmap","encoding/internal","encoding/internal/identifier","encoding/japanese","encoding/korean","encoding/simplifiedchinese","encoding/traditionalchinese","internal/gen","transform","unicode/cldr"]
revision = "506f9d5c962f284575e88337e7d9296d27e729d3"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "a83829b6f1293c91addabc89d0571c246397bbf4"
[[projects]]
branch = "master"
name = "layeh.com/gopher-luar"
packages = ["."]
revision = "80196fe2abc5682963fc7a5261f5a5d77509938b"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "aebc6aa50d78830f86e8964c1ebc1804c1f9603477ddba606717a972ca70d261"
solver-name = "gps-cdcl"
solver-version = 1

123
cmd/micro/Gopkg.toml Normal file
View File

@@ -0,0 +1,123 @@
## Gopkg.toml example (these lines may be deleted)
## "metadata" defines metadata about the project that could be used by other independent
## systems. The metadata defined here will be ignored by dep.
# [metadata]
# key1 = "value that convey data to other systems"
# system1-data = "value that is used by a system"
# system2-data = "value that is used by another system"
## "required" lists a set of packages (not projects) that must be included in
## Gopkg.lock. This list is merged with the set of packages imported by the current
## project. Use it when your project needs a package it doesn't explicitly import -
## including "main" packages.
# required = ["github.com/user/thing/cmd/thing"]
## "ignored" lists a set of packages (not projects) that are ignored when
## dep statically analyzes source code. Ignored packages can be in this project,
## or in a dependency.
# ignored = ["github.com/user/project/badpkg"]
## Constraints are rules for how directly imported projects
## may be incorporated into the depgraph. They are respected by
## dep whether coming from the Gopkg.toml of the current project or a dependency.
# [[constraint]]
## Required: the root import path of the project being constrained.
# name = "github.com/user/project"
#
## Recommended: the version constraint to enforce for the project.
## Only one of "branch", "version" or "revision" can be specified.
# version = "1.0.0"
# branch = "master"
# revision = "abc123"
#
## Optional: an alternate location (URL or import path) for the project's source.
# source = "https://github.com/myfork/package.git"
#
## "metadata" defines metadata about the dependency or override that could be used
## by other independent systems. The metadata defined here will be ignored by dep.
# [metadata]
# key1 = "value that convey data to other systems"
# system1-data = "value that is used by a system"
# system2-data = "value that is used by another system"
## Overrides have the same structure as [[constraint]], but supersede all
## [[constraint]] declarations from all projects. Only [[override]] from
## the current project's are applied.
##
## Overrides are a sledgehammer. Use them only as a last resort.
# [[override]]
## Required: the root import path of the project being constrained.
# name = "github.com/user/project"
#
## Optional: specifying a version constraint override will cause all other
## constraints on this project to be ignored; only the overridden constraint
## need be satisfied.
## Again, only one of "branch", "version" or "revision" can be specified.
# version = "1.0.0"
# branch = "master"
# revision = "abc123"
#
## Optional: specifying an alternate source location as an override will
## enforce that the alternate location is used for that project, regardless of
## what source location any dependent projects specify.
# source = "https://github.com/myfork/package.git"
[[constraint]]
branch = "master"
name = "github.com/blang/semver"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-humanize"
[[constraint]]
branch = "master"
name = "github.com/go-errors/errors"
[[constraint]]
branch = "master"
name = "github.com/mattn/go-isatty"
[[constraint]]
branch = "master"
name = "github.com/mattn/go-runewidth"
[[constraint]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
[[constraint]]
branch = "master"
name = "github.com/sergi/go-diff"
[[constraint]]
branch = "master"
name = "github.com/yuin/gopher-lua"
[[constraint]]
branch = "master"
name = "github.com/zyedidia/clipboard"
[[constraint]]
branch = "master"
name = "github.com/zyedidia/glob"
[[constraint]]
branch = "master"
name = "github.com/zyedidia/json5"
[[constraint]]
branch = "master"
name = "github.com/zyedidia/tcell"
[[constraint]]
branch = "v2"
name = "gopkg.in/yaml.v2"
[[constraint]]
branch = "master"
name = "layeh.com/gopher-luar"

1742
cmd/micro/actions.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
package main
import "syscall"
// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs.
// This only works on linux and has no default binding.
// This code was adapted from the suspend code in nsf/godit
func (v *View) Suspend(usePlugin bool) bool {
if usePlugin && !PreActionCall("Suspend", v) {
return false
}
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
// suspend the process
pid := syscall.Getpid()
tid := syscall.Gettid()
err := syscall.Tgkill(pid, tid, syscall.SIGSTOP)
if err != nil {
TermMessage(err)
}
if !screenWasNil {
InitScreen()
}
if usePlugin {
return PostActionCall("Suspend", v)
}
return true
}

View File

@@ -0,0 +1,9 @@
// +build !linux
package main
func (v *View) Suspend(usePlugin bool) bool {
messenger.Error("Suspend is only supported on Linux")
return false
}

196
cmd/micro/autocomplete.go Normal file
View File

@@ -0,0 +1,196 @@
package main
import (
"io/ioutil"
"os"
"strings"
"github.com/mitchellh/go-homedir"
)
var pluginCompletions []func(string) []string
// This file is meant (for now) for autocompletion in command mode, not
// while coding. This helps micro autocomplete commands and then filenames
// for example with `vsplit filename`.
// FileComplete autocompletes filenames
func FileComplete(input string) (string, []string) {
var sep string = string(os.PathSeparator)
dirs := strings.Split(input, sep)
var files []os.FileInfo
var err error
if len(dirs) > 1 {
home, _ := homedir.Dir()
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
if strings.HasPrefix(directories, "~") {
directories = strings.Replace(directories, "~", home, 1)
}
files, err = ioutil.ReadDir(directories)
} else {
files, err = ioutil.ReadDir(".")
}
var suggestions []string
if err != nil {
return "", suggestions
}
for _, f := range files {
name := f.Name()
if f.IsDir() {
name += sep
}
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
suggestions = append(suggestions, name)
}
}
var chosen string
if len(suggestions) == 1 {
if len(dirs) > 1 {
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[0]
} else {
chosen = suggestions[0]
}
} else {
if len(dirs) > 1 {
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep
}
}
return chosen, suggestions
}
// CommandComplete autocompletes commands
func CommandComplete(input string) (string, []string) {
var suggestions []string
for cmd := range commands {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// HelpComplete autocompletes help topics
func HelpComplete(input string) (string, []string) {
var suggestions []string
for _, file := range ListRuntimeFiles(RTHelp) {
topic := file.Name()
if strings.HasPrefix(topic, input) {
suggestions = append(suggestions, topic)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// ColorschemeComplete tab-completes names of colorschemes.
func ColorschemeComplete(input string) (string, []string) {
var suggestions []string
files := ListRuntimeFiles(RTColorscheme)
for _, f := range files {
if strings.HasPrefix(f.Name(), input) {
suggestions = append(suggestions, f.Name())
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// OptionComplete autocompletes options
func OptionComplete(input string) (string, []string) {
var suggestions []string
localSettings := DefaultLocalSettings()
for option := range globalSettings {
if strings.HasPrefix(option, input) {
suggestions = append(suggestions, option)
}
}
for option := range localSettings {
if strings.HasPrefix(option, input) && !contains(suggestions, option) {
suggestions = append(suggestions, option)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// MakeCompletion registers a function from a plugin for autocomplete commands
func MakeCompletion(function string) Completion {
pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
return Completion(-len(pluginCompletions))
}
// PluginComplete autocompletes from plugin function
func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) {
idx := int(-complete) - 1
if len(pluginCompletions) <= idx {
return "", nil
}
suggestions = pluginCompletions[idx](input)
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return
}
func PluginCmdComplete(input string) (chosen string, suggestions []string) {
for _, cmd := range []string{"install", "remove", "search", "update", "list"} {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
func PluginNameComplete(input string) (chosen string, suggestions []string) {
for _, pp := range GetAllPluginPackages() {
if strings.HasPrefix(pp.Name, input) {
suggestions = append(suggestions, pp.Name)
}
}
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}

457
cmd/micro/bindings.go Normal file
View File

@@ -0,0 +1,457 @@
package main
import (
"io/ioutil"
"os"
"strings"
"github.com/zyedidia/json5/encoding/json5"
"github.com/zyedidia/tcell"
)
var bindings map[Key][]func(*View, bool) bool
var helpBinding string
var bindingActions = map[string]func(*View, bool) bool{
"CursorUp": (*View).CursorUp,
"CursorDown": (*View).CursorDown,
"CursorPageUp": (*View).CursorPageUp,
"CursorPageDown": (*View).CursorPageDown,
"CursorLeft": (*View).CursorLeft,
"CursorRight": (*View).CursorRight,
"CursorStart": (*View).CursorStart,
"CursorEnd": (*View).CursorEnd,
"SelectToStart": (*View).SelectToStart,
"SelectToEnd": (*View).SelectToEnd,
"SelectUp": (*View).SelectUp,
"SelectDown": (*View).SelectDown,
"SelectLeft": (*View).SelectLeft,
"SelectRight": (*View).SelectRight,
"WordRight": (*View).WordRight,
"WordLeft": (*View).WordLeft,
"SelectWordRight": (*View).SelectWordRight,
"SelectWordLeft": (*View).SelectWordLeft,
"DeleteWordRight": (*View).DeleteWordRight,
"DeleteWordLeft": (*View).DeleteWordLeft,
"SelectToStartOfLine": (*View).SelectToStartOfLine,
"SelectToEndOfLine": (*View).SelectToEndOfLine,
"InsertNewline": (*View).InsertNewline,
"InsertSpace": (*View).InsertSpace,
"Backspace": (*View).Backspace,
"Delete": (*View).Delete,
"InsertTab": (*View).InsertTab,
"Save": (*View).Save,
"SaveAll": (*View).SaveAll,
"SaveAs": (*View).SaveAs,
"Find": (*View).Find,
"FindNext": (*View).FindNext,
"FindPrevious": (*View).FindPrevious,
"Center": (*View).Center,
"Undo": (*View).Undo,
"Redo": (*View).Redo,
"Copy": (*View).Copy,
"Cut": (*View).Cut,
"CutLine": (*View).CutLine,
"DuplicateLine": (*View).DuplicateLine,
"DeleteLine": (*View).DeleteLine,
"MoveLinesUp": (*View).MoveLinesUp,
"MoveLinesDown": (*View).MoveLinesDown,
"IndentSelection": (*View).IndentSelection,
"OutdentSelection": (*View).OutdentSelection,
"OutdentLine": (*View).OutdentLine,
"Paste": (*View).Paste,
"PastePrimary": (*View).PastePrimary,
"SelectAll": (*View).SelectAll,
"OpenFile": (*View).OpenFile,
"Start": (*View).Start,
"End": (*View).End,
"PageUp": (*View).PageUp,
"PageDown": (*View).PageDown,
"HalfPageUp": (*View).HalfPageUp,
"HalfPageDown": (*View).HalfPageDown,
"StartOfLine": (*View).StartOfLine,
"EndOfLine": (*View).EndOfLine,
"ToggleHelp": (*View).ToggleHelp,
"ToggleRuler": (*View).ToggleRuler,
"JumpLine": (*View).JumpLine,
"ClearStatus": (*View).ClearStatus,
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"Escape": (*View).Escape,
"Quit": (*View).Quit,
"QuitAll": (*View).QuitAll,
"AddTab": (*View).AddTab,
"PreviousTab": (*View).PreviousTab,
"NextTab": (*View).NextTab,
"NextSplit": (*View).NextSplit,
"PreviousSplit": (*View).PreviousSplit,
"Unsplit": (*View).Unsplit,
"VSplit": (*View).VSplitBinding,
"HSplit": (*View).HSplitBinding,
"ToggleMacro": (*View).ToggleMacro,
"PlayMacro": (*View).PlayMacro,
"Suspend": (*View).Suspend,
// This was changed to InsertNewline but I don't want to break backwards compatibility
"InsertEnter": (*View).InsertNewline,
}
var bindingKeys = map[string]tcell.Key{
"Up": tcell.KeyUp,
"Down": tcell.KeyDown,
"Right": tcell.KeyRight,
"Left": tcell.KeyLeft,
"UpLeft": tcell.KeyUpLeft,
"UpRight": tcell.KeyUpRight,
"DownLeft": tcell.KeyDownLeft,
"DownRight": tcell.KeyDownRight,
"Center": tcell.KeyCenter,
"PageUp": tcell.KeyPgUp,
"PageDown": tcell.KeyPgDn,
"Home": tcell.KeyHome,
"End": tcell.KeyEnd,
"Insert": tcell.KeyInsert,
"Delete": tcell.KeyDelete,
"Help": tcell.KeyHelp,
"Exit": tcell.KeyExit,
"Clear": tcell.KeyClear,
"Cancel": tcell.KeyCancel,
"Print": tcell.KeyPrint,
"Pause": tcell.KeyPause,
"Backtab": tcell.KeyBacktab,
"F1": tcell.KeyF1,
"F2": tcell.KeyF2,
"F3": tcell.KeyF3,
"F4": tcell.KeyF4,
"F5": tcell.KeyF5,
"F6": tcell.KeyF6,
"F7": tcell.KeyF7,
"F8": tcell.KeyF8,
"F9": tcell.KeyF9,
"F10": tcell.KeyF10,
"F11": tcell.KeyF11,
"F12": tcell.KeyF12,
"F13": tcell.KeyF13,
"F14": tcell.KeyF14,
"F15": tcell.KeyF15,
"F16": tcell.KeyF16,
"F17": tcell.KeyF17,
"F18": tcell.KeyF18,
"F19": tcell.KeyF19,
"F20": tcell.KeyF20,
"F21": tcell.KeyF21,
"F22": tcell.KeyF22,
"F23": tcell.KeyF23,
"F24": tcell.KeyF24,
"F25": tcell.KeyF25,
"F26": tcell.KeyF26,
"F27": tcell.KeyF27,
"F28": tcell.KeyF28,
"F29": tcell.KeyF29,
"F30": tcell.KeyF30,
"F31": tcell.KeyF31,
"F32": tcell.KeyF32,
"F33": tcell.KeyF33,
"F34": tcell.KeyF34,
"F35": tcell.KeyF35,
"F36": tcell.KeyF36,
"F37": tcell.KeyF37,
"F38": tcell.KeyF38,
"F39": tcell.KeyF39,
"F40": tcell.KeyF40,
"F41": tcell.KeyF41,
"F42": tcell.KeyF42,
"F43": tcell.KeyF43,
"F44": tcell.KeyF44,
"F45": tcell.KeyF45,
"F46": tcell.KeyF46,
"F47": tcell.KeyF47,
"F48": tcell.KeyF48,
"F49": tcell.KeyF49,
"F50": tcell.KeyF50,
"F51": tcell.KeyF51,
"F52": tcell.KeyF52,
"F53": tcell.KeyF53,
"F54": tcell.KeyF54,
"F55": tcell.KeyF55,
"F56": tcell.KeyF56,
"F57": tcell.KeyF57,
"F58": tcell.KeyF58,
"F59": tcell.KeyF59,
"F60": tcell.KeyF60,
"F61": tcell.KeyF61,
"F62": tcell.KeyF62,
"F63": tcell.KeyF63,
"F64": tcell.KeyF64,
"CtrlSpace": tcell.KeyCtrlSpace,
"CtrlA": tcell.KeyCtrlA,
"CtrlB": tcell.KeyCtrlB,
"CtrlC": tcell.KeyCtrlC,
"CtrlD": tcell.KeyCtrlD,
"CtrlE": tcell.KeyCtrlE,
"CtrlF": tcell.KeyCtrlF,
"CtrlG": tcell.KeyCtrlG,
"CtrlH": tcell.KeyCtrlH,
"CtrlI": tcell.KeyCtrlI,
"CtrlJ": tcell.KeyCtrlJ,
"CtrlK": tcell.KeyCtrlK,
"CtrlL": tcell.KeyCtrlL,
"CtrlM": tcell.KeyCtrlM,
"CtrlN": tcell.KeyCtrlN,
"CtrlO": tcell.KeyCtrlO,
"CtrlP": tcell.KeyCtrlP,
"CtrlQ": tcell.KeyCtrlQ,
"CtrlR": tcell.KeyCtrlR,
"CtrlS": tcell.KeyCtrlS,
"CtrlT": tcell.KeyCtrlT,
"CtrlU": tcell.KeyCtrlU,
"CtrlV": tcell.KeyCtrlV,
"CtrlW": tcell.KeyCtrlW,
"CtrlX": tcell.KeyCtrlX,
"CtrlY": tcell.KeyCtrlY,
"CtrlZ": tcell.KeyCtrlZ,
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
"CtrlBackslash": tcell.KeyCtrlBackslash,
"CtrlRightSq": tcell.KeyCtrlRightSq,
"CtrlCarat": tcell.KeyCtrlCarat,
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
"Tab": tcell.KeyTab,
"Esc": tcell.KeyEsc,
"Escape": tcell.KeyEscape,
"Enter": tcell.KeyEnter,
"Backspace": tcell.KeyBackspace2,
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
"PgUp": tcell.KeyPgUp,
"PgDown": tcell.KeyPgDn,
}
// The Key struct holds the data for a keypress (keycode + modifiers)
type Key struct {
keyCode tcell.Key
modifiers tcell.ModMask
r rune
}
// InitBindings initializes the keybindings for micro
func InitBindings() {
bindings = make(map[Key][]func(*View, bool) bool)
var parsed map[string]string
defaults := DefaultBindings()
filename := configDir + "/bindings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading bindings.json file: " + err.Error())
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading bindings.json:", err.Error())
}
}
parseBindings(defaults)
parseBindings(parsed)
}
func parseBindings(userBindings map[string]string) {
for k, v := range userBindings {
BindKey(k, v)
}
}
// findKey will find binding Key 'b' using string 'k'
func findKey(k string) (b Key, ok bool) {
modifiers := tcell.ModNone
// First, we'll strip off all the modifiers in the name and add them to the
// ModMask
modSearch:
for {
switch {
case strings.HasPrefix(k, "-"):
// We optionally support dashes between modifiers
k = k[1:]
case strings.HasPrefix(k, "Ctrl") && k != "CtrlH":
// CtrlH technically does not have a 'Ctrl' modifier because it is really backspace
k = k[4:]
modifiers |= tcell.ModCtrl
case strings.HasPrefix(k, "Alt"):
k = k[3:]
modifiers |= tcell.ModAlt
case strings.HasPrefix(k, "Shift"):
k = k[5:]
modifiers |= tcell.ModShift
default:
break modSearch
}
}
// Control is handled specially, since some character codes in bindingKeys
// are different when Control is depressed. We should check for Control keys
// first.
if modifiers&tcell.ModCtrl != 0 {
// see if the key is in bindingKeys with the Ctrl prefix.
if code, ok := bindingKeys["Ctrl"+k]; ok {
// It is, we're done.
return Key{
keyCode: code,
modifiers: modifiers,
r: 0,
}, true
}
}
// See if we can find the key in bindingKeys
if code, ok := bindingKeys[k]; ok {
return Key{
keyCode: code,
modifiers: modifiers,
r: 0,
}, true
}
// If we were given one character, then we've got a rune.
if len(k) == 1 {
return Key{
keyCode: tcell.KeyRune,
modifiers: modifiers,
r: rune(k[0]),
}, true
}
// We don't know what happened.
return Key{}, false
}
// findAction will find 'action' using string 'v'
func findAction(v string) (action func(*View, bool) bool) {
action, ok := bindingActions[v]
if !ok {
// If the user seems to be binding a function that doesn't exist
// We hope that it's a lua function that exists and bind it to that
action = LuaFunctionBinding(v)
}
return action
}
// BindKey takes a key and an action and binds the two together
func BindKey(k, v string) {
key, ok := findKey(k)
if !ok {
TermMessage("Unknown keybinding: " + k)
return
}
if v == "ToggleHelp" {
helpBinding = k
}
if helpBinding == k && v != "ToggleHelp" {
helpBinding = ""
}
actionNames := strings.Split(v, ",")
if actionNames[0] == "UnbindKey" {
delete(bindings, key)
if len(actionNames) == 1 {
actionNames = make([]string, 0, 0)
} else {
actionNames = append(actionNames[:0], actionNames[1:]...)
}
}
actions := make([]func(*View, bool) bool, 0, len(actionNames))
for _, actionName := range actionNames {
actions = append(actions, findAction(actionName))
}
bindings[key] = actions
}
// DefaultBindings returns a map containing micro's default keybindings
func DefaultBindings() map[string]string {
return map[string]string{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltUp": "MoveLinesUp",
"AltDown": "MoveLinesDown",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"ShiftHome": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"ShiftEnd": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertNewline",
"CtrlH": "Backspace",
"Backspace": "Backspace",
"Alt-CtrlH": "DeleteWordLeft",
"Alt-Backspace": "DeleteWordLeft",
"Tab": "IndentSelection,InsertTab",
"Backtab": "OutdentSelection,OutdentLine",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"Alt,": "PreviousTab",
"Alt.": "NextTab",
"Home": "StartOfLine",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlG": "ToggleHelp",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
"CtrlU": "ToggleMacro",
"CtrlJ": "PlayMacro",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfLine",
"Alt-e": "EndOfLine",
"Alt-p": "CursorUp",
"Alt-n": "CursorDown",
// Integration with file managers
"F1": "ToggleHelp",
"F2": "Save",
"F3": "Find",
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape",
}
}

535
cmd/micro/buffer.go Normal file
View File

@@ -0,0 +1,535 @@
package main
import (
"bytes"
"encoding/gob"
"io"
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/mitchellh/go-homedir"
"github.com/zyedidia/micro/cmd/micro/highlight"
)
// Buffer stores the text for files that are loaded into the text editor
// It uses a rope to efficiently store the string and contains some
// simple functions for saving and wrapper functions for modifying the rope
type Buffer struct {
// The eventhandler for undo/redo
*EventHandler
// This stores all the text in the buffer as an array of lines
*LineArray
Cursor Cursor
// Path to the file on disk
Path string
// Absolute path to the file on disk
AbsPath string
// Name of the buffer on the status line
name string
// Whether or not the buffer has been modified since it was opened
IsModified bool
// Stores the last modification time of the file the buffer is pointing to
ModTime time.Time
NumLines int
syntaxDef *highlight.Def
highlighter *highlight.Highlighter
// Buffer local settings
Settings map[string]interface{}
}
// The SerializedBuffer holds the types that get serialized when a buffer is saved
// These are used for the savecursor and saveundo options
type SerializedBuffer struct {
EventHandler *EventHandler
Cursor Cursor
ModTime time.Time
}
func NewBufferFromString(text, path string) *Buffer {
return NewBuffer(strings.NewReader(text), int64(len(text)), path)
}
// NewBuffer creates a new buffer from a given reader with a given path
func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
if path != "" {
for _, tab := range tabs {
for _, view := range tab.views {
if view.Buf.Path == path {
return view.Buf
}
}
}
}
b := new(Buffer)
b.LineArray = NewLineArray(size, reader)
b.Settings = DefaultLocalSettings()
for k, v := range globalSettings {
if _, ok := b.Settings[k]; ok {
b.Settings[k] = v
}
}
absPath, _ := filepath.Abs(path)
b.Path = path
b.AbsPath = absPath
// The last time this file was modified
b.ModTime, _ = GetModTime(b.Path)
b.EventHandler = NewEventHandler(b)
b.Update()
b.UpdateRules()
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
os.Mkdir(configDir+"/buffers/", os.ModePerm)
}
// Put the cursor at the first spot
cursorStartX := 0
cursorStartY := 0
// If -startpos LINE,COL was passed, use start position LINE,COL
if len(*flagStartPos) > 0 {
positions := strings.Split(*flagStartPos, ",")
if len(positions) == 2 {
lineNum, errPos1 := strconv.Atoi(positions[0])
colNum, errPos2 := strconv.Atoi(positions[1])
if errPos1 == nil && errPos2 == nil {
cursorStartX = colNum
cursorStartY = lineNum - 1
// Check to avoid line overflow
if cursorStartY > b.NumLines {
cursorStartY = b.NumLines - 1
} else if cursorStartY < 0 {
cursorStartY = 0
}
// Check to avoid column overflow
if cursorStartX > len(b.Line(cursorStartY)) {
cursorStartX = len(b.Line(cursorStartY))
} else if cursorStartX < 0 {
cursorStartX = 0
}
}
}
}
b.Cursor = Cursor{
Loc: Loc{
X: cursorStartX,
Y: cursorStartY,
},
buf: b,
}
InitLocalSettings(b)
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
// If either savecursor or saveundo is turned on, we need to load the serialized information
// from ~/.config/micro/buffers
file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
if err == nil {
var buffer SerializedBuffer
decoder := gob.NewDecoder(file)
gob.Register(TextEvent{})
err = decoder.Decode(&buffer)
if err != nil {
TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
}
if b.Settings["savecursor"].(bool) {
b.Cursor = buffer.Cursor
b.Cursor.buf = b
b.Cursor.Relocate()
}
if b.Settings["saveundo"].(bool) {
// We should only use last time's eventhandler if the file wasn't by someone else in the meantime
if b.ModTime == buffer.ModTime {
b.EventHandler = buffer.EventHandler
b.EventHandler.buf = b
}
}
}
file.Close()
}
return b
}
func (b *Buffer) GetName() string {
if b.name == "" {
if b.Path == "" {
return "No name"
}
return b.Path
}
return b.name
}
// UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes
func (b *Buffer) UpdateRules() {
rehighlight := false
var files []*highlight.File
for _, f := range ListRuntimeFiles(RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
file, err := highlight.ParseFile(data)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ftdetect, err := highlight.ParseFtDetect(file)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ft := b.Settings["filetype"].(string)
if ft == "Unknown" || ft == "" {
if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
header := new(highlight.Header)
header.FileType = file.FileType
header.FtDetect = ftdetect
b.syntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
}
} else {
if file.FileType == ft {
header := new(highlight.Header)
header.FileType = file.FileType
header.FtDetect = ftdetect
b.syntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
}
}
files = append(files, file)
}
}
if b.syntaxDef != nil {
highlight.ResolveIncludes(b.syntaxDef, files)
}
files = nil
if b.highlighter == nil || rehighlight {
if b.syntaxDef != nil {
b.Settings["filetype"] = b.syntaxDef.FileType
b.highlighter = highlight.NewHighlighter(b.syntaxDef)
if b.Settings["syntax"].(bool) {
b.highlighter.HighlightStates(b)
}
}
}
}
// FileType returns the buffer's filetype
func (b *Buffer) FileType() string {
return b.Settings["filetype"].(string)
}
// IndentString returns a string representing one level of indentation
func (b *Buffer) IndentString() string {
if b.Settings["tabstospaces"].(bool) {
return Spaces(int(b.Settings["tabsize"].(float64)))
}
return "\t"
}
// CheckModTime makes sure that the file this buffer points to hasn't been updated
// by an external program since it was last read
// If it has, we ask the user if they would like to reload the file
func (b *Buffer) CheckModTime() {
modTime, ok := GetModTime(b.Path)
if ok {
if modTime != b.ModTime {
choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
messenger.Reset()
messenger.Clear()
if !choice || canceled {
// Don't load new changes -- do nothing
b.ModTime, _ = GetModTime(b.Path)
} else {
// Load new changes
b.ReOpen()
}
}
}
}
// ReOpen reloads the current buffer from disk
func (b *Buffer) ReOpen() {
data, err := ioutil.ReadFile(b.Path)
txt := string(data)
if err != nil {
messenger.Error(err.Error())
return
}
b.EventHandler.ApplyDiff(txt)
b.ModTime, _ = GetModTime(b.Path)
b.IsModified = false
b.Update()
b.Cursor.Relocate()
}
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
func (b *Buffer) Update() {
b.NumLines = len(b.lines)
}
// Save saves the buffer to its default path
func (b *Buffer) Save() error {
return b.SaveAs(b.Path)
}
// SaveWithSudo saves the buffer to the default path with sudo
func (b *Buffer) SaveWithSudo() error {
return b.SaveAsWithSudo(b.Path)
}
// Serialize serializes the buffer to configDir/buffers
func (b *Buffer) Serialize() error {
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
if err == nil {
enc := gob.NewEncoder(file)
gob.Register(TextEvent{})
err = enc.Encode(SerializedBuffer{
b.EventHandler,
b.Cursor,
b.ModTime,
})
}
file.Close()
return err
}
return nil
}
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
func (b *Buffer) SaveAs(filename string) error {
b.UpdateRules()
dir, _ := homedir.Dir()
if b.Settings["rmtrailingws"].(bool) {
r, _ := regexp.Compile(`[ \t]+$`)
for lineNum, line := range b.Lines(0, b.NumLines) {
indices := r.FindStringIndex(line)
if indices == nil {
continue
}
startLoc := Loc{indices[0], lineNum}
b.deleteToEnd(startLoc)
}
b.Cursor.Relocate()
}
if b.Settings["eofnewline"].(bool) {
end := b.End()
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
b.Insert(end, "\n")
}
}
str := b.String()
data := []byte(str)
err := ioutil.WriteFile(filename, data, 0644)
if err == nil {
b.Path = strings.Replace(filename, "~", dir, 1)
b.IsModified = false
b.ModTime, _ = GetModTime(filename)
return b.Serialize()
}
b.ModTime, _ = GetModTime(filename)
return err
}
// SaveAsWithSudo is the same as SaveAs except it uses a neat trick
// with tee to use sudo so the user doesn't have to reopen micro with sudo
func (b *Buffer) SaveAsWithSudo(filename string) error {
b.UpdateRules()
b.Path = filename
// The user may have already used sudo in which case we won't need the password
// It's a bit nicer for them if they don't have to enter the password every time
_, err := RunShellCommand("sudo -v")
needPassword := err != nil
// If we need the password, we have to close the screen and ask using the shell
if needPassword {
// Shut down the screen because we're going to interact directly with the shell
screen.Fini()
screen = nil
}
// Set up everything for the command
cmd := exec.Command("sudo", "tee", filename)
cmd.Stdin = bytes.NewBufferString(b.String())
// This is a trap for Ctrl-C so that it doesn't kill micro
// Instead we trap Ctrl-C to kill the program we're running
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
cmd.Process.Kill()
}
}()
// Start the command
cmd.Start()
err = cmd.Wait()
// If we needed the password, we closed the screen, so we have to initialize it again
if needPassword {
// Start the screen back up
InitScreen()
}
if err == nil {
b.IsModified = false
b.ModTime, _ = GetModTime(filename)
b.Serialize()
}
return err
}
func (b *Buffer) insert(pos Loc, value []byte) {
b.IsModified = true
b.LineArray.insert(pos, value)
b.Update()
}
func (b *Buffer) remove(start, end Loc) string {
b.IsModified = true
sub := b.LineArray.remove(start, end)
b.Update()
return sub
}
func (b *Buffer) deleteToEnd(start Loc) {
b.IsModified = true
b.LineArray.DeleteToEnd(start)
b.Update()
}
// Start returns the location of the first character in the buffer
func (b *Buffer) Start() Loc {
return Loc{0, 0}
}
// End returns the location of the last character in the buffer
func (b *Buffer) End() Loc {
return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
}
// RuneAt returns the rune at a given location in the buffer
func (b *Buffer) RuneAt(loc Loc) rune {
line := []rune(b.Line(loc.Y))
if len(line) > 0 {
return line[loc.X]
}
return '\n'
}
// Line returns a single line
func (b *Buffer) Line(n int) string {
if n >= len(b.lines) {
return ""
}
return string(b.lines[n].data)
}
func (b *Buffer) LinesNum() int {
return len(b.lines)
}
// Lines returns an array of strings containing the lines from start to end
func (b *Buffer) Lines(start, end int) []string {
lines := b.lines[start:end]
var slice []string
for _, line := range lines {
slice = append(slice, string(line.data))
}
return slice
}
// Len gives the length of the buffer
func (b *Buffer) Len() int {
return Count(b.String())
}
// MoveLinesUp moves the range of lines up one row
func (b *Buffer) MoveLinesUp(start int, end int) {
// 0 < start < end <= len(b.lines)
if start < 1 || start >= end || end > len(b.lines) {
return // what to do? FIXME
}
if end == len(b.lines) {
b.Insert(
Loc{
utf8.RuneCount(b.lines[end-1].data),
end - 1,
},
"\n"+b.Line(start-1),
)
} else {
b.Insert(
Loc{0, end},
b.Line(start-1)+"\n",
)
}
b.Remove(
Loc{0, start - 1},
Loc{0, start},
)
}
// MoveLinesDown moves the range of lines down one row
func (b *Buffer) MoveLinesDown(start int, end int) {
// 0 <= start < end < len(b.lines)
// if end == len(b.lines), we can't do anything here because the
// last line is unaccessible, FIXME
if start < 0 || start >= end || end >= len(b.lines)-1 {
return // what to do? FIXME
}
b.Insert(
Loc{0, start},
b.Line(end)+"\n",
)
end++
b.Remove(
Loc{0, end},
Loc{0, end + 1},
)
}
// ClearMatches clears all of the syntax highlighting for this buffer
func (b *Buffer) ClearMatches() {
for i := range b.lines {
b.SetMatch(i, nil)
b.SetState(i, nil)
}
}

207
cmd/micro/cellview.go Normal file
View File

@@ -0,0 +1,207 @@
package main
import (
"github.com/mattn/go-runewidth"
"github.com/zyedidia/tcell"
)
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func visualToCharPos(visualIndex int, lineN int, str string, buf *Buffer, tabsize int) (int, int, *tcell.Style) {
charPos := 0
var lineIdx int
var lastWidth int
var style *tcell.Style
var width int
var rw int
for i, c := range str {
// width := StringWidth(str[:i], tabsize)
if group, ok := buf.Match(lineN)[charPos]; ok {
s := GetColor(group.String())
style = &s
}
if width >= visualIndex {
return charPos, visualIndex - lastWidth, style
}
if i != 0 {
charPos++
lineIdx += rw
}
lastWidth = width
rw = 0
if c == '\t' {
rw = tabsize - (lineIdx % tabsize)
width += rw
} else {
rw = runewidth.RuneWidth(c)
width += rw
}
}
return -1, -1, style
}
type Char struct {
visualLoc Loc
realLoc Loc
char rune
// The actual character that is drawn
// This is only different from char if it's for example hidden character
drawChar rune
style tcell.Style
width int
}
type CellView struct {
lines [][]*Char
}
func (c *CellView) Draw(buf *Buffer, top, height, left, width int) {
tabsize := int(buf.Settings["tabsize"].(float64))
softwrap := buf.Settings["softwrap"].(bool)
indentrunes := []rune(buf.Settings["indentchar"].(string))
// if empty indentchar settings, use space
if indentrunes == nil || len(indentrunes) == 0 {
indentrunes = []rune(" ")
}
indentchar := indentrunes[0]
start := buf.Cursor.Y
if buf.Settings["syntax"].(bool) && buf.syntaxDef != nil {
if start > 0 && buf.lines[start-1].rehighlight {
buf.highlighter.ReHighlightLine(buf, start-1)
buf.lines[start-1].rehighlight = false
}
buf.highlighter.ReHighlightStates(buf, start)
buf.highlighter.HighlightMatches(buf, top, top+height)
}
c.lines = make([][]*Char, 0)
viewLine := 0
lineN := top
curStyle := defStyle
for viewLine < height {
if lineN >= len(buf.lines) {
break
}
lineStr := buf.Line(lineN)
line := []rune(lineStr)
colN, startOffset, startStyle := visualToCharPos(left, lineN, lineStr, buf, tabsize)
if colN < 0 {
colN = len(line)
}
viewCol := -startOffset
if startStyle != nil {
curStyle = *startStyle
}
// We'll either draw the length of the line, or the width of the screen
// whichever is smaller
lineLength := min(StringWidth(lineStr, tabsize), width)
c.lines = append(c.lines, make([]*Char, lineLength))
wrap := false
// We only need to wrap if the length of the line is greater than the width of the terminal screen
if softwrap && StringWidth(lineStr, tabsize) > width {
wrap = true
// We're going to draw the entire line now
lineLength = StringWidth(lineStr, tabsize)
}
for viewCol < lineLength {
if colN >= len(line) {
break
}
if group, ok := buf.Match(lineN)[colN]; ok {
curStyle = GetColor(group.String())
}
char := line[colN]
if viewCol >= 0 {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, char, curStyle, 1}
}
if char == '\t' {
charWidth := tabsize - (viewCol+left)%tabsize
if viewCol >= 0 {
c.lines[viewLine][viewCol].drawChar = indentchar
c.lines[viewLine][viewCol].width = charWidth
indentStyle := curStyle
if group, ok := colorscheme["indent-char"]; ok {
indentStyle = group
}
c.lines[viewLine][viewCol].style = indentStyle
}
for i := 1; i < charWidth; i++ {
viewCol++
if viewCol >= 0 && viewCol < lineLength {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1}
}
}
viewCol++
} else if runewidth.RuneWidth(char) > 1 {
charWidth := runewidth.RuneWidth(char)
if viewCol >= 0 {
c.lines[viewLine][viewCol].width = charWidth
}
for i := 1; i < charWidth; i++ {
viewCol++
if viewCol >= 0 && viewCol < lineLength {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1}
}
}
viewCol++
} else {
viewCol++
}
colN++
if wrap && viewCol >= width {
viewLine++
// If we go too far soft wrapping we have to cut off
if viewLine >= height {
break
}
nextLine := line[colN:]
lineLength := min(StringWidth(string(nextLine), tabsize), width)
c.lines = append(c.lines, make([]*Char, lineLength))
viewCol = 0
}
}
if group, ok := buf.Match(lineN)[len(line)]; ok {
curStyle = GetColor(group.String())
}
// newline
viewLine++
lineN++
}
for i := top; i < top+height; i++ {
if i >= buf.NumLines {
break
}
buf.SetMatch(i, nil)
}
}

View File

@@ -1,143 +0,0 @@
package main
import (
"bufio"
"encoding/gob"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
)
func shouldContinue() bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Continue [Y/n]: ")
text, err := reader.ReadString('\n')
if err != nil {
fmt.Println(err)
return false
}
if len(text) <= 1 {
// default continue
return true
}
return strings.ToLower(text)[0] == 'y'
}
// CleanConfig performs cleanup in the user's configuration directory
func CleanConfig() {
fmt.Println("Cleaning your configuration directory at", config.ConfigDir)
fmt.Printf("Please consider backing up %s before continuing\n", config.ConfigDir)
if !shouldContinue() {
fmt.Println("Stopping early")
return
}
// detect unused options
var unusedOptions []string
defaultSettings := config.DefaultAllSettings()
for k := range config.GlobalSettings {
if _, ok := defaultSettings[k]; !ok {
valid := false
for _, p := range config.Plugins {
if strings.HasPrefix(k, p.Name+".") || k == p.Name {
valid = true
}
}
if !valid {
unusedOptions = append(unusedOptions, k)
}
}
}
if len(unusedOptions) > 0 {
fmt.Println("The following options are unused:")
sort.Strings(unusedOptions)
for _, s := range unusedOptions {
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
}
fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json"))
if shouldContinue() {
for _, s := range unusedOptions {
delete(config.GlobalSettings, s)
}
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
if err != nil {
fmt.Println("Error writing settings.json file: " + err.Error())
}
fmt.Println("Removed unused options")
fmt.Print("\n\n")
}
}
// detect incorrectly formatted buffer/ files
files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers"))
if err == nil {
var badFiles []string
var buffer buffer.SerializedBuffer
for _, f := range files {
fname := filepath.Join(config.ConfigDir, "buffers", f.Name())
file, e := os.Open(fname)
if e == nil {
defer file.Close()
decoder := gob.NewDecoder(file)
err = decoder.Decode(&buffer)
if err != nil && f.Name() != "history" {
badFiles = append(badFiles, fname)
}
}
}
if len(badFiles) > 0 {
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers"))
fmt.Println("These files store cursor and undo history.")
fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers"))
if shouldContinue() {
for _, f := range badFiles {
err := os.Remove(f)
if err != nil {
fmt.Println(err)
continue
}
}
fmt.Println("Removed badly formatted files")
fmt.Print("\n\n")
}
}
}
// detect plugins/ directory
plugins := filepath.Join(config.ConfigDir, "plugins")
if stat, err := os.Stat(plugins); err == nil && stat.IsDir() {
fmt.Printf("Found directory %s\n", plugins)
fmt.Printf("Plugins should now be stored in %s\n", filepath.Join(config.ConfigDir, "plug"))
fmt.Printf("Removing %s\n", plugins)
if shouldContinue() {
os.RemoveAll(plugins)
}
fmt.Print("\n\n")
}
fmt.Println("Done cleaning")
}

View File

@@ -1,7 +1,7 @@
package config
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
@@ -9,15 +9,15 @@ import (
"github.com/zyedidia/tcell"
)
// Micro's default style
var DefStyle tcell.Style = tcell.StyleDefault
// Colorscheme is a map from string to style -- it represents a colorscheme
type Colorscheme map[string]tcell.Style
// The current colorscheme
var Colorscheme map[string]tcell.Style
var colorscheme Colorscheme
// GetColor takes in a syntax group and returns the colorscheme's style for that group
// This takes in a syntax group and returns the colorscheme's style for that group
func GetColor(color string) tcell.Style {
st := DefStyle
st := defStyle
if color == "" {
return st
}
@@ -29,11 +29,11 @@ func GetColor(color string) tcell.Style {
curGroup += "."
}
curGroup += g
if style, ok := Colorscheme[curGroup]; ok {
if style, ok := colorscheme[curGroup]; ok {
st = style
}
}
} else if style, ok := Colorscheme[color]; ok {
} else if style, ok := colorscheme[color]; ok {
st = style
} else {
st = StringToStyle(color)
@@ -48,46 +48,47 @@ func ColorschemeExists(colorschemeName string) bool {
}
// InitColorscheme picks and initializes the colorscheme when micro starts
func InitColorscheme() error {
Colorscheme = make(map[string]tcell.Style)
DefStyle = tcell.StyleDefault
func InitColorscheme() {
colorscheme = make(Colorscheme)
defStyle = tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
if screen != nil {
screen.SetStyle(defStyle)
}
return LoadDefaultColorscheme()
LoadDefaultColorscheme()
}
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
func LoadDefaultColorscheme() error {
return LoadColorscheme(GlobalSettings["colorscheme"].(string))
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
func LoadDefaultColorscheme() {
LoadColorscheme(globalSettings["colorscheme"].(string))
}
// LoadColorscheme loads the given colorscheme from a directory
func LoadColorscheme(colorschemeName string) error {
func LoadColorscheme(colorschemeName string) {
file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil {
return errors.New(colorschemeName + " is not a valid colorscheme")
}
if data, err := file.Data(); err != nil {
return errors.New("Error loading colorscheme: " + err.Error())
TermMessage(colorschemeName, "is not a valid colorscheme")
} else {
Colorscheme, err = ParseColorscheme(string(data))
if err != nil {
return err
if data, err := file.Data(); err != nil {
TermMessage("Error loading colorscheme:", err)
} else {
colorscheme = ParseColorscheme(string(data))
}
}
return 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) {
var err error
func ParseColorscheme(text string) Colorscheme {
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
lines := strings.Split(text, "\n")
c := make(map[string]tcell.Style)
c := make(Colorscheme)
for _, line := range lines {
if strings.TrimSpace(line) == "" ||
@@ -105,14 +106,17 @@ func ParseColorscheme(text string) (map[string]tcell.Style, error) {
c[link] = style
if link == "default" {
DefStyle = style
defStyle = style
}
if screen != nil {
screen.SetStyle(defStyle)
}
} else {
err = errors.New("Color-link statement is not valid: " + line)
fmt.Println("Color-link statement is not valid:", line)
}
}
return c, err
return c
}
// StringToStyle returns a style from a string
@@ -137,23 +141,20 @@ func StringToStyle(str string) tcell.Style {
var fgColor, bgColor tcell.Color
if fg == "" {
fgColor, _, _ = DefStyle.Decompose()
fgColor, _, _ = defStyle.Decompose()
} else {
fgColor = StringToColor(fg)
}
if bg == "" {
_, bgColor, _ = DefStyle.Decompose()
_, bgColor, _ = defStyle.Decompose()
} else {
bgColor = StringToColor(bg)
}
style := DefStyle.Foreground(fgColor).Background(bgColor)
style := defStyle.Foreground(fgColor).Background(bgColor)
if strings.Contains(str, "bold") {
style = style.Bold(true)
}
if strings.Contains(str, "italic") {
style = style.Italic(true)
}
if strings.Contains(str, "reverse") {
style = style.Reverse(true)
}
@@ -251,9 +252,5 @@ func GetColor256(color int) tcell.Color {
tcell.Color253, tcell.Color254, tcell.Color255,
}
if color >= 0 && color < len(colors) {
return colors[color]
}
return tcell.ColorDefault
return colors[color]
}

693
cmd/micro/command.go Normal file
View File

@@ -0,0 +1,693 @@
package main
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
humanize "github.com/dustin/go-humanize"
"github.com/mitchellh/go-homedir"
)
// A Command contains a action (a function to call) as well as information about how to autocomplete the command
type Command struct {
action func([]string)
completions []Completion
}
// A StrCommand is similar to a command but keeps the name of the action
type StrCommand struct {
action string
completions []Completion
}
var commands map[string]Command
var commandActions map[string]func([]string)
func init() {
commandActions = map[string]func([]string){
"Set": Set,
"SetLocal": SetLocal,
"Show": Show,
"Run": Run,
"Bind": Bind,
"Quit": Quit,
"Save": Save,
"Replace": Replace,
"VSplit": VSplit,
"HSplit": HSplit,
"Tab": NewTab,
"Help": Help,
"Eval": Eval,
"ToggleLog": ToggleLog,
"Plugin": PluginCmd,
"Reload": Reload,
"Cd": Cd,
"Pwd": Pwd,
"Open": Open,
"TabSwitch": TabSwitch,
"MemUsage": MemUsage,
}
}
// InitCommands initializes the default commands
func InitCommands() {
commands = make(map[string]Command)
defaults := DefaultCommands()
parseCommands(defaults)
}
func parseCommands(userCommands map[string]StrCommand) {
for k, v := range userCommands {
MakeCommand(k, v.action, v.completions...)
}
}
// MakeCommand is a function to easily create new commands
// This can be called by plugins in Lua so that plugins can define their own commands
func MakeCommand(name, function string, completions ...Completion) {
action := commandActions[function]
if _, ok := commandActions[function]; !ok {
// If the user seems to be binding a function that doesn't exist
// We hope that it's a lua function that exists and bind it to that
action = LuaFunctionCommand(function)
}
commands[name] = Command{action, completions}
}
// DefaultCommands returns a map containing micro's default commands
func DefaultCommands() map[string]StrCommand {
return map[string]StrCommand{
"set": {"Set", []Completion{OptionCompletion, NoCompletion}},
"setlocal": {"SetLocal", []Completion{OptionCompletion, NoCompletion}},
"show": {"Show", []Completion{OptionCompletion, NoCompletion}},
"bind": {"Bind", []Completion{NoCompletion}},
"run": {"Run", []Completion{NoCompletion}},
"quit": {"Quit", []Completion{NoCompletion}},
"save": {"Save", []Completion{NoCompletion}},
"replace": {"Replace", []Completion{NoCompletion}},
"vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}},
"hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}},
"tab": {"Tab", []Completion{FileCompletion, NoCompletion}},
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
"eval": {"Eval", []Completion{NoCompletion}},
"log": {"ToggleLog", []Completion{NoCompletion}},
"plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
"reload": {"Reload", []Completion{NoCompletion}},
"cd": {"Cd", []Completion{FileCompletion}},
"pwd": {"Pwd", []Completion{NoCompletion}},
"open": {"Open", []Completion{FileCompletion}},
"tabswitch": {"TabSwitch", []Completion{NoCompletion}},
"memusage": {"MemUsage", []Completion{NoCompletion}},
}
}
// PluginCmd installs, removes, updates, lists, or searches for given plugins
func PluginCmd(args []string) {
if len(args) >= 1 {
switch args[0] {
case "install":
installedVersions := GetInstalledVersions(false)
for _, plugin := range args[1:] {
pp := GetAllPluginPackages().Get(plugin)
if pp == nil {
messenger.Error("Unknown plugin \"" + plugin + "\"")
} else if err := pp.IsInstallable(); err != nil {
messenger.Error("Error installing ", plugin, ": ", err)
} else {
for _, installed := range installedVersions {
if pp.Name == installed.pack.Name {
if pp.Versions[0].Version.Compare(installed.Version) == 1 {
messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
} else {
messenger.Error(pp.Name, " is already installed")
}
}
}
pp.Install()
}
}
case "remove":
removed := ""
for _, plugin := range args[1:] {
// check if the plugin exists.
if _, ok := loadedPlugins[plugin]; ok {
UninstallPlugin(plugin)
removed += plugin + " "
continue
}
}
if !IsSpaces(removed) {
messenger.Message("Removed ", removed)
} else {
messenger.Error("The requested plugins do not exist")
}
case "update":
UpdatePlugins(args[1:])
case "list":
plugins := GetInstalledVersions(false)
messenger.AddLog("----------------")
messenger.AddLog("The following plugins are currently installed:\n")
for _, p := range plugins {
messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
}
messenger.AddLog("----------------")
if len(plugins) > 0 {
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
case "search":
plugins := SearchPlugin(args[1:])
messenger.Message(len(plugins), " plugins found")
for _, p := range plugins {
messenger.AddLog("----------------")
messenger.AddLog(p.String())
}
messenger.AddLog("----------------")
if len(plugins) > 0 {
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
case "available":
packages := GetAllPluginPackages()
messenger.AddLog("Available Plugins:")
for _, pkg := range packages {
messenger.AddLog(pkg.Name)
}
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
} else {
messenger.Error("Not enough arguments")
}
}
// TabSwitch switches to a given tab either by name or by number
func TabSwitch(args []string) {
if len(args) > 0 {
num, err := strconv.Atoi(args[0])
if err != nil {
// Check for tab with this name
found := false
for _, t := range tabs {
v := t.views[t.CurView]
if v.Buf.GetName() == args[0] {
curTab = v.TabNum
found = true
}
}
if !found {
messenger.Error("Could not find tab: ", err)
}
} else {
num--
if num >= 0 && num < len(tabs) {
curTab = num
} else {
messenger.Error("Invalid tab index")
}
}
}
}
// Cd changes the current working directory
func Cd(args []string) {
if len(args) > 0 {
home, _ := homedir.Dir()
path := strings.Replace(args[0], "~", home, 1)
os.Chdir(path)
for _, tab := range tabs {
for _, view := range tab.views {
wd, _ := os.Getwd()
view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd)
if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) {
view.Buf.Path = view.Buf.AbsPath
}
}
}
}
}
// MemUsage prints micro's memory usage
// Alloc shows how many bytes are currently in use
// Sys shows how many bytes have been requested from the operating system
// NumGC shows how many times the GC has been run
// Note that Go commonly reserves more memory from the OS than is currently in-use/required
// Additionally, even if Go returns memory to the OS, the OS does not always claim it because
// there may be plenty of memory to spare
func MemUsage(args []string) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
messenger.Message(fmt.Sprintf("Alloc: %v, Sys: %v, NumGC: %v", humanize.Bytes(mem.Alloc), humanize.Bytes(mem.Sys), mem.NumGC))
}
// Pwd prints the current working directory
func Pwd(args []string) {
wd, err := os.Getwd()
if err != nil {
messenger.Message(err.Error())
} else {
messenger.Message(wd)
}
}
// Open opens a new buffer with a given filename
func Open(args []string) {
if len(args) > 0 {
filename := args[0]
// the filename might or might not be quoted, so unquote first then join the strings.
filename = strings.Join(SplitCommandArgs(filename), " ")
CurView().Open(filename)
} else {
messenger.Error("No filename")
}
}
// ToggleLog toggles the log view
func ToggleLog(args []string) {
buffer := messenger.getBuffer()
if CurView().Type != vtLog {
CurView().HSplit(buffer)
CurView().Type = vtLog
RedrawAll()
buffer.Cursor.Loc = buffer.Start()
CurView().Relocate()
buffer.Cursor.Loc = buffer.End()
CurView().Relocate()
} else {
CurView().Quit(true)
}
}
// Reload reloads all files (syntax files, colorschemes...)
func Reload(args []string) {
LoadAll()
}
// Help tries to open the given help page in a horizontal split
func Help(args []string) {
if len(args) < 1 {
// Open the default help if the user just typed "> help"
CurView().openHelp("help")
} else {
helpPage := args[0]
if FindRuntimeFile(RTHelp, helpPage) != nil {
CurView().openHelp(helpPage)
} else {
messenger.Error("Sorry, no help for ", helpPage)
}
}
}
// VSplit opens a vertical split with file given in the first argument
// If no file is given, it opens an empty buffer in a new split
func VSplit(args []string) {
if len(args) == 0 {
CurView().VSplit(NewBufferFromString("", ""))
} else {
filename := args[0]
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
CurView().VSplit(buf)
}
}
// HSplit opens a horizontal split with file given in the first argument
// If no file is given, it opens an empty buffer in a new split
func HSplit(args []string) {
if len(args) == 0 {
CurView().HSplit(NewBufferFromString("", ""))
} else {
filename := args[0]
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
CurView().HSplit(buf)
}
}
// Eval evaluates a lua expression
func Eval(args []string) {
if len(args) >= 1 {
err := L.DoString(args[0])
if err != nil {
messenger.Error(err)
}
} else {
messenger.Error("Not enough arguments")
}
}
// NewTab opens the given file in a new tab
func NewTab(args []string) {
if len(args) == 0 {
CurView().AddTab(true)
} else {
filename := args[0]
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
tab := NewTabFromView(NewView(buf))
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
curTab = len(tabs) - 1
if len(tabs) == 2 {
for _, t := range tabs {
for _, v := range t.views {
v.ToggleTabbar()
}
}
}
}
}
// Set sets an option
func Set(args []string) {
if len(args) < 2 {
messenger.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
SetOptionAndSettings(option, value)
}
// SetLocal sets an option local to the buffer
func SetLocal(args []string) {
if len(args) < 2 {
messenger.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
err := SetLocalOption(option, value, CurView())
if err != nil {
messenger.Error(err.Error())
}
}
// Show shows the value of the given option
func Show(args []string) {
if len(args) < 1 {
messenger.Error("Please provide an option to show")
return
}
option := GetOption(args[0])
if option == nil {
messenger.Error(args[0], " is not a valid option")
return
}
messenger.Message(option)
}
// Bind creates a new keybinding
func Bind(args []string) {
if len(args) < 2 {
messenger.Error("Not enough arguments")
return
}
BindKey(args[0], args[1])
}
// Run runs a shell command in the background
func Run(args []string) {
// Run a shell command in the background (openTerm is false)
HandleShellCommand(JoinCommandArgs(args...), false, true)
}
// Quit closes the main view
func Quit(args []string) {
// Close the main view
CurView().Quit(true)
}
// Save saves the buffer in the main view
func Save(args []string) {
if len(args) == 0 {
// Save the main view
CurView().Save(true)
} else {
CurView().Buf.SaveAs(args[0])
}
}
// Replace runs search and replace
func Replace(args []string) {
if len(args) < 2 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
var flags string
if len(args) == 3 {
// The user included some flags
flags = args[2]
}
search := string(args[0])
replace := string(args[1])
regex, err := regexp.Compile("(?m)" + search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
return
}
view := CurView()
found := 0
if strings.Contains(flags, "c") {
for {
// The 'check' flag was used
Search(search, view, true)
if !view.Cursor.HasSelection() {
break
}
view.Relocate()
RedrawAll()
choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
if canceled {
if view.Cursor.HasSelection() {
view.Cursor.Loc = view.Cursor.CurSelection[0]
view.Cursor.ResetSelection()
}
messenger.Reset()
break
}
if choice {
view.Cursor.DeleteSelection()
view.Buf.Insert(view.Cursor.Loc, replace)
view.Cursor.ResetSelection()
messenger.Reset()
found++
} else {
if view.Cursor.HasSelection() {
searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
} else {
searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
}
continue
}
}
} else {
// var deltas []Delta
for i := 0; i < view.Buf.LinesNum(); i++ {
// view.Buf.lines[i].data = regex.ReplaceAll(view.Buf.lines[i].data, []byte(replace))
for {
m := regex.FindIndex(view.Buf.lines[i].data)
if m != nil {
from := Loc{m[0], i}
to := Loc{m[1], i}
// deltas = append(deltas, Delta{replace, from, to})
view.Buf.Replace(from, to, replace)
found++
} else {
break
}
}
}
// view.Buf.MultipleReplace(deltas)
}
view.Cursor.Relocate()
if found > 1 {
messenger.Message("Replaced ", found, " occurrences of ", search)
} else if found == 1 {
messenger.Message("Replaced ", found, " occurrence of ", search)
} else {
messenger.Message("Nothing matched ", search)
}
}
// RunShellCommand executes a shell command and returns the output/error
func RunShellCommand(input string) (string, error) {
inputCmd := SplitCommandArgs(input)[0]
args := SplitCommandArgs(input)[1:]
cmd := exec.Command(inputCmd, args...)
outputBytes := &bytes.Buffer{}
cmd.Stdout = outputBytes
cmd.Stderr = outputBytes
cmd.Start()
err := cmd.Wait() // wait for command to finish
outstring := outputBytes.String()
return outstring, err
}
// HandleShellCommand runs the shell command
// The openTerm argument specifies whether a terminal should be opened (for viewing output
// or interacting with stdin)
func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
inputCmd := SplitCommandArgs(input)[0]
if !openTerm {
// Simply run the command in the background and notify the user when it's done
messenger.Message("Running...")
go func() {
output, err := RunShellCommand(input)
totalLines := strings.Split(output, "\n")
if len(totalLines) < 3 {
if err == nil {
messenger.Message(inputCmd, " exited without error")
} else {
messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
}
} else {
messenger.Message(output)
}
// We have to make sure to redraw
RedrawAll()
}()
} else {
// Shut down the screen because we're going to interact directly with the shell
screen.Fini()
screen = nil
args := SplitCommandArgs(input)[1:]
// Set up everything for the command
var outputBuf bytes.Buffer
cmd := exec.Command(inputCmd, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf)
cmd.Stderr = os.Stderr
// This is a trap for Ctrl-C so that it doesn't kill micro
// Instead we trap Ctrl-C to kill the program we're running
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
cmd.Process.Kill()
}
}()
cmd.Start()
err := cmd.Wait()
output := outputBuf.String()
if err != nil {
output = err.Error()
}
if waitToFinish {
// This is just so we don't return right away and let the user press enter to return
TermMessage("")
}
// Start the screen back up
InitScreen()
return output
}
return ""
}
// HandleCommand handles input from the user
func HandleCommand(input string) {
args := SplitCommandArgs(input)
inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok {
messenger.Error("Unknown command ", inputCmd)
} else {
commands[inputCmd].action(args[1:])
}
}

View File

@@ -1,21 +1,13 @@
package buffer
package main
import (
"github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/v2/internal/util"
)
import "github.com/zyedidia/clipboard"
// InBounds returns whether the given location is a valid character position in the given buffer
func InBounds(pos Loc, buf *Buffer) bool {
if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > util.CharacterCount(buf.LineBytes(pos.Y)) {
return false
}
return true
}
// The Cursor struct stores the location of the cursor in the buffer
// as well as the selection
// The Cursor struct stores the location of the cursor in the view
// The complicated part about the cursor is storing its location.
// The cursor must be displayed at an x, y location, but since the buffer
// uses a rope to store text, to insert text we must have an index. It
// is also simpler to use character indicies for other tasks such as
// selection.
type Cursor struct {
buf *Buffer
Loc
@@ -29,109 +21,19 @@ type Cursor struct {
// This is used for line and word selection where it is necessary
// to know what the original selection was
OrigSelection [2]Loc
// Which cursor index is this (for multiple cursors)
Num int
}
func NewCursor(b *Buffer, l Loc) *Cursor {
c := &Cursor{
buf: b,
Loc: l,
}
c.StoreVisualX()
return c
}
func (c *Cursor) SetBuf(b *Buffer) {
c.buf = b
}
func (c *Cursor) Buf() *Buffer {
return c.buf
}
// Goto puts the cursor at the given cursor's location and gives
// the current cursor its selection too
// Goto puts the cursor at the given cursor's location and gives the current cursor its selection too
func (c *Cursor) Goto(b Cursor) {
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
}
// GotoLoc puts the cursor at the given cursor's location and gives
// the current cursor its selection too
func (c *Cursor) GotoLoc(l Loc) {
c.X, c.Y = l.X, l.Y
c.StoreVisualX()
}
// GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int {
if c.X <= 0 {
c.X = 0
return 0
}
bytes := c.buf.LineBytes(c.Y)
tabsize := int(c.buf.Settings["tabsize"].(float64))
if c.X > util.CharacterCount(bytes) {
c.X = util.CharacterCount(bytes) - 1
}
return util.StringWidth(bytes, c.X, tabsize)
}
// GetCharPosInLine gets the char position of a visual x y
// coordinate (this is necessary because tabs are 1 char but
// 4 visual spaces)
func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
tabsize := int(c.buf.Settings["tabsize"].(float64))
return util.GetCharPosInLine(b, visualPos, tabsize)
}
// Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() {
c.X = 0
c.LastVisualX = c.GetVisualX()
}
// StartOfText moves the cursor to the first non-whitespace rune of
// the line it is on
func (c *Cursor) StartOfText() {
c.Start()
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
break
}
c.Right()
}
}
// IsStartOfText returns whether the cursor is at the first
// non-whitespace rune of the line it is on
func (c *Cursor) IsStartOfText() bool {
x := 0
for util.IsWhitespace(c.RuneUnder(x)) {
if x == util.CharacterCount(c.buf.LineBytes(c.Y)) {
break
}
x++
}
return c.X == x
}
// End moves the cursor to the end of the line it is on
func (c *Cursor) End() {
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
c.LastVisualX = c.GetVisualX()
}
// CopySelection copies the user's selection to either "primary"
// or "clipboard"
// CopySelection copies the user's selection to either "primary" or "clipboard"
func (c *Cursor) CopySelection(target string) {
if c.HasSelection() {
if target != "primary" || c.buf.Settings["useprimary"].(bool) {
clipboard.WriteAll(string(c.GetSelection()), target)
clipboard.WriteAll(c.GetSelection(), target)
}
}
}
@@ -170,30 +72,15 @@ func (c *Cursor) DeleteSelection() {
}
}
// Deselect closes the cursor's current selection
// Start indicates whether the cursor should be placed
// at the start or end of the selection
func (c *Cursor) Deselect(start bool) {
if c.HasSelection() {
if start {
c.Loc = c.CurSelection[0]
} else {
c.Loc = c.CurSelection[1].Move(-1, c.buf)
}
c.ResetSelection()
c.StoreVisualX()
}
}
// GetSelection returns the cursor's selection
func (c *Cursor) GetSelection() []byte {
func (c *Cursor) GetSelection() string {
if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) {
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
}
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
}
return []byte{}
return ""
}
// SelectLine selects the current line
@@ -201,7 +88,7 @@ func (c *Cursor) SelectLine() {
c.Start()
c.SetSelectionStart(c.Loc)
c.End()
if len(c.buf.lines)-1 > c.Y {
if c.buf.NumLines-1 > c.Y {
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
} else {
c.SetSelectionEnd(c.Loc)
@@ -228,23 +115,149 @@ func (c *Cursor) AddLineToSelection() {
}
}
// SelectWord selects the word the cursor is currently on
func (c *Cursor) SelectWord() {
if len(c.buf.Line(c.Y)) == 0 {
return
}
if !IsWordChar(string(c.RuneUnder(c.X))) {
c.SetSelectionStart(c.Loc)
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
c.OrigSelection = c.CurSelection
return
}
forward, backward := c.X, c.X
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.SetSelectionStart(Loc{backward, c.Y})
c.OrigSelection[0] = c.CurSelection[0]
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
forward++
}
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.OrigSelection[1] = c.CurSelection[1]
c.Loc = c.CurSelection[1]
}
// AddWordToSelection adds the word the cursor is currently on to the selection
func (c *Cursor) AddWordToSelection() {
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
c.CurSelection = c.OrigSelection
return
}
if c.Loc.LessThan(c.OrigSelection[0]) {
backward := c.X
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.SetSelectionStart(Loc{backward, c.Y})
c.SetSelectionEnd(c.OrigSelection[1])
}
if c.Loc.GreaterThan(c.OrigSelection[1]) {
forward := c.X
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
forward++
}
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.SetSelectionStart(c.OrigSelection[0])
}
c.Loc = c.CurSelection[1]
}
// SelectTo selects from the current cursor location to the given location
func (c *Cursor) SelectTo(loc Loc) {
if loc.GreaterThan(c.OrigSelection[0]) {
c.SetSelectionStart(c.OrigSelection[0])
c.SetSelectionEnd(loc)
} else {
c.SetSelectionStart(loc)
c.SetSelectionEnd(c.OrigSelection[0])
}
}
// WordRight moves the cursor one word to the right
func (c *Cursor) WordRight() {
for IsWhitespace(c.RuneUnder(c.X)) {
if c.X == Count(c.buf.Line(c.Y)) {
c.Right()
return
}
c.Right()
}
c.Right()
for IsWordChar(string(c.RuneUnder(c.X))) {
if c.X == Count(c.buf.Line(c.Y)) {
return
}
c.Right()
}
}
// WordLeft moves the cursor one word to the left
func (c *Cursor) WordLeft() {
c.Left()
for IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Left()
for IsWordChar(string(c.RuneUnder(c.X))) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
}
// RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune {
line := []rune(c.buf.Line(c.Y))
if len(line) == 0 {
return '\n'
}
if x >= len(line) {
return '\n'
} else if x < 0 {
x = 0
}
return line[x]
}
// UpN moves the cursor up N lines (if possible)
func (c *Cursor) UpN(amount int) {
proposedY := c.Y - amount
if proposedY < 0 {
proposedY = 0
} else if proposedY >= len(c.buf.lines) {
proposedY = len(c.buf.lines) - 1
} else if proposedY >= c.buf.NumLines {
proposedY = c.buf.NumLines - 1
}
bytes := c.buf.LineBytes(proposedY)
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
if c.X > util.CharacterCount(bytes) || (amount < 0 && proposedY == c.Y) {
c.X = util.CharacterCount(bytes)
if proposedY == c.Y {
return
}
c.Y = proposedY
runes := []rune(c.buf.Line(c.Y))
c.X = c.GetCharPosInLine(c.Y, c.LastVisualX)
if c.X > len(runes) {
c.X = len(runes)
}
}
// DownN moves the cursor down N lines (if possible)
@@ -262,8 +275,7 @@ func (c *Cursor) Down() {
c.DownN(1)
}
// Left moves the cursor left one cell (if possible) or to
// the previous line if it is at the beginning
// Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
func (c *Cursor) Left() {
if c.Loc == c.buf.Start() {
return
@@ -274,178 +286,69 @@ func (c *Cursor) Left() {
c.Up()
c.End()
}
c.StoreVisualX()
c.LastVisualX = c.GetVisualX()
}
// Right moves the cursor right one cell (if possible) or
// to the next line if it is at the end
// Right moves the cursor right one cell (if possible) or to the next line if it is at the end
func (c *Cursor) Right() {
if c.Loc == c.buf.End() {
return
}
if c.X < util.CharacterCount(c.buf.LineBytes(c.Y)) {
if c.X < Count(c.buf.Line(c.Y)) {
c.X++
} else {
c.Down()
c.Start()
}
c.StoreVisualX()
c.LastVisualX = c.GetVisualX()
}
// Relocate makes sure that the cursor is inside the bounds
// of the buffer If it isn't, it moves it to be within the
// buffer's lines
// End moves the cursor to the end of the line it is on
func (c *Cursor) End() {
c.X = Count(c.buf.Line(c.Y))
c.LastVisualX = c.GetVisualX()
}
// Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() {
c.X = 0
c.LastVisualX = c.GetVisualX()
}
// GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
// Get the tab size
tabSize := int(c.buf.Settings["tabsize"].(float64))
visualLineLen := StringWidth(c.buf.Line(lineNum), tabSize)
if visualPos > visualLineLen {
visualPos = visualLineLen
}
width := WidthOfLargeRunes(c.buf.Line(lineNum), tabSize)
if visualPos >= width {
return visualPos - width
}
return visualPos / tabSize
}
// GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int {
runes := []rune(c.buf.Line(c.Y))
tabSize := int(c.buf.Settings["tabsize"].(float64))
return StringWidth(string(runes[:c.X]), tabSize)
}
// Relocate makes sure that the cursor is inside the bounds of the buffer
// If it isn't, it moves it to be within the buffer's lines
func (c *Cursor) Relocate() {
if c.Y < 0 {
c.Y = 0
} else if c.Y >= len(c.buf.lines) {
c.Y = len(c.buf.lines) - 1
} else if c.Y >= c.buf.NumLines {
c.Y = c.buf.NumLines - 1
}
if c.X < 0 {
c.X = 0
} else if c.X > util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
} else if c.X > Count(c.buf.Line(c.Y)) {
c.X = Count(c.buf.Line(c.Y))
}
}
// SelectWord selects the word the cursor is currently on
func (c *Cursor) SelectWord() {
if len(c.buf.LineBytes(c.Y)) == 0 {
return
}
if !util.IsWordChar(c.RuneUnder(c.X)) {
c.SetSelectionStart(c.Loc)
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
c.OrigSelection = c.CurSelection
return
}
forward, backward := c.X, c.X
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
backward--
}
c.SetSelectionStart(Loc{backward, c.Y})
c.OrigSelection[0] = c.CurSelection[0]
lineLen := util.CharacterCount(c.buf.LineBytes(c.Y)) - 1
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
forward++
}
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.OrigSelection[1] = c.CurSelection[1]
c.Loc = c.CurSelection[1]
}
// AddWordToSelection adds the word the cursor is currently on
// to the selection
func (c *Cursor) AddWordToSelection() {
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
c.CurSelection = c.OrigSelection
return
}
if c.Loc.LessThan(c.OrigSelection[0]) {
backward := c.X
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
backward--
}
c.SetSelectionStart(Loc{backward, c.Y})
c.SetSelectionEnd(c.OrigSelection[1])
}
if c.Loc.GreaterThan(c.OrigSelection[1]) {
forward := c.X
lineLen := util.CharacterCount(c.buf.LineBytes(c.Y)) - 1
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
forward++
}
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.SetSelectionStart(c.OrigSelection[0])
}
c.Loc = c.CurSelection[1]
}
// SelectTo selects from the current cursor location to the given
// location
func (c *Cursor) SelectTo(loc Loc) {
if loc.GreaterThan(c.OrigSelection[0]) {
c.SetSelectionStart(c.OrigSelection[0])
c.SetSelectionEnd(loc)
} else {
c.SetSelectionStart(loc)
c.SetSelectionEnd(c.OrigSelection[0])
}
}
// WordRight moves the cursor one word to the right
func (c *Cursor) WordRight() {
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return
}
c.Right()
}
c.Right()
for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
}
// WordLeft moves the cursor one word to the left
func (c *Cursor) WordLeft() {
c.Left()
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Left()
for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
}
// RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune {
line := c.buf.LineBytes(c.Y)
if len(line) == 0 || x >= util.CharacterCount(line) {
return '\n'
} else if x < 0 {
x = 0
}
i := 0
for len(line) > 0 {
r, _, size := util.DecodeCharacter(line)
line = line[size:]
if i == x {
return r
}
i++
}
return '\n'
}
func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX()
}

View File

@@ -1,31 +0,0 @@
package main
import (
"log"
"os"
"github.com/zyedidia/micro/v2/internal/util"
)
// NullWriter simply sends writes into the void
type NullWriter struct{}
// Write is empty
func (NullWriter) Write(data []byte) (n int, err error) {
return 0, nil
}
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
func InitLog() {
if util.Debug == "ON" {
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
log.SetOutput(f)
log.Println("Micro started")
} else {
log.SetOutput(NullWriter{})
}
}

245
cmd/micro/eventhandler.go Normal file
View File

@@ -0,0 +1,245 @@
package main
import (
"strings"
"time"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/yuin/gopher-lua"
)
const (
// Opposite and undoing events must have opposite values
// TextEventInsert represents an insertion event
TextEventInsert = 1
// TextEventRemove represents a deletion event
TextEventRemove = -1
// TextEventReplace represents a replace event
TextEventReplace = 0
)
// TextEvent holds data for a manipulation on some text that can be undone
type TextEvent struct {
C Cursor
EventType int
Deltas []Delta
Time time.Time
}
type Delta struct {
Text string
Start Loc
End Loc
}
// ExecuteTextEvent runs a text event
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
if t.EventType == TextEventInsert {
for _, d := range t.Deltas {
buf.insert(d.Start, []byte(d.Text))
}
} else if t.EventType == TextEventRemove {
for i, d := range t.Deltas {
t.Deltas[i].Text = buf.remove(d.Start, d.End)
}
} else if t.EventType == TextEventReplace {
for i, d := range t.Deltas {
t.Deltas[i].Text = buf.remove(d.Start, d.End)
buf.insert(d.Start, []byte(d.Text))
}
}
}
// UndoTextEvent undoes a text event
func UndoTextEvent(t *TextEvent, buf *Buffer) {
t.EventType = -t.EventType
ExecuteTextEvent(t, buf)
}
// EventHandler executes text manipulations and allows undoing and redoing
type EventHandler struct {
buf *Buffer
UndoStack *Stack
RedoStack *Stack
}
// NewEventHandler returns a new EventHandler
func NewEventHandler(buf *Buffer) *EventHandler {
eh := new(EventHandler)
eh.UndoStack = new(Stack)
eh.RedoStack = new(Stack)
eh.buf = buf
return eh
}
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
// the buffer equal to that string
// This means that we can transform the buffer into any string and still preserve undo/redo
// through insert and delete events
func (eh *EventHandler) ApplyDiff(new string) {
differ := dmp.New()
diff := differ.DiffMain(eh.buf.String(), new, false)
loc := eh.buf.Start()
for _, d := range diff {
if d.Type == dmp.DiffDelete {
eh.Remove(loc, loc.Move(Count(d.Text), eh.buf))
} else {
if d.Type == dmp.DiffInsert {
eh.Insert(loc, d.Text)
}
loc = loc.Move(Count(d.Text), eh.buf)
}
}
}
// Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start Loc, text string) {
e := &TextEvent{
C: eh.buf.Cursor,
EventType: TextEventInsert,
Deltas: []Delta{Delta{text, start, Loc{0, 0}}},
Time: time.Now(),
}
eh.Execute(e)
e.Deltas[0].End = start.Move(Count(text), eh.buf)
}
// Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end Loc) {
e := &TextEvent{
C: eh.buf.Cursor,
EventType: TextEventRemove,
Deltas: []Delta{Delta{"", start, end}},
Time: time.Now(),
}
eh.Execute(e)
}
// MultipleReplace creates an multiple insertions executes them
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
e := &TextEvent{
C: eh.buf.Cursor,
EventType: TextEventReplace,
Deltas: deltas,
Time: time.Now(),
}
eh.Execute(e)
}
// Replace deletes from start to end and replaces it with the given string
func (eh *EventHandler) Replace(start, end Loc, replace string) {
eh.Remove(start, end)
eh.Insert(start, replace)
}
// Execute a textevent and add it to the undo stack
func (eh *EventHandler) Execute(t *TextEvent) {
if eh.RedoStack.Len() > 0 {
eh.RedoStack = new(Stack)
}
eh.UndoStack.Push(t)
for pl := range loadedPlugins {
ret, err := Call(pl+".onBeforeTextEvent", t)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
return
}
}
ExecuteTextEvent(t, eh.buf)
}
// Undo the first event in the undo stack
func (eh *EventHandler) Undo() {
t := eh.UndoStack.Peek()
if t == nil {
return
}
startTime := t.Time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent()
for {
t = eh.UndoStack.Peek()
if t == nil {
return
}
if startTime-(t.Time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
return
}
startTime = t.Time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent()
}
}
// UndoOneEvent undoes one event
func (eh *EventHandler) UndoOneEvent() {
// This event should be undone
// Pop it off the stack
t := eh.UndoStack.Pop()
if t == nil {
return
}
// Undo it
// Modifies the text event
UndoTextEvent(t, eh.buf)
// Set the cursor in the right place
teCursor := t.C
t.C = eh.buf.Cursor
eh.buf.Cursor.Goto(teCursor)
// Push it to the redo stack
eh.RedoStack.Push(t)
}
// Redo the first event in the redo stack
func (eh *EventHandler) Redo() {
t := eh.RedoStack.Peek()
if t == nil {
return
}
startTime := t.Time.UnixNano() / int64(time.Millisecond)
eh.RedoOneEvent()
for {
t = eh.RedoStack.Peek()
if t == nil {
return
}
if (t.Time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
return
}
eh.RedoOneEvent()
}
}
// RedoOneEvent redoes one event
func (eh *EventHandler) RedoOneEvent() {
t := eh.RedoStack.Pop()
if t == nil {
return
}
// Modifies the text event
UndoTextEvent(t, eh.buf)
teCursor := t.C
t.C = eh.buf.Cursor
eh.buf.Cursor.Goto(teCursor)
eh.UndoStack.Push(t)
}

View File

@@ -2,11 +2,11 @@ package highlight
import "regexp"
// MatchFiletype will use the list of syntax definitions provided and the filename and first line of the file
// DetectFiletype 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) {
if ftdetect[0].MatchString(filename) {
return true
}

View File

@@ -3,52 +3,19 @@ package highlight
import (
"regexp"
"strings"
"unicode/utf8"
)
func sliceStart(slc []byte, index int) []byte {
len := len(slc)
i := 0
totalSize := 0
for totalSize < len {
if i >= index {
return slc[totalSize:]
}
_, _, size := DecodeCharacter(slc[totalSize:])
totalSize += size
i++
}
return slc[totalSize:]
}
func sliceEnd(slc []byte, index int) []byte {
len := len(slc)
i := 0
totalSize := 0
for totalSize < len {
if i >= index {
return slc[:totalSize]
}
_, _, size := DecodeCharacter(slc[totalSize:])
totalSize += size
i++
}
return slc[:totalSize]
}
// RunePos returns the rune index of a given byte index
// This could cause problems if the byte index is between code points
func runePos(p int, str []byte) int {
func runePos(p int, str string) int {
if p < 0 {
return 0
}
if p >= len(str) {
return CharacterCount(str)
return utf8.RuneCountInString(str)
}
return CharacterCount(str[:p])
return utf8.RuneCountInString(str[:p])
}
func combineLineMatch(src, dst LineMatch) LineMatch {
@@ -67,11 +34,9 @@ func combineLineMatch(src, dst LineMatch) LineMatch {
// A State represents the region at the end of a line
type State *region
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
Line(n int) string
LinesNum() int
State(lineN int) State
SetState(lineN int, s State)
@@ -95,7 +60,7 @@ func NewHighlighter(def *Def) *Highlighter {
// color's group (represented as one byte)
type LineMatch map[int]Group
func findIndex(regex *regexp.Regexp, skip *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
func findIndex(regex *regexp.Regexp, skip *regexp.Regexp, str []rune, canMatchStart, canMatchEnd bool) []int {
regexStr := regex.String()
if strings.Contains(regexStr, "^") {
if !canMatchStart {
@@ -110,12 +75,12 @@ func findIndex(regex *regexp.Regexp, skip *regexp.Regexp, str []byte, canMatchSt
var strbytes []byte
if skip != nil {
strbytes = skip.ReplaceAllFunc(str, func(match []byte) []byte {
res := make([]byte, CharacterCount(match))
strbytes = skip.ReplaceAllFunc([]byte(string(str)), func(match []byte) []byte {
res := make([]byte, utf8.RuneCount(match))
return res
})
} else {
strbytes = str
strbytes = []byte(string(str))
}
match := regex.FindIndex(strbytes)
@@ -123,10 +88,10 @@ func findIndex(regex *regexp.Regexp, skip *regexp.Regexp, str []byte, canMatchSt
return nil
}
// return []int{match.Index, match.Index + match.Length}
return []int{runePos(match[0], str), runePos(match[1], str)}
return []int{runePos(match[0], string(str)), runePos(match[1], string(str))}
}
func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
func findAllIndex(regex *regexp.Regexp, str []rune, canMatchStart, canMatchEnd bool) [][]int {
regexStr := regex.String()
if strings.Contains(regexStr, "^") {
if !canMatchStart {
@@ -138,16 +103,17 @@ func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd b
return nil
}
}
matches := regex.FindAllIndex(str, -1)
matches := regex.FindAllIndex([]byte(string(str)), -1)
for i, m := range matches {
matches[i][0] = runePos(m[0], str)
matches[i][1] = runePos(m[1], str)
matches[i][0] = runePos(m[0], string(str))
matches[i][1] = runePos(m[1], string(str))
}
return matches
}
func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, curRegion *region, statesOnly bool) LineMatch {
lineLen := CharacterCount(line)
func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []rune, curRegion *region, statesOnly bool) LineMatch {
// highlights := make(LineMatch)
if start == 0 {
if !statesOnly {
if _, ok := highlights[0]; !ok {
@@ -164,20 +130,20 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
if curRegion.parent == nil {
if !statesOnly {
highlights[start+loc[1]] = 0
h.highlightRegion(highlights, start, false, lineNum, sliceEnd(line, loc[0]), curRegion, statesOnly)
h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], curRegion, statesOnly)
}
h.highlightEmptyRegion(highlights, start+loc[1], canMatchEnd, lineNum, sliceStart(line, loc[1]), statesOnly)
h.highlightEmptyRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], statesOnly)
return highlights
}
if !statesOnly {
highlights[start+loc[1]] = curRegion.parent.group
h.highlightRegion(highlights, start, false, lineNum, sliceEnd(line, loc[0]), curRegion, statesOnly)
h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], curRegion, statesOnly)
}
h.highlightRegion(highlights, start+loc[1], canMatchEnd, lineNum, sliceStart(line, loc[1]), curRegion.parent, statesOnly)
h.highlightRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], curRegion.parent, statesOnly)
return highlights
}
if lineLen == 0 {
if len(line) == 0 || statesOnly {
if canMatchEnd {
h.lastRegion = curRegion
}
@@ -185,7 +151,7 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
return highlights
}
firstLoc := []int{lineLen, 0}
firstLoc := []int{len(line), 0}
var firstRegion *region
for _, r := range curRegion.rules.regions {
@@ -197,33 +163,31 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
}
}
}
if firstLoc[0] != lineLen {
if !statesOnly {
highlights[start+firstLoc[0]] = firstRegion.limitGroup
}
h.highlightRegion(highlights, start, false, lineNum, sliceEnd(line, firstLoc[0]), curRegion, statesOnly)
h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, sliceStart(line, firstLoc[1]), firstRegion, statesOnly)
if firstLoc[0] != len(line) {
highlights[start+firstLoc[0]] = firstRegion.limitGroup
h.highlightRegion(highlights, start, false, lineNum, line[:firstLoc[0]], curRegion, statesOnly)
h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
return highlights
}
if !statesOnly {
fullHighlights := make([]Group, lineLen)
for i := 0; i < len(fullHighlights); i++ {
fullHighlights[i] = curRegion.group
}
fullHighlights := make([]Group, len([]rune(string(line))))
for i := 0; i < len(fullHighlights); i++ {
fullHighlights[i] = curRegion.group
}
for _, p := range curRegion.rules.patterns {
matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
for _, m := range matches {
for i := m[0]; i < m[1]; i++ {
fullHighlights[i] = p.group
}
for _, p := range curRegion.rules.patterns {
matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
for _, m := range matches {
for i := m[0]; i < m[1]; i++ {
fullHighlights[i] = p.group
}
}
for i, h := range fullHighlights {
if i == 0 || h != fullHighlights[i-1] {
highlights[start+i] = h
}
}
for i, h := range fullHighlights {
if i == 0 || h != fullHighlights[i-1] {
// if _, ok := highlights[start+i]; !ok {
highlights[start+i] = h
// }
}
}
@@ -234,16 +198,15 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
return highlights
}
func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, statesOnly bool) LineMatch {
lineLen := CharacterCount(line)
if lineLen == 0 {
func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []rune, statesOnly bool) LineMatch {
if len(line) == 0 {
if canMatchEnd {
h.lastRegion = nil
}
return highlights
}
firstLoc := []int{lineLen, 0}
firstLoc := []int{len(line), 0}
var firstRegion *region
for _, r := range h.Def.rules.regions {
loc := findIndex(r.start, nil, line, start == 0, canMatchEnd)
@@ -254,12 +217,12 @@ func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canM
}
}
}
if firstLoc[0] != lineLen {
if firstLoc[0] != len(line) {
if !statesOnly {
highlights[start+firstLoc[0]] = firstRegion.limitGroup
}
h.highlightEmptyRegion(highlights, start, false, lineNum, sliceEnd(line, firstLoc[0]), statesOnly)
h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, sliceStart(line, firstLoc[1]), firstRegion, statesOnly)
h.highlightEmptyRegion(highlights, start, false, lineNum, line[:firstLoc[0]], statesOnly)
h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
return highlights
}
@@ -304,7 +267,7 @@ func (h *Highlighter) HighlightString(input string) []LineMatch {
var lineMatches []LineMatch
for i := 0; i < len(lines); i++ {
line := []byte(lines[i])
line := []rune(lines[i])
highlights := make(LineMatch)
if i == 0 || h.lastRegion == nil {
@@ -320,7 +283,7 @@ 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++ {
line := input.LineBytes(i)
line := []rune(input.Line(i))
// highlights := make(LineMatch)
if i == 0 || h.lastRegion == nil {
@@ -335,16 +298,16 @@ func (h *Highlighter) HighlightStates(input LineStates) {
}
}
// HighlightMatches sets the matches for each line from startline to endline
// HighlightMatches sets the matches for each line in between startline and endline
// It sets all other matches in the buffer to nil to conserve memory
// This assumes that all the states are set correctly
func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
for i := startline; i <= endline; i++ {
for i := startline; i < endline; i++ {
if i >= input.LinesNum() {
break
}
line := input.LineBytes(i)
line := []rune(input.Line(i))
highlights := make(LineMatch)
var match LineMatch
@@ -359,9 +322,8 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int)
}
// ReHighlightStates will scan down from `startline` and set the appropriate end of line state
// for each line until it comes across a line whose state does not change
// returns the number of the final line
func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
// for each line until it comes across the same state in two consecutive lines
func (h *Highlighter) ReHighlightStates(input LineStates, startline int) {
// lines := input.LineData()
h.lastRegion = nil
@@ -369,7 +331,7 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
h.lastRegion = input.State(startline - 1)
}
for i := startline; i < input.LinesNum(); i++ {
line := input.LineBytes(i)
line := []rune(input.Line(i))
// highlights := make(LineMatch)
// var match LineMatch
@@ -384,16 +346,14 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
input.SetState(i, curState)
if curState == lastState {
return i
break
}
}
return input.LinesNum() - 1
}
// ReHighlightLine will rehighlight the state and match for a single line
func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
line := input.LineBytes(lineN)
line := []rune(input.Line(lineN))
highlights := make(LineMatch)
h.lastRegion = nil

View File

@@ -1,8 +1,6 @@
package highlight
import (
"bytes"
"errors"
"fmt"
"regexp"
@@ -42,14 +40,6 @@ type Header struct {
FtDetect [2]*regexp.Regexp
}
type HeaderYaml struct {
FileType string `yaml:"filetype"`
Detect struct {
FNameRgx string `yaml:"filename"`
HeaderRgx string `yaml:"header"`
} `yaml:"detect"`
}
type File struct {
FileType string
@@ -91,70 +81,45 @@ func init() {
Groups = make(map[string]Group)
}
// MakeHeader takes a header (.hdr file) file and parses the header
// Header files make parsing more efficient when you only want to compute
// on the headers of syntax files
// 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 {
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])
func ParseFtDetect(file *File) (r [2]*regexp.Regexp, err error) {
rules := file.yamlSrc
if fnameRgx != "" {
header.FtDetect[0], err = regexp.Compile(fnameRgx)
}
if headerRgx != "" {
header.FtDetect[1], err = regexp.Compile(headerRgx)
loaded := 0
for k, v := range rules {
if k == "detect" {
ftdetect := v.(map[interface{}]interface{})
if len(ftdetect) >= 1 {
syntax, err := regexp.Compile(ftdetect["filename"].(string))
if err != nil {
return r, err
}
r[0] = syntax
}
if len(ftdetect) >= 2 {
header, err := regexp.Compile(ftdetect["header"].(string))
if err != nil {
return r, err
}
r[1] = header
}
loaded++
}
if loaded >= 2 {
break
}
}
if err != nil {
return nil, err
}
return header, nil
}
// MakeHeaderYaml takes a yaml spec for a syntax file and parses the
// header
func MakeHeaderYaml(data []byte) (*Header, error) {
var hdrYaml HeaderYaml
err := yaml.Unmarshal(data, &hdrYaml)
if err != nil {
return nil, err
}
header := new(Header)
header.FileType = hdrYaml.FileType
if hdrYaml.Detect.FNameRgx != "" {
header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx)
}
if hdrYaml.Detect.HeaderRgx != "" {
header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx)
}
if err != nil {
return nil, err
}
return header, nil
return r, err
}
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() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
if e := recover(); e != nil {
err = e.(error)
}
}()
@@ -182,12 +147,8 @@ func ParseFile(input []byte) (f *File, err error) {
func ParseDef(f *File, header *Header) (s *Def, 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() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
if e := recover(); e != nil {
err = e.(error)
}
}()
@@ -212,40 +173,6 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
return s, err
}
// HasIncludes returns whether this syntax def has any include statements
func HasIncludes(d *Def) bool {
hasIncludes := len(d.rules.includes) > 0
for _, r := range d.rules.regions {
hasIncludes = hasIncludes || hasIncludesInRegion(r)
}
return hasIncludes
}
func hasIncludesInRegion(region *region) bool {
hasIncludes := len(region.rules.includes) > 0
for _, r := range region.rules.regions {
hasIncludes = hasIncludes || hasIncludesInRegion(r)
}
return hasIncludes
}
// GetIncludes returns a list of filetypes that are included by this syntax def
func GetIncludes(d *Def) []string {
includes := d.rules.includes
for _, r := range d.rules.regions {
includes = append(includes, getIncludesInRegion(r)...)
}
return includes
}
func getIncludesInRegion(region *region) []string {
includes := region.rules.includes
for _, r := range region.rules.regions {
includes = append(includes, getIncludesInRegion(r)...)
}
return includes
}
// ResolveIncludes will sort out the rules for including other filetypes
// You should call this after parsing all the Defs
func ResolveIncludes(def *Def, files []*File) {
@@ -284,17 +211,8 @@ func resolveIncludesInRegion(files []*File, region *region) {
}
}
func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
}
}()
ru = new(rules)
func parseRules(input []interface{}, curRegion *region) (*rules, error) {
rules := new(rules)
for _, v := range input {
rule := v.(map[interface{}]interface{})
@@ -304,7 +222,7 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
switch object := val.(type) {
case string:
if k == "include" {
ru.includes = append(ru.includes, object)
rules.includes = append(rules.includes, object)
} else {
// Pattern
r, err := regexp.Compile(object)
@@ -318,7 +236,7 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
Groups[groupStr] = numGroups
}
groupNum := Groups[groupStr]
ru.patterns = append(ru.patterns, &pattern{groupNum, r})
rules.patterns = append(rules.patterns, &pattern{groupNum, r})
}
case map[interface{}]interface{}:
// region
@@ -326,43 +244,35 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
if err != nil {
return nil, err
}
ru.regions = append(ru.regions, region)
rules.regions = append(rules.regions, region)
default:
return nil, fmt.Errorf("Bad type %T", object)
}
}
}
return ru, nil
return rules, nil
}
func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegion *region) (r *region, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
}
}()
func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegion *region) (*region, error) {
var err error
r = new(region)
region := new(region)
if _, ok := Groups[group]; !ok {
numGroups++
Groups[group] = numGroups
}
groupNum := Groups[group]
r.group = groupNum
r.parent = prevRegion
region.group = groupNum
region.parent = prevRegion
r.start, err = regexp.Compile(regionInfo["start"].(string))
region.start, err = regexp.Compile(regionInfo["start"].(string))
if err != nil {
return nil, err
}
r.end, err = regexp.Compile(regionInfo["end"].(string))
region.end, err = regexp.Compile(regionInfo["end"].(string))
if err != nil {
return nil, err
@@ -370,7 +280,7 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
// skip is optional
if _, ok := regionInfo["skip"]; ok {
r.skip, err = regexp.Compile(regionInfo["skip"].(string))
region.skip, err = regexp.Compile(regionInfo["skip"].(string))
if err != nil {
return nil, err
@@ -385,20 +295,20 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
Groups[groupStr] = numGroups
}
groupNum := Groups[groupStr]
r.limitGroup = groupNum
region.limitGroup = groupNum
if err != nil {
return nil, err
}
} else {
r.limitGroup = r.group
region.limitGroup = region.group
}
r.rules, err = parseRules(regionInfo["rules"].([]interface{}), r)
region.rules, err = parseRules(regionInfo["rules"].([]interface{}), region)
if err != nil {
return nil, err
}
return r, nil
return region, nil
}

28
cmd/micro/highlighter.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import "github.com/zyedidia/micro/cmd/micro/highlight"
var syntaxFiles []*highlight.File
func LoadSyntaxFiles() {
InitColorscheme()
for _, f := range ListRuntimeFiles(RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
LoadSyntaxFile(data, f.Name())
}
}
}
func LoadSyntaxFile(text []byte, filename string) {
f, err := highlight.ParseFile(text)
if err != nil {
TermMessage("Syntax file error: " + filename + ": " + err.Error())
return
}
syntaxFiles = append(syntaxFiles, f)
}

View File

@@ -1,150 +0,0 @@
package main
import (
"log"
lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
"github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
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"
)
func init() {
ulua.L = lua.NewState()
ulua.L.SetGlobal("import", luar.New(ulua.L, LuaImport))
}
// LuaImport is meant to be called from lua by a plugin and will import the given micro package
func LuaImport(pkg string) *lua.LTable {
switch pkg {
case "micro":
return luaImportMicro()
case "micro/shell":
return luaImportMicroShell()
case "micro/buffer":
return luaImportMicroBuffer()
case "micro/config":
return luaImportMicroConfig()
case "micro/util":
return luaImportMicroUtil()
default:
return ulua.Import(pkg)
}
}
func luaImportMicro() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "TermMessage", luar.New(ulua.L, screen.TermMessage))
ulua.L.SetField(pkg, "TermError", luar.New(ulua.L, screen.TermError))
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() action.Pane {
return action.MainTab().CurPane()
}))
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, func() *action.Tab {
return action.MainTab()
}))
return pkg
}
func luaImportMicroConfig() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "MakeCommand", luar.New(ulua.L, action.MakeCommand))
ulua.L.SetField(pkg, "FileComplete", luar.New(ulua.L, buffer.FileComplete))
ulua.L.SetField(pkg, "HelpComplete", luar.New(ulua.L, action.HelpComplete))
ulua.L.SetField(pkg, "OptionComplete", luar.New(ulua.L, action.OptionComplete))
ulua.L.SetField(pkg, "OptionValueComplete", luar.New(ulua.L, action.OptionValueComplete))
ulua.L.SetField(pkg, "NoComplete", luar.New(ulua.L, nil))
ulua.L.SetField(pkg, "TryBindKey", luar.New(ulua.L, action.TryBindKey))
ulua.L.SetField(pkg, "Reload", luar.New(ulua.L, action.ReloadConfig))
ulua.L.SetField(pkg, "AddRuntimeFileFromMemory", luar.New(ulua.L, config.PluginAddRuntimeFileFromMemory))
ulua.L.SetField(pkg, "AddRuntimeFilesFromDirectory", luar.New(ulua.L, config.PluginAddRuntimeFilesFromDirectory))
ulua.L.SetField(pkg, "AddRuntimeFile", luar.New(ulua.L, config.PluginAddRuntimeFile))
ulua.L.SetField(pkg, "ListRuntimeFiles", luar.New(ulua.L, config.PluginListRuntimeFiles))
ulua.L.SetField(pkg, "ReadRuntimeFile", luar.New(ulua.L, config.PluginReadRuntimeFile))
ulua.L.SetField(pkg, "NewRTFiletype", luar.New(ulua.L, config.NewRTFiletype))
ulua.L.SetField(pkg, "RTColorscheme", luar.New(ulua.L, config.RTColorscheme))
ulua.L.SetField(pkg, "RTSyntax", luar.New(ulua.L, config.RTSyntax))
ulua.L.SetField(pkg, "RTHelp", luar.New(ulua.L, config.RTHelp))
ulua.L.SetField(pkg, "RTPlugin", luar.New(ulua.L, config.RTPlugin))
ulua.L.SetField(pkg, "RegisterCommonOption", luar.New(ulua.L, config.RegisterCommonOptionPlug))
ulua.L.SetField(pkg, "RegisterGlobalOption", luar.New(ulua.L, config.RegisterGlobalOptionPlug))
ulua.L.SetField(pkg, "GetGlobalOption", luar.New(ulua.L, config.GetGlobalOption))
ulua.L.SetField(pkg, "SetGlobalOption", luar.New(ulua.L, action.SetGlobalOption))
ulua.L.SetField(pkg, "SetGlobalOptionNative", luar.New(ulua.L, action.SetGlobalOptionNative))
return pkg
}
func luaImportMicroShell() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "ExecCommand", luar.New(ulua.L, shell.ExecCommand))
ulua.L.SetField(pkg, "RunCommand", luar.New(ulua.L, shell.RunCommand))
ulua.L.SetField(pkg, "RunBackgroundShell", luar.New(ulua.L, shell.RunBackgroundShell))
ulua.L.SetField(pkg, "RunInteractiveShell", luar.New(ulua.L, shell.RunInteractiveShell))
ulua.L.SetField(pkg, "JobStart", luar.New(ulua.L, shell.JobStart))
ulua.L.SetField(pkg, "JobSpawn", luar.New(ulua.L, shell.JobSpawn))
ulua.L.SetField(pkg, "JobStop", luar.New(ulua.L, shell.JobStop))
ulua.L.SetField(pkg, "JobSend", luar.New(ulua.L, shell.JobSend))
ulua.L.SetField(pkg, "RunTermEmulator", luar.New(ulua.L, action.RunTermEmulator))
ulua.L.SetField(pkg, "TermEmuSupported", luar.New(ulua.L, action.TermEmuSupported))
return pkg
}
func luaImportMicroBuffer() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "NewMessage", luar.New(ulua.L, buffer.NewMessage))
ulua.L.SetField(pkg, "NewMessageAtLine", luar.New(ulua.L, buffer.NewMessageAtLine))
ulua.L.SetField(pkg, "MTInfo", luar.New(ulua.L, buffer.MTInfo))
ulua.L.SetField(pkg, "MTWarning", luar.New(ulua.L, buffer.MTWarning))
ulua.L.SetField(pkg, "MTError", luar.New(ulua.L, buffer.MTError))
ulua.L.SetField(pkg, "Loc", luar.New(ulua.L, func(x, y int) buffer.Loc {
return buffer.Loc{x, y}
}))
ulua.L.SetField(pkg, "BTDefault", luar.New(ulua.L, buffer.BTDefault.Kind))
ulua.L.SetField(pkg, "BTHelp", luar.New(ulua.L, buffer.BTHelp.Kind))
ulua.L.SetField(pkg, "BTLog", luar.New(ulua.L, buffer.BTLog.Kind))
ulua.L.SetField(pkg, "BTScratch", luar.New(ulua.L, buffer.BTScratch.Kind))
ulua.L.SetField(pkg, "BTRaw", luar.New(ulua.L, buffer.BTRaw.Kind))
ulua.L.SetField(pkg, "BTInfo", luar.New(ulua.L, buffer.BTInfo.Kind))
ulua.L.SetField(pkg, "NewBuffer", luar.New(ulua.L, func(text, path string) *buffer.Buffer {
return buffer.NewBufferFromString(text, path, buffer.BTDefault)
}))
ulua.L.SetField(pkg, "NewBufferFromFile", luar.New(ulua.L, func(path string) (*buffer.Buffer, error) {
return buffer.NewBufferFromFile(path, buffer.BTDefault)
}))
ulua.L.SetField(pkg, "ByteOffset", luar.New(ulua.L, buffer.ByteOffset))
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, buffer.WriteLog))
ulua.L.SetField(pkg, "LogBuf", luar.New(ulua.L, buffer.GetLogBuf))
return pkg
}
func luaImportMicroUtil() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "RuneAt", luar.New(ulua.L, util.LuaRuneAt))
ulua.L.SetField(pkg, "GetLeadingWhitespace", luar.New(ulua.L, util.LuaGetLeadingWhitespace))
ulua.L.SetField(pkg, "IsWordChar", luar.New(ulua.L, util.LuaIsWordChar))
ulua.L.SetField(pkg, "String", luar.New(ulua.L, util.String))
ulua.L.SetField(pkg, "CharacterCountInString", luar.New(ulua.L, util.CharacterCountInString))
ulua.L.SetField(pkg, "RuneStr", luar.New(ulua.L, func(r rune) string {
return string(r)
}))
return pkg
}

View File

@@ -1,4 +1,4 @@
package shell
package main
import (
"bytes"
@@ -6,12 +6,6 @@ import (
"os/exec"
)
var Jobs chan JobFunction
func init() {
Jobs = make(chan JobFunction, 100)
}
// Jobs are the way plugins can run processes in the background
// A job is simply a process that gets executed asynchronously
// There are callbacks for when the job exits, when the job creates stdout
@@ -24,46 +18,46 @@ func init() {
// JobFunction is a representation of a job (this data structure is what is loaded
// into the jobs channel)
type JobFunction struct {
Function func(string, []interface{})
Output string
Args []interface{}
function func(string, ...string)
output string
args []string
}
// A CallbackFile is the data structure that makes it possible to catch stderr and stdout write events
type CallbackFile struct {
io.Writer
callback func(string, []interface{})
args []interface{}
callback func(string, ...string)
args []string
}
func (f *CallbackFile) Write(data []byte) (int, error) {
// This is either stderr or stdout
// In either case we create a new job function callback and put it in the jobs channel
jobFunc := JobFunction{f.callback, string(data), f.args}
Jobs <- jobFunc
jobs <- jobFunc
return f.Writer.Write(data)
}
// JobStart starts a shell command in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobStart(cmd string, onStdout, onStderr, onExit func(string, []interface{}), userargs ...interface{}) *exec.Cmd {
func JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
return JobSpawn("sh", []string{"-c", cmd}, onStdout, onStderr, onExit, userargs...)
}
// JobSpawn starts a process with args in the background with the given callbacks
// It returns an *exec.Cmd as the job id
func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit func(string, []interface{}), userargs ...interface{}) *exec.Cmd {
func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit string, userargs ...string) *exec.Cmd {
// Set up everything correctly if the functions have been provided
proc := exec.Command(cmdName, cmdArgs...)
var outbuf bytes.Buffer
if onStdout != nil {
proc.Stdout = &CallbackFile{&outbuf, onStdout, userargs}
if onStdout != "" {
proc.Stdout = &CallbackFile{&outbuf, LuaFunctionJob(onStdout), userargs}
} else {
proc.Stdout = &outbuf
}
if onStderr != nil {
proc.Stderr = &CallbackFile{&outbuf, onStderr, userargs}
if onStderr != "" {
proc.Stderr = &CallbackFile{&outbuf, LuaFunctionJob(onStderr), userargs}
} else {
proc.Stderr = &outbuf
}
@@ -71,8 +65,8 @@ func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit func(
go func() {
// Run the process in the background and create the onExit callback
proc.Run()
jobFunc := JobFunction{onExit, string(outbuf.Bytes()), userargs}
Jobs <- jobFunc
jobFunc := JobFunction{LuaFunctionJob(onExit), string(outbuf.Bytes()), userargs}
jobs <- jobFunc
}()
return proc

233
cmd/micro/lineArray.go Normal file
View File

@@ -0,0 +1,233 @@
package main
import (
"bufio"
"io"
"unicode/utf8"
"github.com/zyedidia/micro/cmd/micro/highlight"
)
func runeToByteIndex(n int, txt []byte) int {
if n == 0 {
return 0
}
count := 0
i := 0
for len(txt) > 0 {
_, size := utf8.DecodeRune(txt)
txt = txt[size:]
count += size
i++
if i == n {
break
}
}
return count
}
type Line struct {
data []byte
state highlight.State
match highlight.LineMatch
rehighlight bool
}
// A LineArray simply stores and array of lines and makes it easy to insert
// and delete in it
type LineArray struct {
lines []Line
}
func Append(slice []Line, data ...Line) []Line {
l := len(slice)
if l+len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]Line, (l+len(data))+10000)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
// NewLineArray returns a new line array from an array of bytes
func NewLineArray(size int64, reader io.Reader) *LineArray {
la := new(LineArray)
la.lines = make([]Line, 0, 1000)
br := bufio.NewReader(reader)
var loaded int
n := 0
for {
data, err := br.ReadBytes('\n')
if n >= 1000 && loaded >= 0 {
totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
// The copy function is predeclared and works for any slice type.
copy(newSlice, la.lines)
la.lines = newSlice
loaded = -1
}
if loaded >= 0 {
loaded += len(data)
}
if err != nil {
if err == io.EOF {
la.lines = Append(la.lines, Line{data[:len(data)], nil, nil, false})
// la.lines = Append(la.lines, Line{data[:len(data)]})
}
// Last line was read
break
} else {
// la.lines = Append(la.lines, Line{data[:len(data)-1]})
la.lines = Append(la.lines, Line{data[:len(data)-1], nil, nil, false})
}
n++
}
return la
}
// Returns the String representation of the LineArray
func (la *LineArray) String() string {
str := ""
for i, l := range la.lines {
str += string(l.data)
if i != len(la.lines)-1 {
str += "\n"
}
}
return str
}
// NewlineBelow adds a newline below the given line number
func (la *LineArray) NewlineBelow(y int) {
la.lines = append(la.lines, Line{[]byte(" "), nil, nil, false})
copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = Line{[]byte(""), la.lines[y].state, nil, false}
}
// inserts a byte array at a given location
func (la *LineArray) insert(pos Loc, value []byte) {
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
// x, y := pos.x, pos.y
for i := 0; i < len(value); i++ {
if value[i] == '\n' {
la.Split(Loc{x, y})
x = 0
y++
continue
}
la.insertByte(Loc{x, y}, value[i])
x++
}
}
// inserts a byte at a given location
func (la *LineArray) insertByte(pos Loc, value byte) {
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
la.lines[pos.Y].data[pos.X] = value
}
// JoinLines joins the two lines a and b
func (la *LineArray) JoinLines(a, b int) {
la.insert(Loc{len(la.lines[a].data), a}, 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].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) string {
sub := la.Substr(start, end)
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
} else {
for i := start.Y + 1; i <= end.Y-1; i++ {
la.DeleteLine(start.Y + 1)
}
la.DeleteToEnd(Loc{startX, start.Y})
la.DeleteFromStart(Loc{endX - 1, start.Y + 1})
la.JoinLines(start.Y, start.Y+1)
}
return sub
}
// DeleteToEnd deletes from the end of a line to the position
func (la *LineArray) DeleteToEnd(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
}
// DeleteFromStart deletes from the start of a line to the position
func (la *LineArray) DeleteFromStart(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
}
// DeleteLine deletes the line number
func (la *LineArray) DeleteLine(y int) {
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
}
// DeleteByte deletes the byte at a position
func (la *LineArray) DeleteByte(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
}
// Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) string {
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
return string(la.lines[start.Y].data[startX:endX])
}
var str string
str += string(la.lines[start.Y].data[startX:]) + "\n"
for i := start.Y + 1; i <= end.Y-1; i++ {
str += string(la.lines[i].data) + "\n"
}
str += string(la.lines[end.Y].data[:endX])
return str
}
func (la *LineArray) State(lineN int) highlight.State {
return la.lines[lineN].state
}
func (la *LineArray) SetState(lineN int, s highlight.State) {
la.lines[lineN].state = s
}
func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
la.lines[lineN].match = m
}
func (la *LineArray) Match(lineN int) highlight.LineMatch {
return la.lines[lineN].match
}

View File

@@ -1,128 +1,40 @@
package buffer
package main
import (
"github.com/zyedidia/micro/v2/internal/util"
)
// FromCharPos converts from a character position to an x, y position
func FromCharPos(loc int, buf *Buffer) Loc {
charNum := 0
x, y := 0, 0
// Loc stores a location
type Loc struct {
X, Y int
lineLen := Count(buf.Line(y)) + 1
for charNum+lineLen <= loc {
charNum += lineLen
y++
lineLen = Count(buf.Line(y)) + 1
}
x = loc - charNum
return Loc{x, y}
}
// LessThan returns true if b is smaller
func (l Loc) LessThan(b Loc) bool {
if l.Y < b.Y {
return true
}
return l.Y == b.Y && l.X < b.X
}
// GreaterThan returns true if b is bigger
func (l Loc) GreaterThan(b Loc) bool {
if l.Y > b.Y {
return true
}
return l.Y == b.Y && l.X > b.X
}
// GreaterEqual returns true if b is greater than or equal to b
func (l Loc) GreaterEqual(b Loc) bool {
if l.Y > b.Y {
return true
}
if l.Y == b.Y && l.X > b.X {
return true
}
return l == b
}
// LessEqual returns true if b is less than or equal to b
func (l Loc) LessEqual(b Loc) bool {
if l.Y < b.Y {
return true
}
if l.Y == b.Y && l.X < b.X {
return true
}
return l == b
}
// The following functions require a buffer to know where newlines are
// Diff returns the distance between two locations
func DiffLA(a, b Loc, buf *LineArray) int {
if a.Y == b.Y {
if a.X > b.X {
return a.X - b.X
}
return b.X - a.X
}
// Make sure a is guaranteed to be less than b
if b.LessThan(a) {
a, b = b, a
}
// ToCharPos converts from an x, y position to a character position
func ToCharPos(start Loc, buf *Buffer) int {
x, y := start.X, start.Y
loc := 0
for i := a.Y + 1; i < b.Y; i++ {
for i := 0; i < y; i++ {
// + 1 for the newline
loc += util.CharacterCount(buf.LineBytes(i)) + 1
loc += Count(buf.Line(i)) + 1
}
loc += util.CharacterCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
loc += x
return loc
}
// This moves the location one character to the right
func (l Loc) right(buf *LineArray) Loc {
if l == buf.End() {
return Loc{l.X + 1, l.Y}
// InBounds returns whether the given location is a valid character position in the given buffer
func InBounds(pos Loc, buf *Buffer) bool {
if pos.Y < 0 || pos.Y >= buf.NumLines || pos.X < 0 || pos.X > Count(buf.Line(pos.Y)) {
return false
}
var res Loc
if l.X < util.CharacterCount(buf.LineBytes(l.Y)) {
res = Loc{l.X + 1, l.Y}
} else {
res = Loc{0, l.Y + 1}
}
return res
}
// This moves the given location one character to the left
func (l Loc) left(buf *LineArray) Loc {
if l == buf.Start() {
return Loc{l.X - 1, l.Y}
}
var res Loc
if l.X > 0 {
res = Loc{l.X - 1, l.Y}
} else {
res = Loc{util.CharacterCount(buf.LineBytes(l.Y - 1)), l.Y - 1}
}
return res
}
// MoveLA moves the cursor n characters to the left or right
// It moves the cursor left if n is negative
func (l Loc) MoveLA(n int, buf *LineArray) Loc {
if n > 0 {
for i := 0; i < n; i++ {
l = l.right(buf)
}
return l
}
for i := 0; i < util.Abs(n); i++ {
l = l.left(buf)
}
return l
}
// Diff returns the difference between two locs
func (l Loc) Diff(b Loc, buf *Buffer) int {
return DiffLA(l, b, buf.LineArray)
}
// Move moves a loc n characters
func (l Loc) Move(n int, buf *Buffer) Loc {
return l.MoveLA(n, buf.LineArray)
return true
}
// ByteOffset is just like ToCharPos except it counts bytes instead of runes
@@ -137,12 +49,100 @@ func ByteOffset(pos Loc, buf *Buffer) int {
return loc
}
// clamps a loc within a buffer
func clamp(pos Loc, la *LineArray) Loc {
if pos.GreaterEqual(la.End()) {
return la.End()
} else if pos.LessThan(la.Start()) {
return la.Start()
}
return pos
// Loc stores a location
type Loc struct {
X, Y int
}
// LessThan returns true if b is smaller
func (l Loc) LessThan(b Loc) bool {
if l.Y < b.Y {
return true
}
if l.Y == b.Y && l.X < b.X {
return true
}
return false
}
// GreaterThan returns true if b is bigger
func (l Loc) GreaterThan(b Loc) bool {
if l.Y > b.Y {
return true
}
if l.Y == b.Y && l.X > b.X {
return true
}
return false
}
// GreaterEqual returns true if b is greater than or equal to b
func (l Loc) GreaterEqual(b Loc) bool {
if l.Y > b.Y {
return true
}
if l.Y == b.Y && l.X > b.X {
return true
}
if l == b {
return true
}
return false
}
// LessEqual returns true if b is less than or equal to b
func (l Loc) LessEqual(b Loc) bool {
if l.Y < b.Y {
return true
}
if l.Y == b.Y && l.X < b.X {
return true
}
if l == b {
return true
}
return false
}
// This moves the location one character to the right
func (l Loc) right(buf *Buffer) Loc {
if l == buf.End() {
return Loc{l.X + 1, l.Y}
}
var res Loc
if l.X < Count(buf.Line(l.Y)) {
res = Loc{l.X + 1, l.Y}
} else {
res = Loc{0, l.Y + 1}
}
return res
}
// This moves the given location one character to the left
func (l Loc) left(buf *Buffer) Loc {
if l == buf.Start() {
return Loc{l.X - 1, l.Y}
}
var res Loc
if l.X > 0 {
res = Loc{l.X - 1, l.Y}
} else {
res = Loc{Count(buf.Line(l.Y - 1)), l.Y - 1}
}
return res
}
// Move moves the cursor n characters to the left or right
// It moves the cursor left if n is negative
func (l Loc) Move(n int, buf *Buffer) Loc {
if n > 0 {
for i := 0; i < n; i++ {
l = l.right(buf)
}
return l
}
for i := 0; i < Abs(n); i++ {
l = l.left(buf)
}
return l
}

493
cmd/micro/messenger.go Normal file
View File

@@ -0,0 +1,493 @@
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"strconv"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
)
// TermMessage sends a message to the user in the terminal. This usually occurs before
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
// regular expressions
// The function must be called when the screen is not initialized
// This will write the message, and wait for the user
// to press and key to continue
func TermMessage(msg ...interface{}) {
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
fmt.Println(msg...)
fmt.Print("\nPress enter to continue")
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
if !screenWasNil {
InitScreen()
}
}
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
// as an error
func TermError(filename string, lineNum int, err string) {
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
}
// Messenger is an object that makes it easy to send messages to the user
// and get input from the user
type Messenger struct {
log *Buffer
// Are we currently prompting the user?
hasPrompt bool
// Is there a message to print
hasMessage bool
// Message to print
message string
// The user's response to a prompt
response string
// style to use when drawing the message
style tcell.Style
// We have to keep track of the cursor for prompting
cursorx int
// This map stores the history for all the different kinds of uses Prompt has
// It's a map of history type -> history array
history map[string][]string
historyNum int
// Is the current message a message from the gutter
gutterMessage bool
}
func (m *Messenger) AddLog(msg string) {
buffer := m.getBuffer()
buffer.insert(buffer.End(), []byte(msg+"\n"))
buffer.Cursor.Loc = buffer.End()
buffer.Cursor.Relocate()
}
func (m *Messenger) getBuffer() *Buffer {
if m.log == nil {
m.log = NewBufferFromString("", "")
m.log.name = "Log"
}
return m.log
}
// Message sends a message to the user
func (m *Messenger) Message(msg ...interface{}) {
displayMessage := fmt.Sprint(msg...)
// only display a new message if there isn't an active prompt
// this is to prevent overwriting an existing prompt to the user
if m.hasPrompt == false {
// if there is no active prompt then style and display the message as normal
m.message = displayMessage
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
}
// add the message to the log regardless of active prompts
m.AddLog(displayMessage)
}
// Error sends an error message to the user
func (m *Messenger) Error(msg ...interface{}) {
buf := new(bytes.Buffer)
fmt.Fprint(buf, msg...)
// only display a new message if there isn't an active prompt
// this is to prevent overwriting an existing prompt to the user
if m.hasPrompt == false {
// if there is no active prompt then style and display the message as normal
m.message = buf.String()
m.style = defStyle.
Foreground(tcell.ColorBlack).
Background(tcell.ColorMaroon)
if _, ok := colorscheme["error-message"]; ok {
m.style = colorscheme["error-message"]
}
m.hasMessage = true
}
// add the message to the log regardless of active prompts
m.AddLog(buf.String())
}
func (m *Messenger) PromptText(msg ...interface{}) {
displayMessage := fmt.Sprint(msg...)
// if there is no active prompt then style and display the message as normal
m.message = displayMessage
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
// add the message to the log regardless of active prompts
m.AddLog(displayMessage)
}
// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
m.hasPrompt = true
m.PromptText(prompt)
_, h := screen.Size()
for {
m.Clear()
m.Display()
screen.ShowCursor(Count(m.message), h-1)
screen.Show()
event := <-events
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyRune:
if e.Rune() == 'y' || e.Rune() == 'Y' {
m.AddLog("\t--> y")
m.hasPrompt = false
return true, false
} else if e.Rune() == 'n' || e.Rune() == 'N' {
m.AddLog("\t--> n")
m.hasPrompt = false
return false, false
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
return false, true
}
}
}
}
// LetterPrompt gives the user a prompt and waits for a one letter response
func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
m.hasPrompt = true
m.PromptText(prompt)
_, h := screen.Size()
for {
m.Clear()
m.Display()
screen.ShowCursor(Count(m.message), h-1)
screen.Show()
event := <-events
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyRune:
for _, r := range responses {
if e.Rune() == r {
m.AddLog("\t--> " + string(r))
m.Clear()
m.Reset()
m.hasPrompt = false
return r, false
}
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
return ' ', true
}
}
}
}
type Completion int
const (
NoCompletion Completion = iota
FileCompletion
CommandCompletion
HelpCompletion
OptionCompletion
PluginCmdCompletion
PluginNameCompletion
)
// Prompt sends the user a message and waits for a response to be typed in
// This function blocks the main loop while waiting for input
func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
m.hasPrompt = true
m.PromptText(prompt)
if _, ok := m.history[historyType]; !ok {
m.history[historyType] = []string{""}
} else {
m.history[historyType] = append(m.history[historyType], "")
}
m.historyNum = len(m.history[historyType]) - 1
response, canceled := placeholder, true
m.response = response
m.cursorx = Count(placeholder)
RedrawAll()
for m.hasPrompt {
var suggestions []string
m.Clear()
event := <-events
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
// Cancel
m.AddLog("\t--> (cancel)")
m.hasPrompt = false
case tcell.KeyEnter:
// User is done entering their response
m.AddLog("\t--> " + m.response)
m.hasPrompt = false
response, canceled = m.response, false
m.history[historyType][len(m.history[historyType])-1] = response
case tcell.KeyTab:
args := SplitCommandArgs(m.response)
currentArgNum := len(args) - 1
currentArg := args[currentArgNum]
var completionType Completion
if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
if command, ok := commands[args[0]]; ok {
completionTypes = append([]Completion{CommandCompletion}, command.completions...)
}
}
if currentArgNum >= len(completionTypes) {
completionType = completionTypes[len(completionTypes)-1]
} else {
completionType = completionTypes[currentArgNum]
}
var chosen string
if completionType == FileCompletion {
chosen, suggestions = FileComplete(currentArg)
} else if completionType == CommandCompletion {
chosen, suggestions = CommandComplete(currentArg)
} else if completionType == HelpCompletion {
chosen, suggestions = HelpComplete(currentArg)
} else if completionType == OptionCompletion {
chosen, suggestions = OptionComplete(currentArg)
} else if completionType == PluginCmdCompletion {
chosen, suggestions = PluginCmdComplete(currentArg)
} else if completionType == PluginNameCompletion {
chosen, suggestions = PluginNameComplete(currentArg)
} else if completionType < NoCompletion {
chosen, suggestions = PluginComplete(completionType, currentArg)
}
if len(suggestions) > 1 {
chosen = chosen + CommonSubstring(suggestions...)
}
if chosen != "" {
m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...)
m.cursorx = Count(m.response)
}
}
}
m.HandleEvent(event, m.history[historyType])
m.Clear()
for _, v := range tabs[curTab].views {
v.Display()
}
DisplayTabs()
m.Display()
if len(suggestions) > 1 {
m.DisplaySuggestions(suggestions)
}
screen.Show()
}
m.Clear()
m.Reset()
return response, canceled
}
// HandleEvent handles an event for the prompter
func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
switch e := event.(type) {
case *tcell.EventKey:
if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
for key, actions := range bindings {
if e.Key() == key.keyCode {
if e.Key() == tcell.KeyRune {
if e.Rune() != key.r {
continue
}
}
if e.Modifiers() == key.modifiers {
for _, action := range actions {
funcName := FuncName(action)
switch funcName {
case "main.(*View).CursorUp":
if m.historyNum > 0 {
m.historyNum--
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
case "main.(*View).CursorDown":
if m.historyNum < len(history)-1 {
m.historyNum++
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
case "main.(*View).CursorLeft":
if m.cursorx > 0 {
m.cursorx--
}
case "main.(*View).CursorRight":
if m.cursorx < Count(m.response) {
m.cursorx++
}
case "main.(*View).CursorStart", "main.(*View).StartOfLine":
m.cursorx = 0
case "main.(*View).CursorEnd", "main.(*View).EndOfLine":
m.cursorx = Count(m.response)
case "main.(*View).Backspace":
if m.cursorx > 0 {
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
m.cursorx--
}
case "main.(*View).Paste":
clip, _ := clipboard.ReadAll("clipboard")
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
}
}
}
}
}
}
switch e.Key() {
case tcell.KeyRune:
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
m.cursorx++
}
history[m.historyNum] = m.response
case *tcell.EventPaste:
clip := e.Text()
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
case *tcell.EventMouse:
x, y := e.Position()
x -= Count(m.message)
button := e.Buttons()
_, screenH := screen.Size()
if y == screenH-1 {
switch button {
case tcell.Button1:
m.cursorx = x
if m.cursorx < 0 {
m.cursorx = 0
} else if m.cursorx > Count(m.response) {
m.cursorx = Count(m.response)
}
}
}
}
}
// Reset resets the messenger's cursor, message and response
func (m *Messenger) Reset() {
m.cursorx = 0
m.message = ""
m.response = ""
}
// Clear clears the line at the bottom of the editor
func (m *Messenger) Clear() {
w, h := screen.Size()
for x := 0; x < w; x++ {
screen.SetContent(x, h-1, ' ', nil, defStyle)
}
}
func (m *Messenger) DisplaySuggestions(suggestions []string) {
w, screenH := screen.Size()
y := screenH - 2
statusLineStyle := defStyle.Reverse(true)
if style, ok := colorscheme["statusline"]; ok {
statusLineStyle = style
}
for x := 0; x < w; x++ {
screen.SetContent(x, y, ' ', nil, statusLineStyle)
}
x := 0
for _, suggestion := range suggestions {
for _, c := range suggestion {
screen.SetContent(x, y, c, nil, statusLineStyle)
x++
}
screen.SetContent(x, y, ' ', nil, statusLineStyle)
x++
}
}
// Display displays messages or prompts
func (m *Messenger) Display() {
_, h := screen.Size()
if m.hasMessage {
if m.hasPrompt || globalSettings["infobar"].(bool) {
runes := []rune(m.message + m.response)
for x := 0; x < len(runes); x++ {
screen.SetContent(x, h-1, runes[x], nil, m.style)
}
}
}
if m.hasPrompt {
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
screen.Show()
}
}
// A GutterMessage is a message displayed on the side of the editor
type GutterMessage struct {
lineNum int
msg string
kind int
}
// These are the different types of messages
const (
// GutterInfo represents a simple info message
GutterInfo = iota
// GutterWarning represents a compiler warning
GutterWarning
// GutterError represents a compiler error
GutterError
)

View File

@@ -5,133 +5,71 @@ import (
"fmt"
"io/ioutil"
"os"
"regexp"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
"github.com/go-errors/errors"
isatty "github.com/mattn/go-isatty"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/mattn/go-isatty"
"github.com/mitchellh/go-homedir"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
"github.com/zyedidia/tcell/encoding"
"layeh.com/gopher-luar"
)
const (
synLinesUp = 75 // How many lines up to look to do syntax highlighting
synLinesDown = 75 // How many lines down to look to do syntax highlighting
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
autosaveTime = 8 // Number of seconds to wait before autosaving
)
var (
// The main screen
screen tcell.Screen
// Object to send messages and prompts to the user
messenger *Messenger
// The default highlighting style
// This simply defines the default foreground and background colors
defStyle tcell.Style
// Where the user's configuration is
// This should be $XDG_CONFIG_HOME/micro
// If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
configDir string
// Version is the version number or commit hash
// These variables should be set by the linker when compiling
Version = "0.0.0-unknown"
CommitHash = "Unknown"
CompileDate = "Unknown"
// L is the lua state
// This is the VM that runs the plugins
L *lua.LState
// The list of views
tabs []*Tab
// This is the currently open tab
// It's just an index to the tab in the tabs array
curTab int
// Channel of jobs running in the background
jobs chan JobFunction
// Event channel
events chan tcell.Event
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")
flagOptions = flag.Bool("options", false, "Show all option help")
flagDebug = flag.Bool("debug", false, "Enable debug mode (prints debug info to ./log.txt)")
flagPlugin = flag.String("plugin", "", "Plugin command")
flagClean = flag.Bool("clean", false, "Clean configuration directory")
optionFlags map[string]*string
)
func InitFlags() {
flag.Usage = func() {
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
fmt.Println("-clean")
fmt.Println(" \tCleans the configuration directory")
fmt.Println("-config-dir dir")
fmt.Println(" \tSpecify a custom location for the configuration directory")
fmt.Println("[FILE]:LINE:COL")
fmt.Println("+LINE:COL")
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
fmt.Println("-options")
fmt.Println(" \tShow all option help")
fmt.Println("-debug")
fmt.Println(" \tEnable debug mode (enables logging to ./log.txt)")
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.Println("-plugin install [PLUGIN]...")
fmt.Println(" \tInstall plugin(s)")
fmt.Println("-plugin remove [PLUGIN]...")
fmt.Println(" \tRemove plugin(s)")
fmt.Println("-plugin update [PLUGIN]...")
fmt.Println(" \tUpdate plugin(s) (if no argument is given, updates all plugins)")
fmt.Println("-plugin search [PLUGIN]...")
fmt.Println(" \tSearch for a plugin")
fmt.Println("-plugin list")
fmt.Println(" \tList installed plugins")
fmt.Println("-plugin available")
fmt.Println(" \tList available plugins")
fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
fmt.Println("-option value")
fmt.Println(" \tSet `option` to `value` for this session")
fmt.Println(" \tFor example: `micro -syntax off file.c`")
fmt.Println("\nUse `micro -options` to see the full list of configuration options")
}
optionFlags = make(map[string]*string)
for k, v := range config.DefaultAllSettings() {
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v))
}
flag.Parse()
if *flagVersion {
// If -version was passed
fmt.Println("Version:", util.Version)
fmt.Println("Commit hash:", util.CommitHash)
fmt.Println("Compiled on", util.CompileDate)
os.Exit(0)
}
if *flagOptions {
// If -options was passed
var keys []string
m := config.DefaultAllSettings()
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := m[k]
fmt.Printf("-%s value\n", k)
fmt.Printf(" \tDefault value: '%v'\n", v)
}
os.Exit(0)
}
if util.Debug == "OFF" && *flagDebug {
util.Debug = "ON"
}
}
// DoPluginFlags parses and executes any flags that require LoadAllPlugins (-plugin and -clean)
func DoPluginFlags() {
if *flagClean || *flagPlugin != "" {
config.LoadAllPlugins()
if *flagPlugin != "" {
args := flag.Args()
config.PluginCommand(os.Stdout, *flagPlugin, args)
} else if *flagClean {
CleanConfig()
}
os.Exit(0)
}
}
// LoadInput determines which files should be loaded into buffers
// based on the input stored in flag.Args()
func LoadInput() []*buffer.Buffer {
func LoadInput() []*Buffer {
// There are a number of ways micro should start given its input
// 1. If it is given a files in flag.Args(), it should open those
@@ -147,40 +85,36 @@ func LoadInput() []*buffer.Buffer {
var input []byte
var err error
args := flag.Args()
buffers := make([]*buffer.Buffer, 0, len(args))
buffers := make([]*Buffer, 0, len(args))
btype := buffer.BTDefault
if !isatty.IsTerminal(os.Stdout.Fd()) {
btype = buffer.BTStdout
}
files := make([]string, 0, len(args))
flagStartPos := ""
flagr := regexp.MustCompile(`^\+\d+(:\d+)?$`)
for _, a := range args {
if flagr.MatchString(a) {
flagStartPos = a[1:]
} else {
if flagStartPos != "" {
files = append(files, a+":"+flagStartPos)
flagStartPos = ""
} else {
files = append(files, a)
}
}
}
if len(files) > 0 {
if len(args) > 0 {
// Option 1
// We go through each file and load it
for i := 0; i < len(files); i++ {
buf, err := buffer.NewBufferFromFile(files[i], btype)
if err != nil {
screen.TermMessage(err)
continue
for i := 0; i < len(args); i++ {
filename = args[i]
// Check that the file exists
var input *os.File
if _, e := os.Stat(filename); e == nil {
// If it exists we load it into a buffer
input, err = os.Open(filename)
stat, _ := input.Stat()
defer input.Close()
if err != nil {
TermMessage(err)
continue
}
if stat.IsDir() {
TermMessage("Cannot read", filename, "because it is a directory")
continue
}
}
// If the file didn't exist, input will be empty, and we'll open an empty buffer
buffers = append(buffers, buf)
if input != nil {
buffers = append(buffers, NewBuffer(input, FSize(input), filename))
} else {
buffers = append(buffers, NewBufferFromString("", filename))
}
}
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
// Option 2
@@ -188,189 +122,376 @@ func LoadInput() []*buffer.Buffer {
// and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin)
if err != nil {
screen.TermMessage("Error reading from stdin: ", err)
TermMessage("Error reading from stdin: ", err)
input = []byte{}
}
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, btype))
buffers = append(buffers, NewBufferFromString(string(input), filename))
} else {
// Option 3, just open an empty buffer
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, btype))
buffers = append(buffers, NewBufferFromString(string(input), filename))
}
return buffers
}
func main() {
defer func() {
if util.Stdout.Len() > 0 {
fmt.Fprint(os.Stdout, util.Stdout.String())
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
// If no directory is found, it creates one.
func InitConfigDir() {
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome == "" {
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
home, err := homedir.Dir()
if err != nil {
TermMessage("Error finding your home directory\nCan't load config files")
return
}
os.Exit(0)
}()
xdgHome = home + "/.config"
}
configDir = xdgHome + "/micro"
// runtime.SetCPUProfileRate(400)
// f, _ := os.Create("micro.prof")
// pprof.StartCPUProfile(f)
// defer pprof.StopCPUProfile()
if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
// If the xdgHome doesn't exist we should create it
err = os.Mkdir(xdgHome, os.ModePerm)
if err != nil {
TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
}
}
if _, err := os.Stat(configDir); os.IsNotExist(err) {
// If the micro specific config directory doesn't exist we should create that too
err = os.Mkdir(configDir, os.ModePerm)
if err != nil {
TermMessage("Error creating configuration directory: " + err.Error())
}
}
}
// InitScreen creates and initializes the tcell screen
func InitScreen() {
// Should we enable true color?
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
// In order to enable true color, we have to set the TERM to `xterm-truecolor` when
// initializing tcell, but after that, we can set the TERM back to whatever it was
oldTerm := os.Getenv("TERM")
if truecolor {
os.Setenv("TERM", "xterm-truecolor")
}
// Initilize tcell
var err error
InitFlags()
InitLog()
err = config.InitConfigDir(*flagConfigDir)
screen, err = tcell.NewScreen()
if err != nil {
screen.TermMessage(err)
fmt.Println(err)
if err == tcell.ErrTermNotFound {
fmt.Println("Micro does not recognize your terminal:", oldTerm)
fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
}
os.Exit(1)
}
if err = screen.Init(); err != nil {
fmt.Println(err)
os.Exit(1)
}
config.InitRuntimeFiles()
err = config.ReadSettings()
if err != nil {
screen.TermMessage(err)
// Now we can put the TERM back to what it was before
if truecolor {
os.Setenv("TERM", oldTerm)
}
config.InitGlobalSettings()
// flag options
for k, v := range optionFlags {
if *v != "" {
nativeValue, err := config.GetNativeValue(k, config.DefaultAllSettings()[k], *v)
if err != nil {
screen.TermMessage(err)
continue
}
config.GlobalSettings[k] = nativeValue
screen.SetStyle(defStyle)
screen.EnableMouse()
}
// RedrawAll redraws everything -- all the views and the messenger
func RedrawAll() {
messenger.Clear()
w, h := screen.Size()
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
screen.SetContent(x, y, ' ', nil, defStyle)
}
}
DoPluginFlags()
for _, v := range tabs[curTab].views {
v.Display()
}
DisplayTabs()
messenger.Display()
screen.Show()
}
screen.Init()
func LoadAll() {
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir()
// Build a list of available Extensions (Syntax, Colorscheme etc.)
InitRuntimeFiles()
// Load the user's settings
InitGlobalSettings()
InitCommands()
InitBindings()
InitColorscheme()
for _, tab := range tabs {
for _, v := range tab.views {
v.Buf.UpdateRules()
}
}
}
// Passing -version as a flag will have micro print out the version number
var flagVersion = flag.Bool("version", false, "Show the version number and information")
var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
func main() {
flag.Usage = func() {
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
fmt.Print("Micro's options can be set via command line arguments for quick adjustments. For real configuration, please use the bindings.json file (see 'help options').\n\n")
flag.PrintDefaults()
}
optionFlags := make(map[string]*string)
for k, v := range DefaultGlobalSettings() {
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
}
flag.Parse()
if *flagVersion {
// If -version was passed
fmt.Println("Version:", Version)
fmt.Println("Commit hash:", CommitHash)
fmt.Println("Compiled on", CompileDate)
os.Exit(0)
}
// Start the Lua VM for running plugins
L = lua.NewState()
defer L.Close()
// Some encoding stuff in case the user isn't using UTF-8
encoding.Register()
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir()
// Build a list of available Extensions (Syntax, Colorscheme etc.)
InitRuntimeFiles()
// Load the user's settings
InitGlobalSettings()
InitCommands()
InitBindings()
// Start the screen
InitScreen()
// This is just so if we have an error, we can exit cleanly and not completely
// mess up the terminal being worked in
// In other words we need to shut down tcell before the program crashes
defer func() {
if err := recover(); err != nil {
screen.Screen.Fini()
screen.Fini()
fmt.Println("Micro encountered an error:", err)
// backup all open buffers
for _, b := range buffer.OpenBuffers {
b.Backup(false)
}
// Print the stack trace too
fmt.Print(errors.Wrap(err, 2).ErrorStack())
os.Exit(1)
}
}()
err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
// Create a new messenger
// This is used for sending the user messages in the bottom of the editor
messenger = new(Messenger)
messenger.history = make(map[string][]string)
// Now we load the input
buffers := LoadInput()
if len(buffers) == 0 {
screen.Fini()
os.Exit(1)
}
action.InitBindings()
action.InitCommands()
for _, buf := range buffers {
// For each buffer we create a new tab and place the view in that tab
tab := NewTabFromView(NewView(buf))
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
for _, t := range tabs {
for _, v := range t.views {
v.Center(false)
}
err = config.InitColorscheme()
if err != nil {
screen.TermMessage(err)
t.Resize()
}
}
b := LoadInput()
if len(b) == 0 {
// No buffers to open
screen.Screen.Fini()
runtime.Goexit()
for k, v := range optionFlags {
if *v != "" {
SetOption(k, *v)
}
}
action.InitTabs(b)
action.InitGlobals()
// Load all the plugin stuff
// We give plugins access to a bunch of variables here which could be useful to them
L.SetGlobal("OS", luar.New(L, runtime.GOOS))
L.SetGlobal("tabs", luar.New(L, tabs))
L.SetGlobal("curTab", luar.New(L, curTab))
L.SetGlobal("messenger", luar.New(L, messenger))
L.SetGlobal("GetOption", luar.New(L, GetOption))
L.SetGlobal("AddOption", luar.New(L, AddOption))
L.SetGlobal("SetOption", luar.New(L, SetOption))
L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
L.SetGlobal("BindKey", luar.New(L, BindKey))
L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
L.SetGlobal("CurView", luar.New(L, CurView))
L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
return string(r)
}))
L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
return Loc{x, y}
}))
L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
L.SetGlobal("configDir", luar.New(L, configDir))
L.SetGlobal("Reload", luar.New(L, LoadAll))
L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
err = config.RunPluginFn("init")
if err != nil {
screen.TermMessage(err)
// Used for asynchronous jobs
L.SetGlobal("JobStart", luar.New(L, JobStart))
L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
L.SetGlobal("JobSend", luar.New(L, JobSend))
L.SetGlobal("JobStop", luar.New(L, JobStop))
// Extension Files
L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
jobs = make(chan JobFunction, 100)
events = make(chan tcell.Event, 100)
autosave = make(chan bool)
LoadPlugins()
for _, t := range tabs {
for _, v := range t.views {
for pl := range loadedPlugins {
_, err := Call(pl+".onViewOpen", v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
continue
}
}
}
}
events = make(chan tcell.Event)
InitColorscheme()
// Here is the event loop which runs in a separate thread
go func() {
for {
screen.Lock()
e := screen.Screen.PollEvent()
screen.Unlock()
if e != nil {
events <- e
events <- screen.PollEvent()
}
}()
go func() {
for {
time.Sleep(autosaveTime * time.Second)
if globalSettings["autosave"].(bool) {
autosave <- true
}
}
}()
// clear the drawchan so we don't redraw excessively
// if someone requested a redraw before we started displaying
for len(screen.DrawChan()) > 0 {
<-screen.DrawChan()
}
// wait for initial resize event
select {
case event := <-events:
action.Tabs.HandleEvent(event)
case <-time.After(10 * time.Millisecond):
// time out after 10ms
}
// Since this loop is very slow (waits for user input every time) it's
// okay to be inefficient and run it via a function every time
// We do this so we can recover from panics without crashing the editor
for {
DoEvent()
}
}
// Display everything
RedrawAll()
// DoEvent runs the main action loop of the editor
func DoEvent() {
var event tcell.Event
var event tcell.Event
// recover from errors without crashing the editor
defer func() {
if err := recover(); err != nil {
if e, ok := err.(*lua.ApiError); ok {
screen.TermMessage("Lua API error:", e)
// Check for new events
select {
case f := <-jobs:
// If a new job has finished while running in the background we should execute the callback
f.function(f.output, f.args...)
continue
case <-autosave:
CurView().Save(true)
case event = <-events:
}
for event != nil {
switch e := event.(type) {
case *tcell.EventResize:
for _, t := range tabs {
t.Resize()
}
case *tcell.EventMouse:
if !searching {
if e.Buttons() == tcell.Button1 {
// If the user left clicked we check a couple things
_, h := screen.Size()
x, y := e.Position()
if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
// If the user clicked in the bottom bar, and there is a message down there
// we copy it to the clipboard.
// Often error messages are displayed down there so it can be useful to easily
// copy the message
clipboard.WriteAll(messenger.message, "primary")
break
}
if CurView().mouseReleased {
// We loop through each view in the current tab and make sure the current view
// is the one being clicked in
for _, v := range tabs[curTab].views {
if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
tabs[curTab].CurView = v.Num
}
}
}
}
}
}
// This function checks the mouse event for the possibility of changing the current tab
// If the tab was changed it returns true
if TabbarHandleMouseEvent(event) {
break
}
if searching {
// Since searching is done in real time, we need to redraw every time
// there is a new event in the search bar so we need a special function
// to run instead of the standard HandleEvent.
HandleSearchEvent(event, CurView())
} else {
screen.TermMessage("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/zyedidia/micro/issues")
// Send it to the view
CurView().HandleEvent(event)
}
select {
case event = <-events:
default:
event = nil
}
}
}()
// Display everything
screen.Screen.Fill(' ', config.DefStyle)
screen.Screen.HideCursor()
action.Tabs.Display()
for _, ep := range action.MainTab().Panes {
ep.Display()
}
action.MainTab().Display()
action.InfoBar.Display()
screen.Screen.Show()
// Check for new events
select {
case f := <-shell.Jobs:
// If a new job has finished while running in the background we should execute the callback
f.Function(f.Output, f.Args)
case <-config.Autosave:
for _, b := range buffer.OpenBuffers {
b.Save()
}
case <-shell.CloseTerms:
case event = <-events:
case <-screen.DrawChan():
for len(screen.DrawChan()) > 0 {
<-screen.DrawChan()
}
}
if action.InfoBar.HasPrompt {
action.InfoBar.HandleEvent(event)
} else {
action.Tabs.HandleEvent(event)
}
}

158
cmd/micro/plugin.go Normal file
View File

@@ -0,0 +1,158 @@
package main
import (
"errors"
"io/ioutil"
"os"
"strings"
"github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"
)
var loadedPlugins map[string]string
// Call calls the lua function 'function'
// If it does not exist nothing happens, if there is an error,
// the error is returned
func Call(function string, args ...interface{}) (lua.LValue, error) {
var luaFunc lua.LValue
if strings.Contains(function, ".") {
plugin := L.GetGlobal(strings.Split(function, ".")[0])
if plugin.String() == "nil" {
return nil, errors.New("function does not exist: " + function)
}
luaFunc = L.GetField(plugin, strings.Split(function, ".")[1])
} else {
luaFunc = L.GetGlobal(function)
}
if luaFunc.String() == "nil" {
return nil, errors.New("function does not exist: " + function)
}
var luaArgs []lua.LValue
for _, v := range args {
luaArgs = append(luaArgs, luar.New(L, v))
}
err := L.CallByParam(lua.P{
Fn: luaFunc,
NRet: 1,
Protect: true,
}, luaArgs...)
ret := L.Get(-1) // returned value
if ret.String() != "nil" {
L.Pop(1) // remove received value
}
return ret, err
}
// LuaFunctionBinding is a function generator which takes the name of a lua function
// and creates a function that will call that lua function
// Specifically it creates a function that can be called as a binding because this is used
// to bind keys to lua functions
func LuaFunctionBinding(function string) func(*View, bool) bool {
return func(v *View, _ bool) bool {
_, err := Call(function, nil)
if err != nil {
TermMessage(err)
}
return false
}
}
func unpack(old []string) []interface{} {
new := make([]interface{}, len(old))
for i, v := range old {
new[i] = v
}
return new
}
// LuaFunctionCommand is the same as LuaFunctionBinding except it returns a normal function
// so that a command can be bound to a lua function
func LuaFunctionCommand(function string) func([]string) {
return func(args []string) {
_, err := Call(function, unpack(args)...)
if err != nil {
TermMessage(err)
}
}
}
// LuaFunctionComplete returns a function which can be used for autocomplete in plugins
func LuaFunctionComplete(function string) func(string) []string {
return func(input string) (result []string) {
res, err := Call(function, input)
if err != nil {
TermMessage(err)
}
if tbl, ok := res.(*lua.LTable); !ok {
TermMessage(function, "should return a table of strings")
} else {
for i := 1; i <= tbl.Len(); i++ {
val := tbl.RawGetInt(i)
if v, ok := val.(lua.LString); !ok {
TermMessage(function, "should return a table of strings")
} else {
result = append(result, string(v))
}
}
}
return result
}
}
func LuaFunctionJob(function string) func(string, ...string) {
return func(output string, args ...string) {
_, err := Call(function, unpack(append([]string{output}, args...))...)
if err != nil {
TermMessage(err)
}
}
}
// luaPluginName convert a human-friendly plugin name into a valid lua variable name.
func luaPluginName(name string) string {
return strings.Replace(name, "-", "_", -1)
}
// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins
func LoadPlugins() {
loadedPlugins = make(map[string]string)
for _, plugin := range ListRuntimeFiles(RTPlugin) {
pluginName := plugin.Name()
if _, ok := loadedPlugins[pluginName]; ok {
continue
}
data, err := plugin.Data()
if err != nil {
TermMessage("Error loading plugin: " + pluginName)
continue
}
pluginLuaName := luaPluginName(pluginName)
pluginDef := "\nlocal P = {}\n" + pluginLuaName + " = P\nsetmetatable(" + pluginLuaName + ", {__index = _G})\nsetfenv(1, P)\n"
if err := L.DoString(pluginDef + string(data)); err != nil {
TermMessage(err)
continue
}
loadedPlugins[pluginName] = pluginLuaName
}
if _, err := os.Stat(configDir + "/init.lua"); err == nil {
pluginDef := "\nlocal P = {}\n" + "init" + " = P\nsetmetatable(" + "init" + ", {__index = _G})\nsetfenv(1, P)\n"
data, _ := ioutil.ReadFile(configDir + "/init.lua")
if err := L.DoString(pluginDef + string(data)); err != nil {
TermMessage(err)
}
loadedPlugins["init"] = "init"
}
}

View File

@@ -1,4 +1,4 @@
package config
package main
import (
"archive/zip"
@@ -14,10 +14,8 @@ import (
"sync"
"github.com/blang/semver"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/json5"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/json5/encoding/json5"
)
var (
@@ -56,10 +54,6 @@ type PluginVersion struct {
Require PluginDependencies
}
func (pv *PluginVersion) Pack() *PluginPackage {
return pv.pack
}
// PluginVersions is a slice of PluginVersion
type PluginVersions []*PluginVersion
@@ -118,17 +112,18 @@ func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackag
}
// Fetch retrieves all available PluginPackages from the given channels
func (pc PluginChannels) Fetch(out io.Writer) PluginPackages {
func (pc PluginChannels) Fetch() PluginPackages {
return fetchAllSources(len(pc), func(i int) PluginPackages {
return pc[i].Fetch(out)
return pc[i].Fetch()
})
}
// Fetch retrieves all available PluginPackages from the given channel
func (pc PluginChannel) Fetch(out io.Writer) PluginPackages {
func (pc PluginChannel) Fetch() PluginPackages {
// messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
resp, err := http.Get(string(pc))
if err != nil {
fmt.Fprintln(out, "Failed to query plugin channel:\n", err)
TermMessage("Failed to query plugin channel:\n", err)
return PluginPackages{}
}
defer resp.Body.Close()
@@ -136,19 +131,20 @@ func (pc PluginChannel) Fetch(out io.Writer) PluginPackages {
var repositories []PluginRepository
if err := decoder.Decode(&repositories); err != nil {
fmt.Fprintln(out, "Failed to decode channel data:\n", err)
TermMessage("Failed to decode channel data:\n", err)
return PluginPackages{}
}
return fetchAllSources(len(repositories), func(i int) PluginPackages {
return repositories[i].Fetch(out)
return repositories[i].Fetch()
})
}
// Fetch retrieves all available PluginPackages from the given repository
func (pr PluginRepository) Fetch(out io.Writer) PluginPackages {
func (pr PluginRepository) Fetch() PluginPackages {
// messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
resp, err := http.Get(string(pr))
if err != nil {
fmt.Fprintln(out, "Failed to query plugin repository:\n", err)
TermMessage("Failed to query plugin repository:\n", err)
return PluginPackages{}
}
defer resp.Body.Close()
@@ -156,7 +152,7 @@ func (pr PluginRepository) Fetch(out io.Writer) PluginPackages {
var plugins PluginPackages
if err := decoder.Decode(&plugins); err != nil {
fmt.Fprintln(out, "Failed to decode repository data:\n", err)
TermMessage("Failed to decode repository data:\n", err)
return PluginPackages{}
}
if len(plugins) > 0 {
@@ -218,10 +214,10 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
}
// GetAllPluginPackages gets all PluginPackages which may be available.
func GetAllPluginPackages(out io.Writer) PluginPackages {
func GetAllPluginPackages() PluginPackages {
if allPluginPackages == nil {
getOption := func(name string) []string {
data := GetGlobalOption(name)
data := GetOption(name)
if strs, ok := data.([]string); ok {
return strs
}
@@ -249,9 +245,9 @@ func GetAllPluginPackages(out io.Writer) PluginPackages {
}
allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
if i == 0 {
return channels.Fetch(out)
return channels.Fetch()
}
return repos[i-1].Fetch(out)
return repos[i-1].Fetch()
})
}
return allPluginPackages
@@ -301,8 +297,8 @@ func (pp PluginPackage) Match(text string) bool {
}
// IsInstallable returns true if the package can be installed.
func (pp PluginPackage) IsInstallable(out io.Writer) error {
_, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{
func (pp PluginPackage) IsInstallable() error {
_, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
&PluginDependency{
Name: pp.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
@@ -312,18 +308,18 @@ func (pp PluginPackage) IsInstallable(out io.Writer) error {
// SearchPlugin retrieves a list of all PluginPackages which match the given search text and
// could be or are already installed
func SearchPlugin(out io.Writer, texts []string) (plugins PluginPackages) {
func SearchPlugin(texts []string) (plugins PluginPackages) {
plugins = make(PluginPackages, 0)
pluginLoop:
for _, pp := range GetAllPluginPackages(out) {
for _, pp := range GetAllPluginPackages() {
for _, text := range texts {
if !pp.Match(text) {
continue pluginLoop
}
}
if err := pp.IsInstallable(out); err == nil {
if err := pp.IsInstallable(); err == nil {
plugins = append(plugins, pp)
}
}
@@ -331,7 +327,7 @@ pluginLoop:
}
func isUnknownCoreVersion() bool {
_, err := semver.ParseTolerant(util.Version)
_, err := semver.ParseTolerant(Version)
return err != nil
}
@@ -359,15 +355,12 @@ func newStaticPluginVersion(name, version string) *PluginVersion {
func GetInstalledVersions(withCore bool) PluginVersions {
result := PluginVersions{}
if withCore {
result = append(result, newStaticPluginVersion(CorePluginName, util.Version))
result = append(result, newStaticPluginVersion(CorePluginName, Version))
}
for _, p := range Plugins {
if !p.IsEnabled() {
continue
}
version := GetInstalledPluginVersion(p.Name)
if pv := newStaticPluginVersion(p.Name, version); pv != nil {
for name, lpname := range loadedPlugins {
version := GetInstalledPluginVersion(lpname)
if pv := newStaticPluginVersion(name, version); pv != nil {
result = append(result, pv)
}
}
@@ -377,9 +370,9 @@ func GetInstalledVersions(withCore bool) PluginVersions {
// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
func GetInstalledPluginVersion(name string) string {
plugin := ulua.L.GetGlobal(name)
plugin := L.GetGlobal(name)
if plugin != lua.LNil {
version := ulua.L.GetField(plugin, "VERSION")
version := L.GetField(plugin, "VERSION")
if str, ok := version.(lua.LString); ok {
return string(str)
@@ -389,8 +382,8 @@ func GetInstalledPluginVersion(name string) string {
}
// DownloadAndInstall downloads and installs the given plugin and version
func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
fmt.Fprintf(out, "Downloading %q (%s) from %q\n", pv.pack.Name, pv.Version, pv.Url)
func (pv *PluginVersion) DownloadAndInstall() error {
messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
resp, err := http.Get(pv.Url)
if err != nil {
return err
@@ -405,7 +398,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
if err != nil {
return err
}
targetDir := filepath.Join(ConfigDir, "plug", pv.pack.Name)
targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
dirPerm := os.FileMode(0755)
if err = os.MkdirAll(targetDir, dirPerm); err != nil {
return err
@@ -429,7 +422,6 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
}
}
// Install files and directory's
for _, f := range z.File {
parts := strings.Split(f.Name, "/")
if allPrefixed {
@@ -442,12 +434,6 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
return err
}
} else {
basepath := filepath.Dir(targetName)
if err := os.MkdirAll(basepath, dirPerm); err != nil {
return err
}
content, err := f.Open()
if err != nil {
return err
@@ -539,7 +525,7 @@ func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDe
return selectedVersions, nil
}
func (pv PluginVersions) install(out io.Writer) {
func (pv PluginVersions) install() {
anyInstalled := false
currentlyInstalled := GetInstalledVersions(true)
@@ -548,16 +534,16 @@ func (pv PluginVersions) install(out io.Writer) {
shouldInstall := true
if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
if pv.Version.NE(sel.Version) {
fmt.Fprintln(out, "Uninstalling", sel.pack.Name)
UninstallPlugin(out, sel.pack.Name)
messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
UninstallPlugin(sel.pack.Name)
} else {
shouldInstall = false
}
}
if shouldInstall {
if err := sel.DownloadAndInstall(out); err != nil {
fmt.Fprintln(out, err)
if err := sel.DownloadAndInstall(); err != nil {
messenger.Error(err)
return
}
anyInstalled = true
@@ -565,58 +551,47 @@ func (pv PluginVersions) install(out io.Writer) {
}
}
if anyInstalled {
fmt.Fprintln(out, "One or more plugins installed.")
messenger.Message("One or more plugins installed. Please restart micro.")
} else {
fmt.Fprintln(out, "Nothing to install / update")
messenger.AddLog("Nothing to install / update")
}
}
// UninstallPlugin deletes the plugin folder of the given plugin
func UninstallPlugin(out io.Writer, name string) {
for _, p := range Plugins {
if !p.IsEnabled() {
continue
}
if p.Name == name {
p.Loaded = false
if err := os.RemoveAll(filepath.Join(ConfigDir, "plug", p.DirName)); err != nil {
fmt.Fprintln(out, err)
return
}
break
}
func UninstallPlugin(name string) {
if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
messenger.Error(err)
return
}
delete(loadedPlugins, name)
}
// Install installs the plugin
func (pl PluginPackage) Install(out io.Writer) {
selected, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{
func (pl PluginPackage) Install() {
selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
&PluginDependency{
Name: pl.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
if err != nil {
fmt.Fprintln(out, err)
TermMessage(err)
return
}
selected.install(out)
selected.install()
}
// UpdatePlugins updates the given plugins
func UpdatePlugins(out io.Writer, plugins []string) {
func UpdatePlugins(plugins []string) {
// if no plugins are specified, update all installed plugins.
if len(plugins) == 0 {
for _, p := range Plugins {
if !p.IsEnabled() {
continue
}
plugins = append(plugins, p.Name)
for name := range loadedPlugins {
plugins = append(plugins, name)
}
}
fmt.Fprintln(out, "Checking for plugin updates")
messenger.AddLog("Checking for plugin updates")
microVersion := PluginVersions{
newStaticPluginVersion(CorePluginName, util.Version),
newStaticPluginVersion(CorePluginName, Version),
}
var updates = make(PluginDependencies, 0)
@@ -631,82 +606,10 @@ func UpdatePlugins(out io.Writer, plugins []string) {
}
}
selected, err := GetAllPluginPackages(out).Resolve(microVersion, updates)
selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
if err != nil {
fmt.Fprintln(out, err)
TermMessage(err)
return
}
selected.install(out)
}
func PluginCommand(out io.Writer, cmd string, args []string) {
switch cmd {
case "install":
installedVersions := GetInstalledVersions(false)
for _, plugin := range args {
pp := GetAllPluginPackages(out).Get(plugin)
if pp == nil {
fmt.Fprintln(out, "Unknown plugin \""+plugin+"\"")
} else if err := pp.IsInstallable(out); err != nil {
fmt.Fprintln(out, "Error installing ", plugin, ": ", err)
} else {
for _, installed := range installedVersions {
if pp.Name == installed.Pack().Name {
if pp.Versions[0].Version.Compare(installed.Version) == 1 {
fmt.Fprintln(out, pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
} else {
fmt.Fprintln(out, pp.Name, " is already installed")
}
}
}
pp.Install(out)
}
}
case "remove":
removed := ""
for _, plugin := range args {
// check if the plugin exists.
for _, p := range Plugins {
if p.Name == plugin && p.Default {
fmt.Fprintln(out, "Default plugins cannot be removed, but can be disabled via settings.")
continue
}
if p.Name == plugin {
UninstallPlugin(out, plugin)
removed += plugin + " "
continue
}
}
}
if removed != "" {
fmt.Fprintln(out, "Removed ", removed)
} else {
fmt.Fprintln(out, "No plugins removed")
}
case "update":
UpdatePlugins(out, args)
case "list":
plugins := GetInstalledVersions(false)
fmt.Fprintln(out, "The following plugins are currently installed:")
for _, p := range plugins {
fmt.Fprintf(out, "%s (%s)\n", p.Pack().Name, p.Version)
}
case "search":
plugins := SearchPlugin(out, args)
fmt.Fprintln(out, len(plugins), " plugins found")
for _, p := range plugins {
fmt.Fprintln(out, "----------------")
fmt.Fprintln(out, p.String())
}
fmt.Fprintln(out, "----------------")
case "available":
packages := GetAllPluginPackages(out)
fmt.Fprintln(out, "Available Plugins:")
for _, pkg := range packages {
fmt.Fprintln(out, pkg.Name)
}
default:
fmt.Fprintln(out, "Invalid plugin command")
}
selected.install()
}

View File

@@ -1,11 +1,10 @@
package config
package main
import (
"github.com/blang/semver"
"testing"
"github.com/blang/semver"
"github.com/zyedidia/json5"
"github.com/zyedidia/json5/encoding/json5"
)
func TestDependencyResolving(t *testing.T) {
@@ -36,7 +35,7 @@ func TestDependencyResolving(t *testing.T) {
if v == nil {
t.Errorf("Failed to resolve %s", name)
} else if expected.NE(v.Version) {
t.Errorf("%s resolved in wrong version %v", name, v)
t.Errorf("%s resolved in wrong version got %s", name, v)
}
}

207
cmd/micro/rtfiles.go Normal file
View File

@@ -0,0 +1,207 @@
package main
import (
"io/ioutil"
"os"
"path"
"path/filepath"
)
const (
RTColorscheme = "colorscheme"
RTSyntax = "syntax"
RTHelp = "help"
RTPlugin = "plugin"
)
// RuntimeFile allows the program to read runtime data like colorschemes or syntax files
type RuntimeFile interface {
// Name returns a name of the file without paths or extensions
Name() string
// Data returns the content of the file.
Data() ([]byte, error)
}
// allFiles contains all available files, mapped by filetype
var allFiles map[string][]RuntimeFile
// some file on filesystem
type realFile string
// some asset file
type assetFile string
// some file on filesystem but with a different name
type namedFile struct {
realFile
name string
}
// a file with the data stored in memory
type memoryFile struct {
name string
data []byte
}
func (mf memoryFile) Name() string {
return mf.name
}
func (mf memoryFile) Data() ([]byte, error) {
return mf.data, nil
}
func (rf realFile) Name() string {
fn := filepath.Base(string(rf))
return fn[:len(fn)-len(filepath.Ext(fn))]
}
func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf))
}
func (af assetFile) Name() string {
fn := path.Base(string(af))
return fn[:len(fn)-len(path.Ext(fn))]
}
func (af assetFile) Data() ([]byte, error) {
return Asset(string(af))
}
func (nf namedFile) Name() string {
return nf.name
}
// AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType string, file RuntimeFile) {
if allFiles == nil {
allFiles = make(map[string][]RuntimeFile)
}
allFiles[fileType] = append(allFiles[fileType], file)
}
// AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) {
files, _ := ioutil.ReadDir(directory)
for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name())
AddRuntimeFile(fileType, realFile(fullPath))
}
}
}
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
files, err := AssetDir(directory)
if err != nil {
return
}
for _, f := range files {
if ok, _ := path.Match(pattern, f); ok {
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
}
}
}
// FindRuntimeFile finds a runtime file of the given filetype and name
// will return nil if no file was found
func FindRuntimeFile(fileType, name string) RuntimeFile {
for _, f := range ListRuntimeFiles(fileType) {
if f.Name() == name {
return f
}
}
return nil
}
// ListRuntimeFiles lists all known runtime files for the given filetype
func ListRuntimeFiles(fileType string) []RuntimeFile {
if files, ok := allFiles[fileType]; ok {
return files
}
return []RuntimeFile{}
}
// InitRuntimeFiles initializes all assets file and the config directory
func InitRuntimeFiles() {
add := func(fileType, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(configDir, dir), pattern)
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
}
add(RTColorscheme, "colorschemes", "*.micro")
add(RTSyntax, "syntax", "*.yaml")
add(RTHelp, "help", "*.md")
// Search configDir for plugin-scripts
files, _ := ioutil.ReadDir(filepath.Join(configDir, "plugins"))
for _, f := range files {
realpath, _ := filepath.EvalSymlinks(filepath.Join(configDir, "plugins", f.Name()))
realpathStat, _ := os.Stat(realpath)
if realpathStat.IsDir() {
scriptPath := filepath.Join(configDir, "plugins", f.Name(), f.Name()+".lua")
if _, err := os.Stat(scriptPath); err == nil {
AddRuntimeFile(RTPlugin, realFile(scriptPath))
}
}
}
if files, err := AssetDir("runtime/plugins"); err == nil {
for _, f := range files {
scriptPath := path.Join("runtime/plugins", f, f+".lua")
if _, err := AssetInfo(scriptPath); err == nil {
AddRuntimeFile(RTPlugin, assetFile(scriptPath))
}
}
}
}
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
func PluginReadRuntimeFile(fileType, name string) string {
if file := FindRuntimeFile(fileType, name); file != nil {
if data, err := file.Data(); err == nil {
return string(data)
}
}
return ""
}
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
func PluginListRuntimeFiles(fileType string) []string {
files := ListRuntimeFiles(fileType)
result := make([]string, len(files))
for i, f := range files {
result[i] = f.Name()
}
return result
}
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
func PluginAddRuntimeFile(plugin, filetype, filePath string) {
fullpath := filepath.Join(configDir, "plugins", plugin, filePath)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFile(filetype, realFile(fullpath))
} else {
fullpath = path.Join("runtime", "plugins", plugin, filePath)
AddRuntimeFile(filetype, assetFile(fullpath))
}
}
// PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin
func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern string) {
fullpath := filepath.Join(configDir, "plugins", plugin, directory)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else {
fullpath = path.Join("runtime", "plugins", plugin, directory)
AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
}
}
// PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string
func PluginAddRuntimeFileFromMemory(plugin, filetype, filename, data string) {
AddRuntimeFile(filetype, memoryFile{filename, []byte(data)})
}

3889
cmd/micro/runtime.go Normal file

File diff suppressed because one or more lines are too long

150
cmd/micro/search.go Normal file
View File

@@ -0,0 +1,150 @@
package main
import (
"regexp"
"github.com/zyedidia/tcell"
)
var (
// What was the last search
lastSearch string
// Where should we start the search down from (or up from)
searchStart int
// Is there currently a search in progress
searching bool
// Stores the history for searching
searchHistory []string
)
// BeginSearch starts a search
func BeginSearch(searchStr string) {
searchHistory = append(searchHistory, "")
messenger.historyNum = len(searchHistory) - 1
searching = true
messenger.response = searchStr
messenger.cursorx = Count(searchStr)
messenger.Message("Find: ")
messenger.hasPrompt = true
}
// EndSearch stops the current search
func EndSearch() {
searchHistory[len(searchHistory)-1] = messenger.response
searching = false
messenger.hasPrompt = false
messenger.Clear()
messenger.Reset()
if lastSearch != "" {
messenger.Message("^P Previous ^N Next")
}
}
// exit the search mode, reset active search phrase, and clear status bar
func ExitSearch(v *View) {
lastSearch = ""
searching = false
messenger.hasPrompt = false
messenger.Clear()
messenger.Reset()
v.Cursor.ResetSelection()
}
// HandleSearchEvent takes an event and a view and will do a real time match from the messenger's output
// to the current buffer. It searches down the buffer.
func HandleSearchEvent(event tcell.Event, v *View) {
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyEscape:
// Exit the search mode
ExitSearch(v)
return
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEnter:
// Done
EndSearch()
return
}
}
messenger.HandleEvent(event, searchHistory)
if messenger.cursorx < 0 {
// Done
EndSearch()
return
}
if messenger.response == "" {
v.Cursor.ResetSelection()
// We don't end the search though
return
}
Search(messenger.response, v, true)
v.Relocate()
return
}
// Search searches in the view for the given regex. The down bool
// specifies whether it should search down from the searchStart position
// or up from there
func Search(searchStr string, v *View, down bool) {
if searchStr == "" {
return
}
var str string
var charPos int
text := v.Buf.String()
if down {
str = string([]rune(text)[searchStart:])
charPos = searchStart
} else {
str = string([]rune(text)[:searchStart])
}
r, err := regexp.Compile(searchStr)
if v.Buf.Settings["ignorecase"].(bool) {
r, err = regexp.Compile("(?i)" + searchStr)
}
if err != nil {
return
}
matches := r.FindAllStringIndex(str, -1)
var match []int
if matches == nil {
// Search the entire buffer now
matches = r.FindAllStringIndex(text, -1)
charPos = 0
if matches == nil {
v.Cursor.ResetSelection()
return
}
if !down {
match = matches[len(matches)-1]
} else {
match = matches[0]
}
str = text
}
if !down {
match = matches[len(matches)-1]
} else {
match = matches[0]
}
if match[0] == match[1] {
return
}
v.Cursor.SetSelectionStart(FromCharPos(charPos+runePos(match[0], str), v.Buf))
v.Cursor.SetSelectionEnd(FromCharPos(charPos+runePos(match[1], str), v.Buf))
v.Cursor.Loc = v.Cursor.CurSelection[1]
lastSearch = searchStr
}

432
cmd/micro/settings.go Normal file
View File

@@ -0,0 +1,432 @@
package main
import (
"errors"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"github.com/zyedidia/glob"
"github.com/zyedidia/json5/encoding/json5"
)
type optionValidator func(string, interface{}) error
// The options that the user can set
var globalSettings map[string]interface{}
// Options with validators
var optionValidators = map[string]optionValidator{
"tabsize": validatePositiveValue,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"colorscheme": validateColorscheme,
"colorcolumn": validateNonNegativeValue,
}
// InitGlobalSettings initializes the options map and sets all options to their default values
func InitGlobalSettings() {
defaults := DefaultGlobalSettings()
var parsed map[string]interface{}
filename := configDir + "/settings.json"
writeSettings := false
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if !strings.HasPrefix(string(input), "null") {
if err != nil {
TermMessage("Error reading settings.json file: " + err.Error())
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
}
} else {
writeSettings = true
}
}
globalSettings = make(map[string]interface{})
for k, v := range defaults {
globalSettings[k] = v
}
for k, v := range parsed {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
globalSettings[k] = v
}
}
if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings {
err := WriteSettings(filename)
if err != nil {
TermMessage("Error writing settings.json file: " + err.Error())
}
}
}
// InitLocalSettings scans the json in settings.json and sets the options locally based
// on whether the buffer matches the glob
func InitLocalSettings(buf *Buffer) {
var parsed map[string]interface{}
filename := configDir + "/settings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading settings.json file: " + err.Error())
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
}
}
for k, v := range parsed {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
g, err := glob.Compile(k)
if err != nil {
TermMessage("Error with glob setting ", k, ": ", err)
continue
}
if g.MatchString(buf.Path) {
for k1, v1 := range v.(map[string]interface{}) {
buf.Settings[k1] = v1
}
}
}
}
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
var err error
if _, e := os.Stat(configDir); e == nil {
parsed := make(map[string]interface{})
filename := configDir + "/settings.json"
for k, v := range globalSettings {
parsed[k] = v
}
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if string(input) != "null" {
if err != nil {
return err
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
}
for k, v := range parsed {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if _, ok := globalSettings[k]; ok {
parsed[k] = globalSettings[k]
}
}
}
}
}
txt, _ := json5.MarshalIndent(parsed, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
// AddOption creates a new option. This is meant to be called by plugins to add options.
func AddOption(name string, value interface{}) {
globalSettings[name] = value
err := WriteSettings(configDir + "/settings.json")
if err != nil {
TermMessage("Error writing settings.json file: " + err.Error())
}
}
// GetGlobalOption returns the global value of the given option
func GetGlobalOption(name string) interface{} {
return globalSettings[name]
}
// GetLocalOption returns the local value of the given option
func GetLocalOption(name string, buf *Buffer) interface{} {
return buf.Settings[name]
}
// GetOption returns the value of the given option
// If there is a local version of the option, it returns that
// otherwise it will return the global version
func GetOption(name string) interface{} {
if GetLocalOption(name, CurView().Buf) != nil {
return GetLocalOption(name, CurView().Buf)
}
return GetGlobalOption(name)
}
// DefaultGlobalSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultGlobalSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"keepautoindent": false,
"autosave": false,
"colorcolumn": float64(0),
"colorscheme": "default",
"cursorline": true,
"eofnewline": false,
"rmtrailingws": false,
"ignorecase": false,
"indentchar": " ",
"infobar": true,
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollspeed": float64(2),
"scrollmargin": float64(3),
"softwrap": false,
"splitRight": true,
"splitBottom": true,
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"termtitle": false,
"pluginchannels": []string{
"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json",
},
"pluginrepos": []string{},
"useprimary": true,
}
}
// DefaultLocalSettings returns the default local settings
// Note that filetype is a local only option
func DefaultLocalSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"keepautoindent": false,
"autosave": false,
"colorcolumn": float64(0),
"cursorline": true,
"eofnewline": false,
"rmtrailingws": false,
"filetype": "Unknown",
"ignorecase": false,
"indentchar": " ",
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollspeed": float64(2),
"scrollmargin": float64(3),
"softwrap": false,
"splitRight": true,
"splitBottom": true,
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"useprimary": true,
}
}
// SetOption attempts to set the given option to the value
// By default it will set the option as global, but if the option
// is local only it will set the local version
// Use setlocal to force an option to be set locally
func SetOption(option, value string) error {
if _, ok := globalSettings[option]; !ok {
if _, ok := CurView().Buf.Settings[option]; !ok {
return errors.New("Invalid option")
}
SetLocalOption(option, value, CurView())
return nil
}
var nativeValue interface{}
kind := reflect.TypeOf(globalSettings[option]).Kind()
if kind == reflect.Bool {
b, err := ParseBool(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = b
} else if kind == reflect.String {
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
globalSettings[option] = nativeValue
if option == "colorscheme" {
// LoadSyntaxFiles()
InitColorscheme()
for _, tab := range tabs {
for _, view := range tab.views {
view.Buf.UpdateRules()
}
}
}
if option == "infobar" {
for _, tab := range tabs {
tab.Resize()
}
}
if _, ok := CurView().Buf.Settings[option]; ok {
for _, tab := range tabs {
for _, view := range tab.views {
SetLocalOption(option, value, view)
}
}
}
return nil
}
// SetLocalOption sets the local version of this option
func SetLocalOption(option, value string, view *View) error {
buf := view.Buf
if _, ok := buf.Settings[option]; !ok {
return errors.New("Invalid option")
}
var nativeValue interface{}
kind := reflect.TypeOf(buf.Settings[option]).Kind()
if kind == reflect.Bool {
b, err := ParseBool(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = b
} else if kind == reflect.String {
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
buf.Settings[option] = nativeValue
if option == "statusline" {
view.ToggleStatusLine()
}
if option == "filetype" {
// LoadSyntaxFiles()
InitColorscheme()
buf.UpdateRules()
}
if option == "syntax" {
if !nativeValue.(bool) {
buf.ClearMatches()
} else {
buf.highlighter.HighlightStates(buf)
}
}
return nil
}
// SetOptionAndSettings sets the given option and saves the option setting to the settings config file
func SetOptionAndSettings(option, value string) {
filename := configDir + "/settings.json"
err := SetOption(option, value)
if err != nil {
messenger.Error(err.Error())
return
}
err = WriteSettings(filename)
if err != nil {
messenger.Error("Error writing to settings.json: " + err.Error())
return
}
}
func optionIsValid(option string, value interface{}) error {
if validator, ok := optionValidators[option]; ok {
return validator(option, value)
}
return nil
}
// Option validators
func validatePositiveValue(option string, value interface{}) error {
tabsize, ok := value.(float64)
if !ok {
return errors.New("Expected numeric type for " + option)
}
if tabsize < 1 {
return errors.New(option + " must be greater than 0")
}
return nil
}
func validateNonNegativeValue(option string, value interface{}) error {
nativeValue, ok := value.(float64)
if !ok {
return errors.New("Expected numeric type for " + option)
}
if nativeValue < 0 {
return errors.New(option + " must be non-negative")
}
return nil
}
func validateColorscheme(option string, value interface{}) error {
colorscheme, ok := value.(string)
if !ok {
return errors.New("Expected string type for colorscheme")
}
if !ColorschemeExists(colorscheme) {
return errors.New(colorscheme + " is not a valid colorscheme")
}
return nil
}

317
cmd/micro/split_tree.go Normal file
View File

@@ -0,0 +1,317 @@
package main
// SplitType specifies whether a split is horizontal or vertical
type SplitType bool
const (
// VerticalSplit type
VerticalSplit = false
// HorizontalSplit type
HorizontalSplit = true
)
// A Node on the split tree
type Node interface {
VSplit(buf *Buffer, splitIndex int)
HSplit(buf *Buffer, splitIndex int)
String() string
}
// A LeafNode is an actual split so it contains a view
type LeafNode struct {
view *View
parent *SplitTree
}
// NewLeafNode returns a new leaf node containing the given view
func NewLeafNode(v *View, parent *SplitTree) *LeafNode {
n := new(LeafNode)
n.view = v
n.view.splitNode = n
n.parent = parent
return n
}
// A SplitTree is a Node itself and it contains other nodes
type SplitTree struct {
kind SplitType
parent *SplitTree
children []Node
x int
y int
width int
height int
lockWidth bool
lockHeight bool
tabNum int
}
// VSplit creates a vertical split
func (l *LeafNode) VSplit(buf *Buffer, splitIndex int) {
if splitIndex < 0 {
splitIndex = 0
}
tab := tabs[l.parent.tabNum]
if l.parent.kind == VerticalSplit {
if splitIndex > len(l.parent.children) {
splitIndex = len(l.parent.children)
}
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
l.parent.children = append(l.parent.children, nil)
copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:])
l.parent.children[splitIndex] = NewLeafNode(newView, l.parent)
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
} else {
if splitIndex > 1 {
splitIndex = 1
}
s := new(SplitTree)
s.kind = VerticalSplit
s.parent = l.parent
s.tabNum = l.parent.tabNum
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
if splitIndex == 1 {
s.children = []Node{l, NewLeafNode(newView, s)}
} else {
s.children = []Node{NewLeafNode(newView, s), l}
}
l.parent.children[search(l.parent.children, l)] = s
l.parent = s
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
}
tab.Resize()
}
// HSplit creates a horizontal split
func (l *LeafNode) HSplit(buf *Buffer, splitIndex int) {
if splitIndex < 0 {
splitIndex = 0
}
tab := tabs[l.parent.tabNum]
if l.parent.kind == HorizontalSplit {
if splitIndex > len(l.parent.children) {
splitIndex = len(l.parent.children)
}
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
l.parent.children = append(l.parent.children, nil)
copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:])
l.parent.children[splitIndex] = NewLeafNode(newView, l.parent)
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
} else {
if splitIndex > 1 {
splitIndex = 1
}
s := new(SplitTree)
s.kind = HorizontalSplit
s.tabNum = l.parent.tabNum
s.parent = l.parent
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
newView.Num = len(tab.views)
if splitIndex == 1 {
s.children = []Node{l, NewLeafNode(newView, s)}
} else {
s.children = []Node{NewLeafNode(newView, s), l}
}
l.parent.children[search(l.parent.children, l)] = s
l.parent = s
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
}
tab.Resize()
}
// Delete deletes a split
func (l *LeafNode) Delete() {
i := search(l.parent.children, l)
copy(l.parent.children[i:], l.parent.children[i+1:])
l.parent.children[len(l.parent.children)-1] = nil
l.parent.children = l.parent.children[:len(l.parent.children)-1]
tab := tabs[l.parent.tabNum]
j := findView(tab.views, l.view)
copy(tab.views[j:], tab.views[j+1:])
tab.views[len(tab.views)-1] = nil // or the zero value of T
tab.views = tab.views[:len(tab.views)-1]
for i, v := range tab.views {
v.Num = i
}
if tab.CurView > 0 {
tab.CurView--
}
}
// Cleanup rearranges all the parents after a split has been deleted
func (s *SplitTree) Cleanup() {
for i, node := range s.children {
if n, ok := node.(*SplitTree); ok {
if len(n.children) == 1 {
if child, ok := n.children[0].(*LeafNode); ok {
s.children[i] = child
child.parent = s
continue
}
}
n.Cleanup()
}
}
}
// ResizeSplits resizes all the splits correctly
func (s *SplitTree) ResizeSplits() {
lockedWidth := 0
lockedHeight := 0
lockedChildren := 0
for _, node := range s.children {
if n, ok := node.(*LeafNode); ok {
if s.kind == VerticalSplit {
if n.view.LockWidth {
lockedWidth += n.view.Width
lockedChildren++
}
} else {
if n.view.LockHeight {
lockedHeight += n.view.Height
lockedChildren++
}
}
} else if n, ok := node.(*SplitTree); ok {
if s.kind == VerticalSplit {
if n.lockWidth {
lockedWidth += n.width
lockedChildren++
}
} else {
if n.lockHeight {
lockedHeight += n.height
lockedChildren++
}
}
}
}
x, y := 0, 0
for _, node := range s.children {
if n, ok := node.(*LeafNode); ok {
if s.kind == VerticalSplit {
if !n.view.LockWidth {
n.view.Width = (s.width - lockedWidth) / (len(s.children) - lockedChildren)
}
n.view.Height = s.height
n.view.x = s.x + x
n.view.y = s.y
x += n.view.Width
} else {
if !n.view.LockHeight {
n.view.Height = (s.height - lockedHeight) / (len(s.children) - lockedChildren)
}
n.view.Width = s.width
n.view.y = s.y + y
n.view.x = s.x
y += n.view.Height
}
if n.view.Buf.Settings["statusline"].(bool) {
n.view.Height--
}
n.view.ToggleTabbar()
} else if n, ok := node.(*SplitTree); ok {
if s.kind == VerticalSplit {
if !n.lockWidth {
n.width = (s.width - lockedWidth) / (len(s.children) - lockedChildren)
}
n.height = s.height
n.x = s.x + x
n.y = s.y
x += n.width
} else {
if !n.lockHeight {
n.height = (s.height - lockedHeight) / (len(s.children) - lockedChildren)
}
n.width = s.width
n.y = s.y + y
n.x = s.x
y += n.height
}
n.ResizeSplits()
}
}
}
func (l *LeafNode) String() string {
return l.view.Buf.GetName()
}
func search(haystack []Node, needle Node) int {
for i, x := range haystack {
if x == needle {
return i
}
}
return 0
}
func findView(haystack []*View, needle *View) int {
for i, x := range haystack {
if x == needle {
return i
}
}
return 0
}
// VSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) VSplit(buf *Buffer, splitIndex int) {}
// HSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) HSplit(buf *Buffer, splitIndex int) {}
func (s *SplitTree) String() string {
str := "["
for _, child := range s.children {
str += child.String() + ", "
}
return str + "]"
}

View File

@@ -1,7 +1,7 @@
package buffer
package main
// TEStack is a simple implementation of a LIFO stack for text events
type TEStack struct {
// Stack is a simple implementation of a LIFO stack for text events
type Stack struct {
Top *Element
Size int
}
@@ -13,19 +13,19 @@ type Element struct {
}
// Len returns the stack's length
func (s *TEStack) Len() int {
func (s *Stack) Len() int {
return s.Size
}
// Push a new element onto the stack
func (s *TEStack) Push(value *TextEvent) {
func (s *Stack) Push(value *TextEvent) {
s.Top = &Element{value, s.Top}
s.Size++
}
// Pop removes the top element from the stack and returns its value
// If the stack is empty, return nil
func (s *TEStack) Pop() (value *TextEvent) {
func (s *Stack) Pop() (value *TextEvent) {
if s.Size > 0 {
value, s.Top = s.Top.Value, s.Top.Next
s.Size--
@@ -35,7 +35,7 @@ func (s *TEStack) Pop() (value *TextEvent) {
}
// Peek returns the top element of the stack without removing it
func (s *TEStack) Peek() *TextEvent {
func (s *Stack) Peek() *TextEvent {
if s.Size > 0 {
return s.Top.Value
}

68
cmd/micro/statusline.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"strconv"
)
// Statusline represents the information line at the bottom
// of each view
// It gives information such as filename, whether the file has been
// modified, filetype, cursor location
type Statusline struct {
view *View
}
// Display draws the statusline to the screen
func (sline *Statusline) Display() {
// We'll draw the line at the lowest line in the view
y := sline.view.Height + sline.view.y
file := sline.view.Buf.GetName()
// If the buffer is dirty (has been modified) write a little '+'
if sline.view.Buf.IsModified {
file += " +"
}
// Add one to cursor.x and cursor.y because (0,0) is the top left,
// but users will be used to (1,1) (first line,first column)
// We use GetVisualX() here because otherwise we get the column number in runes
// so a '\t' is only 1, when it should be tabSize
columnNum := strconv.Itoa(sline.view.Cursor.GetVisualX() + 1)
lineNum := strconv.Itoa(sline.view.Cursor.Y + 1)
file += " (" + lineNum + "," + columnNum + ")"
// Add the filetype
file += " " + sline.view.Buf.FileType()
rightText := ""
if len(helpBinding) > 0 {
rightText = helpBinding + " for help "
if sline.view.Type == vtHelp {
rightText = helpBinding + " to close help "
}
}
statusLineStyle := defStyle.Reverse(true)
if style, ok := colorscheme["statusline"]; ok {
statusLineStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(file)
viewX := sline.view.x
if viewX != 0 {
screen.SetContent(viewX, y, ' ', nil, statusLineStyle)
viewX++
}
for x := 0; x < sline.view.Width; x++ {
if x < len(fileRunes) {
screen.SetContent(viewX+x, y, fileRunes[x], nil, statusLineStyle)
} else if x >= sline.view.Width-len(rightText) && x < len(rightText)+sline.view.Width-len(rightText) {
screen.SetContent(viewX+x, y, []rune(rightText)[x-sline.view.Width+len(rightText)], nil, statusLineStyle)
} else {
screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle)
}
}
}

265
cmd/micro/tab.go Normal file
View File

@@ -0,0 +1,265 @@
package main
import (
"sort"
"github.com/zyedidia/tcell"
)
var tabBarOffset int
type Tab struct {
// This contains all the views in this tab
// There is generally only one view per tab, but you can have
// multiple views with splits
views []*View
// This is the current view for this tab
CurView int
tree *SplitTree
}
// NewTabFromView creates a new tab and puts the given view in the tab
func NewTabFromView(v *View) *Tab {
t := new(Tab)
t.views = append(t.views, v)
t.views[0].Num = 0
t.tree = new(SplitTree)
t.tree.kind = VerticalSplit
t.tree.children = []Node{NewLeafNode(t.views[0], t.tree)}
w, h := screen.Size()
t.tree.width = w
t.tree.height = h
if globalSettings["infobar"].(bool) {
t.tree.height--
}
t.Resize()
return t
}
// SetNum sets all this tab's views to have the correct tab number
func (t *Tab) SetNum(num int) {
t.tree.tabNum = num
for _, v := range t.views {
v.TabNum = num
}
}
func (t *Tab) Cleanup() {
t.tree.Cleanup()
}
func (t *Tab) Resize() {
w, h := screen.Size()
t.tree.width = w
t.tree.height = h
if globalSettings["infobar"].(bool) {
t.tree.height--
}
t.tree.ResizeSplits()
for i, v := range t.views {
v.Num = i
}
}
// CurView returns the current view
func CurView() *View {
curTab := tabs[curTab]
return curTab.views[curTab.CurView]
}
// TabbarString returns the string that should be displayed in the tabbar
// It also returns a map containing which indicies correspond to which tab number
// This is useful when we know that the mouse click has occurred at an x location
// but need to know which tab that corresponds to to accurately change the tab
func TabbarString() (string, map[int]int) {
str := ""
indicies := make(map[int]int)
for i, t := range tabs {
if i == curTab {
str += "["
} else {
str += " "
}
buf := t.views[t.CurView].Buf
str += buf.GetName()
if buf.IsModified {
str += " +"
}
if i == curTab {
str += "]"
} else {
str += " "
}
indicies[Count(str)-1] = i + 1
str += " "
}
return str, indicies
}
// TabbarHandleMouseEvent checks the given mouse event if it is clicking on the tabbar
// If it is it changes the current tab accordingly
// This function returns true if the tab is changed
func TabbarHandleMouseEvent(event tcell.Event) bool {
// There is no tabbar displayed if there are less than 2 tabs
if len(tabs) <= 1 {
return false
}
switch e := event.(type) {
case *tcell.EventMouse:
button := e.Buttons()
// Must be a left click
if button == tcell.Button1 {
x, y := e.Position()
if y != 0 {
return false
}
str, indicies := TabbarString()
if x+tabBarOffset >= len(str) {
return false
}
var tabnum int
var keys []int
for k := range indicies {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
if x+tabBarOffset <= k {
tabnum = indicies[k] - 1
break
}
}
curTab = tabnum
return true
}
}
return false
}
// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs
func DisplayTabs() {
if len(tabs) <= 1 {
return
}
str, indicies := TabbarString()
tabBarStyle := defStyle.Reverse(true)
if style, ok := colorscheme["tabbar"]; ok {
tabBarStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(str)
w, _ := screen.Size()
tooWide := (w < len(fileRunes))
// if the entire tab-bar is longer than the screen is wide,
// then it should be truncated appropriately to keep the
// active tab visible on the UI.
if tooWide == true {
// first we have to work out where the selected tab is
// out of the total length of the tab bar. this is done
// by extracting the hit-areas from the indicies map
// that was constructed by `TabbarString()`
var keys []int
for offset := range indicies {
keys = append(keys, offset)
}
// sort them to be in ascending order so that values will
// correctly reflect the displayed ordering of the tabs
sort.Ints(keys)
// record the offset of each tab and the previous tab so
// we can find the position of the tab's hit-box.
previousTabOffset := 0
currentTabOffset := 0
for _, k := range keys {
tabIndex := indicies[k] - 1
if tabIndex == curTab {
currentTabOffset = k
break
}
// this is +2 because there are two padding spaces that aren't accounted
// for in the display. please note that this is for cosmetic purposes only.
previousTabOffset = k + 2
}
// get the width of the hitbox of the active tab, from there calculate the offsets
// to the left and right of it to approximately center it on the tab bar display.
centeringOffset := (w - (currentTabOffset - previousTabOffset))
leftBuffer := previousTabOffset - (centeringOffset / 2)
rightBuffer := currentTabOffset + (centeringOffset / 2)
// check to make sure we haven't overshot the bounds of the string,
// if we have, then take that remainder and put it on the left side
overshotRight := rightBuffer - len(fileRunes)
if overshotRight > 0 {
leftBuffer = leftBuffer + overshotRight
}
overshotLeft := leftBuffer - 0
if overshotLeft < 0 {
leftBuffer = 0
rightBuffer = leftBuffer + (w - 1)
} else {
rightBuffer = leftBuffer + (w - 2)
}
if rightBuffer > len(fileRunes)-1 {
rightBuffer = len(fileRunes) - 1
}
// construct a new buffer of text to put the
// newly formatted tab bar text into.
var displayText []rune
// if the left-side of the tab bar isn't at the start
// of the constructed tab bar text, then show that are
// more tabs to the left by displaying a "+"
if leftBuffer != 0 {
displayText = append(displayText, '+')
}
// copy the runes in from the original tab bar text string
// into the new display buffer
for x := leftBuffer; x < rightBuffer; x++ {
displayText = append(displayText, fileRunes[x])
}
// if there is more text to the right of the right-most
// column in the tab bar text, then indicate there are more
// tabs to the right by displaying a "+"
if rightBuffer < len(fileRunes)-1 {
displayText = append(displayText, '+')
}
// now store the offset from zero of the left-most text
// that is being displayed. This is to ensure that when
// clicking on the tab bar, the correct tab gets selected.
tabBarOffset = leftBuffer
// use the constructed buffer as the display buffer to print
// onscreen.
fileRunes = displayText
} else {
tabBarOffset = 0
}
// iterate over the width of the terminal display and for each column,
// write a character into the tab display area with the appropriate style.
for x := 0; x < w; x++ {
if x < len(fileRunes) {
screen.SetContent(x, 0, fileRunes[x], nil, tabBarStyle)
} else {
screen.SetContent(x, 0, ' ', nil, tabBarStyle)
}
}
}

369
cmd/micro/util.go Normal file
View File

@@ -0,0 +1,369 @@
package main
import (
"bytes"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/mattn/go-runewidth"
)
// Util.go is a collection of utility functions that are used throughout
// the program
// Count returns the length of a string in runes
// This is exactly equivalent to utf8.RuneCountInString(), just less characters
func Count(s string) int {
return utf8.RuneCountInString(s)
}
// NumOccurrences counts the number of occurrences of a byte in a string
func NumOccurrences(s string, c byte) int {
var n int
for i := 0; i < len(s); i++ {
if s[i] == c {
n++
}
}
return n
}
// Spaces returns a string with n spaces
func Spaces(n int) string {
var str string
for i := 0; i < n; i++ {
str += " "
}
return str
}
// Min takes the min of two ints
func Min(a, b int) int {
if a > b {
return b
}
return a
}
// Max takes the max of two ints
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func FSize(f *os.File) int64 {
fi, _ := f.Stat()
// get the size
return fi.Size()
}
// IsWordChar returns whether or not the string is a 'word character'
// If it is a unicode character, then it does not match
// Word characters are defined as [A-Za-z0-9_]
func IsWordChar(str string) bool {
if len(str) > 1 {
// Unicode
return true
}
c := str[0]
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
}
// IsWhitespace returns true if the given rune is a space, tab, or newline
func IsWhitespace(c rune) bool {
return c == ' ' || c == '\t' || c == '\n'
}
// IsStrWhitespace returns true if the given string is all whitespace
func IsStrWhitespace(str string) bool {
for _, c := range str {
if !IsWhitespace(c) {
return false
}
}
return true
}
// Contains returns whether or not a string array contains a given string
func Contains(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// Insert makes a simple insert into a string at the given position
func Insert(str string, pos int, value string) string {
return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
}
// MakeRelative will attempt to make a relative path between path and base
func MakeRelative(path, base string) (string, error) {
if len(path) > 0 {
rel, err := filepath.Rel(base, path)
if err != nil {
return path, err
}
return rel, nil
}
return path, nil
}
// GetLeadingWhitespace returns the leading whitespace of the given string
func GetLeadingWhitespace(str string) string {
ws := ""
for _, c := range str {
if c == ' ' || c == '\t' {
ws += string(c)
} else {
break
}
}
return ws
}
// IsSpaces checks if a given string is only spaces
func IsSpaces(str string) bool {
for _, c := range str {
if c != ' ' {
return false
}
}
return true
}
// IsSpacesOrTabs checks if a given string contains only spaces and tabs
func IsSpacesOrTabs(str string) bool {
for _, c := range str {
if c != ' ' && c != '\t' {
return false
}
}
return true
}
// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
// as 'true' and 'false' respectively
func ParseBool(str string) (bool, error) {
if str == "on" {
return true, nil
}
if str == "off" {
return false, nil
}
return strconv.ParseBool(str)
}
// EscapePath replaces every path separator in a given path with a %
func EscapePath(path string) string {
path = filepath.ToSlash(path)
return strings.Replace(path, "/", "%", -1)
}
// GetModTime returns the last modification time for a given file
// It also returns a boolean if there was a problem accessing the file
func GetModTime(path string) (time.Time, bool) {
info, err := os.Stat(path)
if err != nil {
return time.Now(), false
}
return info.ModTime(), true
}
// StringWidth returns the width of a string where tabs count as `tabsize` width
func StringWidth(str string, tabsize int) int {
sw := runewidth.StringWidth(str)
lineIdx := 0
for _, ch := range str {
switch ch {
case '\t':
ts := tabsize - (lineIdx % tabsize)
sw += ts
lineIdx += ts
case '\n':
lineIdx = 0
default:
lineIdx++
}
}
return sw
}
// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
// that have a width larger than 1 (this also counts tabs as `tabsize` width)
func WidthOfLargeRunes(str string, tabsize int) int {
count := 0
lineIdx := 0
for _, ch := range str {
var w int
if ch == '\t' {
w = tabsize - (lineIdx % tabsize)
} else {
w = runewidth.RuneWidth(ch)
}
if w > 1 {
count += (w - 1)
}
if ch == '\n' {
lineIdx = 0
} else {
lineIdx += w
}
}
return count
}
// RunePos returns the rune index of a given byte index
// This could cause problems if the byte index is between code points
func runePos(p int, str string) int {
return utf8.RuneCountInString(str[:p])
}
func lcs(a, b string) string {
arunes := []rune(a)
brunes := []rune(b)
lcs := ""
for i, r := range arunes {
if i >= len(brunes) {
break
}
if r == brunes[i] {
lcs += string(r)
} else {
break
}
}
return lcs
}
func CommonSubstring(arr ...string) string {
commonStr := arr[0]
for _, str := range arr[1:] {
commonStr = lcs(commonStr, str)
}
return commonStr
}
// Abs is a simple absolute value function for ints
func Abs(n int) int {
if n < 0 {
return -n
}
return n
}
// FuncName returns the full name of a given function object
func FuncName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// ShortFuncName returns the name only of a given function object
func ShortFuncName(i interface{}) string {
return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).")
}
// SplitCommandArgs separates multiple command arguments which may be quoted.
// The returned slice contains at least one string
func SplitCommandArgs(input string) []string {
var result []string
var curQuote *bytes.Buffer
curArg := new(bytes.Buffer)
escape := false
finishQuote := func() {
if curQuote == nil {
return
}
str := curQuote.String()
if unquoted, err := strconv.Unquote(str); err == nil {
str = unquoted
}
curArg.WriteString(str)
curQuote = nil
}
appendResult := func() {
finishQuote()
escape = false
str := curArg.String()
result = append(result, str)
curArg.Reset()
}
for _, r := range input {
if r == ' ' && curQuote == nil {
appendResult()
} else {
runeHandled := false
appendRuneToBuff := func() {
if curQuote != nil {
curQuote.WriteRune(r)
} else {
curArg.WriteRune(r)
}
runeHandled = true
}
if r == '"' && curQuote == nil {
curQuote = new(bytes.Buffer)
appendRuneToBuff()
} else {
if curQuote != nil && !escape {
if r == '"' {
appendRuneToBuff()
finishQuote()
} else if r == '\\' {
appendRuneToBuff()
escape = true
continue
}
}
}
if !runeHandled {
appendRuneToBuff()
}
}
escape = false
}
appendResult()
return result
}
// JoinCommandArgs joins multiple command arguments and quote the strings if needed.
func JoinCommandArgs(args ...string) string {
buf := new(bytes.Buffer)
first := true
for _, arg := range args {
if first {
first = false
} else {
buf.WriteRune(' ')
}
quoted := strconv.Quote(arg)
if quoted[1:len(quoted)-1] != arg || strings.ContainsRune(arg, ' ') {
buf.WriteString(quoted)
} else {
buf.WriteString(arg)
}
}
return buf.String()
}

156
cmd/micro/util_test.go Normal file
View File

@@ -0,0 +1,156 @@
package main
import (
"reflect"
"testing"
)
func TestNumOccurences(t *testing.T) {
var tests = []struct {
inputStr string
inputChar byte
want int
}{
{"aaaa", 'a', 4},
{"\trfd\ta", '\t', 2},
{"∆ƒ\tø ® \t\t", '\t', 3},
}
for _, test := range tests {
if got := NumOccurrences(test.inputStr, test.inputChar); got != test.want {
t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got)
}
}
}
func TestSpaces(t *testing.T) {
var tests = []struct {
input int
want string
}{
{4, " "},
{0, ""},
}
for _, test := range tests {
if got := Spaces(test.input); got != test.want {
t.Errorf("Spaces(%d) = \"%s\"", test.input, got)
}
}
}
func TestIsWordChar(t *testing.T) {
if IsWordChar("t") == false {
t.Errorf("IsWordChar(t) = false")
}
if IsWordChar("T") == false {
t.Errorf("IsWordChar(T) = false")
}
if IsWordChar("5") == false {
t.Errorf("IsWordChar(5) = false")
}
if IsWordChar("_") == false {
t.Errorf("IsWordChar(_) = false")
}
if IsWordChar("ß") == false {
t.Errorf("IsWordChar(ß) = false")
}
if IsWordChar("~") == true {
t.Errorf("IsWordChar(~) = true")
}
if IsWordChar(" ") == true {
t.Errorf("IsWordChar( ) = true")
}
if IsWordChar(")") == true {
t.Errorf("IsWordChar()) = true")
}
if IsWordChar("\n") == true {
t.Errorf("IsWordChar(\n)) = true")
}
}
func TestJoinAndSplitCommandArgs(t *testing.T) {
tests := []struct {
Query []string
Wanted string
}{
{[]string{`test case`}, `"test case"`},
{[]string{`quote "test"`}, `"quote \"test\""`},
{[]string{`slash\\\ test`}, `"slash\\\\\\ test"`},
{[]string{`path 1`, `path\" 2`}, `"path 1" "path\\\" 2"`},
{[]string{`foo`}, `foo`},
{[]string{`foo\"bar`}, `"foo\\\"bar"`},
{[]string{``}, ``},
{[]string{`"`}, `"\""`},
{[]string{`a`, ``}, `a `},
{[]string{``, ``, ``, ``}, ` `},
{[]string{"\n"}, `"\n"`},
{[]string{"foo\tbar"}, `"foo\tbar"`},
}
for i, test := range tests {
if result := JoinCommandArgs(test.Query...); test.Wanted != result {
t.Errorf("JoinCommandArgs failed at Test %d\nGot: %q", i, result)
}
if result := SplitCommandArgs(test.Wanted); !reflect.DeepEqual(test.Query, result) {
t.Errorf("SplitCommandArgs failed at Test %d\nGot: `%q`", i, result)
}
}
splitTests := []struct {
Query string
Wanted []string
}{
{`"hallo""Welt"`, []string{`halloWelt`}},
{`"hallo" "Welt"`, []string{`hallo`, `Welt`}},
{`\"`, []string{`\"`}},
{`"foo`, []string{`"foo`}},
{`"foo"`, []string{`foo`}},
{`"\"`, []string{`"\"`}},
{`"C:\\"foo.txt`, []string{`C:\foo.txt`}},
{`"\n"new"\n"line`, []string{"\nnew\nline"}},
}
for i, test := range splitTests {
if result := SplitCommandArgs(test.Query); !reflect.DeepEqual(test.Wanted, result) {
t.Errorf("SplitCommandArgs failed at Split-Test %d\nGot: `%q`", i, result)
}
}
}
func TestStringWidth(t *testing.T) {
tabsize := 4
if w := StringWidth("1\t2", tabsize); w != 5 {
t.Error("StringWidth 1 Failed. Got", w)
}
if w := StringWidth("\t", tabsize); w != 4 {
t.Error("StringWidth 2 Failed. Got", w)
}
if w := StringWidth("1\t", tabsize); w != 4 {
t.Error("StringWidth 3 Failed. Got", w)
}
if w := StringWidth("\t\t", tabsize); w != 8 {
t.Error("StringWidth 4 Failed. Got", w)
}
if w := StringWidth("12\t2\t", tabsize); w != 8 {
t.Error("StringWidth 5 Failed. Got", w)
}
}
func TestWidthOfLargeRunes(t *testing.T) {
tabsize := 4
if w := WidthOfLargeRunes("1\t2", tabsize); w != 2 {
t.Error("WidthOfLargeRunes 1 Failed. Got", w)
}
if w := WidthOfLargeRunes("\t", tabsize); w != 3 {
t.Error("WidthOfLargeRunes 2 Failed. Got", w)
}
if w := WidthOfLargeRunes("1\t", tabsize); w != 2 {
t.Error("WidthOfLargeRunes 3 Failed. Got", w)
}
if w := WidthOfLargeRunes("\t\t", tabsize); w != 6 {
t.Error("WidthOfLargeRunes 4 Failed. Got", w)
}
if w := WidthOfLargeRunes("12\t2\t", tabsize); w != 3 {
t.Error("WidthOfLargeRunes 5 Failed. Got", w)
}
}

974
cmd/micro/view.go Normal file
View File

@@ -0,0 +1,974 @@
package main
import (
"os"
"strconv"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/zyedidia/tcell"
)
type ViewType struct {
kind int
readonly bool // The file cannot be edited
scratch bool // The file cannot be saved
}
var (
vtDefault = ViewType{0, false, false}
vtHelp = ViewType{1, true, true}
vtLog = ViewType{2, true, true}
vtScratch = ViewType{3, false, true}
)
// The View struct stores information about a view into a buffer.
// It stores information about the cursor, and the viewport
// that the user sees the buffer from.
type View struct {
// A pointer to the buffer's cursor for ease of access
Cursor *Cursor
// The topmost line, used for vertical scrolling
Topline int
// The leftmost column, used for horizontal scrolling
leftCol int
// Specifies whether or not this view holds a help buffer
Type ViewType
// Actual width and height
Width int
Height int
LockWidth bool
LockHeight bool
// Where this view is located
x, y int
// How much to offset because of line numbers
lineNumOffset int
// Holds the list of gutter messages
messages map[string][]GutterMessage
// This is the index of this view in the views array
Num int
// What tab is this view stored in
TabNum int
// The buffer
Buf *Buffer
// The statusline
sline Statusline
// Since tcell doesn't differentiate between a mouse release event
// and a mouse move event with no keys pressed, we need to keep
// track of whether or not the mouse was pressed (or not released) last event to determine
// mouse release events
mouseReleased bool
// This stores when the last click was
// This is useful for detecting double and triple clicks
lastClickTime time.Time
// lastCutTime stores when the last ctrl+k was issued.
// It is used for clearing the clipboard to replace it with fresh cut lines.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
freshClip bool
// Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click
doubleClick bool
// Same here, just to keep track for mouse move events
tripleClick bool
cellview *CellView
splitNode *LeafNode
}
// NewView returns a new fullscreen view
func NewView(buf *Buffer) *View {
screenW, screenH := screen.Size()
return NewViewWidthHeight(buf, screenW, screenH)
}
// NewViewWidthHeight returns a new view with the specified width and height
// Note that w and h are raw column and row values
func NewViewWidthHeight(buf *Buffer, w, h int) *View {
v := new(View)
v.x, v.y = 0, 0
v.Width = w
v.Height = h
v.cellview = new(CellView)
v.ToggleTabbar()
v.OpenBuffer(buf)
v.messages = make(map[string][]GutterMessage)
v.sline = Statusline{
view: v,
}
if v.Buf.Settings["statusline"].(bool) {
v.Height--
}
for pl := range loadedPlugins {
_, err := Call(pl+".onViewOpen", v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
continue
}
}
return v
}
// ToggleStatusLine creates an extra row for the statusline if necessary
func (v *View) ToggleStatusLine() {
if v.Buf.Settings["statusline"].(bool) {
v.Height--
} else {
v.Height++
}
}
// ToggleTabbar creates an extra row for the tabbar if necessary
func (v *View) ToggleTabbar() {
if len(tabs) > 1 {
if v.y == 0 {
// Include one line for the tab bar at the top
v.Height--
v.y = 1
}
} else {
if v.y == 1 {
v.y = 0
v.Height++
}
}
}
func (v *View) paste(clip string) {
leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
v.Buf.Insert(v.Cursor.Loc, clip)
v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
v.freshClip = false
messenger.Message("Pasted clipboard")
}
// ScrollUp scrolls the view up n lines (if possible)
func (v *View) ScrollUp(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
if v.Topline-n >= 0 {
v.Topline -= n
} else if v.Topline > 0 {
v.Topline--
}
}
// ScrollDown scrolls the view down n lines (if possible)
func (v *View) ScrollDown(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
if v.Topline+n <= v.Buf.NumLines {
v.Topline += n
} else if v.Topline < v.Buf.NumLines-1 {
v.Topline++
}
}
// CanClose returns whether or not the view can be closed
// If there are unsaved changes, the user will be asked if the view can be closed
// causing them to lose the unsaved changes
func (v *View) CanClose() bool {
if v.Type == vtDefault && v.Buf.IsModified {
var choice bool
var canceled bool
if v.Buf.Settings["autosave"].(bool) {
choice = true
} else {
choice, canceled = messenger.YesNoPrompt("Save changes to " + v.Buf.GetName() + " before closing? (y,n,esc) ")
}
if !canceled {
//if char == 'y' {
if choice {
v.Save(true)
return true
} else {
return true
}
}
} else {
return true
}
return false
}
// OpenBuffer opens a new buffer in this view.
// This resets the topline, event handler and cursor.
func (v *View) OpenBuffer(buf *Buffer) {
screen.Clear()
v.CloseBuffer()
v.Buf = buf
v.Cursor = &buf.Cursor
v.Topline = 0
v.leftCol = 0
v.Cursor.ResetSelection()
v.Relocate()
v.Center(false)
v.messages = make(map[string][]GutterMessage)
// Set mouseReleased to true because we assume the mouse is not being pressed when
// the editor is opened
v.mouseReleased = true
v.lastClickTime = time.Time{}
}
// Open opens the given file in the view
func (v *View) Open(filename string) {
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
messenger.Message(err.Error())
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
v.OpenBuffer(buf)
}
// CloseBuffer performs any closing functions on the buffer
func (v *View) CloseBuffer() {
if v.Buf != nil {
v.Buf.Serialize()
}
}
// ReOpen reloads the current buffer
func (v *View) ReOpen() {
if v.CanClose() {
screen.Clear()
v.Buf.ReOpen()
v.Relocate()
}
}
// HSplit opens a horizontal split with the given buffer
func (v *View) HSplit(buf *Buffer) {
i := 0
if v.Buf.Settings["splitBottom"].(bool) {
i = 1
}
v.splitNode.HSplit(buf, v.Num+i)
}
// VSplit opens a vertical split with the given buffer
func (v *View) VSplit(buf *Buffer) {
i := 0
if v.Buf.Settings["splitRight"].(bool) {
i = 1
}
v.splitNode.VSplit(buf, v.Num+i)
}
// HSplitIndex opens a horizontal split with the given buffer at the given index
func (v *View) HSplitIndex(buf *Buffer, splitIndex int) {
v.splitNode.HSplit(buf, splitIndex)
}
// VSplitIndex opens a vertical split with the given buffer at the given index
func (v *View) VSplitIndex(buf *Buffer, splitIndex int) {
v.splitNode.VSplit(buf, splitIndex)
}
// GetSoftWrapLocation gets the location of a visual click on the screen and converts it to col,line
func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) {
if !v.Buf.Settings["softwrap"].(bool) {
if vy >= v.Buf.NumLines {
vy = v.Buf.NumLines - 1
}
vx = v.Cursor.GetCharPosInLine(vy, vx)
return vx, vy
}
screenX, screenY := 0, v.Topline
for lineN := v.Topline; lineN < v.Bottomline(); lineN++ {
line := v.Buf.Line(lineN)
if lineN >= v.Buf.NumLines {
return 0, v.Buf.NumLines - 1
}
colN := 0
for _, ch := range line {
if screenX >= v.Width-v.lineNumOffset {
screenX = 0
screenY++
}
if screenX == vx && screenY == vy {
return colN, lineN
}
if ch == '\t' {
screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
}
screenX++
colN++
}
if screenY == vy {
return colN, lineN
}
screenX = 0
screenY++
}
return 0, 0
}
func (v *View) Bottomline() int {
if !v.Buf.Settings["softwrap"].(bool) {
return v.Topline + v.Height
}
screenX, screenY := 0, 0
numLines := 0
for lineN := v.Topline; lineN < v.Topline+v.Height; lineN++ {
line := v.Buf.Line(lineN)
colN := 0
for _, ch := range line {
if screenX >= v.Width-v.lineNumOffset {
screenX = 0
screenY++
}
if ch == '\t' {
screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
}
screenX++
colN++
}
screenX = 0
screenY++
numLines++
if screenY >= v.Height {
break
}
}
return numLines + v.Topline
}
// Relocate moves the view window so that the cursor is in view
// This is useful if the user has scrolled far away, and then starts typing
func (v *View) Relocate() bool {
height := v.Bottomline() - v.Topline
ret := false
cy := v.Cursor.Y
scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
v.Topline = cy - scrollmargin
ret = true
} else if cy < v.Topline {
v.Topline = cy
ret = true
}
if cy > v.Topline+height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
v.Topline = cy - height + 1 + scrollmargin
ret = true
} else if cy >= v.Buf.NumLines-scrollmargin && cy > height {
v.Topline = v.Buf.NumLines - height
ret = true
}
if !v.Buf.Settings["softwrap"].(bool) {
cx := v.Cursor.GetVisualX()
if cx < v.leftCol {
v.leftCol = cx
ret = true
}
if cx+v.lineNumOffset+1 > v.leftCol+v.Width {
v.leftCol = cx - v.Width + v.lineNumOffset + 1
ret = true
}
}
return ret
}
// MoveToMouseClick moves the cursor to location x, y assuming x, y were given
// by a mouse click
func (v *View) MoveToMouseClick(x, y int) {
if y-v.Topline > v.Height-1 {
v.ScrollDown(1)
y = v.Height + v.Topline - 1
}
if y < 0 {
y = 0
}
if x < 0 {
x = 0
}
x, y = v.GetSoftWrapLocation(x, y)
// x = v.Cursor.GetCharPosInLine(y, x)
if x > Count(v.Buf.Line(y)) {
x = Count(v.Buf.Line(y))
}
v.Cursor.X = x
v.Cursor.Y = y
v.Cursor.LastVisualX = v.Cursor.GetVisualX()
}
// HandleEvent handles an event passed by the main loop
func (v *View) HandleEvent(event tcell.Event) {
// This bool determines whether the view is relocated at the end of the function
// By default it's true because most events should cause a relocate
relocate := true
v.Buf.CheckModTime()
switch e := event.(type) {
case *tcell.EventKey:
// Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune
isBinding := false
readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"}
if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
for key, actions := range bindings {
if e.Key() == key.keyCode {
if e.Key() == tcell.KeyRune {
if e.Rune() != key.r {
continue
}
}
if e.Modifiers() == key.modifiers {
relocate = false
isBinding = true
for _, action := range actions {
readonlyBindingsResult := false
funcName := ShortFuncName(action)
if v.Type.readonly == true {
// check for readonly and if true only let key bindings get called if they do not change the contents.
for _, readonlyBindings := range readonlyBindingsList {
if strings.Contains(funcName, readonlyBindings) {
readonlyBindingsResult = true
}
}
}
if !readonlyBindingsResult {
// call the key binding
relocate = action(v, true) || relocate
// Macro
if funcName != "ToggleMacro" && funcName != "PlayMacro" {
if recordingMacro {
curMacro = append(curMacro, action)
}
}
}
}
break
}
}
}
}
if !isBinding && e.Key() == tcell.KeyRune {
// Check viewtype if readonly don't insert a rune (readonly help and log view etc.)
if v.Type.readonly == false {
// Insert a character
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
v.Buf.Insert(v.Cursor.Loc, string(e.Rune()))
v.Cursor.Right()
for pl := range loadedPlugins {
_, err := Call(pl+".onRune", string(e.Rune()), v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
if recordingMacro {
curMacro = append(curMacro, e.Rune())
}
}
}
case *tcell.EventPaste:
// Check viewtype if readonly don't paste (readonly help and log view etc.)
if v.Type.readonly == false {
if !PreActionCall("Paste", v) {
break
}
v.paste(e.Text())
PostActionCall("Paste", v)
}
case *tcell.EventMouse:
x, y := e.Position()
x -= v.lineNumOffset - v.leftCol + v.x
y += v.Topline - v.y
// Don't relocate for mouse events
relocate = false
button := e.Buttons()
switch button {
case tcell.Button1:
// Left click
if v.mouseReleased {
v.MoveToMouseClick(x, y)
if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
if v.doubleClick {
// Triple click
v.lastClickTime = time.Now()
v.tripleClick = true
v.doubleClick = false
v.Cursor.SelectLine()
v.Cursor.CopySelection("primary")
} else {
// Double click
v.lastClickTime = time.Now()
v.doubleClick = true
v.tripleClick = false
v.Cursor.SelectWord()
v.Cursor.CopySelection("primary")
}
} else {
v.doubleClick = false
v.tripleClick = false
v.lastClickTime = time.Now()
v.Cursor.OrigSelection[0] = v.Cursor.Loc
v.Cursor.CurSelection[0] = v.Cursor.Loc
v.Cursor.CurSelection[1] = v.Cursor.Loc
}
v.mouseReleased = false
} else if !v.mouseReleased {
v.MoveToMouseClick(x, y)
if v.tripleClick {
v.Cursor.AddLineToSelection()
} else if v.doubleClick {
v.Cursor.AddWordToSelection()
} else {
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
v.Cursor.CopySelection("primary")
}
}
case tcell.Button2:
// Check viewtype if readonly don't paste (readonly help and log view etc.)
if v.Type.readonly == false {
// Middle mouse button was clicked,
// We should paste primary
v.PastePrimary(true)
}
case tcell.ButtonNone:
// Mouse event with no click
if !v.mouseReleased {
// Mouse was just released
// Relocating here isn't really necessary because the cursor will
// be in the right place from the last mouse event
// However, if we are running in a terminal that doesn't support mouse motion
// events, this still allows the user to make selections, except only after they
// release the mouse
if !v.doubleClick && !v.tripleClick {
v.MoveToMouseClick(x, y)
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
v.Cursor.CopySelection("primary")
}
v.mouseReleased = true
}
case tcell.WheelUp:
// Scroll up
scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
v.ScrollUp(scrollspeed)
case tcell.WheelDown:
// Scroll down
scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
v.ScrollDown(scrollspeed)
}
}
if relocate {
v.Relocate()
// We run relocate again because there's a bug with relocating with softwrap
// when for example you jump to the bottom of the buffer and it tries to
// calculate where to put the topline so that the bottom line is at the bottom
// of the terminal and it runs into problems with visual lines vs real lines.
// This is (hopefully) a temporary solution
v.Relocate()
}
}
// GutterMessage creates a message in this view's gutter
func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
lineN--
gutterMsg := GutterMessage{
lineNum: lineN,
msg: msg,
kind: kind,
}
for _, v := range v.messages {
for _, gmsg := range v {
if gmsg.lineNum == lineN {
return
}
}
}
messages := v.messages[section]
v.messages[section] = append(messages, gutterMsg)
}
// ClearGutterMessages clears all gutter messages from a given section
func (v *View) ClearGutterMessages(section string) {
v.messages[section] = []GutterMessage{}
}
// ClearAllGutterMessages clears all the gutter messages
func (v *View) ClearAllGutterMessages() {
for k := range v.messages {
v.messages[k] = []GutterMessage{}
}
}
// Opens the given help page in a new horizontal split
func (v *View) openHelp(helpPage string) {
if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil {
TermMessage("Unable to load help text", helpPage, "\n", err)
} else {
helpBuffer := NewBufferFromString(string(data), helpPage+".md")
helpBuffer.name = "Help"
if v.Type == vtHelp {
v.OpenBuffer(helpBuffer)
} else {
v.HSplit(helpBuffer)
CurView().Type = vtHelp
}
}
}
func (v *View) DisplayView() {
if v.Buf.Settings["softwrap"].(bool) && v.leftCol != 0 {
v.leftCol = 0
}
if v.Type == vtLog {
// Log views should always follow the cursor...
v.Relocate()
}
// We need to know the string length of the largest line number
// so we can pad appropriately when displaying line numbers
maxLineNumLength := len(strconv.Itoa(v.Buf.NumLines))
if v.Buf.Settings["ruler"] == true {
// + 1 for the little space after the line number
v.lineNumOffset = maxLineNumLength + 1
} else {
v.lineNumOffset = 0
}
// We need to add to the line offset if there are gutter messages
var hasGutterMessages bool
for _, v := range v.messages {
if len(v) > 0 {
hasGutterMessages = true
}
}
if hasGutterMessages {
v.lineNumOffset += 2
}
divider := 0
if v.x != 0 {
// One space for the extra split divider
v.lineNumOffset++
divider = 1
}
xOffset := v.x + v.lineNumOffset
yOffset := v.y
height := v.Height
width := v.Width
left := v.leftCol
top := v.Topline
v.cellview.Draw(v.Buf, top, height, left, width-v.lineNumOffset)
screenX := v.x
realLineN := top - 1
visualLineN := 0
var line []*Char
for visualLineN, line = range v.cellview.lines {
var firstChar *Char
if len(line) > 0 {
firstChar = line[0]
}
var softwrapped bool
if firstChar != nil {
if firstChar.realLoc.Y == realLineN {
softwrapped = true
}
realLineN = firstChar.realLoc.Y
} else {
realLineN++
}
colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
if colorcolumn != 0 {
style := GetColor("color-column")
fg, _, _ := style.Decompose()
st := defStyle.Background(fg)
screen.SetContent(xOffset+colorcolumn, yOffset+visualLineN, ' ', nil, st)
}
screenX = v.x
// If there are gutter messages we need to display the '>>' symbol here
if hasGutterMessages {
// msgOnLine stores whether or not there is a gutter message on this line in particular
msgOnLine := false
for k := range v.messages {
for _, msg := range v.messages[k] {
if msg.lineNum == realLineN {
msgOnLine = true
gutterStyle := defStyle
switch msg.kind {
case GutterInfo:
if style, ok := colorscheme["gutter-info"]; ok {
gutterStyle = style
}
case GutterWarning:
if style, ok := colorscheme["gutter-warning"]; ok {
gutterStyle = style
}
case GutterError:
if style, ok := colorscheme["gutter-error"]; ok {
gutterStyle = style
}
}
screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle)
screenX++
screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle)
screenX++
if v.Cursor.Y == realLineN && !messenger.hasPrompt {
messenger.Message(msg.msg)
messenger.gutterMessage = true
}
}
}
}
// If there is no message on this line we just display an empty offset
if !msgOnLine {
screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle)
screenX++
screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle)
screenX++
if v.Cursor.Y == realLineN && messenger.gutterMessage {
messenger.Reset()
messenger.gutterMessage = false
}
}
}
lineNumStyle := defStyle
if v.Buf.Settings["ruler"] == true {
// Write the line number
if style, ok := colorscheme["line-number"]; ok {
lineNumStyle = style
}
if style, ok := colorscheme["current-line-number"]; ok {
if realLineN == v.Cursor.Y && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() {
lineNumStyle = style
}
}
lineNum := strconv.Itoa(realLineN + 1)
// Write the spaces before the line number if necessary
for i := 0; i < maxLineNumLength-len(lineNum); i++ {
screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
screenX++
}
if softwrapped && visualLineN != 0 {
// Pad without the line number because it was written on the visual line before
for range lineNum {
screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
screenX++
}
} else {
// Write the actual line number
for _, ch := range lineNum {
screen.SetContent(screenX+divider, yOffset+visualLineN, ch, nil, lineNumStyle)
screenX++
}
}
// Write the extra space
screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
screenX++
}
var lastChar *Char
cursorSet := false
for _, char := range line {
if char != nil {
lineStyle := char.style
colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
if colorcolumn != 0 && char.visualLoc.X == colorcolumn {
style := GetColor("color-column")
fg, _, _ := style.Decompose()
lineStyle = lineStyle.Background(fg)
}
charLoc := char.realLoc
if v.Cursor.HasSelection() &&
(charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) ||
charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) {
// The current character is selected
lineStyle = defStyle.Reverse(true)
if style, ok := colorscheme["selection"]; ok {
lineStyle = style
}
}
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && !cursorSet {
screen.ShowCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y)
cursorSet = true
}
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
!v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
style := GetColor("cursor-line")
fg, _, _ := style.Decompose()
lineStyle = lineStyle.Background(fg)
}
screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle)
lastChar = char
}
}
lastX := 0
var realLoc Loc
var visualLoc Loc
var cx, cy int
if lastChar != nil {
lastX = xOffset + lastChar.visualLoc.X + lastChar.width
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 {
screen.ShowCursor(lastX, yOffset+lastChar.visualLoc.Y)
cx, cy = lastX, yOffset+lastChar.visualLoc.Y
}
realLoc = Loc{lastChar.realLoc.X, realLineN}
visualLoc = Loc{lastX - xOffset, lastChar.visualLoc.Y}
} else if len(line) == 0 {
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == realLineN {
screen.ShowCursor(xOffset, yOffset+visualLineN)
cx, cy = xOffset, yOffset+visualLineN
}
lastX = xOffset
realLoc = Loc{0, realLineN}
visualLoc = Loc{0, visualLineN}
}
if v.Cursor.HasSelection() &&
(realLoc.GreaterEqual(v.Cursor.CurSelection[0]) && realLoc.LessThan(v.Cursor.CurSelection[1]) ||
realLoc.LessThan(v.Cursor.CurSelection[0]) && realLoc.GreaterEqual(v.Cursor.CurSelection[1])) {
// The current character is selected
selectStyle := defStyle.Reverse(true)
if style, ok := colorscheme["selection"]; ok {
selectStyle = style
}
screen.SetContent(xOffset+visualLoc.X, yOffset+visualLoc.Y, ' ', nil, selectStyle)
}
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
!v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
for i := lastX; i < xOffset+v.Width; i++ {
style := GetColor("cursor-line")
fg, _, _ := style.Decompose()
style = style.Background(fg)
if !(tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && i == cx && yOffset+visualLineN == cy) {
screen.SetContent(i, yOffset+visualLineN, ' ', nil, style)
}
}
}
}
if divider != 0 {
dividerStyle := defStyle
if style, ok := colorscheme["divider"]; ok {
dividerStyle = style
}
for i := 0; i < v.Height; i++ {
screen.SetContent(v.x, yOffset+i, '|', nil, dividerStyle.Reverse(true))
}
}
}
// Display renders the view, the cursor, and statusline
func (v *View) Display() {
if globalSettings["termtitle"].(bool) {
screen.SetTitle("micro: " + v.Buf.GetName())
}
v.DisplayView()
// Don't draw the cursor if it is out of the viewport or if it has a selection
if v.Num == tabs[curTab].CurView && (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.Height-1 || v.Cursor.HasSelection()) {
screen.HideCursor()
}
_, screenH := screen.Size()
if v.Buf.Settings["statusline"].(bool) {
v.sline.Display()
} else if (v.y + v.Height) != screenH-1 {
for x := 0; x < v.Width; x++ {
screen.SetContent(v.x+x, v.y+v.Height, '-', nil, defStyle.Reverse(true))
}
}
}

View File

@@ -1,23 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<component>
<id>com.github.zyedidia.micro</id>
<name>Micro Text Editor</name>
<summary>A modern and intuitive terminal-based text editor</summary>
<metadata_license>MIT</metadata_license>
<categories>
<category>Development</category>
<category>TextEditor</category>
</categories>
<provides>
<binary>micro</binary>
</provides>
<developer_name>Zachary Yedidia</developer_name>
<screenshots>
<screenshot type="default">
<caption>Micro Text Editor editing its source code.</caption>
<image type="source">https://raw.githubusercontent.com/zyedidia/micro/master/assets/micro-solarized.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://micro-editor.github.io</url>
<url type="bugtracker">https://github.com/zyedidia/micro/issues</url>
<id>com.github.zyedidia.micro</id>
<name>Micro Text Editor</name>
<summary>A modern and intuitive terminal-based text editor</summary>
<url type="homepage">https://micro-editor.github.io</url>
<url type="bugtracker">https://github.com/zyedidia/micro</url>
<metadata_license>MIT</metadata_license>
<categories>
<category>Development</category>
<category>TextEditor</category>
</categories>
<provides>
<binary>micro</binary>
</provides>
<releases>
<release version="1.2.0" date="2017-05-28" />
</releases>
<developer_name>Zachary Yedidia</developer_name>
<screenshots>
<screenshot type="default">
<caption>Micro Text Editor editing its source code.</caption>
<image type="source">https://raw.githubusercontent.com/zyedidia/micro/master/assets/micro-solarized.png</image>
</screenshot>
</screenshots>
</component>

28
go.mod
View File

@@ -1,28 +0,0 @@
module github.com/zyedidia/micro/v2
require (
github.com/blang/semver v3.5.1+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/go-errors/errors v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.11
github.com/mattn/go-runewidth v0.0.7
github.com/mitchellh/go-homedir v1.1.0
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
github.com/zyedidia/clipboard v0.0.0-20200421031010-7c45b8673834
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d
github.com/zyedidia/pty v2.0.0+incompatible // indirect
github.com/zyedidia/tcell v1.4.5
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415
golang.org/x/text v0.3.2
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.2.7
layeh.com/gopher-luar v1.0.7
)
go 1.11

81
go.sum
View File

@@ -1,81 +0,0 @@
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1KO6SAfUX918c+Tab0+tGAM/mtdlUyA=
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197 h1:gYTNnAW6azuB3BbA6QYWO/H4F2ABSOjjw3Z03tlXd2c=
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY=
github.com/zyedidia/clipboard v0.0.0-20200421031010-7c45b8673834 h1:0nOfq3JwYRiY3+nwfWVQYEaXDmGCQgj3RKoqTifLzP4=
github.com/zyedidia/clipboard v0.0.0-20200421031010-7c45b8673834/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 h1:Zs6mpwXvlqpF9zHl5XaN0p5V4J9XvP+WBuiuXyIgqvc=
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5/go.mod h1:c1r+Ob9tUTPB0FKWO1+x+Hsc/zNa45WdGq7Y38Ybip0=
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d h1:zmDMkh22zXOB7gz8jFaI4GpI7llsPgzm38/jG0UgxjE=
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d/go.mod h1:NDJSTTYWivnza6zkRapeX2/LwhKPEMQ7bJxqgDVT78I=
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
github.com/zyedidia/pty v2.0.0+incompatible h1:Ou5vXL6tvjst+RV8sUFISbuKDnUJPhnpygApMFGweqw=
github.com/zyedidia/pty v2.0.0+incompatible/go.mod h1:4y9l9yJZNxRa7GB/fB+mmDmGkG3CqmzLf4vUxGGotEA=
github.com/zyedidia/tcell v1.4.4 h1:o34LXujNuSueuyTy+5eoQW+rQr8g0UbY8k1NczZyskQ=
github.com/zyedidia/tcell v1.4.4/go.mod h1:HhlbMSCcGX15rFDB+Q1Lk3pKEOocsCUAQC3zhZ9sadA=
github.com/zyedidia/tcell v1.4.5 h1:JFmOiWLxr3Fsk2vjRL3n8oRUoJeyrazGhkhZqW31kEY=
github.com/zyedidia/tcell v1.4.5/go.mod h1:HhlbMSCcGX15rFDB+Q1Lk3pKEOocsCUAQC3zhZ9sadA=
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415 h1:752dTQ5OatJ9M5ULK2+9lor+nzyZz+LYDo3WGngg3Rc=
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415/go.mod h1:8leT8G0Cm8NoJHdrrKHyR9MirWoF4YW7pZh06B6H+1E=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
layeh.com/gopher-luar v1.0.7 h1:53iv6CCkRs5wyofZ+qVXcyAYQOIG52s6pt4xkqZdq7k=
layeh.com/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
// +build plan9 nacl windows
package action
func (*BufPane) Suspend() bool {
InfoBar.Error("Suspend is only supported on BSD/Linux")
return false
}

View File

@@ -1,27 +0,0 @@
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
package action
import (
"syscall"
"github.com/zyedidia/micro/v2/internal/screen"
)
// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs.
// This only works on linux and has no default binding.
// This code was adapted from the suspend code in nsf/godit
func (*BufPane) Suspend() bool {
screenb := screen.TempFini()
// suspend the process
pid := syscall.Getpid()
err := syscall.Kill(pid, syscall.SIGSTOP)
if err != nil {
screen.TermMessage(err)
}
screen.TempStart(screenb)
return false
}

View File

@@ -1,401 +0,0 @@
package action
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
"unicode"
"github.com/zyedidia/json5"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell"
)
func createBindingsIfNotExist(fname string) {
if _, e := os.Stat(fname); os.IsNotExist(e) {
ioutil.WriteFile(fname, []byte("{}"), 0644)
}
}
// InitBindings intializes the bindings map by reading from bindings.json
func InitBindings() {
config.Bindings = DefaultBindings()
var parsed map[string]string
defaults := DefaultBindings()
filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename)
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
screen.TermMessage("Error reading bindings.json file: " + err.Error())
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
screen.TermMessage("Error reading bindings.json:", err.Error())
}
}
for k, v := range defaults {
BindKey(k, v)
}
for k, v := range parsed {
BindKey(k, v)
}
}
func BindKey(k, v string) {
event, ok := findEvent(k)
if !ok {
screen.TermMessage(k, "is not a bindable event")
}
switch e := event.(type) {
case KeyEvent:
BufMapKey(e, v)
case MouseEvent:
BufMapMouse(e, v)
case RawEvent:
BufMapKey(e, v)
}
config.Bindings[k] = v
}
// findEvent will find binding Key 'b' using string 'k'
func findEvent(k string) (b Event, ok bool) {
modifiers := tcell.ModNone
// First, we'll strip off all the modifiers in the name and add them to the
// ModMask
modSearch:
for {
switch {
case strings.HasPrefix(k, "-"):
// We optionally support dashes between modifiers
k = k[1:]
case strings.HasPrefix(k, "Ctrl") && k != "CtrlH":
// CtrlH technically does not have a 'Ctrl' modifier because it is really backspace
k = k[4:]
modifiers |= tcell.ModCtrl
case strings.HasPrefix(k, "Alt"):
k = k[3:]
modifiers |= tcell.ModAlt
case strings.HasPrefix(k, "Shift"):
k = k[5:]
modifiers |= tcell.ModShift
case strings.HasPrefix(k, "\x1b"):
screen.Screen.RegisterRawSeq(k)
return RawEvent{
esc: k,
}, true
default:
break modSearch
}
}
if len(k) == 0 {
return KeyEvent{}, false
}
// Control is handled in a special way, since the terminal sends explicitly
// marked escape sequences for control keys
// We should check for Control keys first
if modifiers&tcell.ModCtrl != 0 {
// see if the key is in bindingKeys with the Ctrl prefix.
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
if code, ok := keyEvents["Ctrl"+k]; ok {
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
}
// See if we can find the key in bindingMouse
if code, ok := mouseEvents[k]; ok {
return MouseEvent{
btn: code,
mod: modifiers,
}, true
}
// If we were given one character, then we've got a rune.
if len(k) == 1 {
return KeyEvent{
code: tcell.KeyRune,
mod: modifiers,
r: rune(k[0]),
}, true
}
// We don't know what happened.
return KeyEvent{}, false
}
// 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) {
var e error
var parsed map[string]string
filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
return false, errors.New("Error reading bindings.json file: " + err.Error())
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
return false, errors.New("Error reading bindings.json: " + err.Error())
}
key, ok := findEvent(k)
if !ok {
return false, errors.New("Invalid event " + k)
}
found := false
for ev := range parsed {
if e, ok := findEvent(ev); ok {
if e == key {
if overwrite {
parsed[ev] = v
}
found = true
break
}
}
}
if found && !overwrite {
return true, nil
} else if !found {
parsed[k] = v
}
BindKey(k, v)
txt, _ := json.MarshalIndent(parsed, "", " ")
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return false, e
}
// UnbindKey removes the binding for a key from the bindings.json file
func UnbindKey(k string) error {
var e error
var parsed map[string]string
filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
return errors.New("Error reading bindings.json file: " + err.Error())
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
return errors.New("Error reading bindings.json: " + err.Error())
}
key, ok := findEvent(k)
if !ok {
return errors.New("Invalid event " + k)
}
for ev := range parsed {
if e, ok := findEvent(ev); ok {
if e == key {
delete(parsed, ev)
break
}
}
}
defaults := DefaultBindings()
if a, ok := defaults[k]; ok {
BindKey(k, a)
} else if _, ok := config.Bindings[k]; ok {
BufUnmap(key)
delete(config.Bindings, k)
}
txt, _ := json.MarshalIndent(parsed, "", " ")
return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return e
}
var mouseEvents = map[string]tcell.ButtonMask{
"MouseLeft": tcell.Button1,
"MouseMiddle": tcell.Button2,
"MouseRight": tcell.Button3,
"MouseWheelUp": tcell.WheelUp,
"MouseWheelDown": tcell.WheelDown,
"MouseWheelLeft": tcell.WheelLeft,
"MouseWheelRight": tcell.WheelRight,
}
var keyEvents = map[string]tcell.Key{
"Up": tcell.KeyUp,
"Down": tcell.KeyDown,
"Right": tcell.KeyRight,
"Left": tcell.KeyLeft,
"UpLeft": tcell.KeyUpLeft,
"UpRight": tcell.KeyUpRight,
"DownLeft": tcell.KeyDownLeft,
"DownRight": tcell.KeyDownRight,
"Center": tcell.KeyCenter,
"PageUp": tcell.KeyPgUp,
"PageDown": tcell.KeyPgDn,
"Home": tcell.KeyHome,
"End": tcell.KeyEnd,
"Insert": tcell.KeyInsert,
"Delete": tcell.KeyDelete,
"Help": tcell.KeyHelp,
"Exit": tcell.KeyExit,
"Clear": tcell.KeyClear,
"Cancel": tcell.KeyCancel,
"Print": tcell.KeyPrint,
"Pause": tcell.KeyPause,
"Backtab": tcell.KeyBacktab,
"F1": tcell.KeyF1,
"F2": tcell.KeyF2,
"F3": tcell.KeyF3,
"F4": tcell.KeyF4,
"F5": tcell.KeyF5,
"F6": tcell.KeyF6,
"F7": tcell.KeyF7,
"F8": tcell.KeyF8,
"F9": tcell.KeyF9,
"F10": tcell.KeyF10,
"F11": tcell.KeyF11,
"F12": tcell.KeyF12,
"F13": tcell.KeyF13,
"F14": tcell.KeyF14,
"F15": tcell.KeyF15,
"F16": tcell.KeyF16,
"F17": tcell.KeyF17,
"F18": tcell.KeyF18,
"F19": tcell.KeyF19,
"F20": tcell.KeyF20,
"F21": tcell.KeyF21,
"F22": tcell.KeyF22,
"F23": tcell.KeyF23,
"F24": tcell.KeyF24,
"F25": tcell.KeyF25,
"F26": tcell.KeyF26,
"F27": tcell.KeyF27,
"F28": tcell.KeyF28,
"F29": tcell.KeyF29,
"F30": tcell.KeyF30,
"F31": tcell.KeyF31,
"F32": tcell.KeyF32,
"F33": tcell.KeyF33,
"F34": tcell.KeyF34,
"F35": tcell.KeyF35,
"F36": tcell.KeyF36,
"F37": tcell.KeyF37,
"F38": tcell.KeyF38,
"F39": tcell.KeyF39,
"F40": tcell.KeyF40,
"F41": tcell.KeyF41,
"F42": tcell.KeyF42,
"F43": tcell.KeyF43,
"F44": tcell.KeyF44,
"F45": tcell.KeyF45,
"F46": tcell.KeyF46,
"F47": tcell.KeyF47,
"F48": tcell.KeyF48,
"F49": tcell.KeyF49,
"F50": tcell.KeyF50,
"F51": tcell.KeyF51,
"F52": tcell.KeyF52,
"F53": tcell.KeyF53,
"F54": tcell.KeyF54,
"F55": tcell.KeyF55,
"F56": tcell.KeyF56,
"F57": tcell.KeyF57,
"F58": tcell.KeyF58,
"F59": tcell.KeyF59,
"F60": tcell.KeyF60,
"F61": tcell.KeyF61,
"F62": tcell.KeyF62,
"F63": tcell.KeyF63,
"F64": tcell.KeyF64,
"CtrlSpace": tcell.KeyCtrlSpace,
"CtrlA": tcell.KeyCtrlA,
"CtrlB": tcell.KeyCtrlB,
"CtrlC": tcell.KeyCtrlC,
"CtrlD": tcell.KeyCtrlD,
"CtrlE": tcell.KeyCtrlE,
"CtrlF": tcell.KeyCtrlF,
"CtrlG": tcell.KeyCtrlG,
"CtrlH": tcell.KeyCtrlH,
"CtrlI": tcell.KeyCtrlI,
"CtrlJ": tcell.KeyCtrlJ,
"CtrlK": tcell.KeyCtrlK,
"CtrlL": tcell.KeyCtrlL,
"CtrlM": tcell.KeyCtrlM,
"CtrlN": tcell.KeyCtrlN,
"CtrlO": tcell.KeyCtrlO,
"CtrlP": tcell.KeyCtrlP,
"CtrlQ": tcell.KeyCtrlQ,
"CtrlR": tcell.KeyCtrlR,
"CtrlS": tcell.KeyCtrlS,
"CtrlT": tcell.KeyCtrlT,
"CtrlU": tcell.KeyCtrlU,
"CtrlV": tcell.KeyCtrlV,
"CtrlW": tcell.KeyCtrlW,
"CtrlX": tcell.KeyCtrlX,
"CtrlY": tcell.KeyCtrlY,
"CtrlZ": tcell.KeyCtrlZ,
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
"CtrlBackslash": tcell.KeyCtrlBackslash,
"CtrlRightSq": tcell.KeyCtrlRightSq,
"CtrlCarat": tcell.KeyCtrlCarat,
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
"Tab": tcell.KeyTab,
"Esc": tcell.KeyEsc,
"Escape": tcell.KeyEscape,
"Enter": tcell.KeyEnter,
"Backspace": tcell.KeyBackspace2,
"OldBackspace": tcell.KeyBackspace,
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
"PgUp": tcell.KeyPgUp,
"PgDown": tcell.KeyPgDn,
}

View File

@@ -1,706 +0,0 @@
package action
import (
"strings"
"time"
luar "layeh.com/gopher-luar"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell"
)
type BufKeyAction func(*BufPane) bool
type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
var BufKeyBindings map[Event]BufKeyAction
var BufKeyStrings map[Event]string
var BufMouseBindings map[MouseEvent]BufMouseAction
func init() {
BufKeyBindings = make(map[Event]BufKeyAction)
BufKeyStrings = make(map[Event]string)
BufMouseBindings = make(map[MouseEvent]BufMouseAction)
}
func LuaAction(fn string) func(*BufPane) bool {
luaFn := strings.Split(fn, ".")
if len(luaFn) <= 1 {
return nil
}
plName, plFn := luaFn[0], luaFn[1]
pl := config.FindPlugin(plName)
if pl == nil {
return nil
}
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)
}
}
}
// BufMapKey maps a key event to an action
func BufMapKey(k Event, action string) {
BufKeyStrings[k] = action
var actionfns []func(*BufPane) bool
var names []string
var types []byte
for i := 0; ; i++ {
if action == "" {
break
}
// TODO: fix problem when complex bindings have these
// characters (escape them?)
idx := strings.IndexAny(action, "&|,")
a := action
if idx >= 0 {
a = action[:idx]
types = append(types, action[idx])
action = action[idx+1:]
} else {
types = append(types, ' ')
action = ""
}
var afn func(*BufPane) bool
if strings.HasPrefix(a, "command:") {
a = strings.SplitN(a, ":", 2)[1]
afn = CommandAction(a)
names = append(names, "")
} else if strings.HasPrefix(a, "command-edit:") {
a = strings.SplitN(a, ":", 2)[1]
afn = CommandEditAction(a)
names = append(names, "")
} else if strings.HasPrefix(a, "lua:") {
a = strings.SplitN(a, ":", 2)[1]
afn = LuaAction(a)
if afn == nil {
screen.TermMessage("Lua Error:", a, "does not exist")
continue
}
split := strings.SplitN(a, ".", 2)
if len(split) > 1 {
a = strings.Title(split[0]) + strings.Title(split[1])
} else {
a = strings.Title(a)
}
names = append(names, a)
} else if f, ok := BufKeyActions[a]; ok {
afn = f
names = append(names, a)
} else {
screen.TermMessage("Error in bindings: action", a, "does not exist")
continue
}
actionfns = append(actionfns, afn)
}
BufKeyBindings[k] = func(h *BufPane) bool {
cursors := h.Buf.GetCursors()
success := true
for i, a := range actionfns {
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] == ',') {
success = h.execAction(a, names[i], j)
} else {
break
}
}
}
return true
}
}
// BufMapMouse maps a mouse event to an action
func BufMapMouse(k MouseEvent, action string) {
if f, ok := BufMouseActions[action]; ok {
BufMouseBindings[k] = f
} else {
delete(BufMouseBindings, k)
BufMapKey(k, action)
}
}
// BufUnmap unmaps a key or mouse event from any action
func BufUnmap(k Event) {
delete(BufKeyBindings, k)
delete(BufKeyStrings, k)
switch e := k.(type) {
case MouseEvent:
delete(BufMouseBindings, e)
}
}
// The BufPane connects the buffer and the window
// It provides a cursor (or multiple) and defines a set of actions
// that can be taken on the buffer
// The ActionHandler can access the window for necessary info about
// visual positions for mouse clicks and scrolling
type BufPane struct {
display.BWindow
Buf *buffer.Buffer
Cursor *buffer.Cursor // the active 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
// We need to keep track of insert key press toggle
isOverwriteMode bool
// This stores when the last click was
// This is useful for detecting double and triple clicks
lastClickTime time.Time
lastLoc buffer.Loc
// lastCutTime stores when the last ctrl+k was issued.
// It is used for clearing the clipboard to replace it with fresh cut lines.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
freshClip bool
// Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click
doubleClick bool
// Same here, just to keep track for mouse move events
tripleClick bool
// Last search stores the last successful search for FindNext and FindPrev
lastSearch string
lastSearchRegex bool
// Should the current multiple cursor selection search based on word or
// based on selection (false for selection, true for word)
multiWord bool
splitID uint64
tab *Tab
// remember original location of a search in case the search is canceled
searchOrig buffer.Loc
}
func NewBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
h := new(BufPane)
h.Buf = buf
h.BWindow = win
h.tab = tab
h.Cursor = h.Buf.GetActiveCursor()
h.mouseReleased = true
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
return h
}
func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
w := display.NewBufWindow(0, 0, 0, 0, buf)
return NewBufPane(buf, w, tab)
}
func (h *BufPane) SetTab(t *Tab) {
h.tab = t
}
func (h *BufPane) Tab() *Tab {
return h.tab
}
func (h *BufPane) ResizePane(size int) {
n := h.tab.GetNode(h.splitID)
n.ResizeSplit(size)
h.tab.Resize()
}
// PluginCB calls all plugin callbacks with a certain name and
// displays an error if there is one and returns the aggregrate
// boolean response
func (h *BufPane) PluginCB(cb string) bool {
b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
return b
}
// PluginCBRune is the same as PluginCB but also passes a rune to
// the plugins
func (h *BufPane) PluginCBRune(cb string, r rune) bool {
b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h), luar.New(ulua.L, string(r)))
if err != nil {
screen.TermMessage(err)
}
return b
}
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.Buf.Close()
h.Buf = b
h.BWindow.SetBuffer(b)
h.Cursor = b.GetActiveCursor()
h.Resize(h.GetView().Width, h.GetView().Height)
h.Relocate()
// Set mouseReleased to true because we assume the mouse is not being pressed when
// the editor is opened
h.mouseReleased = true
// Set isOverwriteMode to false, because we assume we are in the default mode when editor
// is opened
h.isOverwriteMode = false
h.lastClickTime = time.Time{}
}
func (h *BufPane) ID() uint64 {
return h.splitID
}
func (h *BufPane) SetID(i uint64) {
h.splitID = i
}
func (h *BufPane) Name() string {
n := h.Buf.GetName()
if h.Buf.Modified() {
n += " +"
}
return n
}
// HandleEvent executes the tcell event properly
func (h *BufPane) HandleEvent(event tcell.Event) {
if h.Buf.ExternallyModified() && !h.Buf.ReloadDisabled {
InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n,esc)", func(yes, canceled bool) {
if canceled {
h.Buf.DisableReload()
}
if !yes || canceled {
h.Buf.UpdateModTime()
} else {
h.Buf.ReOpen()
}
})
}
switch e := event.(type) {
case *tcell.EventRaw:
re := RawEvent{
esc: e.EscSeq(),
}
h.DoKeyEvent(re)
case *tcell.EventPaste:
h.paste(e.Text())
h.Relocate()
case *tcell.EventKey:
ke := KeyEvent{
code: e.Key(),
mod: e.Modifiers(),
r: e.Rune(),
}
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.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("primary")
}
h.mouseReleased = true
}
}
if !cancel {
me := MouseEvent{
btn: e.Buttons(),
mod: e.Modifiers(),
}
h.DoMouseEvent(me, e)
}
}
h.Buf.MergeCursors()
if h.IsActive() {
// Display any gutter messages for this line
c := h.Buf.GetActiveCursor()
none := true
for _, m := range h.Buf.Messages {
if c.Y == m.Start.Y || c.Y == m.End.Y {
InfoBar.GutterMessage(m.Msg)
none = false
break
}
}
if none && InfoBar.HasGutter {
InfoBar.ClearGutter()
}
}
}
// DoKeyEvent executes a key event by finding the action it is bound
// to and executing it (possibly multiple times for multiple cursors)
func (h *BufPane) DoKeyEvent(e Event) bool {
if action, ok := BufKeyBindings[e]; ok {
return action(h)
}
return false
}
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) 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 isMulti {
if recording_macro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
}
}
}
return success
}
}
return false
}
func (h *BufPane) completeAction(action string) {
h.PluginCB("on" + action)
}
func (h *BufPane) HasKeyEvent(e Event) bool {
_, ok := BufKeyBindings[e]
return ok
}
// DoMouseEvent executes a mouse event by finding the action it is bound
// to and executing it
func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
if action, ok := BufMouseBindings[e]; ok {
if action(h, te) {
h.Relocate()
}
return true
} else if h.HasKeyEvent(e) {
return h.DoKeyEvent(e)
}
return false
}
// DoRuneInsert inserts a given rune into the current buffer
// (possibly multiple times for multiple cursors)
func (h *BufPane) DoRuneInsert(r rune) {
cursors := h.Buf.GetCursors()
for _, c := range cursors {
// Insert a character
h.Buf.SetCurCursor(c.Num)
h.Cursor = c
if !h.PluginCBRune("preRune", r) {
continue
}
if c.HasSelection() {
c.DeleteSelection()
c.ResetSelection()
}
if h.isOverwriteMode {
next := c.Loc
next.X++
h.Buf.Replace(c.Loc, next, string(r))
} else {
h.Buf.Insert(c.Loc, string(r))
}
if recording_macro {
curmacro = append(curmacro, r)
}
h.Relocate()
h.PluginCBRune("onRune", r)
}
}
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
return e
}
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
return e
}
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
return h.VSplitIndex(buf, h.Buf.Settings["splitright"].(bool))
}
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
return h.HSplitIndex(buf, h.Buf.Settings["splitbottom"].(bool))
}
func (h *BufPane) Close() {
h.Buf.Close()
}
func (h *BufPane) SetActive(b bool) {
h.BWindow.SetActive(b)
if b {
// Display any gutter messages for this line
c := h.Buf.GetActiveCursor()
none := true
for _, m := range h.Buf.Messages {
if c.Y == m.Start.Y || c.Y == m.End.Y {
InfoBar.GutterMessage(m.Msg)
none = false
break
}
}
if none && InfoBar.HasGutter {
InfoBar.ClearGutter()
}
}
}
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
var BufKeyActions = map[string]BufKeyAction{
"CursorUp": (*BufPane).CursorUp,
"CursorDown": (*BufPane).CursorDown,
"CursorPageUp": (*BufPane).CursorPageUp,
"CursorPageDown": (*BufPane).CursorPageDown,
"CursorLeft": (*BufPane).CursorLeft,
"CursorRight": (*BufPane).CursorRight,
"CursorStart": (*BufPane).CursorStart,
"CursorEnd": (*BufPane).CursorEnd,
"SelectToStart": (*BufPane).SelectToStart,
"SelectToEnd": (*BufPane).SelectToEnd,
"SelectUp": (*BufPane).SelectUp,
"SelectDown": (*BufPane).SelectDown,
"SelectLeft": (*BufPane).SelectLeft,
"SelectRight": (*BufPane).SelectRight,
"WordRight": (*BufPane).WordRight,
"WordLeft": (*BufPane).WordLeft,
"SelectWordRight": (*BufPane).SelectWordRight,
"SelectWordLeft": (*BufPane).SelectWordLeft,
"DeleteWordRight": (*BufPane).DeleteWordRight,
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
"SelectLine": (*BufPane).SelectLine,
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
"SelectToStartOfTextToggle": (*BufPane).SelectToStartOfTextToggle,
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
"ParagraphNext": (*BufPane).ParagraphNext,
"InsertNewline": (*BufPane).InsertNewline,
"Backspace": (*BufPane).Backspace,
"Delete": (*BufPane).Delete,
"InsertTab": (*BufPane).InsertTab,
"Save": (*BufPane).Save,
"SaveAll": (*BufPane).SaveAll,
"SaveAs": (*BufPane).SaveAs,
"Find": (*BufPane).Find,
"FindLiteral": (*BufPane).FindLiteral,
"FindNext": (*BufPane).FindNext,
"FindPrevious": (*BufPane).FindPrevious,
"Center": (*BufPane).Center,
"Undo": (*BufPane).Undo,
"Redo": (*BufPane).Redo,
"Copy": (*BufPane).Copy,
"CopyLine": (*BufPane).CopyLine,
"Cut": (*BufPane).Cut,
"CutLine": (*BufPane).CutLine,
"DuplicateLine": (*BufPane).DuplicateLine,
"DeleteLine": (*BufPane).DeleteLine,
"MoveLinesUp": (*BufPane).MoveLinesUp,
"MoveLinesDown": (*BufPane).MoveLinesDown,
"IndentSelection": (*BufPane).IndentSelection,
"OutdentSelection": (*BufPane).OutdentSelection,
"Autocomplete": (*BufPane).Autocomplete,
"CycleAutocompleteBack": (*BufPane).CycleAutocompleteBack,
"OutdentLine": (*BufPane).OutdentLine,
"IndentLine": (*BufPane).IndentLine,
"Paste": (*BufPane).Paste,
"PastePrimary": (*BufPane).PastePrimary,
"SelectAll": (*BufPane).SelectAll,
"OpenFile": (*BufPane).OpenFile,
"Start": (*BufPane).Start,
"End": (*BufPane).End,
"PageUp": (*BufPane).PageUp,
"PageDown": (*BufPane).PageDown,
"SelectPageUp": (*BufPane).SelectPageUp,
"SelectPageDown": (*BufPane).SelectPageDown,
"HalfPageUp": (*BufPane).HalfPageUp,
"HalfPageDown": (*BufPane).HalfPageDown,
"StartOfText": (*BufPane).StartOfText,
"StartOfTextToggle": (*BufPane).StartOfTextToggle,
"StartOfLine": (*BufPane).StartOfLine,
"EndOfLine": (*BufPane).EndOfLine,
"ToggleHelp": (*BufPane).ToggleHelp,
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
"ToggleDiffGutter": (*BufPane).ToggleDiffGutter,
"ToggleRuler": (*BufPane).ToggleRuler,
"ClearStatus": (*BufPane).ClearStatus,
"ShellMode": (*BufPane).ShellMode,
"CommandMode": (*BufPane).CommandMode,
"ToggleOverwriteMode": (*BufPane).ToggleOverwriteMode,
"Escape": (*BufPane).Escape,
"Quit": (*BufPane).Quit,
"QuitAll": (*BufPane).QuitAll,
"AddTab": (*BufPane).AddTab,
"PreviousTab": (*BufPane).PreviousTab,
"NextTab": (*BufPane).NextTab,
"NextSplit": (*BufPane).NextSplit,
"PreviousSplit": (*BufPane).PreviousSplit,
"Unsplit": (*BufPane).Unsplit,
"VSplit": (*BufPane).VSplitAction,
"HSplit": (*BufPane).HSplitAction,
"ToggleMacro": (*BufPane).ToggleMacro,
"PlayMacro": (*BufPane).PlayMacro,
"Suspend": (*BufPane).Suspend,
"ScrollUp": (*BufPane).ScrollUpAction,
"ScrollDown": (*BufPane).ScrollDownAction,
"SpawnMultiCursor": (*BufPane).SpawnMultiCursor,
"SpawnMultiCursorUp": (*BufPane).SpawnMultiCursorUp,
"SpawnMultiCursorDown": (*BufPane).SpawnMultiCursorDown,
"SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
"JumpLine": (*BufPane).JumpLine,
"None": (*BufPane).None,
// This was changed to InsertNewline but I don't want to break backwards compatibility
"InsertEnter": (*BufPane).InsertNewline,
}
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
var BufMouseActions = map[string]BufMouseAction{
"MousePress": (*BufPane).MousePress,
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
}
// MultiActions is a list of actions that should be executed multiple
// times if there are multiple cursors (one per cursor)
// Generally actions that modify global editor state like quitting or
// saving should not be included in this list
var MultiActions = map[string]bool{
"CursorUp": true,
"CursorDown": true,
"CursorPageUp": true,
"CursorPageDown": true,
"CursorLeft": true,
"CursorRight": true,
"CursorStart": true,
"CursorEnd": true,
"SelectToStart": true,
"SelectToEnd": true,
"SelectUp": true,
"SelectDown": true,
"SelectLeft": true,
"SelectRight": true,
"WordRight": true,
"WordLeft": true,
"SelectWordRight": true,
"SelectWordLeft": true,
"DeleteWordRight": true,
"DeleteWordLeft": true,
"SelectLine": true,
"SelectToStartOfLine": true,
"SelectToStartOfText": true,
"SelectToStartOfTextToggle": true,
"SelectToEndOfLine": true,
"ParagraphPrevious": true,
"ParagraphNext": true,
"InsertNewline": true,
"Backspace": true,
"Delete": true,
"InsertTab": true,
"FindNext": true,
"FindPrevious": true,
"CopyLine": true,
"Cut": true,
"CutLine": true,
"DuplicateLine": true,
"DeleteLine": true,
"MoveLinesUp": true,
"MoveLinesDown": true,
"IndentSelection": true,
"OutdentSelection": true,
"OutdentLine": true,
"IndentLine": true,
"Paste": true,
"PastePrimary": true,
"SelectPageUp": true,
"SelectPageDown": true,
"StartOfLine": true,
"StartOfText": true,
"StartOfTextToggle": true,
"EndOfLine": true,
"JumpToMatchingBrace": true,
}

View File

@@ -1,958 +0,0 @@
package action
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
shellquote "github.com/kballard/go-shellquote"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util"
)
// A Command contains information about how to execute a command
// It has the action for that command as well as a completer function
type Command struct {
action func(*BufPane, []string)
completer buffer.Completer
}
var commands map[string]Command
func InitCommands() {
commands = map[string]Command{
"set": {(*BufPane).SetCmd, OptionValueComplete},
"reset": {(*BufPane).ResetCmd, OptionValueComplete},
"setlocal": {(*BufPane).SetLocalCmd, OptionValueComplete},
"show": {(*BufPane).ShowCmd, OptionComplete},
"showkey": {(*BufPane).ShowKeyCmd, nil},
"run": {(*BufPane).RunCmd, nil},
"bind": {(*BufPane).BindCmd, nil},
"unbind": {(*BufPane).UnbindCmd, nil},
"quit": {(*BufPane).QuitCmd, nil},
"goto": {(*BufPane).GotoCmd, nil},
"save": {(*BufPane).SaveCmd, nil},
"replace": {(*BufPane).ReplaceCmd, nil},
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
"vsplit": {(*BufPane).VSplitCmd, buffer.FileComplete},
"hsplit": {(*BufPane).HSplitCmd, buffer.FileComplete},
"tab": {(*BufPane).NewTabCmd, buffer.FileComplete},
"help": {(*BufPane).HelpCmd, HelpComplete},
"eval": {(*BufPane).EvalCmd, nil},
"log": {(*BufPane).ToggleLogCmd, nil},
"plugin": {(*BufPane).PluginCmd, PluginComplete},
"reload": {(*BufPane).ReloadCmd, nil},
"reopen": {(*BufPane).ReopenCmd, nil},
"cd": {(*BufPane).CdCmd, buffer.FileComplete},
"pwd": {(*BufPane).PwdCmd, nil},
"open": {(*BufPane).OpenCmd, buffer.FileComplete},
"tabmove": {(*BufPane).TabMoveCmd, nil},
"tabswitch": {(*BufPane).TabSwitchCmd, nil},
"term": {(*BufPane).TermCmd, nil},
"memusage": {(*BufPane).MemUsageCmd, nil},
"retab": {(*BufPane).RetabCmd, nil},
"raw": {(*BufPane).RawCmd, nil},
"textfilter": {(*BufPane).TextFilterCmd, nil},
}
}
// MakeCommand is a function to easily create new commands
// This can be called by plugins in Lua so that plugins can define their own commands
func MakeCommand(name string, action func(bp *BufPane, args []string), completer buffer.Completer) {
if action != nil {
commands[name] = Command{action, completer}
}
}
// CommandEditAction returns a bindable function that opens a prompt with
// the given string and executes the command when the user presses
// enter
func CommandEditAction(prompt string) BufKeyAction {
return func(h *BufPane) bool {
InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
if !canceled {
MainTab().CurPane().HandleCommand(resp)
}
})
return false
}
}
// CommandAction returns a bindable function which executes the
// given command
func CommandAction(cmd string) BufKeyAction {
return func(h *BufPane) bool {
MainTab().CurPane().HandleCommand(cmd)
return false
}
}
var PluginCmds = []string{"install", "remove", "update", "available", "list", "search"}
// PluginCmd installs, removes, updates, lists, or searches for given plugins
func (h *BufPane) PluginCmd(args []string) {
if len(args) < 1 {
InfoBar.Error("Not enough arguments")
return
}
if h.Buf.Type != buffer.BTLog {
h.OpenLogBuf()
}
config.PluginCommand(buffer.LogBuf, args[0], args[1:])
}
// RetabCmd changes all spaces to tabs or all tabs to spaces
// depending on the user's settings
func (h *BufPane) RetabCmd(args []string) {
h.Buf.Retab()
}
// RawCmd opens a new raw view which displays the escape sequences micro
// is receiving in real-time
func (h *BufPane) RawCmd(args []string) {
width, height := screen.Screen.Size()
iOffset := config.GetInfoBarOffset()
tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane(nil))
Tabs.AddTab(tp)
Tabs.SetActive(len(Tabs.List) - 1)
}
// TextFilterCmd filters the selection through the command.
// Selection goes to the command input.
// On successful run command output replaces the current selection.
func (h *BufPane) TextFilterCmd(args []string) {
if len(args) == 0 {
InfoBar.Error("usage: textfilter arguments")
return
}
sel := h.Cursor.GetSelection()
if len(sel) == 0 {
h.Cursor.SelectWord()
sel = h.Cursor.GetSelection()
}
var bout, berr bytes.Buffer
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(string(sel))
cmd.Stderr = &berr
cmd.Stdout = &bout
err := cmd.Run()
if err != nil {
InfoBar.Error(err.Error() + " " + berr.String())
return
}
h.Cursor.DeleteSelection()
h.Buf.Insert(h.Cursor.Loc, bout.String())
}
// TabMoveCmd moves the current tab to a given index (starts at 1). The
// displaced tabs are moved up.
func (h *BufPane) TabMoveCmd(args []string) {
if len(args) <= 0 {
InfoBar.Error("Not enough arguments: provide an index, starting at 1")
return
}
if len(args[0]) <= 0 {
InfoBar.Error("Invalid argument: empty string")
return
}
num, err := strconv.Atoi(args[0])
if err != nil {
InfoBar.Error("Invalid argument: ", err)
return
}
// Preserve sign for relative move, if one exists
var shiftDirection byte
if strings.Contains("-+", string([]byte{args[0][0]})) {
shiftDirection = args[0][0]
}
// Relative positions -> absolute positions
idxFrom := Tabs.Active()
idxTo := 0
offset := util.Abs(num)
if shiftDirection == '-' {
idxTo = idxFrom - offset
} else if shiftDirection == '+' {
idxTo = idxFrom + offset
} else {
idxTo = offset - 1
}
// Restrain position to within the valid range
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
activeTab := Tabs.List[idxFrom]
Tabs.RemoveTab(activeTab.ID())
Tabs.List = append(Tabs.List, nil)
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
Tabs.List[idxTo] = activeTab
Tabs.UpdateNames()
Tabs.SetActive(idxTo)
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
}
// TabSwitchCmd switches to a given tab either by name or by number
func (h *BufPane) TabSwitchCmd(args []string) {
if len(args) > 0 {
num, err := strconv.Atoi(args[0])
if err != nil {
// Check for tab with this name
found := false
for i, t := range Tabs.List {
if t.Panes[t.active].Name() == args[0] {
Tabs.SetActive(i)
found = true
}
}
if !found {
InfoBar.Error("Could not find tab: ", err)
}
} else {
num--
if num >= 0 && num < len(Tabs.List) {
Tabs.SetActive(num)
} else {
InfoBar.Error("Invalid tab index")
}
}
}
}
// CdCmd changes the current working directory
func (h *BufPane) CdCmd(args []string) {
if len(args) > 0 {
path, err := util.ReplaceHome(args[0])
if err != nil {
InfoBar.Error(err)
return
}
err = os.Chdir(path)
if err != nil {
InfoBar.Error(err)
return
}
wd, _ := os.Getwd()
for _, b := range buffer.OpenBuffers {
if len(b.Path) > 0 {
b.Path, _ = util.MakeRelative(b.AbsPath, wd)
if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
b.Path = b.AbsPath
}
}
}
}
}
// MemUsageCmd prints micro's memory usage
// Alloc shows how many bytes are currently in use
// Sys shows how many bytes have been requested from the operating system
// NumGC shows how many times the GC has been run
// Note that Go commonly reserves more memory from the OS than is currently in-use/required
// Additionally, even if Go returns memory to the OS, the OS does not always claim it because
// there may be plenty of memory to spare
func (h *BufPane) MemUsageCmd(args []string) {
InfoBar.Message(util.GetMemStats())
}
// PwdCmd prints the current working directory
func (h *BufPane) PwdCmd(args []string) {
wd, err := os.Getwd()
if err != nil {
InfoBar.Message(err.Error())
} else {
InfoBar.Message(wd)
}
}
// OpenCmd opens a new buffer with a given filename
func (h *BufPane) OpenCmd(args []string) {
if len(args) > 0 {
filename := args[0]
// the filename might or might not be quoted, so unquote first then join the strings.
args, err := shellquote.Split(filename)
if err != nil {
InfoBar.Error("Error parsing args ", err)
return
}
if len(args) == 0 {
return
}
filename = strings.Join(args, " ")
open := func() {
b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
h.OpenBuffer(b)
}
if h.Buf.Modified() {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
open()
} else if !canceled && yes {
h.Save()
open()
}
})
} else {
open()
}
} else {
InfoBar.Error("No filename")
}
}
// ToggleLogCmd toggles the log view
func (h *BufPane) ToggleLogCmd(args []string) {
if h.Buf.Type != buffer.BTLog {
h.OpenLogBuf()
} else {
h.Quit()
}
}
// ReloadCmd reloads all files (syntax files, colorschemes...)
func (h *BufPane) ReloadCmd(args []string) {
ReloadConfig()
}
func ReloadConfig() {
config.InitRuntimeFiles()
err := config.ReadSettings()
if err != nil {
screen.TermMessage(err)
}
config.InitGlobalSettings()
InitBindings()
InitCommands()
err = config.InitColorscheme()
if err != nil {
screen.TermMessage(err)
}
for _, b := range buffer.OpenBuffers {
b.UpdateRules()
}
}
// ReopenCmd reopens the buffer (reload from disk)
func (h *BufPane) ReopenCmd(args []string) {
if h.Buf.Modified() {
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
if !canceled && yes {
h.Save()
h.Buf.ReOpen()
} else if !canceled {
h.Buf.ReOpen()
}
})
} else {
h.Buf.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))
} else {
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
helpBuffer.SetName("Help " + page)
if h.Buf.Type == buffer.BTHelp {
h.OpenBuffer(helpBuffer)
} else {
h.HSplitBuf(helpBuffer)
}
}
return nil
}
// HelpCmd tries to open the given help page in a horizontal split
func (h *BufPane) HelpCmd(args []string) {
if len(args) < 1 {
// Open the default help if the user just typed "> help"
h.openHelp("help")
} else {
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
err := h.openHelp(args[0])
if err != nil {
InfoBar.Error(err)
}
} else {
InfoBar.Error("Sorry, no help for ", args[0])
}
}
}
// VSplitCmd opens a vertical split with file given in the first argument
// If no file is given, it opens an empty buffer in a new split
func (h *BufPane) VSplitCmd(args []string) {
if len(args) == 0 {
// Open an empty vertical split
h.VSplitAction()
return
}
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
h.VSplitBuf(buf)
}
// HSplitCmd opens a horizontal split with file given in the first argument
// If no file is given, it opens an empty buffer in a new split
func (h *BufPane) HSplitCmd(args []string) {
if len(args) == 0 {
// Open an empty horizontal split
h.HSplitAction()
return
}
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
h.HSplitBuf(buf)
}
// EvalCmd evaluates a lua expression
func (h *BufPane) EvalCmd(args []string) {
InfoBar.Error("Eval unsupported")
}
// NewTabCmd opens the given file in a new tab
func (h *BufPane) NewTabCmd(args []string) {
width, height := screen.Screen.Size()
iOffset := config.GetInfoBarOffset()
if len(args) > 0 {
for _, a := range args {
b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
Tabs.AddTab(tp)
Tabs.SetActive(len(Tabs.List) - 1)
}
} else {
b := buffer.NewBufferFromString("", "", buffer.BTDefault)
tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
Tabs.AddTab(tp)
Tabs.SetActive(len(Tabs.List) - 1)
}
}
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
local := false
for _, s := range config.LocalSettings {
if s == option {
local = true
break
}
}
if !local {
config.GlobalSettings[option] = nativeValue
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 {
for _, pl := range config.Plugins {
if option == pl.Name {
if nativeValue.(bool) && !pl.Loaded {
pl.Load()
_, err := pl.Call("init")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
} else if !nativeValue.(bool) && pl.Loaded {
_, err := pl.Call("deinit")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
}
}
}
}
}
for _, b := range buffer.OpenBuffers {
b.SetOptionNative(option, nativeValue)
}
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
}
func SetGlobalOption(option, value string) error {
if _, ok := config.GlobalSettings[option]; !ok {
return config.ErrInvalidOption
}
nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
if err != nil {
return err
}
return SetGlobalOptionNative(option, nativeValue)
}
// ResetCmd resets a setting to its default value
func (h *BufPane) ResetCmd(args []string) {
if len(args) < 1 {
InfoBar.Error("Not enough arguments")
return
}
option := args[0]
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])
return
}
InfoBar.Error(config.ErrInvalidOption)
}
// SetCmd sets an option
func (h *BufPane) SetCmd(args []string) {
if len(args) < 2 {
InfoBar.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
err := SetGlobalOption(option, value)
if err == config.ErrInvalidOption {
err := h.Buf.SetOption(option, value)
if err != nil {
InfoBar.Error(err)
}
} else if err != nil {
InfoBar.Error(err)
}
}
// SetLocalCmd sets an option local to the buffer
func (h *BufPane) SetLocalCmd(args []string) {
if len(args) < 2 {
InfoBar.Error("Not enough arguments")
return
}
option := args[0]
value := args[1]
err := h.Buf.SetOption(option, value)
if err != nil {
InfoBar.Error(err)
}
}
// ShowCmd shows the value of the given option
func (h *BufPane) ShowCmd(args []string) {
if len(args) < 1 {
InfoBar.Error("Please provide an option to show")
return
}
var option interface{}
if opt, ok := h.Buf.Settings[args[0]]; ok {
option = opt
} else if opt, ok := config.GlobalSettings[args[0]]; ok {
option = opt
}
if option == nil {
InfoBar.Error(args[0], " is not a valid option")
return
}
InfoBar.Message(option)
}
// ShowKeyCmd displays the action that a key is bound to
func (h *BufPane) ShowKeyCmd(args []string) {
if len(args) < 1 {
InfoBar.Error("Please provide a key to show")
return
}
if action, ok := config.Bindings[args[0]]; ok {
InfoBar.Message(action)
} else {
InfoBar.Message(args[0], " has no binding")
}
}
// BindCmd creates a new keybinding
func (h *BufPane) BindCmd(args []string) {
if len(args) < 2 {
InfoBar.Error("Not enough arguments")
return
}
_, err := TryBindKey(args[0], args[1], true)
if err != nil {
InfoBar.Error(err)
}
}
// UnbindCmd binds a key to its default action
func (h *BufPane) UnbindCmd(args []string) {
if len(args) < 1 {
InfoBar.Error("Not enough arguments")
return
}
err := UnbindKey(args[0])
if err != nil {
InfoBar.Error(err)
}
}
// RunCmd runs a shell command in the background
func (h *BufPane) RunCmd(args []string) {
runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
if err != nil {
InfoBar.Error(err)
} else {
go func() {
InfoBar.Message(runf())
screen.Redraw()
}()
}
}
// QuitCmd closes the main view
func (h *BufPane) QuitCmd(args []string) {
h.Quit()
}
// GotoCmd is a command that will send the cursor to a certain
// position in the buffer
// For example: `goto line`, or `goto line:col`
func (h *BufPane) GotoCmd(args []string) {
if len(args) <= 0 {
InfoBar.Error("Not enough arguments")
} 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
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.Cursor.GotoLoc(buffer.Loc{col, line})
} else {
line, err := strconv.Atoi(args[0])
if err != nil {
InfoBar.Error(err)
return
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
h.Cursor.GotoLoc(buffer.Loc{0, line})
}
h.Relocate()
}
}
// SaveCmd saves the buffer optionally with an argument file name
func (h *BufPane) SaveCmd(args []string) {
if len(args) == 0 {
h.Save()
} else {
h.Buf.SaveAs(args[0])
}
}
// ReplaceCmd runs search and replace
func (h *BufPane) ReplaceCmd(args []string) {
if len(args) < 2 || len(args) > 4 {
// We need to find both a search and replace expression
InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
all := false
noRegex := false
foundSearch := false
foundReplace := false
var search string
var replaceStr string
for _, arg := range args {
switch arg {
case "-a":
all = true
case "-l":
noRegex = true
default:
if !foundSearch {
foundSearch = true
search = arg
} else if !foundReplace {
foundReplace = true
replaceStr = arg
} else {
InfoBar.Error("Invalid flag: " + arg)
return
}
}
}
if noRegex {
search = regexp.QuoteMeta(search)
}
replace := []byte(replaceStr)
var regex *regexp.Regexp
var err error
if h.Buf.Settings["ignorecase"].(bool) {
regex, err = regexp.Compile("(?im)" + search)
} else {
regex, err = regexp.Compile("(?m)" + search)
}
if err != nil {
// There was an error with the user's regex
InfoBar.Error(err)
return
}
nreplaced := 0
start := h.Buf.Start()
end := h.Buf.End()
selection := h.Cursor.HasSelection()
if selection {
start = h.Cursor.CurSelection[0]
end = h.Cursor.CurSelection[1]
}
if all {
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
} else {
inRange := func(l buffer.Loc) bool {
return l.GreaterEqual(start) && l.LessEqual(end)
}
searchLoc := start
var doReplacement func()
doReplacement = func() {
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, !noRegex)
if err != nil {
InfoBar.Error(err)
return
}
if !found || !inRange(locs[0]) || !inRange(locs[1]) {
h.Cursor.ResetSelection()
h.Buf.RelocateCursors()
return
}
h.Cursor.SetSelectionStart(locs[0])
h.Cursor.SetSelectionEnd(locs[1])
h.Cursor.GotoLoc(locs[0])
h.Relocate()
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
if !canceled && yes {
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
searchLoc = locs[0]
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
end.Move(nrunes, h.Buf)
h.Cursor.Loc = searchLoc
nreplaced++
} else if !canceled && !yes {
searchLoc = locs[0]
searchLoc.X += util.CharacterCount(replace)
} else if canceled {
h.Cursor.ResetSelection()
h.Buf.RelocateCursors()
return
}
doReplacement()
})
}
doReplacement()
}
h.Buf.RelocateCursors()
h.Relocate()
var s string
if nreplaced > 1 {
s = fmt.Sprintf("Replaced %d occurrences of %s", nreplaced, search)
} else if nreplaced == 1 {
s = fmt.Sprintf("Replaced 1 occurrence of %s", search)
} else {
s = fmt.Sprintf("Nothing matched %s", search)
}
if selection {
s += " in selection"
}
InfoBar.Message(s)
}
// ReplaceAllCmd replaces search term all at once
func (h *BufPane) ReplaceAllCmd(args []string) {
// aliased to Replace command
h.ReplaceCmd(append(args, "-a"))
}
// TermCmd opens a terminal in the current view
func (h *BufPane) TermCmd(args []string) {
ps := h.tab.Panes
if !TermEmuSupported {
InfoBar.Error("Terminal emulator not supported on this system")
return
}
if len(args) == 0 {
sh := os.Getenv("SHELL")
if sh == "" {
InfoBar.Error("Shell environment not found")
return
}
args = []string{sh}
}
term := func(i int, newtab bool) {
t := new(shell.Terminal)
err := t.Start(args, false, true, nil, nil)
if err != nil {
InfoBar.Error(err)
return
}
id := h.ID()
if newtab {
h.AddTab()
i = 0
id = MainTab().Panes[0].ID()
} else {
MainTab().Panes[i].Close()
}
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
InfoBar.Error(err)
return
}
MainTab().Panes[i] = tp
MainTab().SetActive(i)
}
// If there is only one open file we make a new tab instead of overwriting it
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
if newtab {
term(0, true)
return
}
for i, p := range ps {
if p.ID() == h.ID() {
if h.Buf.Modified() {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
term(i, false)
} else if !canceled && yes {
h.Save()
term(i, false)
}
})
} else {
term(i, false)
}
}
}
}
// HandleCommand handles input from the user
func (h *BufPane) HandleCommand(input string) {
args, err := shellquote.Split(input)
if err != nil {
InfoBar.Error("Error parsing args ", err)
return
}
if len(args) == 0 {
return
}
inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok {
InfoBar.Error("Unknown command ", inputCmd)
} else {
WriteLog("> " + input + "\n")
commands[inputCmd].action(h, args[1:])
WriteLog("\n")
}
}

View File

@@ -1,107 +0,0 @@
package action
// DefaultBindings returns a map containing micro's default keybindings
func DefaultBindings() map[string]string {
return map[string]string{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltUp": "MoveLinesUp",
"AltDown": "MoveLinesDown",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfTextToggle",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfTextToggle",
"ShiftHome": "SelectToStartOfTextToggle",
"CtrlShiftRight": "SelectToEndOfLine",
"ShiftEnd": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Alt-{": "ParagraphPrevious",
"Alt-}": "ParagraphNext",
"Enter": "InsertNewline",
"CtrlH": "Backspace",
"Backspace": "Backspace",
"Alt-CtrlH": "DeleteWordLeft",
"Alt-Backspace": "DeleteWordLeft",
"Tab": "Autocomplete|IndentSelection|InsertTab",
"Backtab": "CycleAutocompleteBack|OutdentSelection|OutdentLine",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "CopyLine|Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"Alt,": "PreviousTab",
"Alt.": "NextTab",
"Home": "StartOfTextToggle",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab",
"CtrlG": "ToggleHelp",
"Alt-g": "ToggleKeyMenu",
"CtrlR": "ToggleRuler",
"CtrlL": "command-edit:goto ",
"Delete": "Delete",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
"CtrlU": "ToggleMacro",
"CtrlJ": "PlayMacro",
"Insert": "ToggleOverwriteMode",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfText",
"Alt-e": "EndOfLine",
// "Alt-p": "CursorUp",
// "Alt-n": "CursorDown",
// Integration with file managers
"F2": "Save",
"F3": "Find",
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape",
// Mouse bindings
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor",
"AltShiftUp": "SpawnMultiCursorUp",
"AltShiftDown": "SpawnMultiCursorDown",
"Alt-m": "SpawnMultiCursorSelect",
"Alt-p": "RemoveMultiCursor",
"Alt-c": "RemoveAllMultiCursors",
"Alt-x": "SkipMultiCursor",
}
}

View File

@@ -1,109 +0,0 @@
// +build !darwin
package action
// DefaultBindings returns a map containing micro's default keybindings
func DefaultBindings() map[string]string {
return map[string]string{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"CtrlLeft": "WordLeft",
"CtrlRight": "WordRight",
"AltUp": "MoveLinesUp",
"AltDown": "MoveLinesDown",
"CtrlShiftRight": "SelectWordRight",
"CtrlShiftLeft": "SelectWordLeft",
"AltLeft": "StartOfTextToggle",
"AltRight": "EndOfLine",
"AltShiftLeft": "SelectToStartOfTextToggle",
"ShiftHome": "SelectToStartOfTextToggle",
"AltShiftRight": "SelectToEndOfLine",
"ShiftEnd": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Alt-{": "ParagraphPrevious",
"Alt-}": "ParagraphNext",
"Enter": "InsertNewline",
"CtrlH": "Backspace",
"Backspace": "Backspace",
"Alt-CtrlH": "DeleteWordLeft",
"Alt-Backspace": "DeleteWordLeft",
"Tab": "Autocomplete|IndentSelection|InsertTab",
"Backtab": "CycleAutocompleteBack|OutdentSelection|OutdentLine",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "CopyLine|Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"Alt,": "PreviousTab",
"Alt.": "NextTab",
"Home": "StartOfTextToggle",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab",
"CtrlG": "ToggleHelp",
"Alt-g": "ToggleKeyMenu",
"CtrlR": "ToggleRuler",
"CtrlL": "command-edit:goto ",
"Delete": "Delete",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
"CtrlU": "ToggleMacro",
"CtrlJ": "PlayMacro",
"Insert": "ToggleOverwriteMode",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfText",
"Alt-e": "EndOfLine",
// "Alt-p": "CursorUp",
// "Alt-n": "CursorDown",
// Integration with file managers
"F2": "Save",
"F3": "Find",
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape",
// Mouse bindings
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor",
"Alt-m": "SpawnMultiCursorSelect",
"AltShiftUp": "SpawnMultiCursorUp",
"AltShiftDown": "SpawnMultiCursorDown",
"Alt-p": "RemoveMultiCursor",
"Alt-c": "RemoveAllMultiCursors",
"Alt-x": "SkipMultiCursor",
}
}

View File

@@ -1,42 +0,0 @@
package action
import (
"github.com/zyedidia/tcell"
)
type Event interface{}
// RawEvent is simply an escape code
// We allow users to directly bind escape codes
// to get around some of a limitations of terminals
type RawEvent struct {
esc string
}
// KeyEvent is a key event containing a key code,
// some possible modifiers (alt, ctrl, etc...) and
// a rune if it was simply a character press
// Note: to be compatible with tcell events,
// for ctrl keys r=code
type KeyEvent struct {
code tcell.Key
mod tcell.ModMask
r rune
}
// MouseEvent is a mouse event with a mouse button and
// any possible key modifiers
type MouseEvent struct {
btn tcell.ButtonMask
mod tcell.ModMask
}
type KeyAction func(Handler) bool
type MouseAction func(Handler, tcell.EventMouse) bool
// A Handler will take a tcell event and execute it
// appropriately
type Handler interface {
HandleEvent(tcell.Event)
HandleCommand(string)
}

View File

@@ -1,48 +0,0 @@
package action
import "github.com/zyedidia/micro/v2/internal/buffer"
var InfoBar *InfoPane
var LogBufPane *BufPane
// InitGlobals initializes the log buffer and the info bar
func InitGlobals() {
InfoBar = NewInfoBar()
buffer.LogBuf = buffer.NewBufferFromString("", "Log", buffer.BTLog)
}
// GetInfoBar returns the infobar pane
func GetInfoBar() *InfoPane {
return InfoBar
}
// WriteLog writes a string to the log buffer
func WriteLog(s string) {
buffer.WriteLog(s)
if LogBufPane != nil {
LogBufPane.CursorEnd()
v := LogBufPane.GetView()
endY := buffer.LogBuf.End().Y
if endY > v.StartLine+v.Height {
v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
LogBufPane.SetView(v)
}
}
}
// OpenLogBuf opens the log buffer from the current bufpane
// If the current bufpane is a log buffer nothing happens,
// otherwise the log buffer is opened in a horizontal split
func (h *BufPane) OpenLogBuf() {
LogBufPane = h.HSplitBuf(buffer.LogBuf)
LogBufPane.CursorEnd()
v := LogBufPane.GetView()
endY := buffer.LogBuf.End().Y
if endY > v.StartLine+v.Height {
v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
LogBufPane.SetView(v)
}
}

View File

@@ -1,280 +0,0 @@
package action
import (
"bytes"
"sort"
"strings"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
)
// This file is meant (for now) for autocompletion in command mode, not
// while coding. This helps micro autocomplete commands and then filenames
// for example with `vsplit filename`.
// CommandComplete autocompletes commands
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
var suggestions []string
for cmd := range commands {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}
// HelpComplete autocompletes help topics
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
var suggestions []string
for _, file := range config.ListRuntimeFiles(config.RTHelp) {
topic := file.Name()
if strings.HasPrefix(topic, input) {
suggestions = append(suggestions, topic)
}
}
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}
// colorschemeComplete tab-completes names of colorschemes.
// This is just a heper value for OptionValueComplete
func colorschemeComplete(input string) (string, []string) {
var suggestions []string
files := config.ListRuntimeFiles(config.RTColorscheme)
for _, f := range files {
if strings.HasPrefix(f.Name(), input) {
suggestions = append(suggestions, f.Name())
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// OptionComplete autocompletes options
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
var suggestions []string
for option := range config.GlobalSettings {
if strings.HasPrefix(option, input) {
suggestions = append(suggestions, option)
}
}
// for option := range localSettings {
// if strings.HasPrefix(option, input) && !contains(suggestions, option) {
// suggestions = append(suggestions, option)
// }
// }
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}
// OptionValueComplete completes values for various options
func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
input, argstart := buffer.GetArg(b)
completeValue := false
args := bytes.Split(l, []byte{' '})
if len(args) >= 2 {
// localSettings := config.DefaultLocalSettings()
for option := range config.GlobalSettings {
if option == string(args[len(args)-2]) {
completeValue = true
break
}
}
// for option := range localSettings {
// if option == string(args[len(args)-2]) {
// completeValue = true
// break
// }
// }
}
if !completeValue {
return OptionComplete(b)
}
inputOpt := string(args[len(args)-2])
inputOpt = strings.TrimSpace(inputOpt)
var suggestions []string
// localSettings := config.DefaultLocalSettings()
var optionVal interface{}
for k, option := range config.GlobalSettings {
if k == inputOpt {
optionVal = option
}
}
// for k, option := range localSettings {
// if k == inputOpt {
// optionVal = option
// }
// }
switch optionVal.(type) {
case bool:
if strings.HasPrefix("on", input) {
suggestions = append(suggestions, "on")
} else if strings.HasPrefix("true", input) {
suggestions = append(suggestions, "true")
}
if strings.HasPrefix("off", input) {
suggestions = append(suggestions, "off")
} else if strings.HasPrefix("false", input) {
suggestions = append(suggestions, "false")
}
case string:
switch inputOpt {
case "colorscheme":
_, suggestions = colorschemeComplete(input)
case "fileformat":
if strings.HasPrefix("unix", input) {
suggestions = append(suggestions, "unix")
}
if strings.HasPrefix("dos", input) {
suggestions = append(suggestions, "dos")
}
case "sucmd":
if strings.HasPrefix("sudo", input) {
suggestions = append(suggestions, "sudo")
}
if strings.HasPrefix("doas", input) {
suggestions = append(suggestions, "doas")
}
}
}
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}
// PluginCmdComplete autocompletes the plugin command
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
var suggestions []string
for _, cmd := range PluginCmds {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}
// PluginComplete completes values for the plugin command
func PluginComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
input, argstart := buffer.GetArg(b)
completeValue := false
args := bytes.Split(l, []byte{' '})
if len(args) >= 2 {
for _, cmd := range PluginCmds {
if cmd == string(args[len(args)-2]) {
completeValue = true
break
}
}
}
if !completeValue {
return PluginCmdComplete(b)
}
var suggestions []string
for _, pl := range config.Plugins {
if strings.HasPrefix(pl.Name, input) {
suggestions = append(suggestions, pl.Name)
}
}
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}
// PluginNameComplete completes with the names of loaded plugins
// func PluginNameComplete(b *buffer.Buffer) ([]string, []string) {
// c := b.GetActiveCursor()
// input, argstart := buffer.GetArg(b)
//
// var suggestions []string
// for _, pp := range config.GetAllPluginPackages(nil) {
// if strings.HasPrefix(pp.Name, input) {
// suggestions = append(suggestions, pp.Name)
// }
// }
//
// sort.Strings(suggestions)
// completions := make([]string, len(suggestions))
// for i := range suggestions {
// completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
// }
// return completions, suggestions
// }
// // MakeCompletion registers a function from a plugin for autocomplete commands
// func MakeCompletion(function string) Completion {
// pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
// return Completion(-len(pluginCompletions))
// }

View File

@@ -1,224 +0,0 @@
package action
import (
"bytes"
"strings"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell"
)
type InfoKeyAction func(*InfoPane)
type InfoPane struct {
*BufPane
*info.InfoBuf
}
func NewInfoPane(ib *info.InfoBuf, w display.BWindow, tab *Tab) *InfoPane {
ip := new(InfoPane)
ip.InfoBuf = ib
ip.BufPane = NewBufPane(ib.Buffer, w, tab)
return ip
}
func NewInfoBar() *InfoPane {
ib := info.NewBuffer()
w := display.NewInfoWindow(ib)
return NewInfoPane(ib, w, nil)
}
func (h *InfoPane) Close() {
h.InfoBuf.Close()
h.BufPane.Close()
}
func (h *InfoPane) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventKey:
ke := KeyEvent{
code: e.Key(),
mod: e.Modifiers(),
r: e.Rune(),
}
done := h.DoKeyEvent(ke)
hasYN := h.HasYN
if e.Key() == tcell.KeyRune && hasYN {
if e.Rune() == 'y' && hasYN {
h.YNResp = true
h.DonePrompt(false)
} else if e.Rune() == 'n' && hasYN {
h.YNResp = false
h.DonePrompt(false)
}
}
if e.Key() == tcell.KeyRune && !done && !hasYN {
h.DoRuneInsert(e.Rune())
done = true
}
if done && h.HasPrompt && !hasYN {
resp := string(h.LineBytes(0))
hist := h.History[h.PromptType]
hist[h.HistoryNum] = resp
if h.EventCallback != nil {
h.EventCallback(resp)
}
}
default:
h.BufPane.HandleEvent(event)
}
}
// DoKeyEvent executes a key event for the command bar, doing any overridden actions
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
done := false
if action, ok := BufKeyBindings[e]; ok {
estr := BufKeyStrings[e]
for _, s := range InfoNones {
if s == estr {
return false
}
}
for s, a := range InfoOverrides {
// TODO this is a hack and really we should have support
// for having binding overrides for different buffers
if strings.HasPrefix(estr, s) {
done = true
a(h)
break
}
}
if !done {
done = action(h.BufPane)
}
}
return done
}
// InfoNones is a list of actions that should have no effect when executed
// by an infohandler
var InfoNones = []string{
"Save",
"SaveAll",
"SaveAs",
"Find",
"FindNext",
"FindPrevious",
"Center",
"DuplicateLine",
"MoveLinesUp",
"MoveLinesDown",
"OpenFile",
"Start",
"End",
"PageUp",
"PageDown",
"SelectPageUp",
"SelectPageDown",
"HalfPageUp",
"HalfPageDown",
"ToggleHelp",
"ToggleKeyMenu",
"ToggleDiffGutter",
"ToggleRuler",
"JumpLine",
"ClearStatus",
"ShellMode",
"CommandMode",
"AddTab",
"PreviousTab",
"NextTab",
"NextSplit",
"PreviousSplit",
"Unsplit",
"VSplit",
"HSplit",
"ToggleMacro",
"PlayMacro",
"Suspend",
"ScrollUp",
"ScrollDown",
"SpawnMultiCursor",
"SpawnMultiCursorSelect",
"RemoveMultiCursor",
"RemoveAllMultiCursors",
"SkipMultiCursor",
}
// InfoOverrides is the list of actions which have been overridden
// by the infohandler
var InfoOverrides = map[string]InfoKeyAction{
"CursorUp": (*InfoPane).CursorUp,
"CursorDown": (*InfoPane).CursorDown,
"InsertNewline": (*InfoPane).InsertNewline,
"Autocomplete": (*InfoPane).Autocomplete,
"Escape": (*InfoPane).Escape,
"Quit": (*InfoPane).Quit,
"QuitAll": (*InfoPane).QuitAll,
}
// CursorUp cycles history up
func (h *InfoPane) CursorUp() {
h.UpHistory(h.History[h.PromptType])
}
// CursorDown cycles history down
func (h *InfoPane) CursorDown() {
h.DownHistory(h.History[h.PromptType])
}
// Autocomplete begins autocompletion
func (h *InfoPane) Autocomplete() {
b := h.Buf
if b.HasSuggestions {
b.CycleAutocomplete(true)
return
}
c := b.GetActiveCursor()
l := b.LineBytes(0)
l = util.SliceStart(l, c.X)
args := bytes.Split(l, []byte{' '})
cmd := string(args[0])
if h.PromptType == "Command" {
if len(args) == 1 {
b.Autocomplete(CommandComplete)
} else if action, ok := commands[cmd]; ok {
if action.completer != nil {
b.Autocomplete(action.completer)
}
}
} else {
// by default use filename autocompletion
b.Autocomplete(buffer.FileComplete)
}
}
// InsertNewline completes the prompt
func (h *InfoPane) InsertNewline() {
if !h.HasYN {
h.DonePrompt(false)
}
}
// Quit cancels the prompt
func (h *InfoPane) Quit() {
h.DonePrompt(true)
}
// QuitAll cancels the prompt
func (h *InfoPane) QuitAll() {
h.DonePrompt(true)
}
// Escape cancels the prompt
func (h *InfoPane) Escape() {
h.DonePrompt(true)
}

View File

@@ -1,16 +0,0 @@
package action
import (
"github.com/zyedidia/micro/v2/internal/display"
)
type Pane interface {
Handler
display.Window
ID() uint64
SetID(i uint64)
Name() string
Close()
SetTab(t *Tab)
Tab() *Tab
}

View File

@@ -1,40 +0,0 @@
package action
import (
"fmt"
"reflect"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/tcell"
)
type RawPane struct {
*BufPane
}
func NewRawPaneFromWin(b *buffer.Buffer, win display.BWindow, tab *Tab) *RawPane {
rh := new(RawPane)
rh.BufPane = NewBufPane(b, win, tab)
return rh
}
func NewRawPane(tab *Tab) *RawPane {
b := buffer.NewBufferFromString("", "", buffer.BTRaw)
w := display.NewBufWindow(0, 0, 0, 0, b)
return NewRawPaneFromWin(b, w, tab)
}
func (h *RawPane) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventKey:
if e.Key() == tcell.KeyCtrlQ {
h.Quit()
}
}
h.Buf.Insert(h.Cursor.Loc, reflect.TypeOf(event).String()[7:])
h.Buf.Insert(h.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq()))
h.Relocate()
}

View File

@@ -1,307 +0,0 @@
package action
import (
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/views"
"github.com/zyedidia/tcell"
)
// The TabList is a list of tabs and a window to display the tab bar
// at the top of the screen
type TabList struct {
*display.TabWindow
List []*Tab
}
// NewTabList creates a TabList from a list of buffers by creating a Tab
// for each buffer
func NewTabList(bufs []*buffer.Buffer) *TabList {
w, h := screen.Screen.Size()
iOffset := config.GetInfoBarOffset()
tl := new(TabList)
tl.List = make([]*Tab, len(bufs))
if len(bufs) > 1 {
for i, b := range bufs {
tl.List[i] = NewTabFromBuffer(0, 1, w, h-1-iOffset, b)
}
} else {
tl.List[0] = NewTabFromBuffer(0, 0, w, h-iOffset, bufs[0])
}
tl.TabWindow = display.NewTabWindow(w, 0)
tl.Names = make([]string, len(bufs))
return tl
}
// UpdateNames makes sure that the list of names the tab window has access to is
// correct
func (t *TabList) UpdateNames() {
t.Names = t.Names[:0]
for _, p := range t.List {
t.Names = append(t.Names, p.Panes[p.active].Name())
}
}
// AddTab adds a new tab to this TabList
func (t *TabList) AddTab(p *Tab) {
t.List = append(t.List, p)
t.Resize()
t.UpdateNames()
}
// RemoveTab removes a tab with the given id from the TabList
func (t *TabList) RemoveTab(id uint64) {
for i, p := range t.List {
if len(p.Panes) == 0 {
continue
}
if p.Panes[0].ID() == id {
copy(t.List[i:], t.List[i+1:])
t.List[len(t.List)-1] = nil
t.List = t.List[:len(t.List)-1]
if t.Active() >= len(t.List) {
t.SetActive(len(t.List) - 1)
}
t.Resize()
t.UpdateNames()
return
}
}
}
// Resize resizes all elements within the tab list
// One thing to note is that when there is only 1 tab
// the tab bar should not be drawn so resizing must take
// that into account
func (t *TabList) Resize() {
w, h := screen.Screen.Size()
iOffset := config.GetInfoBarOffset()
InfoBar.Resize(w, h-1)
if len(t.List) > 1 {
for _, p := range t.List {
p.Y = 1
p.Node.Resize(w, h-1-iOffset)
p.Resize()
}
} else if len(t.List) == 1 {
t.List[0].Y = 0
t.List[0].Node.Resize(w, h-iOffset)
t.List[0].Resize()
}
t.TabWindow.Resize(w, h)
}
// HandleEvent checks for a resize event or a mouse event on the tab bar
// otherwise it will forward the event to the currently active tab
func (t *TabList) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventResize:
t.Resize()
case *tcell.EventMouse:
mx, my := e.Position()
switch e.Buttons() {
case tcell.Button1:
if my == t.Y && mx == 0 {
t.Scroll(-4)
return
} else if my == t.Y && mx == t.Width-1 {
t.Scroll(4)
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.WheelUp:
if my == t.Y {
t.Scroll(4)
return
}
case tcell.WheelDown:
if my == t.Y {
t.Scroll(-4)
return
}
}
}
t.List[t.Active()].HandleEvent(event)
}
// Display updates the names and then displays the tab bar
func (t *TabList) Display() {
t.UpdateNames()
if len(t.List) > 1 {
t.TabWindow.Display()
}
}
// Tabs is the global tab list
var Tabs *TabList
func InitTabs(bufs []*buffer.Buffer) {
Tabs = NewTabList(bufs)
}
func MainTab() *Tab {
return Tabs.List[Tabs.Active()]
}
// A Tab represents a single tab
// It consists of a list of edit panes (the open buffers),
// a split tree (stored as just the root node), and a uiwindow
// to display the UI elements like the borders between splits
type Tab struct {
*views.Node
*display.UIWindow
Panes []Pane
active int
resizing *views.Node // node currently being resized
// captures whether the mouse is released
release bool
}
// NewTabFromBuffer creates a new tab from the given buffer
func NewTabFromBuffer(x, y, width, height int, b *buffer.Buffer) *Tab {
t := new(Tab)
t.Node = views.NewRoot(x, y, width, height)
t.UIWindow = display.NewUIWindow(t.Node)
e := NewBufPaneFromBuf(b, t)
e.SetID(t.ID())
t.Panes = append(t.Panes, e)
return t
}
func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
t := new(Tab)
t.Node = views.NewRoot(x, y, width, height)
t.UIWindow = display.NewUIWindow(t.Node)
pane.SetTab(t)
pane.SetID(t.ID())
t.Panes = append(t.Panes, pane)
return t
}
// HandleEvent takes a tcell event and usually dispatches it to the current
// active pane. However if the event is a resize or a mouse event where the user
// is interacting with the UI (resizing splits) then the event is consumed here
// If the event is a mouse 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:
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
}
t.resizing.ResizeSplit(size)
t.Resize()
return
}
if wasReleased {
resizeID := t.GetMouseSplitID(buffer.Loc{mx, my})
if resizeID != 0 {
t.resizing = t.GetNode(uint64(resizeID))
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
if inpane {
t.SetActive(i)
break
}
}
}
case tcell.ButtonNone:
t.resizing = nil
t.release = true
default:
for _, p := range t.Panes {
v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
if inpane {
p.HandleEvent(event)
return
}
}
}
}
t.Panes[t.active].HandleEvent(event)
}
// SetActive changes the currently active pane to the specified index
func (t *Tab) SetActive(i int) {
t.active = i
for j, p := range t.Panes {
if j == i {
p.SetActive(true)
} else {
p.SetActive(false)
}
}
}
// GetPane returns the pane with the given split index
func (t *Tab) GetPane(splitid uint64) int {
for i, p := range t.Panes {
if p.ID() == splitid {
return i
}
}
return 0
}
// Remove pane removes the pane with the given index
func (t *Tab) RemovePane(i int) {
copy(t.Panes[i:], t.Panes[i+1:])
t.Panes[len(t.Panes)-1] = nil
t.Panes = t.Panes[:len(t.Panes)-1]
}
// Resize resizes all panes according to their corresponding split nodes
func (t *Tab) Resize() {
for _, p := range t.Panes {
n := t.GetNode(p.ID())
pv := p.GetView()
offset := 0
if n.X != 0 {
offset = 1
}
pv.X, pv.Y = n.X+offset, n.Y
p.SetView(pv)
p.Resize(n.W-offset, n.H)
}
}
// CurPane returns the currently active pane
func (t *Tab) CurPane() *BufPane {
p, ok := t.Panes[t.active].(*BufPane)
if !ok {
return nil
}
return p
}

View File

@@ -1,42 +0,0 @@
// +build linux darwin dragonfly openbsd_amd64 freebsd
package action
import (
shellquote "github.com/kballard/go-shellquote"
"github.com/zyedidia/micro/v2/internal/shell"
)
// TermEmuSupported is a constant that marks if the terminal emulator is supported
const TermEmuSupported = true
// RunTermEmulator starts a terminal emulator from a bufpane with the given input (command)
// if wait is true it will wait for the user to exit by pressing enter once the executable has terminated
// if getOutput is true it will redirect the stdout of the process to a pipe which will be passed to the
// callback which is a function that takes a string and a list of optional user arguments
func RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool, callback func(out string, userargs []interface{}), userargs []interface{}) error {
args, err := shellquote.Split(input)
if err != nil {
return err
}
if len(args) == 0 {
return nil
}
t := new(shell.Terminal)
t.Start(args, getOutput, wait, callback, userargs)
h.AddTab()
id := MainTab().Panes[0].ID()
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
return err
}
MainTab().Panes[0] = tp
MainTab().SetActive(0)
return nil
}

View File

@@ -1,13 +0,0 @@
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64
package action
import "errors"
// TermEmuSupported is a constant that marks if the terminal emulator is supported
const TermEmuSupported = false
// RunTermEmulator returns an error for unsupported systems (non-unix systems
func RunTermEmulator(input string, wait bool, getOutput bool, callback func(out string, userargs []interface{}), userargs []interface{}) error {
return errors.New("Unsupported operating system")
}

View File

@@ -1,139 +0,0 @@
package action
import (
"errors"
"runtime"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/tcell"
"github.com/zyedidia/terminal"
)
type TermPane struct {
*shell.Terminal
display.Window
mouseReleased bool
id uint64
tab *Tab
}
func NewTermPane(x, y, w, h int, t *shell.Terminal, id uint64, tab *Tab) (*TermPane, error) {
if !TermEmuSupported {
return nil, errors.New("Terminal emulator is not supported on this system")
}
th := new(TermPane)
th.Terminal = t
th.id = id
th.mouseReleased = true
th.Window = display.NewTermWindow(x, y, w, h, t)
th.tab = tab
return th, nil
}
func (t *TermPane) ID() uint64 {
return t.id
}
func (t *TermPane) SetID(i uint64) {
t.id = i
}
func (t *TermPane) SetTab(tab *Tab) {
t.tab = tab
}
func (t *TermPane) Tab() *Tab {
return t.tab
}
func (t *TermPane) Close() {}
func (t *TermPane) Quit() {
t.Close()
if len(MainTab().Panes) > 1 {
t.Unsplit()
} else if len(Tabs.List) > 1 {
Tabs.RemoveTab(t.id)
} else {
screen.Screen.Fini()
InfoBar.Close()
runtime.Goexit()
}
}
func (t *TermPane) Unsplit() {
n := MainTab().GetNode(t.id)
n.Unsplit()
MainTab().RemovePane(MainTab().GetPane(t.id))
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
}
// HandleEvent handles a tcell event by forwarding it to the terminal emulator
// If the event is a mouse event and the program running in the emulator
// does not have mouse support, the emulator will support selections and
// copy-paste
func (t *TermPane) HandleEvent(event tcell.Event) {
if e, ok := event.(*tcell.EventKey); ok {
if t.Status == shell.TTDone {
switch e.Key() {
case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter:
t.Close()
t.Quit()
default:
}
}
if e.Key() == tcell.KeyCtrlC && t.HasSelection() {
clipboard.WriteAll(t.GetSelection(t.GetView().Width), "clipboard")
InfoBar.Message("Copied selection to clipboard")
} else if t.Status != shell.TTDone {
t.WriteString(event.EscSeq())
}
} else if _, ok := event.(*tcell.EventPaste); ok {
if t.Status != shell.TTDone {
t.WriteString(event.EscSeq())
}
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
// t.WriteString(event.EscSeq())
} else if e != nil {
x, y := e.Position()
v := t.GetView()
x -= v.X
y -= v.Y
if e.Buttons() == tcell.Button1 {
if !t.mouseReleased {
// drag
t.Selection[1].X = x
t.Selection[1].Y = y
} else {
t.Selection[0].X = x
t.Selection[0].Y = y
t.Selection[1].X = x
t.Selection[1].Y = y
}
t.mouseReleased = false
} else if e.Buttons() == tcell.ButtonNone {
if !t.mouseReleased {
t.Selection[1].X = x
t.Selection[1].Y = y
}
t.mouseReleased = true
}
}
if t.Status == shell.TTClose {
t.Quit()
}
}
func (t *TermPane) HandleCommand(input string) {
InfoBar.Error("Commands are unsupported in term for now")
}

View File

@@ -1,205 +0,0 @@
package buffer
import (
"bytes"
"io/ioutil"
"os"
"sort"
"strings"
"github.com/zyedidia/micro/v2/internal/util"
)
// A Completer is a function that takes a buffer and returns info
// describing what autocompletions should be inserted at the current
// cursor location
// It returns a list of string suggestions which will be inserted at
// the current cursor location if selected as well as a list of
// suggestion names which can be displayed in an autocomplete box or
// other UI element
type Completer func(*Buffer) ([]string, []string)
func (b *Buffer) GetSuggestions() {
}
// Autocomplete starts the autocomplete process
func (b *Buffer) Autocomplete(c Completer) bool {
b.Completions, b.Suggestions = c(b)
if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 {
return false
}
b.CurSuggestion = -1
b.CycleAutocomplete(true)
return true
}
// CycleAutocomplete moves to the next suggestion
func (b *Buffer) CycleAutocomplete(forward bool) {
prevSuggestion := b.CurSuggestion
if forward {
b.CurSuggestion++
} else {
b.CurSuggestion--
}
if b.CurSuggestion >= len(b.Suggestions) {
b.CurSuggestion = 0
} else if b.CurSuggestion < 0 {
b.CurSuggestion = len(b.Suggestions) - 1
}
c := b.GetActiveCursor()
start := c.Loc
end := c.Loc
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
start = end.Move(-util.CharacterCountInString(b.Completions[prevSuggestion]), b)
} else {
// end = start.Move(1, b)
}
b.Replace(start, end, b.Completions[b.CurSuggestion])
if len(b.Suggestions) > 1 {
b.HasSuggestions = true
}
}
// GetWord gets the most recent word separated by any separator
// (whitespace, punctuation, any non alphanumeric character)
func GetWord(b *Buffer) ([]byte, int) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
if c.X == 0 || util.IsWhitespace(b.RuneAt(c.Loc)) {
return []byte{}, -1
}
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc)) {
return []byte{}, c.X
}
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
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) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
args := bytes.Split(l, []byte{' '})
input := string(args[len(args)-1])
argstart := 0
for i, a := range args {
if i == len(args)-1 {
break
}
argstart += util.CharacterCount(a) + 1
}
return input, argstart
}
// FileComplete autocompletes filenames
func FileComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := GetArg(b)
sep := string(os.PathSeparator)
dirs := strings.Split(input, sep)
var files []os.FileInfo
var err error
if len(dirs) > 1 {
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
directories, _ = util.ReplaceHome(directories)
files, err = ioutil.ReadDir(directories)
} else {
files, err = ioutil.ReadDir(".")
}
if err != nil {
return nil, nil
}
var suggestions []string
for _, f := range files {
name := f.Name()
if f.IsDir() {
name += sep
}
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
suggestions = append(suggestions, name)
}
}
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
var complete string
if len(dirs) > 1 {
complete = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[i]
} else {
complete = suggestions[i]
}
completions[i] = util.SliceEndStr(complete, c.X-argstart)
}
return completions, suggestions
}
// BufferComplete autocompletes based on previous words in the buffer
func BufferComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := GetWord(b)
if argstart == -1 {
return []string{}, []string{}
}
inputLen := util.CharacterCount(input)
suggestionsSet := make(map[string]struct{})
var suggestions []string
for i := c.Y; i >= 0; i-- {
l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w)
if _, ok := suggestionsSet[strw]; !ok {
suggestionsSet[strw] = struct{}{}
suggestions = append(suggestions, strw)
}
}
}
}
for i := c.Y + 1; i < b.LinesNum(); i++ {
l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w)
if _, ok := suggestionsSet[strw]; !ok {
suggestionsSet[strw] = struct{}{}
suggestions = append(suggestions, strw)
}
}
}
}
if len(suggestions) > 1 {
suggestions = append(suggestions, string(input))
}
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}

View File

@@ -1,116 +0,0 @@
package buffer
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
)
const backupMsg = `A backup was detected for this file. This likely means that micro
crashed while editing this file, or another instance of micro is currently
editing this file.
The backup was created on %s, and the file is
%s
* 'recover' will apply the backup as unsaved changes to the current buffer.
When the buffer is closed, the backup will be removed.
* 'ignore' will ignore the backup, discarding its changes. The backup file
will be removed.
Options: [r]ecover, [i]gnore: `
// Backup saves the current buffer to ConfigDir/backups
func (b *Buffer) Backup(checkTime bool) error {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return nil
}
if checkTime {
sub := time.Now().Sub(b.lastbackup)
if sub < time.Duration(backupTime)*time.Millisecond {
return nil
}
}
b.lastbackup = time.Now()
backupdir := filepath.Join(config.ConfigDir, "backups")
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
os.Mkdir(backupdir, os.ModePerm)
}
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath))
err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) {
if len(b.lines) == 0 {
return
}
// end of line
eol := []byte{'\n'}
// write lines
if _, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
}
return
}, false)
return err
}
// RemoveBackup removes any backup file associated with this buffer
func (b *Buffer) RemoveBackup() {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return
}
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
os.Remove(f)
}
// ApplyBackup applies the corresponding backup file to this buffer (if one exists)
// Returns true if a backup was applied
func (b *Buffer) ApplyBackup(fsize int64) bool {
if b.Settings["backup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
if info, err := os.Stat(backupfile); err == nil {
backup, err := os.Open(backupfile)
if err == nil {
defer backup.Close()
t := info.ModTime()
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath))
choice := screen.TermPrompt(msg, []string{"r", "i", "recover", "ignore"}, true)
if choice%2 == 0 {
// recover
b.LineArray = NewLineArray(uint64(fsize), FFAuto, backup)
b.isModified = true
return true
} else if choice%2 == 1 {
// delete
os.Remove(backupfile)
}
}
}
}
return false
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,322 +0,0 @@
package buffer
import (
"math/rand"
"strings"
"testing"
"github.com/stretchr/testify/assert"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/config"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/util"
)
type operation struct {
start Loc
end Loc
text []string
}
func init() {
ulua.L = lua.NewState()
config.InitGlobalSettings()
config.GlobalSettings["backup"] = false
config.GlobalSettings["fastdirty"] = true
}
func check(t *testing.T, before []string, operations []operation, after []string) {
assert := assert.New(t)
b := NewBufferFromString(strings.Join(before, "\n"), "", BTDefault)
assert.NotEqual("", b.GetName())
assert.Equal(false, b.ExternallyModified())
assert.Equal(false, b.Modified())
assert.Equal(1, b.NumCursors())
checkText := func(lines []string) {
assert.Equal([]byte(strings.Join(lines, "\n")), b.Bytes())
assert.Equal(len(lines), b.LinesNum())
for i, s := range lines {
assert.Equal(s, b.Line(i))
assert.Equal([]byte(s), b.LineBytes(i))
}
}
checkText(before)
var cursors []*Cursor
for _, op := range operations {
cursor := NewCursor(b, op.start)
cursor.SetSelectionStart(op.start)
cursor.SetSelectionEnd(op.end)
b.AddCursor(cursor)
cursors = append(cursors, cursor)
}
assert.Equal(1+len(operations), b.NumCursors())
for i, op := range operations {
cursor := cursors[i]
b.SetCurCursor(cursor.Num)
cursor.DeleteSelection()
b.Insert(cursor.Loc, strings.Join(op.text, "\n"))
}
checkText(after)
// must have exactly two events per operation (delete and insert)
for range operations {
b.UndoOneEvent()
b.UndoOneEvent()
}
checkText(before)
for i, op := range operations {
cursor := cursors[i]
if op.start == op.end {
assert.Equal(op.start, cursor.Loc)
} else {
assert.Equal(op.start, cursor.CurSelection[0])
assert.Equal(op.end, cursor.CurSelection[1])
}
}
for range operations {
b.RedoOneEvent()
b.RedoOneEvent()
}
checkText(after)
b.Close()
}
const maxLineLength = 200
var alphabet = []rune(" abcdeäم📚")
func randomString(length int) string {
runes := make([]rune, length)
for i := range runes {
runes[i] = alphabet[rand.Intn(len(alphabet))]
}
return string(runes)
}
func randomText(nLines int) string {
lines := make([]string, nLines)
for i := range lines {
lines[i] = randomString(rand.Intn(maxLineLength + 1))
}
return strings.Join(lines, "\n")
}
func benchCreateAndClose(testingB *testing.B, nLines int) {
rand.Seed(int64(nLines))
text := randomText(nLines)
testingB.ResetTimer()
for i := 0; i < testingB.N; i++ {
b := NewBufferFromString(text, "", BTDefault)
b.Close()
}
}
func benchRead(testingB *testing.B, nLines int) {
rand.Seed(int64(nLines))
b := NewBufferFromString(randomText(nLines), "", BTDefault)
testingB.ResetTimer()
for i := 0; i < testingB.N; i++ {
b.Bytes()
for j := 0; j < b.LinesNum(); j++ {
b.Line(j)
b.LineBytes(j)
}
}
testingB.StopTimer()
b.Close()
}
func benchEdit(testingB *testing.B, nLines, nCursors int) {
rand.Seed(int64(nLines + nCursors))
b := NewBufferFromString(randomText(nLines), "", BTDefault)
regionSize := nLines / nCursors
operations := make([]operation, nCursors)
for i := range operations {
startLine := (i * regionSize) + rand.Intn(regionSize-5)
startColumn := rand.Intn(util.CharacterCountInString(b.Line(startLine)) + 1)
endLine := startLine + 1 + rand.Intn(5)
endColumn := rand.Intn(util.CharacterCountInString(b.Line(endLine)) + 1)
operations[i] = operation{
start: Loc{startColumn, startLine},
end: Loc{endColumn, endLine},
text: []string{randomText(2 + rand.Intn(4))},
}
}
testingB.ResetTimer()
for i := 0; i < testingB.N; i++ {
b.SetCursors([]*Cursor{})
var cursors []*Cursor
for _, op := range operations {
cursor := NewCursor(b, op.start)
cursor.SetSelectionStart(op.start)
cursor.SetSelectionEnd(op.end)
b.AddCursor(cursor)
cursors = append(cursors, cursor)
}
for j, op := range operations {
cursor := cursors[j]
b.SetCurCursor(cursor.Num)
cursor.DeleteSelection()
b.Insert(cursor.Loc, op.text[0])
}
for b.UndoStack.Peek() != nil {
b.UndoOneEvent()
}
}
testingB.StopTimer()
b.Close()
}
func BenchmarkCreateAndClose10Lines(b *testing.B) {
benchCreateAndClose(b, 10)
}
func BenchmarkCreateAndClose100Lines(b *testing.B) {
benchCreateAndClose(b, 100)
}
func BenchmarkCreateAndClose1000Lines(b *testing.B) {
benchCreateAndClose(b, 1000)
}
func BenchmarkCreateAndClose10000Lines(b *testing.B) {
benchCreateAndClose(b, 10000)
}
func BenchmarkCreateAndClose100000Lines(b *testing.B) {
benchCreateAndClose(b, 100000)
}
func BenchmarkCreateAndClose1000000Lines(b *testing.B) {
benchCreateAndClose(b, 1000000)
}
func BenchmarkRead10Lines(b *testing.B) {
benchRead(b, 10)
}
func BenchmarkRead100Lines(b *testing.B) {
benchRead(b, 100)
}
func BenchmarkRead1000Lines(b *testing.B) {
benchRead(b, 1000)
}
func BenchmarkRead10000Lines(b *testing.B) {
benchRead(b, 10000)
}
func BenchmarkRead100000Lines(b *testing.B) {
benchRead(b, 100000)
}
func BenchmarkRead1000000Lines(b *testing.B) {
benchRead(b, 1000000)
}
func BenchmarkEdit10Lines1Cursor(b *testing.B) {
benchEdit(b, 10, 1)
}
func BenchmarkEdit100Lines1Cursor(b *testing.B) {
benchEdit(b, 100, 1)
}
func BenchmarkEdit100Lines10Cursors(b *testing.B) {
benchEdit(b, 100, 10)
}
func BenchmarkEdit1000Lines1Cursor(b *testing.B) {
benchEdit(b, 1000, 1)
}
func BenchmarkEdit1000Lines10Cursors(b *testing.B) {
benchEdit(b, 1000, 10)
}
func BenchmarkEdit1000Lines100Cursors(b *testing.B) {
benchEdit(b, 1000, 100)
}
func BenchmarkEdit10000Lines1Cursor(b *testing.B) {
benchEdit(b, 10000, 1)
}
func BenchmarkEdit10000Lines10Cursors(b *testing.B) {
benchEdit(b, 10000, 10)
}
func BenchmarkEdit10000Lines100Cursors(b *testing.B) {
benchEdit(b, 10000, 100)
}
func BenchmarkEdit10000Lines1000Cursors(b *testing.B) {
benchEdit(b, 10000, 1000)
}
func BenchmarkEdit100000Lines1Cursor(b *testing.B) {
benchEdit(b, 100000, 1)
}
func BenchmarkEdit100000Lines10Cursors(b *testing.B) {
benchEdit(b, 100000, 10)
}
func BenchmarkEdit100000Lines100Cursors(b *testing.B) {
benchEdit(b, 100000, 100)
}
func BenchmarkEdit100000Lines1000Cursors(b *testing.B) {
benchEdit(b, 100000, 1000)
}
func BenchmarkEdit1000000Lines1Cursor(b *testing.B) {
benchEdit(b, 1000000, 1)
}
func BenchmarkEdit1000000Lines10Cursors(b *testing.B) {
benchEdit(b, 1000000, 10)
}
func BenchmarkEdit1000000Lines100Cursors(b *testing.B) {
benchEdit(b, 1000000, 100)
}
func BenchmarkEdit1000000Lines1000Cursors(b *testing.B) {
benchEdit(b, 1000000, 1000)
}

View File

@@ -1,344 +0,0 @@
package buffer
import (
"bytes"
"time"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"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/util"
luar "layeh.com/gopher-luar"
)
const (
// Opposite and undoing events must have opposite values
// TextEventInsert represents an insertion event
TextEventInsert = 1
// TextEventRemove represents a deletion event
TextEventRemove = -1
// TextEventReplace represents a replace event
TextEventReplace = 0
undoThreshold = 1000 // If two events are less than n milliseconds apart, undo both of them
)
// TextEvent holds data for a manipulation on some text that can be undone
type TextEvent struct {
C Cursor
EventType int
Deltas []Delta
Time time.Time
}
// A Delta is a change to the buffer
type Delta struct {
Text []byte
Start Loc
End Loc
}
// DoTextEvent runs a text event
func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
oldl := eh.buf.LinesNum()
if useUndo {
eh.Execute(t)
} else {
ExecuteTextEvent(t, eh.buf)
}
if len(t.Deltas) != 1 {
return
}
text := t.Deltas[0].Text
start := t.Deltas[0].Start
lastnl := -1
var endX int
var textX int
if t.EventType == TextEventInsert {
linecount := eh.buf.LinesNum() - oldl
textcount := util.CharacterCount(text)
lastnl = bytes.LastIndex(text, []byte{'\n'})
if lastnl >= 0 {
endX = util.CharacterCount(text[lastnl+1:])
textX = endX
} else {
endX = start.X + textcount
textX = textcount
}
t.Deltas[0].End = clamp(Loc{endX, start.Y + linecount}, eh.buf.LineArray)
}
end := t.Deltas[0].End
for _, c := range eh.cursors {
move := func(loc Loc) Loc {
if t.EventType == TextEventInsert {
if start.Y != loc.Y && loc.GreaterThan(start) {
loc.Y += end.Y - start.Y
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
loc.Y += end.Y - start.Y
if lastnl >= 0 {
loc.X += textX - start.X
} else {
loc.X += textX
}
}
return loc
} else {
if loc.Y != end.Y && loc.GreaterThan(end) {
loc.Y -= end.Y - start.Y
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
}
return loc
}
}
c.Loc = move(c.Loc)
c.CurSelection[0] = move(c.CurSelection[0])
c.CurSelection[1] = move(c.CurSelection[1])
c.OrigSelection[0] = move(c.OrigSelection[0])
c.OrigSelection[1] = move(c.OrigSelection[1])
c.Relocate()
c.LastVisualX = c.GetVisualX()
}
}
// ExecuteTextEvent runs a text event
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
if t.EventType == TextEventInsert {
for _, d := range t.Deltas {
buf.insert(d.Start, d.Text)
}
} else if t.EventType == TextEventRemove {
for i, d := range t.Deltas {
t.Deltas[i].Text = buf.remove(d.Start, d.End)
}
} else if t.EventType == TextEventReplace {
for i, d := range t.Deltas {
t.Deltas[i].Text = buf.remove(d.Start, d.End)
buf.insert(d.Start, d.Text)
t.Deltas[i].Start = d.Start
t.Deltas[i].End = Loc{d.Start.X + util.CharacterCount(d.Text), d.Start.Y}
}
for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 {
t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i]
}
}
}
// UndoTextEvent undoes a text event
func (eh *EventHandler) UndoTextEvent(t *TextEvent) {
t.EventType = -t.EventType
eh.DoTextEvent(t, false)
}
// EventHandler executes text manipulations and allows undoing and redoing
type EventHandler struct {
buf *SharedBuffer
cursors []*Cursor
active int
UndoStack *TEStack
RedoStack *TEStack
}
// NewEventHandler returns a new EventHandler
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
eh := new(EventHandler)
eh.UndoStack = new(TEStack)
eh.RedoStack = new(TEStack)
eh.buf = buf
eh.cursors = cursors
return eh
}
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
// the buffer equal to that string
// This means that we can transform the buffer into any string and still preserve undo/redo
// through insert and delete events
func (eh *EventHandler) ApplyDiff(new string) {
differ := dmp.New()
diff := differ.DiffMain(string(eh.buf.Bytes()), new, false)
loc := eh.buf.Start()
for _, d := range diff {
if d.Type == dmp.DiffDelete {
eh.Remove(loc, loc.MoveLA(util.CharacterCountInString(d.Text), eh.buf.LineArray))
} else {
if d.Type == dmp.DiffInsert {
eh.Insert(loc, d.Text)
}
loc = loc.MoveLA(util.CharacterCountInString(d.Text), eh.buf.LineArray)
}
}
}
// Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start Loc, textStr string) {
text := []byte(textStr)
eh.InsertBytes(start, text)
}
// InsertBytes creates an insert text event and executes it
func (eh *EventHandler) InsertBytes(start Loc, text []byte) {
if len(text) == 0 {
return
}
start = clamp(start, eh.buf.LineArray)
e := &TextEvent{
C: *eh.cursors[eh.active],
EventType: TextEventInsert,
Deltas: []Delta{{text, start, Loc{0, 0}}},
Time: time.Now(),
}
eh.DoTextEvent(e, true)
}
// Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end Loc) {
if start == end {
return
}
start = clamp(start, eh.buf.LineArray)
end = clamp(end, eh.buf.LineArray)
e := &TextEvent{
C: *eh.cursors[eh.active],
EventType: TextEventRemove,
Deltas: []Delta{{[]byte{}, start, end}},
Time: time.Now(),
}
eh.DoTextEvent(e, true)
}
// MultipleReplace creates an multiple insertions executes them
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
e := &TextEvent{
C: *eh.cursors[eh.active],
EventType: TextEventReplace,
Deltas: deltas,
Time: time.Now(),
}
eh.Execute(e)
}
// Replace deletes from start to end and replaces it with the given string
func (eh *EventHandler) Replace(start, end Loc, replace string) {
eh.Remove(start, end)
eh.Insert(start, replace)
}
// Execute a textevent and add it to the undo stack
func (eh *EventHandler) Execute(t *TextEvent) {
if eh.RedoStack.Len() > 0 {
eh.RedoStack = new(TEStack)
}
eh.UndoStack.Push(t)
b, err := config.RunPluginFnBool("onBeforeTextEvent", luar.New(ulua.L, eh.buf), luar.New(ulua.L, t))
if err != nil {
screen.TermMessage(err)
}
if !b {
return
}
ExecuteTextEvent(t, eh.buf)
}
// Undo the first event in the undo stack
func (eh *EventHandler) Undo() {
t := eh.UndoStack.Peek()
if t == nil {
return
}
startTime := t.Time.UnixNano() / int64(time.Millisecond)
endTime := startTime - (startTime % undoThreshold)
for {
t = eh.UndoStack.Peek()
if t == nil {
return
}
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
return
}
eh.UndoOneEvent()
}
}
// UndoOneEvent undoes one event
func (eh *EventHandler) UndoOneEvent() {
// This event should be undone
// Pop it off the stack
t := eh.UndoStack.Pop()
if t == nil {
return
}
// Undo it
// Modifies the text event
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
}
// Push it to the redo stack
eh.RedoStack.Push(t)
}
// Redo the first event in the redo stack
func (eh *EventHandler) Redo() {
t := eh.RedoStack.Peek()
if t == nil {
return
}
startTime := t.Time.UnixNano() / int64(time.Millisecond)
endTime := startTime - (startTime % undoThreshold) + undoThreshold
for {
t = eh.RedoStack.Peek()
if t == nil {
return
}
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
return
}
eh.RedoOneEvent()
}
}
// RedoOneEvent redoes one event
func (eh *EventHandler) RedoOneEvent() {
t := eh.RedoStack.Pop()
if t == nil {
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
}
// Modifies the text event
eh.UndoTextEvent(t)
eh.UndoStack.Push(t)
}

View File

@@ -1,351 +0,0 @@
package buffer
import (
"bufio"
"bytes"
"io"
"sync"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight"
)
// Finds the byte index of the nth rune in a byte slice
func runeToByteIndex(n int, txt []byte) int {
if n == 0 {
return 0
}
count := 0
i := 0
for len(txt) > 0 {
_, _, size := util.DecodeCharacter(txt)
txt = txt[size:]
count += size
i++
if i == n {
break
}
}
return count
}
// A Line contains the data in bytes as well as a highlight state, match
// and a flag for whether the highlighting needs to be updated
type Line struct {
data []byte
state highlight.State
match highlight.LineMatch
rehighlight bool
lock sync.Mutex
}
const (
// Line ending file formats
FFAuto = 0 // Autodetect format
FFUnix = 1 // LF line endings (unix style '\n')
FFDos = 2 // CRLF line endings (dos style '\r\n')
)
type FileFormat byte
// A LineArray simply stores and array of lines and makes it easy to insert
// and delete in it
type LineArray struct {
lines []Line
Endings FileFormat
initsize uint64
}
// Append efficiently appends lines together
// It allocates an additional 10000 lines if the original estimate
// is incorrect
func Append(slice []Line, data ...Line) []Line {
l := len(slice)
if l+len(data) > cap(slice) { // reallocate
newSlice := make([]Line, (l+len(data))+10000)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
// NewLineArray returns a new line array from an array of bytes
func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray {
la := new(LineArray)
la.lines = make([]Line, 0, 1000)
la.initsize = size
br := bufio.NewReader(reader)
var loaded int
n := 0
for {
data, err := br.ReadBytes('\n')
// Detect the line ending by checking to see if there is a '\r' char
// before the '\n'
// Even if the file format is set to DOS, the '\r' is removed so
// that all lines end with '\n'
dlen := len(data)
if dlen > 1 && data[dlen-2] == '\r' {
data = append(data[:dlen-2], '\n')
if endings == FFAuto {
la.Endings = FFDos
}
dlen = len(data)
} else if dlen > 0 {
if endings == FFAuto {
la.Endings = FFUnix
}
}
// If we are loading a large file (greater than 1000) we use the file
// size and the length of the first 1000 lines to try to estimate
// how many lines will need to be allocated for the rest of the file
// We add an extra 10000 to the original estimate to be safe and give
// plenty of room for expansion
if n >= 1000 && loaded >= 0 {
totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
copy(newSlice, la.lines)
la.lines = newSlice
loaded = -1
}
// Counter for the number of bytes in the first 1000 lines
if loaded >= 0 {
loaded += dlen
}
if err != nil {
if err == io.EOF {
la.lines = Append(la.lines, Line{
data: data[:],
state: nil,
match: nil,
rehighlight: false,
})
}
// Last line was read
break
} else {
la.lines = Append(la.lines, Line{
data: data[:dlen-1],
state: nil,
match: nil,
rehighlight: false,
})
}
n++
}
return la
}
// Bytes returns the string that should be written to disk when
// the line array is saved
func (la *LineArray) Bytes() []byte {
b := new(bytes.Buffer)
// initsize should provide a good estimate
b.Grow(int(la.initsize + 4096))
for i, l := range la.lines {
b.Write(l.data)
if i != len(la.lines)-1 {
if la.Endings == FFDos {
b.WriteByte('\r')
}
b.WriteByte('\n')
}
}
return b.Bytes()
}
// newlineBelow adds a newline below the given line number
func (la *LineArray) newlineBelow(y int) {
la.lines = append(la.lines, Line{
data: []byte{' '},
state: nil,
match: nil,
rehighlight: false,
})
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,
}
}
// Inserts a byte array at a given location
func (la *LineArray) insert(pos Loc, value []byte) {
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
for i := 0; i < len(value); i++ {
if value[i] == '\n' {
la.split(Loc{x, y})
x = 0
y++
continue
}
la.insertByte(Loc{x, y}, value[i])
x++
}
}
// InsertByte inserts a byte at a given location
func (la *LineArray) insertByte(pos Loc, value byte) {
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
la.lines[pos.Y].data[pos.X] = value
}
// joinLines joins the two lines a and b
func (la *LineArray) joinLines(a, b int) {
la.insert(Loc{len(la.lines[a].data), a}, 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].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 {
sub := la.Substr(start, end)
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
} else {
la.deleteLines(start.Y+1, end.Y-1)
la.deleteToEnd(Loc{startX, start.Y})
la.deleteFromStart(Loc{endX - 1, start.Y + 1})
la.joinLines(start.Y, start.Y+1)
}
return sub
}
// deleteToEnd deletes from the end of a line to the position
func (la *LineArray) deleteToEnd(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
}
// deleteFromStart deletes from the start of a line to the position
func (la *LineArray) deleteFromStart(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
}
// deleteLine deletes the line number
func (la *LineArray) deleteLine(y int) {
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
}
func (la *LineArray) deleteLines(y1, y2 int) {
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
}
// DeleteByte deletes the byte at a position
func (la *LineArray) deleteByte(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
}
// Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) []byte {
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
src := la.lines[start.Y].data[startX:endX]
dest := make([]byte, len(src))
copy(dest, src)
return dest
}
str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y))
str = append(str, la.lines[start.Y].data[startX:]...)
str = append(str, '\n')
for i := start.Y + 1; i <= end.Y-1; i++ {
str = append(str, la.lines[i].data...)
str = append(str, '\n')
}
str = append(str, la.lines[end.Y].data[:endX]...)
return str
}
// LinesNum returns the number of lines in the buffer
func (la *LineArray) LinesNum() int {
return len(la.lines)
}
// Start returns the start of the buffer
func (la *LineArray) Start() Loc {
return Loc{0, 0}
}
// End returns the location of the last character in the buffer
func (la *LineArray) End() Loc {
numlines := len(la.lines)
return Loc{util.CharacterCount(la.lines[numlines-1].data), numlines - 1}
}
// LineBytes returns line n as an array of bytes
func (la *LineArray) LineBytes(n int) []byte {
if n >= len(la.lines) || n < 0 {
return []byte{}
}
return la.lines[n].data
}
// State gets the highlight state for the given line number
func (la *LineArray) State(lineN int) highlight.State {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
return la.lines[lineN].state
}
// SetState sets the highlight state at the given line number
func (la *LineArray) SetState(lineN int, s highlight.State) {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
la.lines[lineN].state = s
}
// SetMatch sets the match at the given line number
func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
la.lines[lineN].match = m
}
// Match retrieves the match for the given line number
func (la *LineArray) Match(lineN int) highlight.LineMatch {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
return la.lines[lineN].match
}
func (la *LineArray) Rehighlight(lineN int) bool {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
return la.lines[lineN].rehighlight
}
func (la *LineArray) SetRehighlight(lineN int, on bool) {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
la.lines[lineN].rehighlight = on
}

View File

@@ -1,61 +0,0 @@
package buffer
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
var unicode_txt = `An preost wes on leoden, Laȝamon was ihoten
He wes Leovenaðes sone -- liðe him be Drihten.
He wonede at Ernleȝe at æðelen are chirechen,
Uppen Sevarne staþe, sel þar him þuhte,
Onfest Radestone, þer he bock radde.`
var la *LineArray
func init() {
reader := strings.NewReader(unicode_txt)
la = NewLineArray(uint64(len(unicode_txt)), FFAuto, reader)
}
func TestSplit(t *testing.T) {
la.insert(Loc{17, 1}, []byte{'\n'})
assert.Equal(t, len(la.lines), 6)
sub1 := la.Substr(Loc{0, 1}, Loc{17, 1})
sub2 := la.Substr(Loc{0, 2}, Loc{30, 2})
assert.Equal(t, []byte("He wes Leovenaðes"), sub1)
assert.Equal(t, []byte(" sone -- liðe him be Drihten."), sub2)
}
func TestJoin(t *testing.T) {
la.remove(Loc{47, 1}, Loc{0, 2})
assert.Equal(t, len(la.lines), 5)
sub := la.Substr(Loc{0, 1}, Loc{47, 1})
bytes := la.Bytes()
assert.Equal(t, []byte("He wes Leovenaðes sone -- liðe him be Drihten."), sub)
assert.Equal(t, unicode_txt, string(bytes))
}
func TestInsert(t *testing.T) {
la.insert(Loc{20, 3}, []byte(" foobar"))
sub1 := la.Substr(Loc{0, 3}, Loc{50, 3})
assert.Equal(t, []byte("Uppen Sevarne staþe, foobar sel þar him þuhte,"), sub1)
la.insert(Loc{25, 2}, []byte("H̼̥̯͇͙̕͘͞e̸̦̞̠̣̰͙̼̥̦̼̖̬͕͕̰̯̫͇̕ĺ̜̠̩̯̯͙̼̭̠͕̮̞͜l̶͓̫̞̮͈͞ͅo̸͔͙̳̠͈̮̼̳͙̥̲͜͠"))
sub2 := la.Substr(Loc{0, 2}, Loc{60, 2})
assert.Equal(t, []byte("He wonede at Ernleȝe at æH̼̥̯͇͙̕͘͞e̸̦̞̠̣̰͙̼̥̦̼̖̬͕͕̰̯̫͇̕ĺ̜̠̩̯̯͙̼̭̠͕̮̞͜l̶͓̫̞̮͈͞ͅo̸͔͙̳̠͈̮̼̳͙̥̲͜͠ðelen are chirechen,"), sub2)
}
func TestRemove(t *testing.T) {
la.remove(Loc{20, 3}, Loc{27, 3})
la.remove(Loc{25, 2}, Loc{30, 2})
bytes := la.Bytes()
assert.Equal(t, unicode_txt, string(bytes))
}

View File

@@ -1,84 +0,0 @@
package buffer
import (
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/tcell"
)
type MsgType int
const (
MTInfo = iota
MTWarning
MTError
)
// Message represents the information for a gutter message
type Message struct {
// The Msg iteslf
Msg string
// Start and End locations for the message
Start, End Loc
// The Kind stores the message type
Kind MsgType
// The Owner of the message
Owner string
}
// NewMessage creates a new gutter message
func NewMessage(owner string, msg string, start, end Loc, kind MsgType) *Message {
return &Message{
Msg: msg,
Start: start,
End: end,
Kind: kind,
Owner: owner,
}
}
// NewMessageAtLine creates a new gutter message at a given line
func NewMessageAtLine(owner string, msg string, line int, kind MsgType) *Message {
start := Loc{-1, line - 1}
end := start
return NewMessage(owner, msg, start, end, kind)
}
func (m *Message) Style() tcell.Style {
switch m.Kind {
case MTInfo:
if style, ok := config.Colorscheme["gutter-info"]; ok {
return style
}
case MTWarning:
if style, ok := config.Colorscheme["gutter-warning"]; ok {
return style
}
case MTError:
if style, ok := config.Colorscheme["gutter-error"]; ok {
return style
}
}
return config.DefStyle
}
func (b *Buffer) AddMessage(m *Message) {
b.Messages = append(b.Messages, m)
}
func (b *Buffer) removeMsg(i int) {
copy(b.Messages[i:], b.Messages[i+1:])
b.Messages[len(b.Messages)-1] = nil
b.Messages = b.Messages[:len(b.Messages)-1]
}
func (b *Buffer) ClearMessages(owner string) {
for i := len(b.Messages) - 1; i >= 0; i-- {
if b.Messages[i].Owner == owner {
b.removeMsg(i)
}
}
}
func (b *Buffer) ClearAllMessages() {
b.Messages = make([]*Message, 0)
}

View File

@@ -1,199 +0,0 @@
package buffer
import (
"bufio"
"bytes"
"errors"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"unicode"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/transform"
)
// LargeFileThreshold is the number of bytes when fastdirty is forced
// because hashing is too slow
const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
// the supplied function with the file as io.Writer object, also making sure the file is
// closed afterwards.
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) {
var writeCloser io.WriteCloser
if withSudo {
cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
if writeCloser, err = cmd.StdinPipe(); err != nil {
return
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
cmd.Process.Kill()
}()
defer func() {
screenb := screen.TempFini()
if e := cmd.Run(); e != nil && err == nil {
err = e
}
screen.TempStart(screenb)
}()
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
return
}
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
err = fn(w)
w.Flush()
if e := writeCloser.Close(); e != nil && err == nil {
err = e
}
return
}
// Save saves the buffer to its default path
func (b *Buffer) Save() error {
return b.SaveAs(b.Path)
}
// 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)
}
func (b *Buffer) SaveWithSudo() error {
return b.SaveAsWithSudo(b.Path)
}
func (b *Buffer) SaveAsWithSudo(filename string) error {
return b.saveToFile(filename, true)
}
func (b *Buffer) saveToFile(filename string, withSudo bool) error {
var err error
if b.Type.Readonly {
return errors.New("Cannot save readonly buffer")
}
if b.Type.Scratch {
return errors.New("Cannot save scratch buffer")
}
if withSudo && runtime.GOOS == "windows" {
return errors.New("Save with sudo not supported on Windows")
}
b.UpdateRules()
if b.Settings["rmtrailingws"].(bool) {
for i, l := range b.lines {
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
linelen := util.CharacterCount(l.data)
b.Remove(Loc{leftover, i}, Loc{linelen, i})
}
b.RelocateCursors()
}
if b.Settings["eofnewline"].(bool) {
end := b.End()
if b.RuneAt(Loc{end.X, end.Y}) != '\n' {
b.insert(end, []byte{'\n'})
}
}
// Update the last time this file was updated after saving
defer func() {
b.ModTime, _ = util.GetModTime(filename)
err = b.Serialize()
}()
// Removes any tilde and replaces with the absolute path to home
absFilename, _ := util.ReplaceHome(filename)
// Get the leading path to the file | "." is returned if there's no leading path provided
if dirname := filepath.Dir(absFilename); dirname != "." {
// Check if the parent dirs don't exist
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
// Prompt to make sure they want to create the dirs that are missing
if b.Settings["mkparents"].(bool) {
// Create all leading dir(s) since they don't exist
if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
// If there was an error creating the dirs
return mkdirallErr
}
} else {
return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation")
}
}
}
var fileSize int
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
return err
}
fwriter := func(file io.Writer) (e error) {
if len(b.lines) == 0 {
return
}
// end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
eol = []byte{'\n'}
}
// write lines
if fileSize, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
fileSize += len(eol) + len(l.data)
}
return
}
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
return err
}
if !b.Settings["fastdirty"].(bool) {
if fileSize > LargeFileThreshold {
// For large files 'fastdirty' needs to be on
b.Settings["fastdirty"] = true
} else {
calcHash(b, &b.origHash)
}
}
b.Path = filename
absPath, _ := filepath.Abs(filename)
b.AbsPath = absPath
b.isModified = false
return err
}

View File

@@ -1,177 +0,0 @@
package buffer
import (
"regexp"
"github.com/zyedidia/micro/v2/internal/util"
)
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
if start.GreaterThan(end) {
start, end = end, start
}
for i := start.Y; i <= end.Y; i++ {
l := b.LineBytes(i)
charpos := 0
if i == start.Y && start.Y == end.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
}
match := r.FindIndex(l)
if match != nil {
start := Loc{charpos + util.RunePos(l, match[0]), i}
end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true
}
}
return [2]Loc{}, false
}
func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
if start.GreaterThan(end) {
start, end = end, start
}
for i := end.Y; i >= start.Y; i-- {
l := b.LineBytes(i)
charpos := 0
if i == start.Y && start.Y == end.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
}
match := r.FindIndex(l)
if match != nil {
start := Loc{charpos + util.RunePos(l, match[0]), i}
end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true
}
}
return [2]Loc{}, false
}
// FindNext finds the next occurrence of a given string in the buffer
// It returns the start and end location of the match (if found) and
// a boolean indicating if it was found
// May also return an error if the search regex is invalid
func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bool) ([2]Loc, bool, error) {
if s == "" {
return [2]Loc{}, false, nil
}
var r *regexp.Regexp
var err error
if !useRegex {
s = regexp.QuoteMeta(s)
}
if b.Settings["ignorecase"].(bool) {
r, err = regexp.Compile("(?i)" + s)
} else {
r, err = regexp.Compile(s)
}
if err != nil {
return [2]Loc{}, false, err
}
var found bool
var l [2]Loc
if down {
l, found = b.findDown(r, from, end)
if !found {
l, found = b.findDown(r, start, end)
}
} else {
l, found = b.findUp(r, from, start)
if !found {
l, found = b.findUp(r, end, start)
}
}
return l, found, nil
}
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
// and returns the number of replacements made and the number of runes
// added or removed
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) {
if start.GreaterThan(end) {
start, end = end, start
}
netrunes := 0
found := 0
var deltas []Delta
for i := start.Y; i <= end.Y; i++ {
l := b.lines[i].data
charpos := 0
if start.Y == end.Y && i == start.Y {
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
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)
}
found++
netrunes += util.CharacterCount(in) - util.CharacterCount(result)
return result
})
from := Loc{charpos, i}
to := Loc{charpos + util.CharacterCount(l), i}
deltas = append(deltas, Delta{newText, from, to})
}
b.MultipleReplace(deltas)
return found, netrunes
}

View File

@@ -1,76 +0,0 @@
package buffer
import (
"encoding/gob"
"errors"
"io"
"os"
"path/filepath"
"time"
"golang.org/x/text/encoding"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
)
// The SerializedBuffer holds the types that get serialized when a buffer is saved
// These are used for the savecursor and saveundo options
type SerializedBuffer struct {
EventHandler *EventHandler
Cursor Loc
ModTime time.Time
}
// Serialize serializes the buffer to config.ConfigDir/buffers
func (b *Buffer) Serialize() error {
if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
return nil
}
if b.Path == "" {
return nil
}
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))
return overwriteFile(name, encoding.Nop, func(file io.Writer) error {
err := gob.NewEncoder(file).Encode(SerializedBuffer{
b.EventHandler,
b.GetActiveCursor().Loc,
b.ModTime,
})
return err
}, false)
}
// Unserialize loads the buffer info from config.ConfigDir/buffers
func (b *Buffer) Unserialize() error {
// If either savecursor or saveundo is turned on, we need to load the serialized information
// from ~/.config/micro/buffers
if b.Path == "" {
return nil
}
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)))
defer file.Close()
if err == nil {
var buffer SerializedBuffer
decoder := gob.NewDecoder(file)
err = decoder.Decode(&buffer)
if err != nil {
return errors.New(err.Error() + "\nYou may want to remove the files in ~/.config/micro/buffers (these files\nstore the information for the 'saveundo' and 'savecursor' options) if\nthis problem persists.\nThis may be caused by upgrading to version 2.0, and removing the 'buffers'\ndirectory will reset the cursor and undo history and solve the problem.")
}
if b.Settings["savecursor"].(bool) {
b.StartCursor = buffer.Cursor
}
if b.Settings["saveundo"].(bool) {
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
if b.ModTime == buffer.ModTime {
b.EventHandler = buffer.EventHandler
b.EventHandler.cursors = b.cursors
b.EventHandler.buf = b.SharedBuffer
}
}
}
return nil
}

View File

@@ -1,57 +0,0 @@
package buffer
import (
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
)
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
b.Settings[option] = nativeValue
if option == "fastdirty" {
if !nativeValue.(bool) {
e := calcHash(b, &b.origHash)
if e == ErrFileTooLarge {
b.Settings["fastdirty"] = false
}
}
} else if option == "statusline" {
screen.Redraw()
} else if option == "filetype" {
b.UpdateRules()
} else if option == "fileformat" {
switch b.Settings["fileformat"].(string) {
case "unix":
b.Endings = FFUnix
case "dos":
b.Endings = FFDos
}
b.isModified = true
} else if option == "syntax" {
if !nativeValue.(bool) {
b.ClearMatches()
} else {
b.UpdateRules()
}
} else if option == "encoding" {
b.isModified = true
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool)
}
return nil
}
// SetOption sets a given option to a value just for this buffer
func (b *Buffer) SetOption(option, value string) error {
if _, ok := b.Settings[option]; !ok {
return config.ErrInvalidOption
}
nativeValue, err := config.GetNativeValue(option, b.Settings[option], value)
if err != nil {
return err
}
return b.SetOptionNative(option, nativeValue)
}

View File

@@ -1,35 +0,0 @@
package buffer
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestStack(t *testing.T) {
s := new(TEStack)
e1 := &TextEvent{
EventType: TextEventReplace,
Time: time.Now(),
}
e2 := &TextEvent{
EventType: TextEventInsert,
Time: time.Now(),
}
s.Push(e1)
s.Push(e2)
p := s.Peek()
assert.Equal(t, p.EventType, TextEventInsert)
p = s.Pop()
assert.Equal(t, p.EventType, TextEventInsert)
p = s.Peek()
assert.Equal(t, p.EventType, TextEventReplace)
p = s.Pop()
assert.Equal(t, p.EventType, TextEventReplace)
p = s.Pop()
assert.Nil(t, p)
p = s.Peek()
assert.Nil(t, p)
}

View File

@@ -1,45 +0,0 @@
package config
import (
"sync"
"time"
)
var Autosave chan bool
var autotime int
// lock for autosave
var autolock sync.Mutex
func init() {
Autosave = make(chan bool)
}
func SetAutoTime(a int) {
autolock.Lock()
autotime = a
autolock.Unlock()
}
func GetAutoTime() int {
autolock.Lock()
a := autotime
autolock.Unlock()
return a
}
func StartAutoSave() {
go func() {
for {
if autotime < 1 {
break
}
time.Sleep(time.Duration(autotime) * time.Second)
// it's possible autotime was changed while sleeping
if autotime < 1 {
break
}
Autosave <- true
}
}()
}

View File

@@ -1,62 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zyedidia/tcell"
)
func TestSimpleStringToStyle(t *testing.T) {
s := StringToStyle("lightblue,magenta")
fg, bg, _ := s.Decompose()
assert.Equal(t, tcell.ColorBlue, fg)
assert.Equal(t, tcell.ColorPurple, bg)
}
func TestAttributeStringToStyle(t *testing.T) {
s := StringToStyle("bold cyan,brightcyan")
fg, bg, attr := s.Decompose()
assert.Equal(t, tcell.ColorTeal, fg)
assert.Equal(t, tcell.ColorAqua, bg)
assert.NotEqual(t, 0, attr&tcell.AttrBold)
}
func TestColor256StringToStyle(t *testing.T) {
s := StringToStyle("128,60")
fg, bg, _ := s.Decompose()
assert.Equal(t, tcell.Color128, fg)
assert.Equal(t, tcell.Color60, bg)
}
func TestColorHexStringToStyle(t *testing.T) {
s := StringToStyle("#deadbe,#ef1234")
fg, bg, _ := s.Decompose()
assert.Equal(t, tcell.NewRGBColor(222, 173, 190), fg)
assert.Equal(t, tcell.NewRGBColor(239, 18, 52), bg)
}
func TestColorschemeParser(t *testing.T) {
testColorscheme := `color-link default "#F8F8F2,#282828"
color-link comment "#75715E,#282828"
# comment
color-link identifier "#66D9EF,#282828" #comment
color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828"
color-link constant.string.char "#BDE6AD,#282828"`
c, err := ParseColorscheme(testColorscheme)
assert.Nil(t, err)
fg, bg, _ := c["comment"].Decompose()
assert.Equal(t, tcell.NewRGBColor(117, 113, 94), fg)
assert.Equal(t, tcell.NewRGBColor(40, 40, 40), bg)
}

View File

@@ -1,52 +0,0 @@
package config
import (
"errors"
"os"
"path/filepath"
homedir "github.com/mitchellh/go-homedir"
)
var ConfigDir string
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
// If no directory is found, it creates one.
func InitConfigDir(flagConfigDir string) error {
var e error
microHome := os.Getenv("MICRO_CONFIG_HOME")
if microHome == "" {
// The user has not set $MICRO_CONFIG_HOME so we'll try $XDG_CONFIG_HOME
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome == "" {
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
home, err := homedir.Dir()
if err != nil {
return errors.New("Error finding your home directory\nCan't load config files: " + err.Error())
}
xdgHome = filepath.Join(home, ".config")
}
microHome = filepath.Join(xdgHome, "micro")
}
ConfigDir = microHome
if len(flagConfigDir) > 0 {
if _, err := os.Stat(flagConfigDir); os.IsNotExist(err) {
e = errors.New("Error: " + flagConfigDir + " does not exist. Defaulting to " + ConfigDir + ".")
} else {
ConfigDir = flagConfigDir
return nil
}
}
// Create micro config home directory if it does not exist
// This creates parent directories and does nothing if it already exists
err := os.MkdirAll(ConfigDir, os.ModePerm)
if err != nil {
return errors.New("Error creating configuration directory: " + err.Error())
}
return e
}

View File

@@ -1,7 +0,0 @@
package config
const (
DoubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
)
var Bindings map[string]string

View File

@@ -1,157 +0,0 @@
package config
import (
"errors"
"log"
lua "github.com/yuin/gopher-lua"
ulua "github.com/zyedidia/micro/v2/internal/lua"
)
// ErrNoSuchFunction is returned when Call is executed on a function that does not exist
var ErrNoSuchFunction = errors.New("No such function exists")
// LoadAllPlugins loads all detected plugins (in runtime/plugins and ConfigDir/plugins)
func LoadAllPlugins() error {
var reterr error
for _, p := range Plugins {
err := p.Load()
if err != nil {
reterr = err
}
}
return reterr
}
// RunPluginFn runs a given function in all plugins
// returns an error if any of the plugins had an error
func RunPluginFn(fn string, args ...lua.LValue) error {
var reterr error
for _, p := range Plugins {
if !p.IsEnabled() {
continue
}
_, err := p.Call(fn, args...)
if err != nil && err != ErrNoSuchFunction {
reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
}
}
return reterr
}
// RunPluginFnBool runs a function in all plugins and returns
// false if any one of them returned false
// also returns an error if any of the plugins had an error
func RunPluginFnBool(fn string, args ...lua.LValue) (bool, error) {
var reterr error
retbool := true
for _, p := range Plugins {
if !p.IsEnabled() {
continue
}
val, err := p.Call(fn, args...)
if err == ErrNoSuchFunction {
continue
}
if err != nil {
reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
continue
}
if v, ok := val.(lua.LBool); ok {
retbool = retbool && bool(v)
}
}
return retbool, reterr
}
// Plugin stores information about the source files/info for a plugin
type Plugin struct {
DirName string // name of plugin folder
Name string // name of plugin
Info *PluginInfo // json file containing info
Srcs []RuntimeFile // lua files
Loaded bool
Default bool // pre-installed plugin
}
// IsEnabled returns if a plugin is enabled
func (p *Plugin) IsEnabled() bool {
if v, ok := GlobalSettings[p.Name]; ok {
return v.(bool) && p.Loaded
}
return true
}
// Plugins is a list of all detected plugins (enabled or disabled)
var Plugins []*Plugin
// Load creates an option for the plugin and runs all source files
func (p *Plugin) Load() error {
if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) {
return nil
}
for _, f := range p.Srcs {
dat, err := f.Data()
if err != nil {
return err
}
err = ulua.LoadFile(p.Name, f.Name(), dat)
if err != nil {
return err
}
}
p.Loaded = true
RegisterGlobalOption(p.Name, true)
return nil
}
// Call calls a given function in this plugin
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
plug := ulua.L.GetGlobal(p.Name)
if plug == lua.LNil {
log.Println("Plugin does not exist:", p.Name, "at", p.DirName, ":", p)
return nil, nil
}
luafn := ulua.L.GetField(plug, fn)
if luafn == lua.LNil {
return nil, ErrNoSuchFunction
}
err := ulua.L.CallByParam(lua.P{
Fn: luafn,
NRet: 1,
Protect: true,
}, args...)
if err != nil {
return nil, err
}
ret := ulua.L.Get(-1)
ulua.L.Pop(1)
return ret, nil
}
// FindPlugin returns the plugin with the given name
func FindPlugin(name string) *Plugin {
var pl *Plugin
for _, p := range Plugins {
if !p.IsEnabled() {
continue
}
if p.Name == name {
pl = p
break
}
}
return pl
}
// FindAnyPlugin does not require the plugin to be enabled
func FindAnyPlugin(name string) *Plugin {
var pl *Plugin
for _, p := range Plugins {
if p.Name == name {
pl = p
break
}
}
return pl
}

View File

@@ -1,46 +0,0 @@
package config
import (
"bytes"
"encoding/json"
"errors"
)
var (
ErrMissingName = errors.New("Missing or empty name field")
ErrMissingDesc = errors.New("Missing or empty description field")
ErrMissingSite = errors.New("Missing or empty website field")
)
// PluginInfo contains all the needed info about a plugin
// The info is just strings and are not used beyond that (except
// the Site and Install fields should be valid URLs). This means
// that the requirements for example can be formatted however the
// plugin maker decides, the fields will only be parsed by humans
// Name: name of plugin
// Desc: description of plugin
// Site: home website of plugin
// Install: install link for plugin (can be link to repo or zip file)
// Vstr: version
// Require: list of dependencies and requirements
type PluginInfo struct {
Name string `json:"Name"`
Desc string `json:"Description"`
Site string `json:"Website"`
}
// NewPluginInfo parses a JSON input into a valid PluginInfo struct
// Returns an error if there are any missing fields or any invalid fields
// There are no optional fields in a plugin info json file
func NewPluginInfo(data []byte) (*PluginInfo, error) {
var info []PluginInfo
dec := json.NewDecoder(bytes.NewReader(data))
// dec.DisallowUnknownFields() // Force errors
if err := dec.Decode(&info); err != nil {
return nil, err
}
return &info[0], nil
}

View File

@@ -1,306 +0,0 @@
package config
import (
"errors"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
const (
RTColorscheme = 0
RTSyntax = 1
RTHelp = 2
RTPlugin = 3
RTSyntaxHeader = 4
)
var (
NumTypes = 5 // How many filetypes are there
)
type RTFiletype int
// RuntimeFile allows the program to read runtime data like colorschemes or syntax files
type RuntimeFile interface {
// Name returns a name of the file without paths or extensions
Name() string
// Data returns the content of the file.
Data() ([]byte, error)
}
// allFiles contains all available files, mapped by filetype
var allFiles [][]RuntimeFile
var realFiles [][]RuntimeFile
func init() {
allFiles = make([][]RuntimeFile, NumTypes)
realFiles = make([][]RuntimeFile, NumTypes)
}
// NewRTFiletype creates a new RTFiletype
func NewRTFiletype() int {
NumTypes++
allFiles = append(allFiles, []RuntimeFile{})
realFiles = append(realFiles, []RuntimeFile{})
return NumTypes - 1
}
// some file on filesystem
type realFile string
// some asset file
type assetFile string
// some file on filesystem but with a different name
type namedFile struct {
realFile
name string
}
// a file with the data stored in memory
type memoryFile struct {
name string
data []byte
}
func (mf memoryFile) Name() string {
return mf.name
}
func (mf memoryFile) Data() ([]byte, error) {
return mf.data, nil
}
func (rf realFile) Name() string {
fn := filepath.Base(string(rf))
return fn[:len(fn)-len(filepath.Ext(fn))]
}
func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf))
}
func (af assetFile) Name() string {
fn := path.Base(string(af))
return fn[:len(fn)-len(path.Ext(fn))]
}
func (af assetFile) Data() ([]byte, error) {
return Asset(string(af))
}
func (nf namedFile) Name() string {
return nf.name
}
// AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file)
}
// AddRealRuntimeFile registers a file for the given filetype
func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file)
realFiles[fileType] = append(realFiles[fileType], file)
}
// AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
files, _ := ioutil.ReadDir(directory)
for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name())
AddRealRuntimeFile(fileType, realFile(fullPath))
}
}
}
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
files, err := AssetDir(directory)
if err != nil {
return
}
for _, f := range files {
if ok, _ := path.Match(pattern, f); ok {
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
}
}
}
// FindRuntimeFile finds a runtime file of the given filetype and name
// will return nil if no file was found
func FindRuntimeFile(fileType RTFiletype, name string) RuntimeFile {
for _, f := range ListRuntimeFiles(fileType) {
if f.Name() == name {
return f
}
}
return nil
}
// ListRuntimeFiles lists all known runtime files for the given filetype
func ListRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return allFiles[fileType]
}
// ListRealRuntimeFiles lists all real runtime files (on disk) for a filetype
// these runtime files will be ones defined by the user and loaded from the config directory
func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return realFiles[fileType]
}
// InitRuntimeFiles initializes all assets file and the config directory
func InitRuntimeFiles() {
add := func(fileType RTFiletype, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
}
add(RTColorscheme, "colorschemes", "*.micro")
add(RTSyntax, "syntax", "*.yaml")
add(RTSyntaxHeader, "syntax", "*.hdr")
add(RTHelp, "help", "*.md")
initlua := filepath.Join(ConfigDir, "init.lua")
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
p := new(Plugin)
p.Name = "initlua"
p.DirName = "initlua"
p.Srcs = append(p.Srcs, realFile(initlua))
Plugins = append(Plugins, p)
}
// Search ConfigDir for plugin-scripts
plugdir := filepath.Join(ConfigDir, "plug")
files, _ := ioutil.ReadDir(plugdir)
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
for _, d := range files {
if d.IsDir() {
srcs, _ := ioutil.ReadDir(filepath.Join(plugdir, d.Name()))
p := new(Plugin)
p.Name = d.Name()
p.DirName = d.Name()
for _, f := range srcs {
if strings.HasSuffix(f.Name(), ".lua") {
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
} else if strings.HasSuffix(f.Name(), ".json") {
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
if err != nil {
continue
}
p.Info, err = NewPluginInfo(data)
if err != nil {
continue
}
p.Name = p.Info.Name
}
}
if !isID(p.Name) || len(p.Srcs) <= 0 {
log.Println(p.Name, "is not a plugin")
continue
}
Plugins = append(Plugins, p)
}
}
plugdir = filepath.Join("runtime", "plugins")
if files, err := AssetDir(plugdir); err == nil {
for _, d := range files {
if srcs, err := AssetDir(filepath.Join(plugdir, d)); err == nil {
p := new(Plugin)
p.Name = d
p.DirName = d
p.Default = true
for _, f := range srcs {
if strings.HasSuffix(f, ".lua") {
p.Srcs = append(p.Srcs, assetFile(filepath.Join(plugdir, d, f)))
} else if strings.HasSuffix(f, ".json") {
data, err := Asset(filepath.Join(plugdir, d, f))
if err != nil {
continue
}
p.Info, err = NewPluginInfo(data)
if err != nil {
continue
}
p.Name = p.Info.Name
}
}
if !isID(p.Name) || len(p.Srcs) <= 0 {
log.Println(p.Name, "is not a plugin")
continue
}
Plugins = append(Plugins, p)
}
}
}
}
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
func PluginReadRuntimeFile(fileType RTFiletype, name string) string {
if file := FindRuntimeFile(fileType, name); file != nil {
if data, err := file.Data(); err == nil {
return string(data)
}
}
return ""
}
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
func PluginListRuntimeFiles(fileType RTFiletype) []string {
files := ListRuntimeFiles(fileType)
result := make([]string, len(files))
for i, f := range files {
result[i] = f.Name()
}
return result
}
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) error {
pl := FindPlugin(plugin)
if pl == nil {
return errors.New("Plugin " + plugin + " does not exist")
}
pldir := pl.DirName
fullpath := filepath.Join(ConfigDir, "plug", pldir, filePath)
if _, err := os.Stat(fullpath); err == nil {
AddRealRuntimeFile(filetype, realFile(fullpath))
} else {
fullpath = path.Join("runtime", "plugins", pldir, filePath)
AddRuntimeFile(filetype, assetFile(fullpath))
}
return nil
}
// PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin
func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, directory, pattern string) error {
pl := FindPlugin(plugin)
if pl == nil {
return errors.New("Plugin " + plugin + " does not exist")
}
pldir := pl.DirName
fullpath := filepath.Join(ConfigDir, "plug", pldir, directory)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else {
fullpath = path.Join("runtime", "plugins", pldir, directory)
AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
}
return nil
}
// PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string
func PluginAddRuntimeFileFromMemory(filetype RTFiletype, filename, data string) {
AddRealRuntimeFile(filetype, memoryFile{filename, []byte(data)})
}

View File

@@ -1,42 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func init() {
InitRuntimeFiles()
}
func TestAddFile(t *testing.T) {
AddRuntimeFile(RTPlugin, memoryFile{"foo.lua", []byte("hello world\n")})
AddRuntimeFile(RTSyntax, memoryFile{"bar", []byte("some syntax file\n")})
f1 := FindRuntimeFile(RTPlugin, "foo.lua")
assert.NotNil(t, f1)
assert.Equal(t, "foo.lua", f1.Name())
data, err := f1.Data()
assert.Nil(t, err)
assert.Equal(t, []byte("hello world\n"), data)
f2 := FindRuntimeFile(RTSyntax, "bar")
assert.NotNil(t, f2)
assert.Equal(t, "bar", f2.Name())
data, err = f2.Data()
assert.Nil(t, err)
assert.Equal(t, []byte("some syntax file\n"), data)
}
func TestFindFile(t *testing.T) {
f := FindRuntimeFile(RTSyntax, "go")
assert.NotNil(t, f)
assert.Equal(t, "go", f.Name())
data, err := f.Data()
assert.Nil(t, err)
assert.Equal(t, []byte("filetype: go"), data[:12])
e := FindRuntimeFile(RTSyntax, "foobar")
assert.Nil(t, e)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,394 +0,0 @@
package config
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/zyedidia/glob"
"github.com/zyedidia/json5"
"github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding/htmlindex"
)
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{}
)
func init() {
parsedSettings = make(map[string]interface{})
}
// Options with validators
var optionValidators = map[string]optionValidator{
"autosave": validateNonNegativeValue,
"tabsize": validatePositiveValue,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"colorscheme": validateColorscheme,
"colorcolumn": validateNonNegativeValue,
"fileformat": validateLineEnding,
"encoding": validateEncoding,
}
func ReadSettings() error {
filename := filepath.Join(ConfigDir, "settings.json")
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
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 {
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
}
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
func InitGlobalSettings() {
GlobalSettings = DefaultGlobalSettings()
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
GlobalSettings[k] = v
}
}
}
// 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{}) {
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{}) {
settings[k1] = v1
}
}
}
}
}
return parseError
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
var err error
if _, e := os.Stat(ConfigDir); e == nil {
for k, v := range GlobalSettings {
parsedSettings[k] = v
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
func OverwriteSettings(filename string) error {
var err error
if _, e := os.Stat(ConfigDir); e == nil {
txt, _ := json.MarshalIndent(GlobalSettings, "", " ")
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 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
}
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterGlobalOption(pl+"."+name, defaultvalue)
}
// 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]
}
var defaultCommonSettings = map[string]interface{}{
"autoindent": true,
"autosu": false,
"backup": true,
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"diffgutter": false,
"encoding": "utf-8",
"eofnewline": true,
"fastdirty": false,
"fileformat": "unix",
"filetype": "unknown",
"ignorecase": false,
"indentchar": " ",
"keepautoindent": false,
"matchbrace": true,
"mkparents": false,
"readonly": false,
"rmtrailingws": false,
"ruler": true,
"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,
}
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
}
// a list of settings that should only be globally modified and their
// default values
var DefaultGlobalOnlySettings = map[string]interface{}{
"autosave": float64(0),
"colorscheme": "default",
"divchars": "|-",
"divreverse": true,
"infobar": true,
"keymenu": false,
"mouse": true,
"paste": false,
"savehistory": true,
"sucmd": "sudo",
"pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
"pluginrepos": []string{},
"xterm": false,
}
// a list of settings that should never be globally modified
var LocalSettings = []string{
"filetype",
"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
}
// DefaultAllSettings returns a map of all settings and their
// default values (both common and global settings)
func DefaultAllSettings() map[string]interface{} {
allsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
allsettings[k] = v
}
for k, v := range DefaultGlobalOnlySettings {
allsettings[k] = v
}
return allsettings
}
// GetNativeValue parses and validates a value for a given option
func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
var native interface{}
kind := reflect.TypeOf(realValue).Kind()
if kind == reflect.Bool {
b, err := util.ParseBool(value)
if err != nil {
return nil, ErrInvalidValue
}
native = b
} else if kind == reflect.String {
native = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return nil, ErrInvalidValue
}
native = float64(i)
} else {
return nil, ErrInvalidValue
}
if err := OptionIsValid(option, native); err != nil {
return nil, err
}
return native, nil
}
// OptionIsValid checks if a value is valid for a certain option
func OptionIsValid(option string, value interface{}) error {
if validator, ok := optionValidators[option]; ok {
return validator(option, value)
}
return nil
}
// Option validators
func validatePositiveValue(option string, value interface{}) error {
tabsize, ok := value.(float64)
if !ok {
return errors.New("Expected numeric type for " + option)
}
if tabsize < 1 {
return errors.New(option + " must be greater than 0")
}
return nil
}
func validateNonNegativeValue(option string, value interface{}) error {
nativeValue, ok := value.(float64)
if !ok {
return errors.New("Expected numeric type for " + option)
}
if nativeValue < 0 {
return errors.New(option + " must be non-negative")
}
return nil
}
func validateColorscheme(option string, value interface{}) error {
colorscheme, ok := value.(string)
if !ok {
return errors.New("Expected string type for colorscheme")
}
if !ColorschemeExists(colorscheme) {
return errors.New(colorscheme + " is not a valid colorscheme")
}
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
}

View File

@@ -1,735 +0,0 @@
package display
import (
"strconv"
runewidth "github.com/mattn/go-runewidth"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell"
)
// The BufWindow provides a way of displaying a certain section
// of a buffer
type BufWindow struct {
*View
// Buffer being shown in this window
Buf *buffer.Buffer
active bool
sline *StatusLine
gutterOffset int
drawStatus bool
}
// NewBufWindow creates a new window at a location in the screen with a width and height
func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow {
w := new(BufWindow)
w.View = new(View)
w.X, w.Y, w.Width, w.Height, w.Buf = x, y, width, height, buf
w.active = true
w.sline = NewStatusLine(w)
return w
}
func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
w.Buf = b
}
func (w *BufWindow) GetView() *View {
return w.View
}
func (w *BufWindow) SetView(view *View) {
w.View = view
}
func (w *BufWindow) Resize(width, height int) {
w.Width, w.Height = width, height
w.Relocate()
}
func (w *BufWindow) SetActive(b bool) {
w.active = b
}
func (w *BufWindow) IsActive() bool {
return w.active
}
func (w *BufWindow) getStartInfo(n, lineN int) ([]byte, int, int, *tcell.Style) {
tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
width := 0
bloc := buffer.Loc{0, lineN}
b := w.Buf.LineBytes(lineN)
curStyle := config.DefStyle
var s *tcell.Style
for len(b) > 0 {
r, _, size := util.DecodeCharacter(b)
curStyle, found := w.getStyle(curStyle, bloc)
if found {
s = &curStyle
}
w := 0
switch r {
case '\t':
ts := tabsize - (width % tabsize)
w = ts
default:
w = runewidth.RuneWidth(r)
}
if width+w > n {
return b, n - width, bloc.X, s
}
width += w
b = b[size:]
bloc.X++
}
return b, n - width, bloc.X, s
}
// Clear resets all cells in this window to the default style
func (w *BufWindow) Clear() {
for y := 0; y < w.Height; y++ {
for x := 0; x < w.Width; x++ {
screen.SetContent(w.X+x, w.Y+y, ' ', nil, config.DefStyle)
}
}
}
// Bottomline returns the line number of the lowest line in the view
// You might think that this is obviously just v.StartLine + v.Height
// but if softwrap is enabled things get complicated since one buffer
// line can take up multiple lines in the view
func (w *BufWindow) Bottomline() int {
if !w.Buf.Settings["softwrap"].(bool) {
h := w.StartLine + w.Height - 1
if w.drawStatus {
h--
}
return h
}
l := w.LocFromVisual(buffer.Loc{0, w.Y + w.Height})
return l.Y
}
// Relocate moves the view window so that the cursor is in view
// This is useful if the user has scrolled far away, and then starts typing
// Returns true if the window location is moved
func (w *BufWindow) Relocate() bool {
b := w.Buf
// how many buffer lines are in the view
height := w.Bottomline() + 1 - w.StartLine
h := w.Height
if w.drawStatus {
h--
}
ret := false
activeC := w.Buf.GetActiveCursor()
cy := activeC.Y
scrollmargin := int(b.Settings["scrollmargin"].(float64))
if cy < w.StartLine+scrollmargin && cy > scrollmargin-1 {
w.StartLine = cy - scrollmargin
ret = true
} else if cy < w.StartLine {
w.StartLine = cy
ret = true
}
if cy > w.StartLine+height-1-scrollmargin && cy < b.LinesNum()-scrollmargin {
w.StartLine = cy - height + 1 + scrollmargin
ret = true
} else if cy >= b.LinesNum()-scrollmargin && cy >= height {
w.StartLine = b.LinesNum() - height
ret = true
}
// horizontal relocation (scrolling)
if !b.Settings["softwrap"].(bool) {
cx := activeC.GetVisualX()
if cx < w.StartCol {
w.StartCol = cx
ret = true
}
if cx+w.gutterOffset+1 > w.StartCol+w.Width {
w.StartCol = cx - w.Width + w.gutterOffset + 1
ret = true
}
}
return ret
}
// LocFromVisual takes a visual location (x and y position) and returns the
// position in the buffer corresponding to the visual location
// Computing the buffer location requires essentially drawing the entire screen
// to account for complications like softwrap, wide characters, and horizontal scrolling
// If the requested position does not correspond to a buffer location it returns
// the nearest position
func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
b := w.Buf
hasMessage := len(b.Messages) > 0
bufHeight := w.Height
if w.drawStatus {
bufHeight--
}
bufWidth := w.Width
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
bufWidth--
}
// We need to know the string length of the largest line number
// so we can pad appropriately when displaying line numbers
maxLineNumLength := len(strconv.Itoa(b.LinesNum()))
tabsize := int(b.Settings["tabsize"].(float64))
softwrap := b.Settings["softwrap"].(bool)
// this represents the current draw position
// within the current window
vloc := buffer.Loc{X: 0, Y: 0}
// this represents the current draw position in the buffer (char positions)
bloc := buffer.Loc{X: -1, Y: w.StartLine}
for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
vloc.X = 0
if hasMessage {
vloc.X += 2
}
if b.Settings["diffgutter"].(bool) {
vloc.X++
}
if b.Settings["ruler"].(bool) {
vloc.X += maxLineNumLength + 1
}
line := b.LineBytes(bloc.Y)
line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, w.StartCol, tabsize)
bloc.X = bslice
draw := func() {
if nColsBeforeStart <= 0 {
vloc.X++
}
nColsBeforeStart--
}
totalwidth := w.StartCol - nColsBeforeStart
if svloc.X <= vloc.X+w.X && vloc.Y+w.Y == svloc.Y {
return bloc
}
for len(line) > 0 {
if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y {
return bloc
}
r, _, size := util.DecodeCharacter(line)
draw()
width := 0
switch r {
case '\t':
ts := tabsize - (totalwidth % tabsize)
width = ts
default:
width = runewidth.RuneWidth(r)
}
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
if width > 1 {
for i := 1; i < width; i++ {
if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y {
return bloc
}
draw()
}
}
bloc.X++
line = line[size:]
totalwidth += width
// If we reach the end of the window then we either stop or we wrap for softwrap
if vloc.X >= bufWidth {
if !softwrap {
break
} else {
vloc.Y++
if vloc.Y >= bufHeight {
break
}
vloc.X = w.gutterOffset
}
}
}
if vloc.Y+w.Y == svloc.Y {
return bloc
}
if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= bufHeight {
return bloc
}
bloc.X = w.StartCol
bloc.Y++
}
return buffer.Loc{}
}
func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
char := ' '
s := config.DefStyle
for _, m := range w.Buf.Messages {
if m.Start.Y == bloc.Y || m.End.Y == bloc.Y {
s = m.Style()
char = '>'
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++
}
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
symbol := ' '
styleName := ""
switch w.Buf.DiffStatus(bloc.Y) {
case buffer.DSAdded:
symbol = '\u258C' // Left half block
styleName = "diff-added"
case buffer.DSModified:
symbol = '\u258C' // Left half block
styleName = "diff-modified"
case buffer.DSDeletedAbove:
if !softwrapped {
symbol = '\u2594' // Upper one eighth block
styleName = "diff-deleted"
}
}
style := backgroundStyle
if s, ok := config.Colorscheme[styleName]; ok {
foreground, _, _ := s.Decompose()
style = style.Foreground(foreground)
}
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, symbol, nil, style)
vloc.X++
}
func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *buffer.Loc, bloc *buffer.Loc) {
lineNum := strconv.Itoa(bloc.Y + 1)
// Write the spaces before the line number if necessary
for i := 0; i < maxLineNumLength-len(lineNum); 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 {
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)
}
vloc.X++
}
// Write the extra space
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
// getStyle returns the highlight style for the given character position
// If there is no change to the current highlight style it just returns that
func (w *BufWindow) getStyle(style tcell.Style, bloc buffer.Loc) (tcell.Style, bool) {
if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok {
s := config.GetColor(group.String())
return s, true
}
return style, false
}
func (w *BufWindow) showCursor(x, y int, main bool) {
if w.active {
if main {
screen.ShowCursor(x, y)
} else {
screen.ShowFakeCursorMulti(x, y)
}
}
}
// displayBuffer draws the buffer being shown in this window on the screen.Screen
func (w *BufWindow) displayBuffer() {
b := w.Buf
if w.Height <= 0 || w.Width <= 0 {
return
}
hasMessage := len(b.Messages) > 0
bufHeight := w.Height
if w.drawStatus {
bufHeight--
}
bufWidth := w.Width
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
bufWidth--
}
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.ModifiedThisFrame = false
}
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
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))
}
}
}
}
}
}
lineNumStyle := config.DefStyle
if style, ok := config.Colorscheme["line-number"]; ok {
lineNumStyle = style
}
curNumStyle := config.DefStyle
if style, ok := config.Colorscheme["current-line-number"]; ok {
if !b.Settings["cursorline"].(bool) {
curNumStyle = lineNumStyle
} else {
curNumStyle = style
}
}
// We need to know the string length of the largest line number
// so we can pad appropriately when displaying line numbers
maxLineNumLength := len(strconv.Itoa(b.LinesNum()))
softwrap := b.Settings["softwrap"].(bool)
tabsize := util.IntOpt(b.Settings["tabsize"])
colorcolumn := util.IntOpt(b.Settings["colorcolumn"])
// this represents the current draw position
// within the current window
vloc := buffer.Loc{X: 0, Y: 0}
// this represents the current draw position in the buffer (char positions)
bloc := buffer.Loc{X: -1, Y: w.StartLine}
cursors := b.GetCursors()
curStyle := config.DefStyle
for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
vloc.X = 0
currentLine := false
for _, c := range cursors {
if bloc.Y == c.Y && w.active {
currentLine = true
break
}
}
s := lineNumStyle
if currentLine {
s = curNumStyle
}
if hasMessage {
w.drawGutter(&vloc, &bloc)
}
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(s, false, &vloc, &bloc)
}
if b.Settings["ruler"].(bool) {
w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc)
}
w.gutterOffset = vloc.X
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
if startStyle != nil {
curStyle = *startStyle
}
bloc.X = bslice
draw := func(r rune, combc []rune, style tcell.Style, showcursor bool) {
if nColsBeforeStart <= 0 {
for _, c := range cursors {
if c.HasSelection() &&
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) {
// The current character is selected
style = config.DefStyle.Reverse(true)
if s, ok := config.Colorscheme["selection"]; ok {
style = s
}
}
if b.Settings["cursorline"].(bool) && w.active &&
!c.HasSelection() && c.Y == bloc.Y {
if s, ok := config.Colorscheme["cursor-line"]; ok {
fg, _, _ := s.Decompose()
style = style.Background(fg)
}
}
}
for _, m := range b.Messages {
if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) ||
bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) {
style = style.Underline(true)
break
}
}
if r == '\t' {
indentrunes := []rune(b.Settings["indentchar"].(string))
// if empty indentchar settings, use space
if indentrunes == nil || len(indentrunes) == 0 {
indentrunes = []rune{' '}
}
r = indentrunes[0]
if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' {
fg, _, _ := s.Decompose()
style = style.Foreground(fg)
}
}
if s, ok := config.Colorscheme["color-column"]; ok {
if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn {
fg, _, _ := s.Decompose()
style = style.Background(fg)
}
}
for _, mb := range matchingBraces {
if mb.X == bloc.X && mb.Y == bloc.Y {
style = style.Underline(true)
}
}
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, combc, style)
if showcursor {
for _, c := range cursors {
if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() {
w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0)
}
}
}
vloc.X++
}
nColsBeforeStart--
}
totalwidth := w.StartCol - nColsBeforeStart
for len(line) > 0 {
r, combc, size := util.DecodeCharacter(line)
curStyle, _ = w.getStyle(curStyle, bloc)
draw(r, combc, curStyle, true)
width := 0
char := ' '
switch r {
case '\t':
ts := tabsize - (totalwidth % tabsize)
width = ts
default:
width = runewidth.RuneWidth(r)
char = '@'
}
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
if width > 1 {
for i := 1; i < width; i++ {
draw(char, nil, curStyle, false)
}
}
bloc.X++
line = line[size:]
totalwidth += width
// If we reach the end of the window then we either stop or we wrap for softwrap
if vloc.X >= bufWidth {
if !softwrap {
break
} else {
vloc.Y++
if vloc.Y >= bufHeight {
break
}
vloc.X = 0
if hasMessage {
w.drawGutter(&vloc, &bloc)
}
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
}
// This will draw an empty line number because the current line is wrapped
if b.Settings["ruler"].(bool) {
w.drawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc)
}
}
}
}
style := config.DefStyle
for _, c := range cursors {
if b.Settings["cursorline"].(bool) && w.active &&
!c.HasSelection() && c.Y == bloc.Y {
if s, ok := config.Colorscheme["cursor-line"]; ok {
fg, _, _ := s.Decompose()
style = style.Background(fg)
}
}
}
for i := vloc.X; i < bufWidth; i++ {
curStyle := style
if s, ok := config.Colorscheme["color-column"]; ok {
if colorcolumn != 0 && i-w.gutterOffset+w.StartCol == colorcolumn {
fg, _, _ := s.Decompose()
curStyle = style.Background(fg)
}
}
screen.SetContent(i+w.X, vloc.Y+w.Y, ' ', nil, curStyle)
}
if vloc.X != bufWidth {
draw(' ', nil, curStyle, true)
}
bloc.X = w.StartCol
bloc.Y++
if bloc.Y >= b.LinesNum() {
break
}
}
}
func (w *BufWindow) displayStatusLine() {
_, h := screen.Screen.Size()
infoY := h
if config.GetGlobalOption("infobar").(bool) {
infoY--
}
if w.Buf.Settings["statusline"].(bool) {
w.drawStatus = true
w.sline.Display()
} else if w.Y+w.Height != infoY {
w.drawStatus = true
divchars := config.GetGlobalOption("divchars").(string)
if util.CharacterCountInString(divchars) != 2 {
divchars = "|-"
}
_, _, size := util.DecodeCharacterInString(divchars)
divchar, combc, _ := util.DecodeCharacterInString(divchars[size:])
dividerStyle := config.DefStyle
if style, ok := config.Colorscheme["divider"]; ok {
dividerStyle = style
}
divreverse := config.GetGlobalOption("divreverse").(bool)
if divreverse {
dividerStyle = dividerStyle.Reverse(true)
}
for x := w.X; x < w.X+w.Width; x++ {
screen.SetContent(x, w.Y+w.Height-1, divchar, combc, dividerStyle)
}
} else {
w.drawStatus = false
}
}
func (w *BufWindow) displayScrollBar() {
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
scrollX := w.X + w.Width - 1
bufHeight := w.Height
if w.drawStatus {
bufHeight--
}
barsize := int(float64(w.Height) / float64(w.Buf.LinesNum()) * float64(w.Height))
if barsize < 1 {
barsize = 1
}
barstart := w.Y + int(float64(w.StartLine)/float64(w.Buf.LinesNum())*float64(w.Height))
for y := barstart; y < util.Min(barstart+barsize, w.Y+bufHeight); y++ {
screen.SetContent(scrollX, y, '|', nil, config.DefStyle.Reverse(true))
}
}
}
// Display displays the buffer and the statusline
func (w *BufWindow) Display() {
w.displayStatusLine()
w.displayScrollBar()
w.displayBuffer()
}

View File

@@ -1,292 +0,0 @@
package display
import (
runewidth "github.com/mattn/go-runewidth"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell"
)
type InfoWindow struct {
*info.InfoBuf
*View
hscroll int
}
func (i *InfoWindow) errStyle() tcell.Style {
errStyle := config.DefStyle.
Foreground(tcell.ColorBlack).
Background(tcell.ColorMaroon)
if _, ok := config.Colorscheme["error-message"]; ok {
errStyle = config.Colorscheme["error-message"]
}
return errStyle
}
func (i *InfoWindow) defStyle() tcell.Style {
defStyle := config.DefStyle
if _, ok := config.Colorscheme["message"]; ok {
defStyle = config.Colorscheme["message"]
}
return defStyle
}
func NewInfoWindow(b *info.InfoBuf) *InfoWindow {
iw := new(InfoWindow)
iw.InfoBuf = b
iw.View = new(View)
iw.Width, iw.Y = screen.Screen.Size()
iw.Y--
return iw
}
func (i *InfoWindow) Resize(w, h int) {
i.Width = w
i.Y = h
}
func (i *InfoWindow) SetBuffer(b *buffer.Buffer) {
i.InfoBuf.Buffer = b
}
func (i *InfoWindow) Relocate() bool { return false }
func (i *InfoWindow) GetView() *View { return i.View }
func (i *InfoWindow) SetView(v *View) {}
func (i *InfoWindow) SetActive(b bool) {}
func (i *InfoWindow) IsActive() bool { return true }
func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc {
c := i.Buffer.GetActiveCursor()
l := i.Buffer.LineBytes(0)
n := util.CharacterCountInString(i.Msg)
return buffer.Loc{c.GetCharPosInLine(l, vloc.X-n), 0}
}
func (i *InfoWindow) Clear() {
for x := 0; x < i.Width; x++ {
screen.SetContent(x, i.Y, ' ', nil, i.defStyle())
}
}
func (i *InfoWindow) displayBuffer() {
b := i.Buffer
line := b.LineBytes(0)
activeC := b.GetActiveCursor()
blocX := 0
vlocX := util.CharacterCountInString(i.Msg)
tabsize := 4
line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, blocX, tabsize)
blocX = bslice
draw := func(r rune, combc []rune, style tcell.Style) {
if nColsBeforeStart <= 0 {
bloc := buffer.Loc{X: blocX, Y: 0}
if activeC.HasSelection() &&
(bloc.GreaterEqual(activeC.CurSelection[0]) && bloc.LessThan(activeC.CurSelection[1]) ||
bloc.LessThan(activeC.CurSelection[0]) && bloc.GreaterEqual(activeC.CurSelection[1])) {
// The current character is selected
style = i.defStyle().Reverse(true)
if s, ok := config.Colorscheme["selection"]; ok {
style = s
}
}
rw := runewidth.RuneWidth(r)
for j := 0; j < rw; j++ {
c := r
if j > 0 {
c = ' '
combc = nil
}
screen.SetContent(vlocX, i.Y, c, combc, style)
}
vlocX++
}
nColsBeforeStart--
}
totalwidth := blocX - nColsBeforeStart
for len(line) > 0 {
curVX := vlocX
curBX := blocX
r, combc, size := util.DecodeCharacter(line)
draw(r, combc, i.defStyle())
width := 0
char := ' '
switch r {
case '\t':
ts := tabsize - (totalwidth % tabsize)
width = ts
default:
width = runewidth.RuneWidth(r)
char = '@'
}
blocX++
line = line[size:]
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
if width > 1 {
for j := 1; j < width; j++ {
draw(char, nil, i.defStyle())
}
}
if activeC.X == curBX {
screen.ShowCursor(curVX, i.Y)
}
totalwidth += width
if vlocX >= i.Width {
break
}
}
if activeC.X == blocX {
screen.ShowCursor(vlocX, i.Y)
}
}
var keydisplay = []string{"^Q Quit, ^S Save, ^O Open, ^G Help, ^E Command Bar, ^K Cut Line", "^F Find, ^Z Undo, ^Y Redo, ^A Select All, ^D Duplicate Line, ^T New Tab"}
func (i *InfoWindow) displayKeyMenu() {
// TODO: maybe make this based on the actual keybindings
for y := 0; y < len(keydisplay); y++ {
for x := 0; x < i.Width; x++ {
if x < len(keydisplay[y]) {
screen.SetContent(x, i.Y-len(keydisplay)+y, rune(keydisplay[y][x]), nil, i.defStyle())
} else {
screen.SetContent(x, i.Y-len(keydisplay)+y, ' ', nil, i.defStyle())
}
}
}
}
func (i *InfoWindow) totalSize() int {
sum := 0
for _, n := range i.Suggestions {
sum += runewidth.StringWidth(n) + 1
}
return sum
}
func (i *InfoWindow) scrollToSuggestion() {
x := 0
s := i.totalSize()
for j, n := range i.Suggestions {
c := util.CharacterCountInString(n)
if j == i.CurSuggestion {
if x+c >= i.hscroll+i.Width {
i.hscroll = util.Clamp(x+c+1-i.Width, 0, s-i.Width)
} else if x < i.hscroll {
i.hscroll = util.Clamp(x-1, 0, s-i.Width)
}
break
}
x += c + 1
}
if s-i.Width <= 0 {
i.hscroll = 0
}
}
func (i *InfoWindow) Display() {
if i.HasPrompt || config.GlobalSettings["infobar"].(bool) {
i.Clear()
x := 0
if config.GetGlobalOption("keymenu").(bool) {
i.displayKeyMenu()
}
if !i.HasPrompt && !i.HasMessage && !i.HasError {
return
}
i.Clear()
style := i.defStyle()
if i.HasError {
style = i.errStyle()
}
display := i.Msg
for _, c := range display {
screen.SetContent(x, i.Y, c, nil, style)
x += runewidth.RuneWidth(c)
}
if i.HasPrompt {
i.displayBuffer()
}
}
if i.HasSuggestions && len(i.Suggestions) > 1 {
i.scrollToSuggestion()
x := -i.hscroll
done := false
statusLineStyle := config.DefStyle.Reverse(true)
if style, ok := config.Colorscheme["statusline"]; ok {
statusLineStyle = style
}
keymenuOffset := 0
if config.GetGlobalOption("keymenu").(bool) {
keymenuOffset = len(keydisplay)
}
draw := func(r rune, s tcell.Style) {
y := i.Y - keymenuOffset - 1
rw := runewidth.RuneWidth(r)
for j := 0; j < rw; j++ {
c := r
if j > 0 {
c = ' '
}
if x == i.Width-1 && !done {
screen.SetContent(i.Width-1, y, '>', nil, s)
x++
break
} else if x == 0 && i.hscroll > 0 {
screen.SetContent(0, y, '<', nil, s)
} else if x >= 0 && x < i.Width {
screen.SetContent(x, y, c, nil, s)
}
x++
}
}
for j, s := range i.Suggestions {
style := statusLineStyle
if i.CurSuggestion == j {
style = style.Reverse(true)
}
for _, r := range s {
draw(r, style)
// screen.SetContent(x, i.Y-keymenuOffset-1, r, nil, style)
}
draw(' ', statusLineStyle)
}
for x < i.Width {
draw(' ', statusLineStyle)
}
}
}

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