1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2025-09-15 14:32:08 +02:00

36 Commits

Author SHA1 Message Date
575283b2be Set version to chylex-18 2023-12-18 05:25:09 +01:00
8bb34d7f75 Make editor fade opacity configurable 2023-12-18 05:24:57 +01:00
8d1ef031d2 Set version to chylex-17 2023-12-18 03:44:20 +01:00
a9375ec414 Remove unused code 2023-12-18 03:44:01 +01:00
b42112cc9e Do not try to tag folded regions 2023-12-18 02:36:41 +01:00
8e42c82821 Use distance from current caret for tag order 2023-12-18 02:32:54 +01:00
48bcf9f284 Add QWERTZ (CZ) layout that remaps top row to digits 2023-12-18 00:58:37 +01:00
2681d9901f Change priority order of two-character tags 2023-12-18 00:45:28 +01:00
b13d629046 Add option to set different colors for two-character tags 2023-12-17 22:41:36 +01:00
92dcd033fb Make priority of unknown characters lower than known characters 2023-12-17 21:49:24 +01:00
fa3505b850 Set version to chylex-16 2023-12-14 00:09:49 +01:00
dacac684f0 Make two-character tag prefixes customizable 2023-12-14 00:09:27 +01:00
e627db3a24 Fix Shift mode not working when accepting a tag 2023-12-13 22:52:18 +01:00
084d729baa Set version to chylex-15 2023-12-13 20:16:51 +01:00
e01edccb5b Redesign tags to look like easymotion vim plugin 2023-12-13 20:16:13 +01:00
655ccde60e Work around IntelliJ terminal plugin stealing Enter keys 2023-12-13 17:48:23 +01:00
eb2ea55fb8 Rework tagging to match easymotion (no search query refinement, no double letter tags) 2023-12-13 15:19:57 +01:00
a6381a2a34 Update Gradle to 8.5 and IntelliJ to 2023.3 2023-12-12 23:19:03 +01:00
e005983d4c Set version to chylex-14 2023-11-17 08:56:13 +01:00
8f4d9748ad Scroll after jumping in vim mode 2023-11-17 08:55:49 +01:00
76c6458ef4 Re-add action to go to declaration after jump 2023-11-17 08:52:46 +01:00
2f53e9da6d Update for IdeaVIM chylex-20 2023-11-17 08:52:23 +01:00
100001ffca Set version to chylex-13 2023-10-04 02:41:33 +02:00
184896a6cb Update for IdeaVIM chylex-16 2023-10-04 02:41:05 +02:00
8563400946 Set plugin version to chylex-12 2023-10-04 02:40:51 +02:00
a07c61a384 Fully depend on IdeaVIM and remake actions 2023-07-28 07:50:39 +02:00
e072003c5c Update dependency versions and gitignore 2023-07-27 22:07:17 +02:00
8062cf5cab Update dependency versions 2023-01-09 07:19:07 +01:00
9151ee376c Set plugin version to chylex-11 2022-07-06 15:46:45 +02:00
19ce1c69fd Improve tag order for non-QWERTY layouts 2022-07-06 15:46:35 +02:00
f2a053505c Remove no longer necessary actions 2022-07-06 15:46:35 +02:00
647cfb14f1 Remove unused code 2022-07-06 15:46:35 +02:00
c31ba60909 Implement a customized Vim easymotion plugin 2022-07-06 15:46:35 +02:00
9c60a8a4ba Update build.gradle IDEA & plugin versions 2022-06-18 20:59:52 +02:00
1e8b7d7963 Remove Kotlin stdlib from distribution 2022-06-18 20:54:58 +02:00
9157ce19b0 Remove all special modes introduced in the rework 2021-11-14 14:35:54 +01:00
45 changed files with 978 additions and 2240 deletions

24
.gitignore vendored
View File

@@ -1,21 +1,5 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
/.idea/*
!/.idea/runConfigurations
## Directory-based project format:
.idea
*.iml
## Plugin-specific files:
# IntelliJ
/out/
### Gradle template
.gradle
build/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
gradle.properties
# Mac OS
.DS_Store
/.gradle/
/build/

View File

@@ -1,32 +1,51 @@
@file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
idea apply true
kotlin("jvm") version "1.5.0"
id("org.jetbrains.intellij") version "0.7.2"
}
tasks {
withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
compileOnly(kotlin("stdlib-jdk8"))
}
repositories {
mavenCentral()
jcenter()
}
intellij {
version = "2021.1"
pluginName = "AceJump"
updateSinceUntilBuild = false
setPlugins("java", "IdeaVIM:0.66")
kotlin("jvm") version "1.9.10"
id("org.jetbrains.intellij") version "1.16.1"
}
group = "org.acejump"
version = "chylex-8"
version = "chylex-18"
repositories {
mavenCentral()
}
intellij {
version.set("2023.3")
updateSinceUntilBuild.set(false)
plugins.add("IdeaVIM:chylex-22")
pluginsRepositories {
custom("https://intellij.chylex.com")
}
}
kotlin {
jvmToolchain(17)
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
tasks.patchPluginXml {
sinceBuild.set("233")
}
tasks.buildSearchableOptions {
enabled = false
}
tasks.test {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
kotlin.stdlib.default.dependency=false

Binary file not shown.

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

294
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,99 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

15
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

@@ -1,17 +1,33 @@
package org.acejump
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.Project
import com.intellij.util.IncorrectOperationException
import it.unimi.dsi.fastutil.ints.IntArrayList
annotation class ExternalUsage
/**
* Returns an immutable version of the currently edited document.
*/
val Editor.immutableText
get() = this.document.immutableCharSequence
/**
* Returns all open editors in the project.
*/
val Project.openEditors: List<Editor>
get() {
return try {
FileEditorManagerEx.getInstanceEx(this)
.splitters
.getSelectedEditors()
.mapNotNull { (it as? TextEditor)?.editor }
} catch (e: IncorrectOperationException) {
emptyList()
}
}
/**
* Returns true if [this] contains [otherText] at the specified offset.
*/
@@ -34,83 +50,6 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
return i
}
/**
* Determines which characters form a "word" for the purposes of functions below.
*/
val Char.isWordPart
get() = this in 'a'..'z' || this.isJavaIdentifierPart()
/**
* Finds index of the first character in a word.
*/
inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var start = pos
while (start > 0 && isPartOfWord(this[start - 1])) {
--start
}
return start
}
/**
* Finds index of the last character in a word.
*/
inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos
val limit = length - 1
while (end < limit && isPartOfWord(this[end + 1])) {
++end
}
return end
}
/**
* Finds index of the previous "camelHumps" hump in a word.
*/
inline fun CharSequence.humpStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var start = pos
while (start > 0 && isPartOfWord(this[start - 1]) && !EditorActionUtil.isHumpBound(this, start, true)) {
--start
}
return start
}
/**
* Finds index of the next "camelHumps" hump in a word.
*/
inline fun CharSequence.humpEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos
while (end < length - 1 && isPartOfWord(this[end + 1]) && !EditorActionUtil.isHumpBound(this, end + 1, false)) {
++end
}
return end
}
/**
* Finds index of the first word character following a sequence of non-word characters following the end of a word.
*/
inline fun CharSequence.wordEndPlus(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = this.wordEnd(pos, isPartOfWord)
val limit = length - 1
while (end < limit && !isPartOfWord(this[end + 1])) {
++end
}
if (end < limit && isPartOfWord(this[end + 1])) {
++end
}
return end
}
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
val clone = HashMap<Editor, IntArrayList>(size)

View File

@@ -4,7 +4,6 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
@@ -42,16 +41,4 @@ abstract class AceEditorAction(private val originalHandler: EditorActionHandler)
class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.tagImmediately()
}
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS)
}
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS)
}
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS)
}
}

View File

@@ -1,81 +0,0 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.util.IncorrectOperationException
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries.*
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.session.SessionManager
/**
* Base class for keyboard-activated actions that create or update an AceJump [Session].
*/
abstract class AceKeyboardAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(EDITOR) ?: return
val project = e.project
if (project != null) {
try {
val openEditors = FileEditorManagerEx.getInstanceEx(project)
.splitters
.selectedEditors
.mapNotNull { (it as? TextEditor)?.editor }
.sortedBy { if (it === editor) 0 else 1 }
invoke(SessionManager.start(editor, openEditors))
} catch (e: IncorrectOperationException) {
invoke(SessionManager.start(editor))
}
}
else {
invoke(SessionManager.start(editor))
}
}
abstract operator fun invoke(session: Session)
/**
* Generic action type that starts a regex search.
*/
abstract class BaseRegexSearchAction(private val pattern: Pattern, private val boundaries: Boundaries) : AceKeyboardAction() {
override fun invoke(session: Session) {
session.defaultBoundary = boundaries
session.startRegexSearch(pattern)
}
}
/**
* Starts or ends an AceJump session in quick jump mode.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.startJumpMode()
}
/**
* Starts or cycles main AceJump modes.
*/
object ActivateAceJumpSpecial : AceKeyboardAction() {
override fun invoke(session: Session) = session.startOrCycleSpecialModes()
}
// @formatter:off
object StartAllWordsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, VISIBLE_ON_SCREEN)
object StartAllWordsBackwardsMode : BaseRegexSearchAction(Pattern.ALL_WORDS, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
object StartAllWordsForwardMode : BaseRegexSearchAction(Pattern.ALL_WORDS, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
object StartAllLineStartsMode : BaseRegexSearchAction(Pattern.LINE_STARTS, VISIBLE_ON_SCREEN)
object StartAllLineEndsMode : BaseRegexSearchAction(Pattern.LINE_ENDS, VISIBLE_ON_SCREEN)
object StartAllLineIndentsMode : BaseRegexSearchAction(Pattern.LINE_INDENTS, VISIBLE_ON_SCREEN)
object StartAllLineMarksMode : BaseRegexSearchAction(Pattern.LINE_ALL_MARKS, VISIBLE_ON_SCREEN)
// @formatter:on
}

View File

@@ -1,163 +1,53 @@
package org.acejump.action
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.codeStyle.CodeStyleManager
import org.acejump.*
import org.acejump.search.SearchProcessor
import kotlin.math.max
import org.acejump.search.Tag
/**
* Base class for actions available after typing a tag.
*/
sealed class AceTagAction {
abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean)
abstract operator fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean)
abstract class BaseJumpAction : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
override fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) {
val editor = tag.editor
val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
recordCaretPosition(editor)
editor.project?.let { addCurrentPositionToHistory(it, editor.document) }
if (isFinal) {
ensureEditorFocused(editor)
}
moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset))
moveCaretTo(editor, tag.offset)
if (shiftMode) {
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
}
abstract fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int
}
abstract class BaseWordAction : BaseJumpAction() {
final override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
val matchingChars = countMatchingCharacters(editor, searchProcessor, offset)
val targetOffset = offset + matchingChars
val isInsideWord = matchingChars > 0 && editor.immutableText.let { it[targetOffset - 1].isWordPart && it[targetOffset].isWordPart }
return getCaretOffset(editor, offset, targetOffset, isInsideWord)
}
abstract fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int
}
abstract class BaseSelectAction : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
if (shiftMode) {
val caretModel = editor.caretModel
val oldCarets = caretModel.caretsAndSelections
val oldOffsetPosition = caretModel.logicalPosition
invoke(editor, searchProcessor, offset)
if (caretModel.caretsAndSelections.any { isSelectionOverlapping(oldOffsetPosition, it) }) {
oldCarets.removeAll { isSelectionOverlapping(oldOffsetPosition, it) }
}
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections
}
else {
invoke(editor, searchProcessor, offset)
}
}
private fun isSelectionOverlapping(offset: LogicalPosition, oldCaret: CaretState): Boolean {
return oldCaret.caretPosition == offset || oldCaret.selectionStart == offset || oldCaret.selectionEnd == offset
}
protected abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int)
}
abstract class BasePerCaretWriteAction(private val selector: AceTagAction) : AceTagAction() {
final override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
val range = editor.selectionModel.let { TextRange(it.selectionStart, it.selectionEnd) }
editor.caretModel.caretsAndSelections = oldCarets
invoke(editor, range, shiftMode)
}
protected abstract operator fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean)
protected fun insertAtCarets(editor: Editor, text: String) {
val document = editor.document
editor.caretModel.runForEachCaret {
if (it.hasSelection()) {
document.replaceString(it.selectionStart, it.selectionEnd, text)
fixIndents(editor, it.selectionStart, it.selectionEnd)
}
else {
document.insertString(it.offset, text)
fixIndents(editor, it.offset, it.offset + text.length)
}
}
}
private fun fixIndents(editor: Editor, startOffset: Int, endOffset: Int) {
val project = editor.project ?: return
val document = editor.document
val documentManager = PsiDocumentManager.getInstance(project)
documentManager.commitAllDocuments()
val file = documentManager.getPsiFile(document) ?: return
val text = document.charsSequence
if (startOffset > 0 && endOffset > startOffset + 1 && text[endOffset - 1] == '\n' && text[startOffset - 1] == '\n') {
CodeStyleManager.getInstance(project).adjustLineIndent(file, TextRange(startOffset, endOffset - 1))
}
else {
CodeStyleManager.getInstance(project).adjustLineIndent(file, TextRange(startOffset, endOffset))
}
}
}
private companion object {
fun countMatchingCharacters(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return editor.immutableText.countMatchingCharacters(offset, searchProcessor.query.rawText)
}
fun recordCaretPosition(editor: Editor) = with(editor) {
project?.let { addCurrentPositionToHistory(it, document) }
}
fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
selectionModel.removeSelection(true)
caretModel.removeSecondaryCarets()
caretModel.moveToOffset(offset)
}
fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int, cursorOffset: Int = toOffset) = with(editor) {
selectionModel.removeSelection(true)
selectionModel.setSelection(fromOffset, toOffset)
caretModel.moveToOffset(cursorOffset)
}
fun performAction(actionName: String) {
val actionManager = ActionManager.getInstance()
val action = actionManager.getAction(actionName)
@@ -170,7 +60,7 @@ sealed class AceTagAction {
val project = editor.project ?: return
val fem = FileEditorManagerEx.getInstanceEx(project)
val window = fem.windows.firstOrNull { (it.selectedEditor?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
val window = fem.windows.firstOrNull { (it.selectedComposite?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
if (window != null && window !== fem.currentWindow) {
fem.currentWindow = window
}
@@ -193,247 +83,7 @@ sealed class AceTagAction {
* On default action, places the caret at the first character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpToSearchStart : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset
}
}
/**
* On default action, places the caret at the last character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpToSearchEnd : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset + max(0, countMatchingCharacters(editor, searchProcessor, offset) - 1)
}
}
/**
* On default action, places the caret just past the last character of the search query.
* On shift action, adds the new caret to existing carets.
*/
object JumpPastSearchEnd : BaseJumpAction() {
override fun getCaretOffset(editor: Editor, searchProcessor: SearchProcessor, offset: Int): Int {
return offset + countMatchingCharacters(editor, searchProcessor, offset)
}
}
/**
* On default action, places the caret at the start of a word. Word detection uses [Character.isJavaIdentifierPart] to count some special
* characters, such as underscores, as part of a word. If there is no word at the last character of the search query, then the caret is
* placed at the first character of the search query.
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordStart : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordStart(queryEndOffset)
else
queryStartOffset
}
}
/**
* On default action, places the caret at the end of a word. Word detection uses [Character.isJavaIdentifierPart] to count some special
* characters, such as underscores, as part of a word. If there is no word at the last character of the search query, then the caret is
* placed after the last character of the search query.
*
* On shift action, adds the new caret to existing carets.
*/
object JumpToWordEnd : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
return if (isInsideWord)
editor.immutableText.wordEnd(queryEndOffset) + 1
else
queryEndOffset
}
}
/**
* On default action, places the caret at the end of the line.
* On shift action, adds the new caret to existing carets.
*/
object JumpToLineEnd : BaseWordAction() {
override fun getCaretOffset(editor: Editor, queryStartOffset: Int, queryEndOffset: Int, isInsideWord: Boolean): Int {
val document = editor.document
val line = document.getLineNumber(queryEndOffset)
return document.getLineEndOffset(line)
}
}
/**
* On default action, selects all characters covered by the search query.
* On shift action, adds the new selection to existing selections.
*/
object SelectQuery : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
recordCaretPosition(editor)
val startOffset = JumpToSearchStart.getCaretOffset(editor, searchProcessor, offset)
val endOffset = JumpPastSearchEnd.getCaretOffset(editor, searchProcessor, offset)
selectRange(editor, startOffset, endOffset)
}
}
/**
* On default action, places the caret at the end of a word, and also selects the entire word. Word detection uses
* [Character.isJavaIdentifierPart] to count some special characters, such as underscores, as part of a word. If there is no word at the
* last character of the search query, then the caret is placed after the last character of the search query, and all text between the
* start and end of the search query is selected.
*
* On shift action, adds the new selection to existing selections.
*/
object SelectWord : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = JumpToWordStart.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
val endOffset = JumpToWordEnd.getCaretOffset(editor, offset, queryEndOffset, isInsideWord = true)
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
}
}
}
/**
* On default action, places the caret at the end of a camel hump inside a word, and also selects the hump. If there is no word at the
* last character of the search query, then the search query is selected. See [SelectWord] and [SelectQuery] for details.
*
* On shift action, adds the new selection to existing selections.
*/
object SelectHump : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val chars = editor.immutableText
val queryEndOffset = JumpToSearchEnd.getCaretOffset(editor, searchProcessor, offset)
if (chars[queryEndOffset].isWordPart) {
recordCaretPosition(editor)
val startOffset = chars.humpStart(queryEndOffset)
val endOffset = chars.humpEnd(queryEndOffset) + 1
selectRange(editor, startOffset, endOffset)
}
else {
SelectQuery(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
}
}
}
/**
* On default action, selects the line at the tag, excluding the indent.
* On shift action, adds the new selection to existing selections.
*/
object SelectLine : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
val document = editor.document
val line = editor.caretModel.logicalPosition.line
val lineStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(document, line)
val lineEnd = document.getLineEndOffset(line)
selectRange(editor, lineStart, lineEnd)
}
}
/**
* On default action, places the caret at the last character of the search query, and then performs Extend Selection a set amount of
* times.
*
* On shift action, adds the new selection to existing selections.
*/
class SelectExtended(private val extendCount: Int) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
JumpToSearchEnd(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
repeat(extendCount) {
performAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
}
}
}
/**
* On default action, selects the range between the caret and a position decided by the provided [BaseJumpAction].
* On shift action, adds the new selection to existing selections.
*/
class SelectToCaret(private val jumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
val caretModel = editor.caretModel
val oldOffset = caretModel.offset
val oldSelection = editor.selectionModel.takeIf { it.hasSelection(false) }?.let { it.selectionStart..it.selectionEnd }
jumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
val newOffset = caretModel.offset
if (oldSelection == null) {
selectRange(editor, oldOffset, newOffset)
}
else {
selectRange(editor, minOf(oldOffset, newOffset, oldSelection.first), maxOf(oldOffset, newOffset, oldSelection.last), newOffset)
}
}
}
/**
* On default action, selects the range between [firstOffset] and a position decided by the provided [BaseJumpAction].
* On shift action, adds the new selection to existing selections.
*/
class SelectBetweenPoints(private val firstOffset: Int, private val secondOffsetJumper: BaseJumpAction) : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
secondOffsetJumper(editor, searchProcessor, offset, shiftMode = false, isFinal = true)
selectRange(editor, firstOffset, editor.caretModel.offset)
}
}
/**
* Selects text based on the provided [selector] action and clones it at every existing caret, selecting the cloned text. If a caret
* has a selection, the selected text will be replaced.
*/
class CloneToCaret(selector: AceTagAction) : BasePerCaretWriteAction(selector) {
override fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean) {
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Clone").run<Throwable> {
insertAtCarets(editor, editor.document.getText(range))
}
}
}
/**
* Selects text based on the provided [selector] action and clones it to every existing caret, selecting the cloned text and deleting
* the original. If a caret has a selection, the selected text will be replaced.
*/
open class MoveToCaret(selector: AceTagAction) : BasePerCaretWriteAction(selector) {
override fun invoke(editor: Editor, range: TextRange, shiftMode: Boolean) {
val difference = if (shiftMode) editor.caretModel.caretsAndSelections.sumBy {
val start = it.selectionStart?.let(editor::logicalPositionToOffset)
val end = it.selectionEnd?.let(editor::logicalPositionToOffset)
if (start == null || end == null || end > range.endOffset) 0 else range.length - (end - start)
} else 0
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Move").run<Throwable> {
val document = editor.document
val text = document.getText(range)
document.deleteString(range.startOffset, range.endOffset)
insertAtCarets(editor, text)
}
if (shiftMode) {
editor.selectionModel.removeSelection(true)
editor.caretModel.moveToOffset(range.startOffset + difference)
}
}
}
object JumpToSearchStart : BaseJumpAction()
/**
* On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
@@ -441,44 +91,9 @@ sealed class AceTagAction {
* Always places the caret at the start of the word.
*/
object GoToDeclaration : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
override fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) {
JumpToSearchStart.invoke(tag, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_GOTO_TYPE_DECLARATION else IdeActions.ACTION_GOTO_DECLARATION) }
}
}
/**
* On default action, performs the Show Usages action, available via the context menu.
* On shift action, performs the Find Usages action, available via the context menu.
* Always places the caret at the start of the word.
*/
object ShowUsages : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_FIND_USAGES else ShowUsagesAction.ID) }
}
}
/**
* Performs the Show Context Actions action, available via the context menu or Alt+Enter.
* Always places the caret at the start of the word.
*/
object ShowIntentions : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS) }
}
}
/**
* On default action, performs the Refactor This action, available via the main menu.
* On shift action, performs the Rename... refactoring, available via the main menu.
* Always places the caret at the start of the word.
*/
object Refactor : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean, isFinal: Boolean) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false, isFinal = isFinal)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_RENAME else "Refactorings.QuickListPopupAction") }
}
}
}

View File

@@ -1,148 +1,185 @@
package org.acejump.action
import org.acejump.boundaries.Boundaries
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.project.DumbAwareAction
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.action.change.change.ChangeVisualAction
import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction
import com.maddyhome.idea.vim.action.copy.YankVisualAction
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode.OP_PENDING
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.vimSetSelection
import com.maddyhome.idea.vim.helper.inVisualMode
import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.SelectionType
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
import org.acejump.boundaries.StandardBoundaries.CARET_LINE
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.modes.ActionMode
import org.acejump.modes.VimJumpMode
import org.acejump.modes.JumpMode
import org.acejump.search.Pattern
import org.acejump.session.Session
import org.acejump.search.Tag
import org.acejump.session.SessionManager
import org.acejump.session.SessionState
sealed class AceVimAction : AceKeyboardAction() {
protected abstract val boundary: Boundaries
sealed class AceVimAction : DumbAwareAction() {
protected abstract val mode: AceVimMode
final override fun invoke(session: Session) {
session.defaultBoundary = boundary
start(session)
}
protected open fun start(session: Session) {
session.startJumpMode(::VimJumpMode)
}
class JumpToChar : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
}
class JumpToCharAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
}
class JumpToCharBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
}
class LWordsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD)
final override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val context = e.dataContext.vim
val caret = editor.caretModel.currentCaret
val initialOffset = caret.offset
val selectionStart = if (editor.inVisualMode) caret.vimSelectionStart else null
val session = SessionManager.start(editor, mode.getJumpEditors(editor))
session.defaultBoundary = mode.boundaries
session.startJumpMode {
object : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag) {
AceTagAction.JumpToSearchStart.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true)
if (selectionStart != null) {
caret.vim.vimSetSelection(selectionStart, caret.offset, moveCaretToSelectionEnd = true)
}
else {
val vim = editor.vim
val commandState = vim.vimStateMachine
if (commandState.isOperatorPending) {
val key = commandState.commandBuilder.keys.singleOrNull()?.keyChar
commandState.reset()
KeyHandler.getInstance().fullReset(vim)
AceVimUtil.enterVisualMode(vim, SelectionType.CHARACTER_WISE)
caret.vim.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true)
val action = when (key) {
'd' -> DeleteVisualAction()
'c' -> ChangeVisualAction()
'y' -> YankVisualAction()
else -> null
}
if (action != null) {
ApplicationManager.getApplication().invokeLater {
WriteAction.run<Nothing> {
commandState.commandBuilder.pushCommandPart(action)
val cmd = commandState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(commandState.mappingState.mappingMode == OP_PENDING, cmd.rawCount, commandState.mode)
commandState.executingCommand = cmd
injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments)
// TODO does not update status
}
}
}
}
}
injector.scroll.scrollCaretIntoView(editor.vim)
mode.finishSession(editor, session)
}
}
}
mode.setupSession(editor, session)
}
class UWordsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD)
class JumpAllEditors : AceVimAction() {
override val mode = AceVimMode.JumpAllEditors
}
class JumpForward : AceVimAction() {
override val mode = AceVimMode.Jump(AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpBackward : AceVimAction() {
override val mode = AceVimMode.Jump(BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpTillForward : AceVimAction() {
override val mode = AceVimMode.JumpTillForward(AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpTillBackward : AceVimAction() {
override val mode = AceVimMode.JumpTillBackward(BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpOnLineForward : AceVimAction() {
override val mode = AceVimMode.Jump(AFTER_CARET.intersection(CARET_LINE))
}
class JumpOnLineBackward : AceVimAction() {
override val mode = AceVimMode.Jump(BEFORE_CARET.intersection(CARET_LINE))
}
class JumpLineIndentsForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLineIndentsBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.LINE_INDENTS, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordEndForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordEndForward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, AFTER_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpLWordEndBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_LWORD_END, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpUWordEndBackward : AceVimAction() {
override val mode = AceVimMode.JumpToPattern(Pattern.VIM_UWORD_END, BEFORE_CARET.intersection(VISIBLE_ON_SCREEN))
}
class JumpAllEditorsGoToDeclaration : DumbAwareAction() {
override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(CommonDataKeys.EDITOR) != null
}
}
class LWordsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD)
}
}
class UWordsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD)
}
}
class LWordEndsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD_END)
}
}
class UWordEndsAfterCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(AFTER_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD_END)
}
}
class LWordEndsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_LWORD_END)
}
}
class UWordEndsBeforeCaret : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN.intersection(BEFORE_CARET)
override fun start(session: Session) {
super.start(session)
session.startRegexSearch(Pattern.VIM_UWORD_END)
}
}
class GoToDeclaration : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.GoToDeclaration, shiftMode = false) }
}
}
class GoToTypeDeclaration : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.GoToDeclaration, shiftMode = true) }
}
}
class ShowIntentions : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.ShowIntentions, shiftMode = false) }
}
}
class ShowUsages : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.ShowUsages, shiftMode = false) }
}
}
class FindUsages : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.ShowUsages, shiftMode = true) }
}
}
class Refactor : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.Refactor, shiftMode = false) }
}
}
class Rename : AceVimAction() {
override val boundary = VISIBLE_ON_SCREEN
override fun start(session: Session) {
session.startJumpMode { ActionMode(AceTagAction.Refactor, shiftMode = true) }
override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val session = SessionManager.start(editor, AceVimMode.JumpAllEditors.getJumpEditors(editor))
session.defaultBoundary = VISIBLE_ON_SCREEN
session.startJumpMode {
object : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag) {
AceTagAction.GoToDeclaration.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true)
}
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
package org.acejump.action
import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.openEditors
import org.acejump.search.Pattern
import org.acejump.session.Session
sealed class AceVimMode {
abstract val boundaries: Boundaries
open fun getJumpEditors(mainEditor: Editor): List<Editor> {
return listOf(mainEditor)
}
open fun setupSession(editor: Editor, session: Session) {}
open fun finishSession(editor: Editor, session: Session) {}
class Jump(override val boundaries: Boundaries) : AceVimMode()
object JumpAllEditors : AceVimMode() {
override val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
override fun getJumpEditors(mainEditor: Editor): List<Editor> {
val project = mainEditor.project ?: return super.getJumpEditors(mainEditor)
return project.openEditors
.sortedBy { if (it === mainEditor) 0 else 1 }
.ifEmpty { listOf(mainEditor) }
}
}
class JumpTillForward(override val boundaries: Boundaries) : AceVimMode() {
override fun finishSession(editor: Editor, session: Session) {
val document = editor.document
for (caret in editor.caretModel.allCarets) {
val offset = caret.offset
if (offset > document.getLineStartOffset(document.getLineNumber(offset))) {
caret.moveToOffset(offset - 1, false)
}
}
}
}
class JumpTillBackward(override val boundaries: Boundaries) : AceVimMode() {
override fun finishSession(editor: Editor, session: Session) {
val document = editor.document
for (caret in editor.caretModel.allCarets) {
val offset = caret.offset
if (offset < document.getLineEndOffset(document.getLineNumber(offset))) {
caret.moveToOffset(offset + 1, false)
}
}
}
}
class JumpToPattern(private val pattern: Pattern, override val boundaries: Boundaries) : AceVimMode() {
override fun setupSession(editor: Editor, session: Session) {
session.startRegexSearch(pattern)
}
}
}

View File

@@ -0,0 +1,13 @@
package org.acejump.action;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.VimEditor;
import com.maddyhome.idea.vim.state.mode.SelectionType;
final class AceVimUtil {
private AceVimUtil() {}
public static void enterVisualMode(final VimEditor vim, final SelectionType mode) {
VimPlugin.getVisualMotion().enterVisualMode(vim, mode);
}
}

View File

@@ -51,5 +51,18 @@ enum class StandardBoundaries : Boundaries {
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset > editor.caretModel.offset
}
},
CARET_LINE {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
val document = editor.document
val offset = editor.caretModel.offset
val line = document.getLineNumber(offset)
return document.getLineStartOffset(line)..document.getLineEndOffset(line)
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset in getOffsetRange(editor, cache)
}
}
}

View File

@@ -20,13 +20,11 @@ class AceConfig : PersistentStateComponent<AceSettings> {
val layout get() = settings.layout
val minQueryLength get() = settings.minQueryLength
val editorFadeOpacity get() = settings.editorFadeOpacity
val jumpModeColor get() = settings.jumpModeColor
val advancedModeColor get() = settings.advancedModeColor
val betweenPointsModeColor get() = settings.betweenPointsModeColor
val textHighlightColor get() = settings.textHighlightColor
val tagForegroundColor get() = settings.tagForegroundColor
val tagBackgroundColor get() = settings.tagBackgroundColor
val acceptedTagColor get() = settings.acceptedTagColor
val tagForegroundColor1 get() = settings.tagForegroundColor1
val tagForegroundColor2 get() = settings.tagForegroundColor2
val searchHighlightColor get() = settings.searchHighlightColor
}
override fun getState(): AceSettings {

View File

@@ -13,27 +13,25 @@ class AceConfigurable : Configurable {
override fun isModified() =
panel.allowedChars != settings.allowedChars ||
panel.prefixChars != settings.prefixChars ||
panel.keyboardLayout != settings.layout ||
panel.minQueryLengthInt != settings.minQueryLength ||
panel.editorFadeOpacityPercent != settings.editorFadeOpacity ||
panel.jumpModeColor != settings.jumpModeColor ||
panel.advancedModeColor != settings.advancedModeColor ||
panel.betweenPointsModeColor != settings.betweenPointsModeColor ||
panel.textHighlightColor != settings.textHighlightColor ||
panel.tagForegroundColor != settings.tagForegroundColor ||
panel.tagBackgroundColor != settings.tagBackgroundColor ||
panel.acceptedTagColor != settings.acceptedTagColor
panel.tagForegroundColor1 != settings.tagForegroundColor1 ||
panel.tagForegroundColor2 != settings.tagForegroundColor2 ||
panel.searchHighlightColor != settings.searchHighlightColor
override fun apply() {
settings.allowedChars = panel.allowedChars
settings.prefixChars = panel.prefixChars
settings.layout = panel.keyboardLayout
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
settings.editorFadeOpacity = panel.editorFadeOpacityPercent
panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.advancedModeColor?.let { settings.advancedModeColor = it }
panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = it }
panel.textHighlightColor?.let { settings.textHighlightColor = it }
panel.tagForegroundColor?.let { settings.tagForegroundColor = it }
panel.tagBackgroundColor?.let { settings.tagBackgroundColor = it }
panel.acceptedTagColor?.let { settings.acceptedTagColor = it }
panel.tagForegroundColor1?.let { settings.tagForegroundColor1 = it }
panel.tagForegroundColor2?.let { settings.tagForegroundColor2 = it }
panel.searchHighlightColor?.let { settings.searchHighlightColor = it }
KeyLayoutCache.reset(settings)
}

View File

@@ -8,26 +8,19 @@ import java.awt.Color
data class AceSettings(
var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars,
var prefixChars: String = ";",
var minQueryLength: Int = 1,
var editorFadeOpacity: Int = 70,
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("advancedModeRGB", converter = ColorConverter::class)
var advancedModeColor: Color = Color(0xFFB700),
@OptionTag("betweenPointsModeRGB", converter = ColorConverter::class)
var betweenPointsModeColor: Color = Color(0x6FC5FF),
@OptionTag("textHighlightRGB", converter = ColorConverter::class)
var textHighlightColor: Color = Color(0x394B58),
@OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var tagForegroundColor: Color = Color(0xFFFFFF),
var tagForegroundColor1: Color = Color(0xFFFFFF),
@OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
var tagBackgroundColor: Color = Color(0x008299),
@OptionTag("tagForeground2RGB", converter = ColorConverter::class)
var tagForegroundColor2: Color = Color(0xFFFFFF),
@OptionTag("acceptedTagRGB", converter = ColorConverter::class)
var acceptedTagColor: Color = Color(0x394B58)
@OptionTag("searchHighlightRGB", converter = ColorConverter::class)
var searchHighlightColor: Color = Color(0x008299),
)

View File

@@ -2,6 +2,7 @@ package org.acejump.config
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBSlider
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.Cell
@@ -11,9 +12,12 @@ import com.intellij.ui.layout.panel
import org.acejump.input.KeyLayout
import java.awt.Color
import java.awt.Font
import java.util.Hashtable
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JSlider
import javax.swing.text.JTextComponent
import kotlin.reflect.KProperty
@@ -22,20 +26,27 @@ import kotlin.reflect.KProperty
*/
@Suppress("UsePropertyAccessSyntax")
internal class AceSettingsPanel {
private val tagCharsField = JBTextField()
private val tagAllowedCharsField = JBTextField()
private val tagPrefixCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val minQueryLengthField = JBTextField()
private val editorFadeOpacitySlider = JBSlider(0, 10).apply {
labelTable = Hashtable((0..10).associateWith { JLabel("${it * 10}") })
paintTrack = true
paintLabels = true
paintTicks = true
minorTickSpacing = 1
majorTickSpacing = 1
}
private val jumpModeColorWheel = ColorPanel()
private val advancedModeColorWheel = ColorPanel()
private val betweenPointsModeColorWheel = ColorPanel()
private val textHighlightColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel()
private val tagBackgroundColorWheel = ColorPanel()
private val acceptedTagColorWheel = ColorPanel()
private val tagForeground1ColorWheel = ColorPanel()
private val tagForeground2ColorWheel = ColorPanel()
private val searchHighlightColorWheel = ColorPanel()
init {
tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
tagAllowedCharsField.apply { font = Font("monospaced", font.style, font.size) }
tagPrefixCharsField.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") }
}
@@ -45,7 +56,8 @@ internal class AceSettingsPanel {
fun Cell.medium(component: JComponent) = component(growPolicy = MEDIUM_TEXT)
titledRow("Characters and Layout") {
row("Allowed characters in tags:") { medium(tagCharsField) }
row("Allowed characters in tags:") { medium(tagAllowedCharsField) }
row("Allowed prefix characters in tags:") { medium(tagPrefixCharsField) }
row("Keyboard layout:") { short(keyboardLayoutCombo) }
row("Keyboard design:") { short(keyboardLayoutArea) }
}
@@ -55,44 +67,58 @@ internal class AceSettingsPanel {
}
titledRow("Colors") {
row("Jump mode caret background:") { short(jumpModeColorWheel) }
row("Advanced mode caret background:") { short(advancedModeColorWheel) }
row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) }
row("Searched text background:") { short(textHighlightColorWheel) }
row("Tag foreground:") { short(tagForegroundColorWheel) }
row("Tag background:") { short(tagBackgroundColorWheel) }
row("Accepted tag position background:") { short(acceptedTagColorWheel) }
row("Caret background:") {
cell {
component(jumpModeColorWheel)
}
}
row("Tag foreground:") {
cell {
component(tagForeground1ColorWheel)
component(tagForeground2ColorWheel)
}
}
row("Search highlight:") {
cell {
component(searchHighlightColorWheel)
}
}
row("Editor fade opacity (%):") {
medium(editorFadeOpacitySlider)
}
}
}
// Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342
internal var allowedChars by tagCharsField
internal var allowedChars by tagAllowedCharsField
internal var prefixChars by tagPrefixCharsField
internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea
internal var minQueryLength by minQueryLengthField
internal var editorFadeOpacity by editorFadeOpacitySlider
internal var jumpModeColor by jumpModeColorWheel
internal var advancedModeColor by advancedModeColorWheel
internal var betweenPointsModeColor by betweenPointsModeColorWheel
internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel
internal var acceptedTagColor by acceptedTagColorWheel
internal var tagForegroundColor1 by tagForeground1ColorWheel
internal var tagForegroundColor2 by tagForeground2ColorWheel
internal var searchHighlightColor by searchHighlightColorWheel
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) { minQueryLength = value.toString() }
internal var editorFadeOpacityPercent
get() = editorFadeOpacity * 10
set(value) { editorFadeOpacity = value / 10 }
fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars
prefixChars = settings.prefixChars
keyboardLayout = settings.layout
minQueryLength = settings.minQueryLength.toString()
editorFadeOpacityPercent = settings.editorFadeOpacity
jumpModeColor = settings.jumpModeColor
advancedModeColor = settings.advancedModeColor
betweenPointsModeColor = settings.betweenPointsModeColor
textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor
tagBackgroundColor = settings.tagBackgroundColor
acceptedTagColor = settings.acceptedTagColor
tagForegroundColor1 = settings.tagForegroundColor1
tagForegroundColor2 = settings.tagForegroundColor2
searchHighlightColor = settings.searchHighlightColor
}
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
@@ -106,6 +132,9 @@ internal class AceSettingsPanel {
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
private operator fun JCheckBox.setValue(a: AceSettingsPanel, p: KProperty<*>, selected: Boolean) = setSelected(selected)
private operator fun JSlider.getValue(a: AceSettingsPanel, p: KProperty<*>) = value
private operator fun JSlider.setValue(a: AceSettingsPanel, p: KProperty<*>, value: Int) = setValue(value)
private operator fun <T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)

View File

@@ -4,13 +4,25 @@ package org.acejump.input
* Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how
* ergonomically difficult they are to press.
*/
@Suppress("unused")
enum class KeyLayout(internal val rows: Array<String>, priority: String) {
@Suppress("unused", "SpellCheckingInspection")
enum class KeyLayout(internal val rows: Array<String>, priority: String, internal val characterRemapping: Map<Char, Char> = emptyMap()) {
COLEMK(arrayOf("1234567890", "qwfpgjluy", "arstdhneio", "zxcvbkm"), priority = "tndhseriaovkcmbxzgjplfuwyq5849673210"),
WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"),
DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"),
QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210"),
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QWERTZ_CZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterRemapping = mapOf(
'+' to '1',
'ě' to '2',
'š' to '3',
'č' to '4',
'ř' to '5',
'ž' to '6',
'ý' to '7',
'á' to '8',
'í' to '9',
'é' to '0'
)),
QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"),
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210");
@@ -18,7 +30,7 @@ enum class KeyLayout(internal val rows: Array<String>, priority: String) {
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int? {
return { allPriorities[tagToChar(it)] }
internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int {
return { allPriorities.getOrDefault(tagToChar(it), Int.MAX_VALUE) }
}
}

View File

@@ -7,66 +7,17 @@ import org.acejump.config.AceSettings
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/
internal object KeyLayoutCache {
/**
* Stores keys ordered by proximity to other keys for the QWERTY layout.
* TODO: Support more layouts, perhaps generate automatically.
*/
private val qwertyCharacterDistances = mapOf(
'j' to "jikmnhuolbgypvftcdrxsezawq8796054321",
'f' to "ftgvcdryhbxseujnzawqikmolp5463728190",
'k' to "kolmjipnhubgyvftcdrxsezawq9807654321",
'd' to "drfcxsetgvzawyhbqujnikmolp4352617890",
'l' to "lkopmjinhubgyvftcdrxsezawq0987654321",
's' to "sedxzawrfcqtgvyhbujnikmolp3241567890",
'a' to "aqwszedxrfctgvyhbujnikmolp1234567890",
'h' to "hujnbgyikmvftolcdrpxsezawq6758493021",
'g' to "gyhbvftujncdrikmxseolzawpq5647382910",
'y' to "yuhgtijnbvfrokmcdeplxswzaq6758493021",
't' to "tygfruhbvcdeijnxswokmzaqpl5647382910",
'u' to "uijhyokmnbgtplvfrcdexswzaq7869504321",
'r' to "rtfdeygvcxswuhbzaqijnokmpl4536271890",
'n' to "nbhjmvgyuiklocftpxdrzseawq7685940321",
'v' to "vcfgbxdrtyhnzseujmawikqolp5463728190",
'm' to "mnjkbhuilvgyopcftxdrzseawq8970654321",
'c' to "cxdfvzsertgbawyhnqujmikolp4352617890",
'b' to "bvghncftyujmxdrikzseolawqp6574839201",
'i' to "iokjuplmnhybgtvfrcdexswzaq8970654321",
'e' to "erdswtfcxzaqygvuhbijnokmpl3425167890",
'x' to "xzsdcawerfvqtgbyhnujmikolp3241567890",
'z' to "zasxqwedcrfvtgbyhnujmikolp1234567890",
'o' to "oplkimjunhybgtvfrcdexswzaq9087654321",
'w' to "wesaqrdxztfcygvuhbijnokmpl2314567890",
'p' to "plokimjunhybgtvfrcdexswzaq0987654321",
'q' to "qwaeszrdxtfcygvuhbijnokmpl1234567890",
'1' to "1234567890qawzsexdrcftvgybhunjimkolp",
'2' to "2134567890qwasezxdrcftvgybhunjimkolp",
'3' to "3241567890weqasdrzxcftvgybhunjimkolp",
'4' to "4352617890erwsdftqazxcvgybhunjimkolp",
'5' to "5463728190rtedfgywsxcvbhuqaznjimkolp",
'6' to "6574839201tyrfghuedcvbnjiwsxmkoqazlp",
'7' to "7685940321yutghjirfvbnmkoedclpwsxqaz",
'8' to "8796054321uiyhjkotgbnmlprfvedcwsxqaz",
'9' to "9807654321ioujklpyhnmtgbrfvedcwsxqaz",
'0' to "0987654321opiklujmyhntgbrfvedcwsxqaz").mapValues { (_, v) -> v.mapIndexed { index, char -> char to index }.toMap() }
/**
* Sorts tags according to current keyboard layout settings, and some predefined rules that force tags with digits, and tags with two
* keys far apart, to be sorted after other (easier to type) tags.
*/
lateinit var tagOrder: Comparator<String>
private set
/**
* Returns all possible two key tags, pre-sorted according to [tagOrder].
*/
lateinit var allPossibleTags: List<String>
lateinit var allPossibleTagsLowercase: List<String>
private set
/**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/
fun ensureInitialized(settings: AceSettings) {
if (!::tagOrder.isInitialized) {
if (!::allPossibleTagsLowercase.isInitialized) {
reset(settings)
}
}
@@ -75,19 +26,22 @@ internal object KeyLayoutCache {
* Re-initializes cached data according to updated settings.
*/
fun reset(settings: AceSettings) {
tagOrder = compareBy(
{ it[0].isDigit() || it[1].isDigit() },
{ qwertyCharacterDistances.getValue(it[0]).getValue(it[1]) },
settings.layout.priority { it[0] }
@Suppress("ConvertLambdaToReference")
val allSuffixChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars).toList() }
val allPrefixChars = processCharList(settings.prefixChars).filterNot(allSuffixChars::contains).plus("")
val tagOrder = compareBy(
String::length,
{ if (it.length == 1) Int.MIN_VALUE else allPrefixChars.indexOf(it.first().toString()) },
settings.layout.priority(String::last)
)
val allPossibleChars = settings.allowedChars
.toCharArray()
.filter(Char::isLetterOrDigit)
.distinct()
.joinToString("")
.ifEmpty(settings.layout::allChars)
allPossibleTags = allPossibleChars.flatMap { a -> allPossibleChars.map { b -> "$a$b".intern() } }.sortedWith(tagOrder)
allPossibleTagsLowercase = allSuffixChars
.flatMap { suffix -> allPrefixChars.map { prefix -> "$prefix$suffix" } }
.sortedWith(tagOrder)
}
private fun processCharList(charList: String): Set<String> {
return charList.toCharArray().map(Char::lowercase).toSet()
}
}

View File

@@ -1,12 +0,0 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.search.Tag
import org.acejump.session.SessionState
class ActionMode(private val action: AceTagAction, private val shiftMode: Boolean) : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
state.act(action, acceptedTag, shiftMode, isFinal = true)
return true
}
}

View File

@@ -1,73 +0,0 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class AdvancedMode : SessionMode {
companion object {
private val JUMP_HINT = arrayOf(
"<f>[J]</f>ump / <f>[L]</f> past Query",
"<f>[E]</f> Word End / <f>[M]</f> Line End"
)
val JUMP_ALT_HINT = JUMP_HINT.map { it.replace("<f>[J]</f>ump ", "<f>[J]</f> at Tag ") }.toTypedArray()
val JUMP_ACTION_MAP = mapOf(
'J' to AceTagAction.JumpToSearchStart,
'L' to AceTagAction.JumpPastSearchEnd,
'E' to AceTagAction.JumpToWordEnd,
'M' to AceTagAction.JumpToLineEnd
)
val SELECT_HINT = arrayOf(
"Select <f>[W]</f>ord / <f>[H]</f>ump",
"Select <f>[Q]</f>uery / <f>[N]</f> Line / <f>[1-9]</f> Expansion"
)
val SELECT_ACTION_MAP = mapOf(
'W' to AceTagAction.SelectWord,
'H' to AceTagAction.SelectHump,
'Q' to AceTagAction.SelectQuery,
'N' to AceTagAction.SelectLine,
*('1'..'9').mapIndexed { index, char -> char to AceTagAction.SelectExtended(index + 1) }.toTypedArray()
)
private val ALL_HINTS = arrayOf(
*JUMP_HINT,
*SELECT_HINT,
)
private val ALL_ACTION_MAP = mapOf(
*JUMP_ACTION_MAP.map { it.key to it.value }.toTypedArray(),
*SELECT_ACTION_MAP.map { it.key to it.value }.toTypedArray(),
)
}
override val caretColor
get() = AceConfig.advancedModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
if (acceptedTag == null) {
return state.type(charTyped)
}
val action = ALL_ACTION_MAP[charTyped.toUpperCase()]
if (action != null) {
state.act(action, acceptedTag, shiftMode = charTyped.isUpperCase(), isFinal = true)
return TypeResult.EndSession
}
return TypeResult.Nothing
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
return false
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return ALL_HINTS.takeIf { acceptedTag != null }
}
}

View File

@@ -1,92 +0,0 @@
package org.acejump.modes
import com.intellij.openapi.editor.CaretState
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class BetweenPointsMode : SessionMode {
private companion object {
private val TYPE_TAG_HINT = arrayOf(
"<b>Type to Search...</b>"
)
private val ACTION_MODE_HINT = arrayOf(
"<f>[C]</f>lone to Caret...",
"<f>[M]</f>ove to Caret..."
)
private val ACTION_MODE_MAP = mapOf(
'C' to (AceTagAction::CloneToCaret),
'M' to (AceTagAction::MoveToCaret)
)
}
override val caretColor
get() = AceConfig.betweenPointsModeColor
private var actionMode: ((AceTagAction.BaseSelectAction) -> AceTagAction)? = null
private var originalCarets: List<CaretState>? = null
private var firstOffset: Int? = null
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult {
val actionMode = actionMode
if (actionMode == null) {
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing
}
if (acceptedTag == null) {
return state.type(charTyped)
}
if (firstOffset == null) {
val selectAction = AdvancedMode.SELECT_ACTION_MAP[charTyped.toUpperCase()]
if (selectAction != null) {
state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase(), isFinal = true)
return TypeResult.EndSession
}
}
val jumpAction = AdvancedMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
val firstOffset = firstOffset
if (firstOffset == null) {
val caretModel = acceptedTag.editor.caretModel
this.originalCarets = caretModel.caretsAndSelections
state.act(jumpAction, acceptedTag, shiftMode = false, isFinal = false)
this.firstOffset = caretModel.offset
return TypeResult.RestartSearch
}
originalCarets?.let { acceptedTag.editor.caretModel.caretsAndSelections = it }
state.act(
actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)),
acceptedTag,
shiftMode = charTyped.isUpperCase(),
isFinal = true
)
return TypeResult.EndSession
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
return false
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return when {
actionMode == null -> ACTION_MODE_HINT
acceptedTag == null -> TYPE_TAG_HINT.takeUnless { hasQuery }
firstOffset == null -> AdvancedMode.JUMP_ALT_HINT + AdvancedMode.SELECT_HINT
else -> AdvancedMode.JUMP_ALT_HINT
}
}
}

View File

@@ -18,12 +18,7 @@ open class JumpMode : SessionMode {
return state.type(charTyped)
}
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
state.act(AceTagAction.JumpToSearchStart, acceptedTag, wasUpperCase, isFinal = true)
return true
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return null
override fun accept(state: SessionState, acceptedTag: Tag) {
AceTagAction.JumpToSearchStart.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true)
}
}

View File

@@ -9,6 +9,5 @@ interface SessionMode {
val caretColor: Color
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult
fun accept(state: SessionState, acceptedTag: Tag): Boolean
fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>?
fun accept(state: SessionState, acceptedTag: Tag)
}

View File

@@ -1,17 +0,0 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.search.Tag
import org.acejump.session.SessionState
class VimJumpMode : JumpMode() {
override fun accept(state: SessionState, acceptedTag: Tag): Boolean {
val action = if (acceptedTag.editor.selectionModel.hasSelection())
AceTagAction.SelectToCaret(AceTagAction.JumpToSearchStart)
else
AceTagAction.JumpToSearchStart
state.act(action, acceptedTag, wasUpperCase, isFinal = true)
return true
}
}

View File

@@ -4,7 +4,6 @@ enum class Pattern(val regex: String) {
LINE_STARTS("^.|^\\n"),
LINE_ENDS("\\n|\\Z"),
LINE_INDENTS("[^\\s].*|^\\n"),
LINE_ALL_MARKS(LINE_ENDS.regex + "|" + LINE_STARTS.regex + "|" + LINE_INDENTS.regex),
ALL_WORDS("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
VIM_LWORD("(?<=[^a-zA-Z0-9_]|\\A)[a-zA-Z0-9_]"),
VIM_UWORD("(?<=\\s|\\A)[^\\s]"),

View File

@@ -4,19 +4,17 @@ import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries
import org.acejump.clone
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.matchesAt
/**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character.
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [refineQuery]s a character.
*/
class SearchProcessor private constructor(
private val editors: List<Editor>, query: SearchQuery, results: MutableMap<Editor, IntArrayList>
) {
class SearchProcessor private constructor(query: SearchQuery, private val results: MutableMap<Editor, IntArrayList>) {
companion object {
fun fromChar(editors: List<Editor>, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.Literal(char.toString()), boundaries)
fun fromString(editors: List<Editor>, query: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.Literal(query), boundaries)
}
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries): SearchProcessor {
@@ -24,7 +22,7 @@ class SearchProcessor private constructor(
}
}
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) {
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
val regex = query.toRegex()
if (regex != null) {
@@ -41,7 +39,7 @@ class SearchProcessor private constructor(
if (highlightEnd > offsetRange.last) {
break
}
else if (boundaries.isOffsetInside(editor, index)) {
else if (boundaries.isOffsetInside(editor, index) && !editor.foldingModel.isOffsetCollapsed(index)) {
offsets.add(index)
}
@@ -56,101 +54,40 @@ class SearchProcessor private constructor(
internal var query = query
private set
internal var results = results
private set
val resultsCopy
get() = results.clone()
/**
* Appends a character to the search query and removes all search results that no longer match the query. If the last typed character
* transitioned the search query from a non-word to a word, it notifies the [Tagger] to reassign all tags. If the new query does not
* make sense because it would remove every result, the change is reverted and this function returns false.
*/
fun type(char: Char, tagger: Tagger): Boolean {
val newQuery = query.rawText + char
val canMatchTag = tagger.canQueryMatchAnyTag(newQuery)
// If the typed character is not compatible with any existing tag or as a continuation of any previous occurrence, reject the query
// change and return false to indicate that nothing else should happen.
if (newQuery.length > 1 && !canMatchTag && !isContinuation(newQuery)) {
return false
}
// If the typed character transitioned the search query from a non-word to a word, and the typed character does not belong to an
// existing tag, we basically restart the search at the beginning of every new word, and unmark existing results so that all tags get
// regenerated immediately afterwards. Although this causes tags to change, it is one solution for conflicts between tag characters and
// search query characters, and moving searches across word boundaries during search should be fairly uncommon.
if (!canMatchTag && newQuery.length >= 2 && !newQuery[newQuery.length - 2].isWordPart && char.isWordPart) {
query = SearchQuery.Literal(char.toString())
tagger.unmark()
for ((editor, offsets) in results) {
val chars = editor.immutableText
val iter = offsets.iterator()
while (iter.hasNext()) {
val movedOffset = iter.nextInt() + newQuery.length - 1
if (movedOffset < chars.length && chars[movedOffset].equals(char, ignoreCase = true)) {
iter.set(movedOffset)
}
else {
iter.remove()
}
}
}
val isQueryFinished
get() = query.rawText.length >= AceConfig.minQueryLength
fun refineQuery(char: Char): Boolean {
if (char == '\n') {
return true
}
else {
removeObsoleteResults(newQuery, tagger)
query = SearchQuery.Literal(newQuery)
query = SearchQuery.Literal(query.rawText + char)
removeObsoleteResults()
return isQueryFinished
}
return true
}
/**
* Returns true if the new query is a continuation of any remaining search query.
*/
private fun isContinuation(newQuery: String): Boolean {
for ((editor, offsets) in results) {
val chars = editor.immutableText
if (offsets.any { chars.matchesAt(it, newQuery, ignoreCase = true) }) {
return true
}
}
return false
}
/**
* After updating the query, removes all results that no longer match the search query.
*/
private fun removeObsoleteResults(newQuery: String, tagger: Tagger) {
val lastCharOffset = newQuery.lastIndex
val lastChar = newQuery[lastCharOffset]
val ignoreCase = newQuery[0].isLowerCase()
private fun removeObsoleteResults() {
val query = query.rawText
for ((editor, offsets) in results.entries.toList()) {
val chars = editor.immutableText
for (entry in results) {
val editor = entry.key
val offsetIter = entry.value.iterator()
val remaining = IntArrayList()
val iter = offsets.iterator()
while (iter.hasNext()) {
val offset = iter.nextInt()
val endOffset = offset + lastCharOffset
val lastTypedCharMatches = endOffset < chars.length && chars[endOffset].equals(lastChar, ignoreCase)
while (offsetIter.hasNext()) {
val offset = offsetIter.nextInt()
if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, Tag(editor, offset))) {
remaining.add(offset)
if (!editor.immutableText.matchesAt(offset, query, ignoreCase = true)) {
offsetIter.remove()
}
}
results[editor] = remaining
}
}
fun clone(): SearchProcessor {
return SearchProcessor(editors, query, results.clone())
}
}

View File

@@ -1,231 +0,0 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache
import org.acejump.isWordPart
import org.acejump.wordEndPlus
import java.util.IdentityHashMap
import kotlin.math.max
/*
* Solves the Tag Assignment Problem. The tag assignment problem can be stated
* thusly: Given a set of indices I in document d, and a set of tags T, find a
* bijection f: T*⊂T → I*⊂I s.t. d[i..k] + t ∉ d[i'..(k + |t|)], ∀ i' ∈ I\{i},
* ∀ k ∈ (i, |d|-|t|], where t ∈ T, i ∈ I. Maximize |I*|. This can be relaxed
* to t=t[0] and ∀ k ∈ (i, i+K] for some fixed K, in most natural documents.
*
* More concretely, tags are typically two-character strings containing alpha-
* numeric symbols. Documents are plaintext files. Indices are produced by a
* search query of length N, i.e. the preceding N characters of every index i in
* document d are identical. For characters proceeding d[i], all bets are off.
* We can assume that P(d[i]|d[i-1]) has some structure for d~D. Ultimately, we
* want a fast algorithm which maximizes the number of tagged document indices.
*
* Tags are used by the typist to select indices within a document. To select an
* index, the typist starts by activating AceJump and searching for a character.
* As soon as the first character is received, we begin to scan the document for
* matching locations and assign as many valid tags as possible. When subsequent
* characters are received, we refine the search results to match either:
*
* 1.) The plaintext query alone, or
* 2.) The concatenation of plaintext query and partial tag
*
* The constraint in paragraph no. 1 tries to impose the following criteria:
*
* 1.) All valid key sequences will lead to a unique location in the document
* 2.) All indices in the document will be reachable by a short key sequence
*
* If there is an insufficient number of two-character tags to cover every index
* (which typically occurs when the user searches for a common character within
* a long document), then we attempt to maximize the number of tags assigned to
* document indices. The key is, all tags must be assigned as soon as possible,
* i.e. as soon as the first character is received or whenever the user ceases
* typing (at the very latest). Once assigned, a visible tag must never change
* at any time during the selection process, so as not to confuse the user.
*/
internal class Solver private constructor(
private val editorPriority: List<Editor>,
private val queryLength: Int,
private val newResults: Map<Editor, IntList>,
private val allResults: Map<Editor, IntList>,
) {
companion object {
fun solve(
editorPriority: List<Editor>,
query: SearchQuery,
newResults: Map<Editor, IntList>,
allResults: Map<Editor, IntList>,
tags: List<String>,
caches: Map<Editor, EditorOffsetCache>,
): Map<String, Tag> {
return Solver(editorPriority, max(1, query.rawText.length), newResults, allResults).map(tags, caches)
}
}
private var newTags = HashMap<String, Tag>(KeyLayoutCache.allPossibleTags.size)
private val newTagIndices = newResults.keys.associateWith { IntOpenHashSet() }
private var allWordFragments = HashSet<String>(allResults.values.sumBy(IntList::size)).apply {
for ((editor, offsets) in allResults) {
val chars = editor.immutableText
val iter = offsets.iterator()
while (iter.hasNext()) {
forEachWordFragment(chars, iter.nextInt(), this::add)
}
}
}
private fun generateEligibleSites(availableTags: List<String>): Map<String, MutableList<Tag>> {
val eligibleSitesByTag = HashMap<String, MutableList<Tag>>(100)
val tagsByFirstLetter = availableTags.groupBy { it[0] }
for ((editor, offsets) in newResults) {
val chars = editor.immutableText
val iter = offsets.iterator()
while (iter.hasNext()) {
val site = iter.nextInt()
for ((firstLetter, tags) in tagsByFirstLetter.entries) {
if (canTagBeginWithChar(chars, site, firstLetter)) {
for (tag in tags) {
eligibleSitesByTag.getOrPut(tag, ::mutableListOf).add(Tag(editor, site))
}
}
}
}
}
return eligibleSitesByTag
}
private fun generateMatchingSites(
eligibleSites: Map<String, MutableList<Tag>>,
caches: Map<Editor, EditorOffsetCache>,
): Map<String, Iterator<Tag>> {
val matchingSites = HashMap<MutableList<Tag>, Iterator<Tag>>()
val matchingSitesSorted = IdentityHashMap<String, Iterator<Tag>>() // Keys are guaranteed to be from a single collection.
val siteOrder = siteOrder(caches)
for ((mark, tags) in eligibleSites.entries) {
matchingSitesSorted[mark] = matchingSites.getOrPut(tags) {
@Suppress("ConvertLambdaToReference")
tags.toMutableList().apply { sortWith(siteOrder) }.iterator()
}
}
return matchingSitesSorted
}
fun map(availableTags: List<String>, caches: Map<Editor, EditorOffsetCache>): Map<String, Tag> {
val eligibleSitesByTag = generateEligibleSites(availableTags)
val matchingSitesSorted = generateMatchingSites(eligibleSitesByTag, caches)
val tagOrder = KeyLayoutCache.tagOrder
.thenComparingInt { eligibleSitesByTag.getValue(it).size }
.thenBy(AceConfig.layout.priority(String::last))
val sortedTags = eligibleSitesByTag.keys.toMutableList().apply {
sortWith(tagOrder)
}
var totalAssigned = 0
val totalResults = newResults.values.sumBy(IntList::size)
for (tag in sortedTags) {
if (totalAssigned == totalResults) {
break
}
if (tryToAssignTag(tag, matchingSitesSorted.getValue(tag))) {
totalAssigned++
}
}
return newTags
}
private fun tryToAssignTag(mark: String, tags: Iterator<Tag>): Boolean {
if (newTags.containsKey(mark)) {
return false
}
while (tags.hasNext()) {
val tag = tags.next()
val assigned = newTagIndices.getValue(tag.editor)
if (tag.offset !in assigned) {
newTags[mark] = tag
assigned.add(tag.offset)
return true
}
}
return false
}
private fun siteOrder(caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b ->
val aEditor = a.editor
val bEditor = b.editor
if (aEditor !== bEditor) {
val aEditorIndex = editorPriority.indexOf(aEditor)
val bEditorIndex = editorPriority.indexOf(bEditor)
// For multiple editors, prioritize them based on the provided order.
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
}
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, caches.getValue(aEditor))
val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, caches.getValue(bEditor))
if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first.
return@Comparator if (aIsVisible) -1 else 1
}
val aIsNotWordStart = aEditor.immutableText[max(0, a.offset - 1)].isWordPart
val bIsNotWordStart = bEditor.immutableText[max(0, b.offset - 1)].isWordPart
if (aIsNotWordStart != bIsNotWordStart) {
// Ensure that the first letter of a word is prioritized for tagging.
return@Comparator if (bIsNotWordStart) -1 else 1
}
when {
a.offset < b.offset -> -1
a.offset > b.offset -> 1
else -> 0
}
}
private fun canTagBeginWithChar(chars: CharSequence, site: Int, char: Char): Boolean {
if (char.toString() in allWordFragments) {
return false
}
forEachWordFragment(chars, site) {
if (it + char in allWordFragments) {
return false
}
}
return true
}
private inline fun forEachWordFragment(chars: CharSequence, site: Int, callback: (String) -> Unit) {
val left = max(0, site + queryLength - 1)
val right = chars.wordEndPlus(site)
val builder = StringBuilder(1 + right - left)
for (i in left..right) {
builder.append(chars[i].toLowerCase())
callback(builder.toString())
}
}
}

View File

@@ -1,233 +1,112 @@
package org.acejump.search
import com.google.common.collect.ArrayListMultimap
import com.google.common.collect.HashBiMap
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.ExternalUsage
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache.allPossibleTags
import org.acejump.isWordPart
import org.acejump.matchesAt
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.input.KeyLayoutCache
import org.acejump.view.TagMarker
import java.util.AbstractMap.SimpleImmutableEntry
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.min
/**
* Assigns tags to search occurrences, updates them when the search query changes, and requests a jump if the search query matches a tag.
* Assigns tags to search occurrences.
* The ordering of [editors] may be used to prioritize tagging editors earlier in the list in case of conflicts.
*/
class Tagger(private val editors: List<Editor>) {
private var tagMap = HashBiMap.create<String, Tag>()
class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
private var tagMap: Map<String, Tag>
private var typedTag = ""
val hasTags
get() = tagMap.isNotEmpty()
@ExternalUsage
internal val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value.offset }
/**
* Removes all markers, allowing them to be regenerated from scratch.
*/
internal fun unmark() {
tagMap = HashBiMap.create()
}
/**
* Assigns tags to as many results as possible, keeping previously assigned tags. Returns a [TaggingResult.Accept] if the current search
* query matches any existing tag and we should jump to it and end the session, or [TaggingResult.Mark] to continue the session with
* updated tag markers.
*
* Note that the [results] collection will be mutated.
*/
internal fun update(query: SearchQuery, results: Map<Editor, IntList>): TaggingResult {
val isRegex = query is SearchQuery.RegularExpression
val queryText = if (isRegex) " ${query.rawText}" else query.rawText[0] + query.rawText.drop(1).toLowerCase()
val availableTags = allPossibleTags.filter { !queryText.endsWith(it[0]) && it !in tagMap }
if (!isRegex) {
for (entry in tagMap.entries) {
if (entry solves queryText) {
return TaggingResult.Accept(entry.value)
}
internal val markers: Map<Editor, Collection<TagMarker>>
get() {
val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tagMap.values.size, 40))
for ((mark, tag) in tagMap) {
val marker = TagMarker.create(mark, tag.offset, typedTag)
markers.put(tag.editor, marker)
}
if (queryText.length == 1) {
for ((editor, offsets) in results) {
removeResultsWithOverlappingTags(editor, offsets)
}
}
return markers.asMap()
}
if (!isRegex || tagMap.isEmpty()) {
tagMap = assignTagsAndMerge(results, availableTags, query, queryText)
}
val resultTags = results.flatMap { (editor, offsets) -> offsets.map { Tag(editor, it) } }
return TaggingResult.Mark(createTagMarkers(resultTags, query.rawText.ifEmpty { null }))
}
fun clone(): Tagger {
return Tagger(editors).also { it.tagMap.putAll(tagMap) }
}
/**
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
*/
private fun assignTagsAndMerge(
results: Map<Editor, IntList>, availableTags: List<String>, query: SearchQuery, queryText: String,
): HashBiMap<String, Tag> {
init {
val caches = results.keys.associateWith { EditorOffsetCache.new() }
for ((editor, offsets) in results) {
val cache = caches.getValue(editor)
offsets.sort { a, b ->
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
when {
aIsVisible && !bIsVisible -> -1
bIsVisible && !aIsVisible -> 1
else -> 0
}
}
}
sortResults(results, caches)
val allAssignedTags = mutableMapOf<String, Tag>()
val oldCompatibleTags = tagMap.filter { (mark, tag) ->
isTagCompatibleWithQuery(mark, tag, queryText) || results[tag.editor]?.contains(tag.offset) == true
}
val tagSites = results
.flatMap { (editor, sites) -> sites.map { site -> Tag(editor, site) } }
.sortedWith(siteOrder(editors, caches))
val vacantResults: Map<Editor, IntList>
if (oldCompatibleTags.isEmpty()) {
vacantResults = results
}
else {
val vacant = mutableMapOf<Editor, IntList>()
for ((editor, offsets) in results) {
val list = IntArrayList()
val iter = offsets.iterator()
while (iter.hasNext()) {
val tag = Tag(editor, iter.nextInt())
if (tag !in oldCompatibleTags.values) {
list.add(tag.offset)
}
}
vacant[editor] = list
}
vacantResults = vacant
}
allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editors, query, vacantResults, results, availableTags, caches))
val assignedMarkers = allAssignedTags.keys.groupBy { it[0] }
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
if (canShortenTag(tag, assignedMarkers, queryText))
tag[0].toString()
else
tag
}
tagMap = KeyLayoutCache.allPossibleTagsLowercase.zip(tagSites).toMap()
}
private infix fun Map.Entry<String, Tag>.solves(query: String): Boolean {
return query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
}
private fun isTagCompatibleWithQuery(marker: String, tag: Tag, query: String): Boolean {
return tag.editor.immutableText.matchesAt(tag.offset, getPlaintextPortion(query, marker), ignoreCase = true)
}
fun isQueryCompatibleWithTagAt(query: String, tag: Tag): Boolean {
return tagMap.inverse()[tag].let { it != null && isTagCompatibleWithQuery(it, tag, query) }
}
fun canQueryMatchAnyTag(query: String): Boolean {
return tagMap.any { (tag, offset) ->
val tagPortion = getTagPortion(query, tag)
tagPortion.isNotEmpty() && tag.startsWith(tagPortion, ignoreCase = true) && isTagCompatibleWithQuery(tag, offset, query)
}
}
private fun removeResultsWithOverlappingTags(editor: Editor, offsets: IntList) {
val iter = offsets.iterator()
val chars = editor.immutableText
while (iter.hasNext()) {
if (!chars.canTagWithoutOverlap(iter.nextInt())) {
iter.remove() // Very uncommon, so slow removal is fine.
}
}
}
private fun createTagMarkers(tags: Collection<Tag>, literalQueryText: String?): MutableMap<Editor, Collection<TagMarker>> {
val tagMapInv = tagMap.inverse()
val markers = ArrayListMultimap.create<Editor, TagMarker>(editors.size, min(tags.size, 50))
for (tag in tags) {
val mark = tagMapInv[tag] ?: continue
val editor = tag.editor
val marker = TagMarker.create(editor, mark, tag.offset, literalQueryText)
markers.put(editor, marker)
internal fun type(char: Char): TaggingResult {
val newTypedTag = typedTag + char.lowercaseChar()
val matchingTag = tagMap[newTypedTag]
if (matchingTag != null) {
return TaggingResult.Accept(matchingTag)
}
return markers.asMap()
val newTagMap = tagMap.filter { it.key.startsWith(newTypedTag) }
if (newTagMap.isEmpty()) {
return TaggingResult.Nothing
}
typedTag = newTypedTag
tagMap = newTagMap
return TaggingResult.Mark(markers)
}
private companion object {
private fun CharSequence.canTagWithoutOverlap(loc: Int) = when {
loc - 1 < 0 -> true
loc + 1 >= length -> true
this[loc] isUnlike this[loc - 1] -> true
this[loc] isUnlike this[loc + 1] -> true
this[loc] != this[loc - 1] -> true
this[loc] != this[loc + 1] -> true
this[loc + 1] == '\r' || this[loc + 1] == '\n' -> true
this[loc - 1] == this[loc] && this[loc] == this[loc + 1] -> false
this[loc + 1].isWhitespace() && this[(loc + 2).coerceAtMost(length - 1)].isWhitespace() -> true
else -> false
private fun sortResults(results: Map<Editor, IntList>, caches: Map<Editor, EditorOffsetCache>) {
for ((editor, offsets) in results) {
val cache = caches.getValue(editor)
offsets.sort { a, b ->
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
when {
aIsVisible && !bIsVisible -> -1
bIsVisible && !aIsVisible -> 1
else -> 0
}
}
}
}
private infix fun Char.isUnlike(other: Char): Boolean {
return this.isWordPart xor other.isWordPart || this.isWhitespace() xor other.isWhitespace()
}
private fun getPlaintextPortion(query: String, marker: String) = when {
query.endsWith(marker, true) -> query.dropLast(marker.length)
query.endsWith(marker.first(), true) -> query.dropLast(1)
else -> query
}
private fun getTagPortion(query: String, marker: String) = when {
query.endsWith(marker, true) -> query.takeLast(marker.length)
query.endsWith(marker.first(), true) -> query.takeLast(1)
else -> ""
}
private fun canShortenTag(marker: String, markers: Map<Char, List<String>>, queryText: String): Boolean {
// Avoid matching query - will trigger a jump.
// TODO: lift this constraint.
val queryEndsWith = queryText.endsWith(marker[0]) || queryText.endsWith(marker)
if (queryEndsWith) {
return false
private fun siteOrder(editorPriority: List<Editor>, caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b ->
val aEditor = a.editor
val bEditor = b.editor
if (aEditor !== bEditor) {
val aEditorIndex = editorPriority.indexOf(aEditor)
val bEditorIndex = editorPriority.indexOf(bEditor)
// For multiple editors, prioritize them based on the provided order.
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
}
val startingWithSameLetter = markers[marker[0]]
return startingWithSameLetter == null || startingWithSameLetter.singleOrNull() == marker
val aCaches = caches.getValue(aEditor)
val bCaches = caches.getValue(bEditor)
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, aCaches)
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, bCaches)
if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first.
return@Comparator if (aIsVisible) -1 else 1
}
val aPosition = aCaches.offsetToXY(aEditor, a.offset)
val bPosition = bCaches.offsetToXY(bEditor, b.offset)
val caretPosition = editorPriority[0].offsetToXY(editorPriority[0].caretModel.offset)
val aDistance = aPosition.distanceSq(caretPosition)
val bDistance = bPosition.distanceSq(caretPosition)
return@Comparator aDistance.compareTo(bDistance)
}
}
}

View File

@@ -4,6 +4,7 @@ import com.intellij.openapi.editor.Editor
import org.acejump.view.TagMarker
internal sealed class TaggingResult {
object Nothing : TaggingResult()
class Accept(val tag: Tag) : TaggingResult()
class Mark(val markers: MutableMap<Editor, Collection<TagMarker>>) : TaggingResult()
class Mark(val markers: Map<Editor, Collection<TagMarker>>) : TaggingResult()
}

View File

@@ -1,29 +1,29 @@
package org.acejump.session
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.codeInsight.hint.HintUtil
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import com.intellij.ui.LightweightHint
import org.acejump.ExternalUsage
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.clone
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.EditorKeyListener
import org.acejump.input.KeyLayoutCache
import org.acejump.modes.AdvancedMode
import org.acejump.modes.BetweenPointsMode
import org.acejump.modes.JumpMode
import org.acejump.modes.SessionMode
import org.acejump.search.*
import org.acejump.search.Pattern
import org.acejump.search.SearchProcessor
import org.acejump.search.SearchQuery
import org.acejump.search.Tag
import org.acejump.session.TypeResult.AcceptTag
import org.acejump.session.TypeResult.ChangeMode
import org.acejump.session.TypeResult.ChangeState
import org.acejump.session.TypeResult.EndSession
import org.acejump.session.TypeResult.Nothing
import org.acejump.view.TagCanvas
import org.acejump.view.TagMarker
import org.acejump.view.TextHighlighter
/**
@@ -33,8 +33,7 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
private val editorSettings = EditorSettings.setup(mainEditor)
private lateinit var mode: SessionMode
private var state: SessionStateImpl? = null
private var tagger = Tagger(jumpEditors)
private var state: SessionState? = null
private var acceptedTag: Tag? = null
set(value) {
@@ -49,102 +48,54 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
private val textHighlighter = TextHighlighter()
private val tagCanvases = jumpEditors.associateWith(::TagCanvas)
@ExternalUsage
val tags
get() = tagger.tags
var defaultBoundary: Boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
private val actions = object : SessionActions {
override fun showHighlights(results: Map<Editor, IntList>, query: SearchQuery) {
textHighlighter.renderOccurrences(results, query)
}
override fun hideHighlights() {
textHighlighter.reset()
}
override fun setTagMarkers(markers: Map<Editor, Collection<TagMarker>>) {
for ((editor, canvas) in tagCanvases) {
canvas.setMarkers(markers[editor].orEmpty())
}
}
}
init {
KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(mainEditor, object : TypedActionHandler {
override fun execute(editor: Editor, charTyped: Char, context: DataContext) {
val state = state ?: return
val hadTags = tagger.hasTags
editorSettings.startEditing(editor)
val result = mode.type(state, charTyped, acceptedTag)
editorSettings.stopEditing(editor)
when (result) {
TypeResult.Nothing -> updateHint()
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
is TypeResult.ChangeMode -> setMode(result.mode)
TypeResult.RestartSearch -> restart().also {
this@Session.state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
updateHint()
}
TypeResult.EndSession -> end()
}
}
})
EditorKeyListener.attach(mainEditor) { editor, charTyped, _ -> typeCharacter(editor, charTyped) }
}
/**
* Updates text highlights and tag markers according to the current search state. Dispatches jumps if the search query matches a tag.
*/
private fun updateSearch(processor: SearchProcessor, markImmediately: Boolean) {
val query = processor.query
val results = processor.results
private fun typeCharacter(editor: Editor, charTyped: Char) {
val state = state ?: return
if (!markImmediately && query.rawText.let { it.length < AceConfig.minQueryLength && it.all(Char::isLetterOrDigit) }) {
textHighlighter.renderOccurrences(results, query)
return
}
editorSettings.startEditing(editor)
val result = mode.type(state, AceConfig.layout.characterRemapping.getOrDefault(charTyped, charTyped), acceptedTag)
editorSettings.stopEditing(editor)
when (val result = tagger.update(query, results.clone())) {
is TaggingResult.Accept -> {
when (result) {
Nothing -> return
is ChangeState -> this.state = result.state
is ChangeMode -> setMode(result.mode)
is AcceptTag -> {
acceptedTag = result.tag
textHighlighter.renderFinal(result.tag, processor.query)
if (state?.let { mode.accept(it, result.tag) } == true) {
end()
return
}
mode.accept(state, result.tag)
end()
}
is TaggingResult.Mark -> {
for ((editor, canvas) in tagCanvases) {
canvas.setMarkers(result.markers[editor].orEmpty())
}
textHighlighter.renderOccurrences(results, query)
}
EndSession -> end()
}
updateHint()
}
private fun setMode(mode: SessionMode) {
this.mode = mode
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
updateHint()
}
private fun updateHint() {
val acceptedTag = acceptedTag
val editor = mainEditor
val offset = when {
acceptedTag == null -> null
acceptedTag.editor === editor -> acceptedTag.offset
else -> mainEditor.caretModel.offset
}
val hintArray = mode.getHint(offset, state?.currentProcessor.let { it != null && it.query.rawText.isNotEmpty() }) ?: return
val hintText = hintArray
.joinToString("\n")
.replace("<f>", "<span style=\"font-family:'${editor.colorsScheme.editorFontName}';font-weight:bold\">")
.replace("</f>", "</span>")
val hint = LightweightHint(HintUtil.createInformationLabel(hintText))
val pos = offset?.let(editor::offsetToLogicalPosition) ?: editor.caretModel.logicalPosition
val point = HintManagerImpl.getHintPosition(hint, editor, pos, HintManager.ABOVE)
val info = HintManagerImpl.createHintHint(editor, point, hint, HintManager.ABOVE).setShowImmediately(true)
val flags = HintManager.UPDATE_BY_SCROLLING or HintManager.HIDE_BY_ESCAPE
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, true, info)
}
fun startJumpMode() {
@@ -162,23 +113,7 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
}
setMode(mode())
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
}
fun startOrCycleSpecialModes() {
if (!this::mode.isInitialized) {
setMode(AdvancedMode())
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
return
}
restart()
setMode(when (mode) {
is AdvancedMode -> BetweenPointsMode()
else -> AdvancedMode()
})
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary)
state = SessionState.WaitForKey(actions, jumpEditors, defaultBoundary)
}
/**
@@ -189,32 +124,18 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
setMode(JumpMode())
}
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach { it.setMarkers(emptyList()) }
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary).also {
state = SessionStateImpl(jumpEditors, tagger, defaultBoundary, it)
for (canvas in tagCanvases.values) {
canvas.setMarkers(emptyList())
}
updateSearch(processor, markImmediately = true)
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary)
textHighlighter.renderOccurrences(processor.resultsCopy, processor.query)
state = SessionState.SelectTag(actions, jumpEditors, processor)
}
fun tagImmediately() {
val state = state ?: return
val processor = state.currentProcessor
if (processor != null) {
updateSearch(processor, markImmediately = true)
}
else if (mode is AdvancedMode) {
val offset = mainEditor.caretModel.offset
val result = mainEditor.immutableText.getOrNull(offset)?.let(state::type)
if (result is TypeResult.UpdateResults) {
val tag = Tag(mainEditor, offset).also { acceptedTag = it }
textHighlighter.renderFinal(tag, result.processor.query)
updateHint()
}
}
typeCharacter(mainEditor, '\n')
}
/**
@@ -229,7 +150,6 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
*/
fun restart() {
state = null
tagger = Tagger(jumpEditors)
acceptedTag = null
tagCanvases.values.forEach(TagCanvas::removeMarkers)
textHighlighter.reset()
@@ -244,7 +164,6 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
* Should only be used from [SessionManager] to dispose a successfully ended session.
*/
internal fun dispose() {
tagger = Tagger(jumpEditors)
tagCanvases.values.forEach(TagCanvas::unbind)
textHighlighter.reset()
EditorKeyListener.detach(mainEditor)

View File

@@ -0,0 +1,13 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.search.SearchQuery
import org.acejump.view.TagMarker
internal interface SessionActions {
fun showHighlights(results: Map<Editor, IntList>, query: SearchQuery)
fun hideHighlights()
fun setTagMarkers(markers: Map<Editor, Collection<TagMarker>>)
}

View File

@@ -1,9 +1,72 @@
package org.acejump.session
import org.acejump.action.AceTagAction
import org.acejump.search.Tag
import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
import org.acejump.search.TaggingResult
interface SessionState {
sealed interface SessionState {
fun type(char: Char): TypeResult
fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean)
class WaitForKey internal constructor(
private val actions: SessionActions,
private val jumpEditors: List<Editor>,
private val defaultBoundary: Boundaries,
) : SessionState {
override fun type(char: Char): TypeResult {
val searchProcessor = SearchProcessor.fromString(jumpEditors, char.toString(), defaultBoundary)
return if (searchProcessor.isQueryFinished) {
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
}
else {
TypeResult.ChangeState(RefineSearchQuery(actions, jumpEditors, searchProcessor))
}
}
}
class RefineSearchQuery internal constructor(
private val actions: SessionActions,
private val jumpEditors: List<Editor>,
private val searchProcessor: SearchProcessor,
) : SessionState {
init {
actions.showHighlights(searchProcessor.resultsCopy, searchProcessor.query)
}
override fun type(char: Char): TypeResult {
return if (searchProcessor.refineQuery(char)) {
actions.hideHighlights()
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
}
else {
actions.showHighlights(searchProcessor.resultsCopy, searchProcessor.query)
TypeResult.Nothing
}
}
}
class SelectTag internal constructor(
private val actions: SessionActions,
jumpEditors: List<Editor>,
searchProcessor: SearchProcessor,
) : SessionState {
private val tagger = Tagger(jumpEditors, searchProcessor.resultsCopy)
init {
actions.setTagMarkers(tagger.markers)
}
override fun type(char: Char): TypeResult {
return when (val result = tagger.type(char)) {
is TaggingResult.Nothing -> TypeResult.Nothing
is TaggingResult.Accept -> TypeResult.AcceptTag(result.tag)
is TaggingResult.Mark -> {
actions.setTagMarkers(result.markers)
TypeResult.Nothing
}
}
}
}
}

View File

@@ -1,36 +0,0 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.boundaries.Boundaries
import org.acejump.search.SearchProcessor
import org.acejump.search.Tag
import org.acejump.search.Tagger
internal class SessionStateImpl(
private val jumpEditors: List<Editor>,
private val tagger: Tagger,
private val defaultBoundary: Boundaries,
processor: SearchProcessor? = null
) : SessionState {
internal var currentProcessor: SearchProcessor? = processor
override fun type(char: Char): TypeResult {
val processor = currentProcessor
if (processor == null) {
val newProcessor = SearchProcessor.fromChar(jumpEditors, char, defaultBoundary)
return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it })
}
if (processor.type(char, tagger)) {
return TypeResult.UpdateResults(processor)
}
return TypeResult.Nothing
}
override fun act(action: AceTagAction, tag: Tag, shiftMode: Boolean, isFinal: Boolean) {
currentProcessor?.let { action(tag.editor, it, tag.offset, shiftMode, isFinal) }
}
}

View File

@@ -1,12 +1,12 @@
package org.acejump.session
import org.acejump.modes.SessionMode
import org.acejump.search.SearchProcessor
import org.acejump.search.Tag
sealed class TypeResult {
object Nothing : TypeResult()
class UpdateResults(val processor: SearchProcessor) : TypeResult()
class ChangeState(val state: SessionState) : TypeResult()
class ChangeMode(val mode: SessionMode) : TypeResult()
object RestartSearch : TypeResult()
class AcceptTag(val tag: Tag) : TypeResult()
object EndSession : TypeResult()
}

View File

@@ -2,10 +2,10 @@ package org.acejump.view
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.ui.ColorUtil
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Rectangle
@@ -16,8 +16,9 @@ import javax.swing.SwingUtilities
/**
* Holds all active tag markers and renders them on top of the editor.
*/
internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener {
private var markers: Collection<TagMarker>? = null
internal class TagCanvas(private val editor: Editor) : JComponent() {
private var markers: Collection<TagMarker> = emptyList()
private var visible = false
init {
val contentComponent = editor.contentComponent
@@ -27,43 +28,38 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
if (ApplicationInfo.getInstance().build.components.first() < 173) {
SwingUtilities.convertPoint(this, location, editor.component.rootPane).let { setLocation(-it.x, -it.y) }
}
editor.caretModel.addCaretListener(this)
}
fun unbind() {
markers = null
editor.contentComponent.remove(this)
editor.caretModel.removeCaretListener(this)
}
/**
* Ensures that all tags and the outline around the selected tag are repainted. It should not be necessary to repaint the entire tag
* canvas, but the cost of repainting visible tags is negligible.
*/
override fun caretPositionChanged(event: CaretEvent) {
repaint()
editor.contentComponent.repaint()
}
fun setMarkers(markers: Collection<TagMarker>) {
this.markers = markers
this.visible = true
repaint()
}
fun removeMarkers() {
this.markers = emptyList()
}
override fun paint(g: Graphics) {
if (!markers.isNullOrEmpty()) {
super.paint(g)
}
this.visible = false
repaint()
}
override fun paintChildren(g: Graphics) {
super.paintChildren(g)
val markers = markers ?: return
if (!visible) {
return
}
g.color = ColorUtil.withAlpha(editor.colorsScheme.defaultBackground, (AceConfig.editorFadeOpacity * 0.01).coerceIn(0.0, 1.0))
g.fillRect(0, 0, width - 1, height - 1)
if (markers.isEmpty()) {
return
}
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
@@ -73,18 +69,8 @@ internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListen
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val occupied = mutableListOf<Rectangle>()
// If there is a tag at the caret location, prioritize its rendering over all other tags. This is helpful for seeing which tag is
// currently selected while navigating highly clustered tags, although it does end up rearranging nearby tags which can be confusing.
// TODO instead of immediately painting, we could calculate the layout of everything first, and then remove tags that interfere with
// the caret tag to avoid changing the alignment of the caret tag
val caretOffset = editor.caretModel.offset
val caretMarker = markers.find { it.offsetL == caretOffset || it.offsetR == caretOffset }
caretMarker?.paint(g, editor, cache, font, occupied)
for (marker in markers) {
if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
if (marker.offset in viewRange) {
marker.paint(g, editor, cache, font, occupied)
}
}

View File

@@ -2,23 +2,19 @@ package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.ui.ColorUtil
import org.acejump.config.AceConfig
import java.awt.Font
import java.awt.FontMetrics
/**
* Stores font metrics for aligning and rendering [TagMarker]s.
*/
internal class TagFont(editor: Editor) {
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.BOLD)
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W')
val foregroundColor = AceConfig.tagForegroundColor
var backgroundColor = AceConfig.tagBackgroundColor
val isForegroundDark = ColorUtil.isDark(foregroundColor)
val foregroundColor1 = AceConfig.tagForegroundColor1
val foregroundColor2 = AceConfig.tagForegroundColor2
val editorFontMetrics: FontMetrics = editor.component.getFontMetrics(editor.colorsScheme.getFont(EditorFontType.PLAIN))
val lineHeight = editor.lineHeight
val baselineDistance = editor.ascent
}

View File

@@ -2,91 +2,34 @@ package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.SystemInfo
import com.intellij.ui.scale.JBUIScale
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.countMatchingCharacters
import org.acejump.immutableText
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import kotlin.math.max
/**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/
internal class TagMarker(
private val tag: String,
val offsetL: Int,
val offsetR: Int,
private val shiftR: Int,
private val hasSpaceRight: Boolean
private val tag: CharArray,
val offset: Int
) {
private val length = tag.length
private val length = tag.size
companion object {
private const val ARC = 1
/**
* TODO This might be due to DPI settings.
*/
private val HIGHLIGHT_OFFSET = if (SystemInfo.isMac) -0.5 else 0.0
private val SHADOW_COLOR = Color(0F, 0F, 0F, 0.35F)
/**
* Creates a new tag, precomputing some information about the nearby characters to reduce rendering overhead. If the last typed
* character ([literalQueryText]) matches the first [tag] character, only the second [tag] character is displayed.
* character ([typedTag]) matches the first [tag] character, only the second [tag] character is displayed.
*/
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): TagMarker {
val chars = editor.immutableText
val matching = literalQueryText?.let { chars.countMatchingCharacters(offset, it) } ?: 0
val hasSpaceRight = offset + 1 >= chars.length || chars[offset + 1].isWhitespace()
val displayedTag = if (literalQueryText != null && literalQueryText.last().equals(tag.first(), ignoreCase = true))
tag.drop(1).toUpperCase()
else
tag.toUpperCase()
return TagMarker(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
fun create(tag: String, offset: Int, typedTag: String): TagMarker {
return TagMarker(tag.drop(typedTag.length).toCharArray(), offset)
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
g.translate(0.0, -HIGHLIGHT_OFFSET)
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) {
val x = point.x + 2
val y = point.y + font.baselineDistance
g.font = font.tagFont
if (!font.isForegroundDark) {
g.color = SHADOW_COLOR
g.drawString(text, x + 1, y + 1)
}
g.color = font.foregroundColor
g.drawString(text, x, y)
}
}
/**
* Returns true if the left-aligned offset is in the range. Use to cull tags outside visible range.
* Only the left offset is checked, because if the tag was right-aligned on the last index of the range, it would not be visible anyway.
*/
fun isOffsetInRange(range: IntRange): Boolean {
return offsetL in range
}
/**
@@ -96,43 +39,43 @@ internal class TagMarker(
fun paint(g: Graphics2D, editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: MutableList<Rectangle>): Rectangle? {
val rect = alignTag(editor, cache, font, occupied) ?: return null
drawHighlight(g, rect, font.backgroundColor)
drawForeground(g, font, rect.location, tag)
drawHighlight(g, rect, editor.colorsScheme.defaultBackground)
drawForeground(g, font, rect.location)
occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) })
occupied.add(rect)
return rect
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRect(rect.x, rect.y, rect.width, rect.height + 1)
g.translate(0.0, -HIGHLIGHT_OFFSET)
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point) {
val x = point.x
val y = point.y + font.baselineDistance
g.font = font.tagFont
g.color = font.foregroundColor1
g.drawChars(tag, 0, 1, x, y)
if (tag.size > 1) {
g.color = font.foregroundColor2
g.drawChars(tag, 1, length - 1, x + font.tagCharWidth, y)
}
}
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
val boundaries = StandardBoundaries.VISIBLE_ON_SCREEN
if (hasSpaceRight || offsetL == 0 || editor.immutableText[offsetL - 1].let { it == '\n' || it == '\r' }) {
val rectR = createRightAlignedTagRect(editor, cache, font)
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) && occupied.none(rectR::intersects) }
}
val rectL = createLeftAlignedTagRect(editor, cache, font)
if (occupied.none(rectL::intersects)) {
return rectL.takeIf { boundaries.isOffsetInside(editor, offsetL, cache) }
}
val rectR = createRightAlignedTagRect(editor, cache, font)
if (occupied.none(rectR::intersects)) {
return rectR.takeIf { boundaries.isOffsetInside(editor, offsetR, cache) }
}
return null
}
private fun createRightAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetR)
val shift = font.editorFontMetrics.charWidth(editor.immutableText[offsetR]) + (font.tagCharWidth * shiftR)
return Rectangle(pos.x + shift, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
}
private fun createLeftAlignedTagRect(editor: Editor, cache: EditorOffsetCache, font: TagFont): Rectangle {
val pos = cache.offsetToXY(editor, offsetL)
val shift = -(font.tagCharWidth * length)
return Rectangle(pos.x + shift - 4, pos.y, (font.tagCharWidth * length) + 4, font.lineHeight)
val pos = cache.offsetToXY(editor, offset)
val rect = Rectangle(pos.x, pos.y, font.tagCharWidth * length, font.lineHeight)
return rect.takeIf { occupied.none(it::intersects) }
}
}

View File

@@ -6,13 +6,12 @@ import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import it.unimi.dsi.fastutil.ints.IntArrayList
import com.intellij.ui.ColorUtil
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.search.SearchQuery
import org.acejump.search.Tag
import java.awt.Color
import java.awt.Graphics
@@ -32,13 +31,6 @@ internal class TextHighlighter {
}, query::getHighlightLength)
}
/**
* Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
*/
fun renderFinal(tag: Tag, query: SearchQuery) {
render(mutableMapOf(tag.editor to IntArrayList(intArrayOf(tag.offset))), AcceptedTagRenderer, query::getHighlightLength)
}
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
for ((editor, offsets) in results) {
val highlights = previousHighlights[editor]
@@ -88,7 +80,7 @@ internal class TextHighlighter {
*/
private object SearchedWordRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.textHighlightColor)
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.searchHighlightColor)
}
}
@@ -97,16 +89,7 @@ internal class TextHighlighter {
*/
private object RegexRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawSingle(g, editor, highlighter.startOffset, AceConfig.textHighlightColor)
}
}
/**
* Renders a filled highlight in the background of the accepted tag position and search query.
*/
private object AcceptedTagRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.acceptedTagColor)
drawSingle(g, editor, highlighter.startOffset, AceConfig.searchHighlightColor)
}
}
@@ -117,10 +100,10 @@ internal class TextHighlighter {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = color
g.color = ColorUtil.withAlpha(AceConfig.searchHighlightColor, 0.2)
g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.color = color
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
}
@@ -130,10 +113,10 @@ internal class TextHighlighter {
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = color
g.color = ColorUtil.withAlpha(AceConfig.searchHighlightColor, 0.2)
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.color = AceConfig.tagBackgroundColor
g.color = color
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
}
}

View File

@@ -9,6 +9,7 @@
</description>
<depends>com.intellij.modules.platform</depends>
<depends>IdeaVIM</depends>
<category>Navigation</category>
<vendor url="https://github.com/acejump/AceJump">AceJump</vendor>
@@ -23,113 +24,28 @@
implementationClass="org.acejump.action.AceEditorAction$Reset"/>
<editorActionHandler action="EditorBackSpace" order="first"
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
<editorActionHandler action="EditorEnter" order="first"
<editorActionHandler action="EditorEnter" order="first, before terminalEnter"
implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/>
<editorActionHandler action="EditorUp" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineStarts"/>
<editorActionHandler action="EditorLeft" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorLineStart" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineIndents"/>
<editorActionHandler action="EditorRight" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
<editorActionHandler action="EditorLineEnd" order="first"
implementationClass="org.acejump.action.AceEditorAction$SearchLineEnds"/>
</extensions>
<actions>
<action id="AceAction"
class="org.acejump.action.AceKeyboardAction$ActivateAceJump"
text="Activate AceJump">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
</action>
<action id="AceSpecialAction"
class="org.acejump.action.AceKeyboardAction$ActivateAceJumpSpecial"
text="Activate / Cycle AceJump Special Modes">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl alt SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl alt SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt SEMICOLON"/>
</action>
<action id="AceLineAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
text="Start AceJump in All Line Marks Mode">
<keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl shift SEMICOLON"/>
<keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
</action>
<action id="AceLineStartsAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineStartsMode"
text="Start AceJump in All Line Starts Mode"/>
<action id="AceLineEndsAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineEndsMode"
text="Start AceJump in All Line Ends Mode"/>
<action id="AceLineIndentsAction"
class="org.acejump.action.AceKeyboardAction$StartAllLineIndentsMode"
text="Start AceJump in All Line Indents Mode"/>
<action id="AceWordAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsMode"
text="Start AceJump in All Words Mode"/>
<action id="AceWordForwardAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsForwardMode"
text="Start AceJump in All Words After Caret Mode"/>
<action id="AceWordBackwardsAction"
class="org.acejump.action.AceKeyboardAction$StartAllWordsBackwardsMode"
text="Start AceJump in All Words Before Caret Mode"/>
<action id="AceVimAction_JumpToChar"
class="org.acejump.action.AceVimAction$JumpToChar"
text="AceJump Vim - JumpToChar"/>
<action id="AceVimAction_JumpToCharBeforeCaret"
class="org.acejump.action.AceVimAction$JumpToCharBeforeCaret"
text="AceJump Vim - JumpToCharBeforeCaret"/>
<action id="AceVimAction_JumpToCharAfterCaret"
class="org.acejump.action.AceVimAction$JumpToCharAfterCaret"
text="AceJump Vim - JumpToCharAfterCaret"/>
<action id="AceVimAction_LWordsAfterCaret"
class="org.acejump.action.AceVimAction$LWordsAfterCaret"
text="AceJump Vim - LWordsAfterCaret"/>
<action id="AceVimAction_UWordsAfterCaret"
class="org.acejump.action.AceVimAction$UWordsAfterCaret"
text="AceJump Vim - UWordsAfterCaret"/>
<action id="AceVimAction_LWordsBeforeCaret"
class="org.acejump.action.AceVimAction$LWordsBeforeCaret"
text="AceJump Vim - LWordsBeforeCaret"/>
<action id="AceVimAction_UWordsBeforeCaret"
class="org.acejump.action.AceVimAction$UWordsBeforeCaret"
text="AceJump Vim - UWordsBeforeCaret"/>
<action id="AceVimAction_LWordEndsAfterCaret"
class="org.acejump.action.AceVimAction$LWordEndsAfterCaret"
text="AceJump Vim - LWordEndsAfterCaret"/>
<action id="AceVimAction_UWordEndsAfterCaret"
class="org.acejump.action.AceVimAction$UWordEndsAfterCaret"
text="AceJump Vim - UWordEndsAfterCaret"/>
<action id="AceVimAction_LWordEndsBeforeCaret"
class="org.acejump.action.AceVimAction$LWordEndsBeforeCaret"
text="AceJump Vim - LWordEndsBeforeCaret"/>
<action id="AceVimAction_UWordEndsBeforeCaret"
class="org.acejump.action.AceVimAction$UWordEndsBeforeCaret"
text="AceJump Vim - UWordEndsBeforeCaret"/>
<action id="AceVimAction_GoToDeclaration"
class="org.acejump.action.AceVimAction$GoToDeclaration"
text="AceJump Vim - GoToDeclaration"/>
<action id="AceVimAction_GoToTypeDeclaration"
class="org.acejump.action.AceVimAction$GoToTypeDeclaration"
text="AceJump Vim - GoToTypeDeclaration"/>
<action id="AceVimAction_ShowIntentions"
class="org.acejump.action.AceVimAction$ShowIntentions"
text="AceJump Vim - ShowIntentions"/>
<action id="AceVimAction_ShowUsages"
class="org.acejump.action.AceVimAction$ShowUsages"
text="AceJump Vim - ShowUsages"/>
<action id="AceVimAction_FindUsages"
class="org.acejump.action.AceVimAction$FindUsages"
text="AceJump Vim - FindUsages"/>
<action id="AceVimAction_Refactor"
class="org.acejump.action.AceVimAction$Refactor"
text="AceJump Vim - Refactor"/>
<action id="AceVimAction_Rename"
class="org.acejump.action.AceVimAction$Rename"
text="AceJump Vim - Rename"/>
<action id="AceVimAction_JumpAllEditors" class="org.acejump.action.AceVimAction$JumpAllEditors" text="AceJump Vim - Jump All Editors" />
<action id="AceVimAction_JumpAllEditors_GoToDeclaration" class="org.acejump.action.AceVimAction$JumpAllEditorsGoToDeclaration" text="AceJump Vim - Jump All Editors - Go To Declaration" />
<action id="AceVimAction_JumpForward" class="org.acejump.action.AceVimAction$JumpForward" text="AceJump Vim - Jump Forward" />
<action id="AceVimAction_JumpBackward" class="org.acejump.action.AceVimAction$JumpBackward" text="AceJump Vim - Jump Backward" />
<action id="AceVimAction_JumpTillForward" class="org.acejump.action.AceVimAction$JumpTillForward" text="AceJump Vim - Jump Till Forward" />
<action id="AceVimAction_JumpTillBackward" class="org.acejump.action.AceVimAction$JumpTillBackward" text="AceJump Vim - Jump Till Backward" />
<action id="AceVimAction_JumpOnLineForward" class="org.acejump.action.AceVimAction$JumpOnLineForward" text="AceJump Vim - Jump On Line Forward" />
<action id="AceVimAction_JumpOnLineBackward" class="org.acejump.action.AceVimAction$JumpOnLineBackward" text="AceJump Vim - Jump On Line Backward" />
<action id="AceVimAction_JumpLineIndentsForward" class="org.acejump.action.AceVimAction$JumpLineIndentsForward" text="AceJump Vim - Jump Line Indents Forward" />
<action id="AceVimAction_JumpLineIndentsBackward" class="org.acejump.action.AceVimAction$JumpLineIndentsBackward" text="AceJump Vim - Jump Line Indents Backward" />
<action id="AceVimAction_JumpLWordForward" class="org.acejump.action.AceVimAction$JumpLWordForward" text="AceJump Vim - Jump LWord Forward" />
<action id="AceVimAction_JumpUWordForward" class="org.acejump.action.AceVimAction$JumpUWordForward" text="AceJump Vim - Jump UWord Forward" />
<action id="AceVimAction_JumpLWordBackward" class="org.acejump.action.AceVimAction$JumpLWordBackward" text="AceJump Vim - Jump LWord Backward" />
<action id="AceVimAction_JumpUWordBackward" class="org.acejump.action.AceVimAction$JumpUWordBackward" text="AceJump Vim - Jump UWord Backward" />
<action id="AceVimAction_JumpLWordEndForward" class="org.acejump.action.AceVimAction$JumpLWordEndForward" text="AceJump Vim - Jump LWord End Forward" />
<action id="AceVimAction_JumpUWordEndForward" class="org.acejump.action.AceVimAction$JumpUWordEndForward" text="AceJump Vim - Jump UWord End Forward" />
<action id="AceVimAction_JumpLWordEndBackward" class="org.acejump.action.AceVimAction$JumpLWordEndBackward" text="AceJump Vim - Jump LWord End Backward" />
<action id="AceVimAction_JumpUWordEndBackward" class="org.acejump.action.AceVimAction$JumpUWordEndBackward" text="AceJump Vim - Jump UWord End Backward" />
</actions>
</idea-plugin>

View File

@@ -1,73 +0,0 @@
import org.acejump.action.AceKeyboardAction
import org.acejump.test.util.BaseTest
/**
* Functional test cases and end-to-end performance tests.
*
* TODO: Add more structure to test cases, use test resources to define files.
*/
class AceTest : BaseTest() {
fun `test that scanner finds all occurrences of single character`() =
assertEquals("test test test".search("t"), setOf(0, 3, 5, 8, 10, 13))
fun `test empty results for an absent query`() =
assertEmpty("test test test".search("best"))
fun `test sticky results on a query with extra characters`() =
assertEquals("test test test".search("testz"), setOf(0, 5, 10))
fun `test a query inside text with some variations`() =
assertEquals("abcd dabc cdab".search("cd"), setOf(2, 10))
fun `test a query containing a space character`() =
assertEquals("abcd dabc cdab".search("cd "), setOf(2))
fun `test a query containing a { character`() =
assertEquals("abcd{dabc cdab".search("cd{"), setOf(2))
fun `test tag selection`() {
"<caret>testing 1234".search("g")
typeAndWaitForResults(session.tags[0].key)
myFixture.checkResult("testin<caret>g 1234")
}
fun `test words before caret action`() {
makeEditor("test words <caret> before caret is two")
takeAction(AceKeyboardAction.StartAllWordsBackwardsMode)
assertEquals(2, session.tags.size)
}
fun `test words after caret action`() {
makeEditor("test words <caret> after caret is four")
takeAction(AceKeyboardAction.StartAllWordsForwardMode)
assertEquals(4, session.tags.size)
}
fun `test word mode`() {
makeEditor("test word action")
takeAction(AceKeyboardAction.StartAllWordsMode)
assertEquals(3, session.tags.size)
typeAndWaitForResults(session.tags[1].key)
myFixture.checkResult("test <caret>word action")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines\n")
takeAction(AceKeyboardAction.StartAllLineMarksMode)
assertEquals(9, session.tags.size)
}
}

View File

@@ -1,11 +1,10 @@
import org.acejump.action.AceKeyboardAction
import org.acejump.action.AceVimAction
import org.acejump.test.util.BaseTest
import org.junit.Ignore
import java.io.File
import kotlin.random.Random
import kotlin.system.measureTimeMillis
@Ignore
class LatencyTest : BaseTest() {
private fun `test tag latency`(editorText: String) {
@@ -15,7 +14,7 @@ class LatencyTest : BaseTest() {
for (query in chars) {
makeEditor(editorText)
myFixture.testAction(AceKeyboardAction.ActivateAceJumpSpecial)
myFixture.testAction(AceVimAction.JumpAllEditors())
time += measureTimeMillis { typeAndWaitForResults("$query") }
// TODO assert(Tagger.markers.isNotEmpty()) { "Should be tagged: $query" }
resetEditor()

View File

@@ -7,7 +7,7 @@ import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.UIUtil
import org.acejump.action.AceKeyboardAction
import org.acejump.action.AceVimAction
import org.acejump.session.SessionManager
abstract class BaseTest : BasePlatformTestCase() {
@@ -58,7 +58,7 @@ abstract class BaseTest : BasePlatformTestCase() {
private fun String.executeQuery(query: String) {
myFixture.run {
makeEditor(this@executeQuery)
testAction(AceKeyboardAction.ActivateAceJump)
testAction(AceVimAction.JumpAllEditors())
typeAndWaitForResults(query)
}
}