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

19 Commits

Author SHA1 Message Date
23e177410b [WIP] Add progressive selection mode 2021-02-06 16:04:02 +01:00
47a84da904 [WIP] Pressing Enter before typing query starts jump mode for character at caret 2021-02-06 10:59:57 +01:00
2f6a5f2a23 [WIP] Interactive modes 2021-02-06 08:49:15 +01:00
054f604f96 Add tag shadow & highlight outline, increase padding 2021-02-06 07:38:27 +01:00
97a5de919f Change default color scheme 2021-02-06 07:38:27 +01:00
9e73deef4f Swap editor shortcuts for searching line starts and indents 2021-02-06 07:20:37 +01:00
26945c6e20 Make Enter immediately tag search occurrences 2021-01-18 20:26:48 +01:00
1f8d6b8d6f Add horizontal padding to tags and tweak highlighting 2021-01-18 20:26:48 +01:00
36c5fdcb45 Remove option for rounded tag corners 2021-01-17 13:07:31 +01:00
4cd91a20e9 Remove tag visiting functionality 2021-01-17 13:07:31 +01:00
983720dbb8 Remove whole file search 2021-01-17 13:07:30 +01:00
91e285a3ff Add option to set minimum typed characters for tagging to simplify tags 2021-01-17 12:49:35 +01:00
18bb6ddb9b Fix exception when regex occurrence highlight goes beyond the end of file 2021-01-16 15:03:25 +01:00
6f950ecf95 Fix occasional conflicts between tags and search query when assigning vacant results 2021-01-16 10:49:09 +01:00
d7789be9a3 Prevent editing document while AceJump is active 2020-12-13 08:04:15 +01:00
ed0fc19cfb Enforce LF line endings & fix build.gradle 2020-12-13 07:13:11 +01:00
ad8c84be27 Make jump mode cycling wrap around & add shortcut to cycle modes in reverse 2020-12-13 06:00:41 +01:00
f44003b47a Add all regex search patterns to keymap 2020-12-13 06:00:36 +01:00
feaffb3640 Major AceJump refactoring!
See https://github.com/acejump/AceJump/issues/348 for information on what's changed and what more needs to be done.
2020-12-06 06:46:10 +01:00
52 changed files with 2325 additions and 1429 deletions

24
.gitignore vendored
View File

@@ -1,5 +1,21 @@
/.idea/* ### JetBrains template
!/.idea/runConfigurations # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
/.gradle/ ## Directory-based project format:
/build/ .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

View File

@@ -1,17 +1,8 @@
# Changelog # Changelog
### 3.7 ### 3.6.4
- Improvements to tag latency - Improvements to tag latency. Thanks to @chylex for [the PR](https://github.com/acejump/AceJump/pull/339)!
- Redesign settings panel
- Add missing configuration for definition mode color
- Adds option to switch between straight and rounded tag corners
- Adds option to only consider visible area
- Add customizable jump mode cycling
- Jump-to-End mode jumps to the end of a word
- Fixes toggle keys not resetting mode when pressed twice
- Increase limit for what is considered a large file
- Thanks to @chylex for [all the PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex)!
### 3.6.3 ### 3.6.3
@@ -313,4 +304,4 @@
### 1.0.0 ### 1.0.0
- Cleaned up code base for release - Cleaned up code base for release

View File

@@ -171,7 +171,7 @@ The following individuals have significantly improved AceJump through their cont
* [John Lindquist](https://github.com/johnlindquist) for creating AceJump and supporting it for many years. * [John Lindquist](https://github.com/johnlindquist) for creating AceJump and supporting it for many years.
* [Breandan Considine](https://github.com/breandan) for maintaining the project and adding some new features. * [Breandan Considine](https://github.com/breandan) for maintaining the project and adding some new features.
* [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292). * [Alex Plate](https://github.com/AlexPl292) for submitting [several PRs](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3AAlexPl292).
* [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pulls?q=is%3Apr+author%3Achylex). * [Daniel Chýlek](https://github.com/chylex) for several [performance optimizations](https://github.com/acejump/AceJump/pull/339).
* [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency. * [Sven Speckmaier](https://github.com/svensp) for [improving](https://github.com/acejump/AceJump/pull/214) search latency.
* [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years. * [Stefan Monnier](https://www.iro.umontreal.ca/~monnier/) for algorithmic advice and maintaining Emacs for several years.
* [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design. * [Fool's Mate](https://www.fools-mate.de/) for the [icon](https://github.com/acejump/AceJump/issues/313) and graphic design.

View File

@@ -1,54 +1,46 @@
@file:Suppress("ConvertLambdaToReference") import org.jetbrains.changelog.closure
import org.jetbrains.intellij.tasks.PatchPluginXmlTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.9.10" idea apply true
id("org.jetbrains.intellij") version "1.17.3" kotlin("jvm") version "1.3.72"
id("org.jetbrains.intellij") version "0.6.5"
id("org.jetbrains.changelog") version "0.6.2"
} }
group = "org.acejump" tasks {
version = "chylex-26" withType<KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
repositories { kotlinOptions.freeCompilerArgs += "-progressive"
mavenCentral() }
}
intellij {
version.set("2024.2")
updateSinceUntilBuild.set(false)
plugins.add("IdeaVIM:chylex-41") withType<PatchPluginXmlTask> {
plugins.add("com.intellij.classic.ui:242.20224.159") sinceBuild("201.6668.0")
changeNotes({ changelog.getLatest().toHTML() })
pluginsRepositories {
custom("https://intellij.chylex.com")
marketplace()
} }
} }
kotlin { changelog {
jvmToolchain(17) path = "${project.projectDir}/CHANGES.md"
header = closure { "${project.version}" }
} }
dependencies { dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") compileOnly(kotlin("stdlib-jdk8"))
} }
tasks.patchPluginXml { repositories {
sinceBuild.set("242") mavenCentral()
jcenter()
} }
tasks.buildSearchableOptions { intellij {
enabled = false version = "2020.2"
pluginName = "AceJump"
updateSinceUntilBuild = false
setPlugins("java")
} }
tasks.test { group = "org.acejump"
useJUnitPlatform() version = "4.0"
}
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}

View File

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

Binary file not shown.

View File

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

284
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/usr/bin/env sh
# #
# Copyright © 2015-2021 the original authors. # Copyright 2015 the original author or authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,111 +17,78 @@
# #
############################################################################## ##############################################################################
# ##
# Gradle start up script for POSIX generated by Gradle. ## Gradle start up script for UN*X
# ##
# 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 # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
app_path=$0 PRG="$0"
# Need this for relative symlinks.
# Need this for daisy-chained symlinks. while [ -h "$PRG" ] ; do
while ls=`ls -ld "$PRG"`
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path link=`expr "$ls" : '.*-> \(.*\)$'`
[ -h "$app_path" ] if expr "$link" : '/.*' > /dev/null; then
do PRG="$link"
ls=$( ls -ld "$app_path" ) else
link=${ls#*' -> '} PRG=`dirname "$PRG"`"/$link"
case $link in #( fi
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
# This is normally unused APP_NAME="Gradle"
# shellcheck disable=SC2034 APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD="maximum"
warn () { warn () {
echo "$*" echo "$*"
} >&2 }
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} >&2 }
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "$( uname )" in #( case "`uname`" in
CYGWIN* ) cygwin=true ;; #( CYGWIN* )
Darwin* ) darwin=true ;; #( cygwin=true
MSYS* | MINGW* ) msys=true ;; #( ;;
NONSTOP* ) nonstop=true ;; Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java JAVACMD="$JAVA_HOME/jre/sh/java"
else else
JAVACMD=$JAVA_HOME/bin/java JAVACMD="$JAVA_HOME/bin/java"
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -130,120 +97,87 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD="java"
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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 Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
case $MAX_FD in #( MAX_FD_LIMIT=`ulimit -H -n`
max*) if [ $? -eq 0 ] ; then
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
# shellcheck disable=SC2039,SC3045 MAX_FD="$MAX_FD_LIMIT"
MAX_FD=$( ulimit -H -n ) || fi
warn "Could not query maximum file descriptor limit" ulimit -n $MAX_FD
esac if [ $? -ne 0 ] ; then
case $MAX_FD in #( warn "Could not set maximum file descriptor limit: $MAX_FD"
'' | soft) :;; #( fi
*) else
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
# shellcheck disable=SC2039,SC3045 fi
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi fi
# Collect all arguments for the java command, stacking in reverse order: # For Darwin, add options to specify how the application appears in the dock
# * args from the command line if $darwin; then
# * the main class name GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
# * -classpath fi
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
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`
# Now convert the arguments - kludge to limit ourselves to /bin/sh SEP=""
for arg do for dir in $ROOTDIRSRAW ; do
if ROOTDIRS="$ROOTDIRS$SEP$dir"
case $arg in #( SEP="|"
-*) 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 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" ;;
esac
fi fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Collect all arguments for the java command, following the shell quoting and substitution rules
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# 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" "$@" exec "$JAVACMD" "$@"

35
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if "%ERRORLEVEL%" == "0" goto init
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -55,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto init
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -65,26 +64,38 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@@ -1,13 +0,0 @@
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

@@ -1,11 +1,9 @@
package org.acejump package org.acejump
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.Project annotation class ExternalUsage
import com.intellij.util.IncorrectOperationException
import it.unimi.dsi.fastutil.ints.IntArrayList
/** /**
* Returns an immutable version of the currently edited document. * Returns an immutable version of the currently edited document.
@@ -13,21 +11,6 @@ import it.unimi.dsi.fastutil.ints.IntArrayList
val Editor.immutableText val Editor.immutableText
get() = this.document.immutableCharSequence 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. * Returns true if [this] contains [otherText] at the specified offset.
*/ */
@@ -50,12 +33,77 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
return i return i
} }
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> { /**
val clone = HashMap<Editor, IntArrayList>(size) * Determines which characters form a "word" for the purposes of functions below.
*/
val Char.isWordPart
get() = 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
for ((editor, offsets) in this) { while (start > 0 && isPartOfWord(this[start - 1])) {
clone[editor] = offsets.clone() --start
} }
return clone 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
while (end < length - 1 && 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)
while (end < length - 1 && !isPartOfWord(this[end + 1])) {
++end
}
if (end < length - 1 && isPartOfWord(this[end + 1])) {
++end
}
return end
} }

View File

@@ -4,13 +4,15 @@ import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.Pattern
import org.acejump.session.Session import org.acejump.session.Session
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
/** /**
* Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session]. * Base class for keyboard-activated overrides of existing editor actions, that have a different meaning during an AceJump [Session].
*/ */
abstract class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() { sealed class AceEditorAction(private val originalHandler: EditorActionHandler) : EditorActionHandler() {
final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { final override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext) return SessionManager[editor] != null || originalHandler.isEnabled(editor, caret, dataContext)
} }
@@ -41,4 +43,16 @@ abstract class AceEditorAction(private val originalHandler: EditorActionHandler)
class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) { class TagImmediately(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.tagImmediately() override fun run(session: Session) = session.tagImmediately()
} }
class SearchLineStarts(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_STARTS, StandardBoundaries.VISIBLE_ON_SCREEN)
}
class SearchLineEnds(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_ENDS, StandardBoundaries.VISIBLE_ON_SCREEN)
}
class SearchLineIndents(originalHandler: EditorActionHandler) : AceEditorAction(originalHandler) {
override fun run(session: Session) = session.startRegexSearch(Pattern.LINE_INDENTS, StandardBoundaries.VISIBLE_ON_SCREEN)
}
} }

View File

@@ -0,0 +1,51 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
import com.intellij.openapi.project.DumbAwareAction
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].
*/
sealed class AceKeyboardAction : DumbAwareAction() {
final override fun update(action: AnActionEvent) {
action.presentation.isEnabled = action.getData(EDITOR) != null
}
final override fun actionPerformed(e: AnActionEvent) {
invoke(SessionManager.start(e.getData(EDITOR) ?: return))
}
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.startRegexSearch(pattern, boundaries)
}
/**
* Starts or ends an AceJump session.
*/
object ActivateAceJump : AceKeyboardAction() {
override fun invoke(session: Session) = session.cycleMode()
}
// @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,69 +1,165 @@
package org.acejump.action package org.acejump.action
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction
import com.intellij.codeInsight.navigation.actions.GotoTypeDeclarationAction
import com.intellij.find.actions.FindUsagesAction
import com.intellij.find.actions.ShowUsagesAction
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy 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.Document
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.playback.commands.ActionCommand import com.intellij.openapi.ui.playback.commands.ActionCommand
import org.acejump.search.Tag import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.refactoring.actions.RefactoringQuickListPopupAction
import com.intellij.refactoring.actions.RenameElementAction
import org.acejump.*
import org.acejump.search.SearchProcessor
import kotlin.math.max
/** /**
* Base class for actions available after typing a tag. * Base class for actions available after typing a tag.
*/ */
sealed class AceTagAction { sealed class AceTagAction {
abstract operator fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) abstract operator fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean)
abstract class BaseJumpAction : AceTagAction() { abstract class BaseJumpAction : AceTagAction() {
override fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val editor = tag.editor
val caretModel = editor.caretModel val caretModel = editor.caretModel
val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList() val oldCarets = if (shiftMode) caretModel.caretsAndSelections else emptyList()
editor.project?.let { addCurrentPositionToHistory(it, editor.document) } recordCaretPosition(editor)
moveCaretTo(editor, getCaretOffset(editor, searchProcessor, offset))
if (isFinal) {
ensureEditorFocused(editor)
}
moveCaretTo(editor, tag.offset)
if (shiftMode) { if (shiftMode) {
caretModel.caretsAndSelections = oldCarets + caretModel.caretsAndSelections 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) {
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) {
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
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 { 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) { fun moveCaretTo(editor: Editor, offset: Int) = with(editor) {
selectionModel.removeSelection(true) selectionModel.removeSelection(true)
caretModel.removeSecondaryCarets() caretModel.removeSecondaryCarets()
caretModel.moveToOffset(offset) caretModel.moveToOffset(offset)
} }
fun performAction(actionName: String) { fun selectRange(editor: Editor, fromOffset: Int, toOffset: Int, cursorOffset: Int = toOffset) = with(editor) {
val actionManager = ActionManager.getInstance() selectionModel.removeSelection(true)
val action = actionManager.getAction(actionName) selectionModel.setSelection(fromOffset, toOffset)
if (action != null) { caretModel.moveToOffset(cursorOffset)
actionManager.tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
}
} }
fun ensureEditorFocused(editor: Editor) { fun performAction(action: AnAction) {
val project = editor.project ?: return ActionManager.getInstance().tryToExecute(action, ActionCommand.getInputEvent(null), null, null, true)
val fem = FileEditorManagerEx.getInstanceEx(project)
val window = fem.windows.firstOrNull { (it.selectedComposite?.selectedWithProvider?.fileEditor as? TextEditor)?.editor === editor }
if (window != null && window !== fem.currentWindow) {
fem.currentWindow = window
}
} }
private fun addCurrentPositionToHistory(project: Project, document: Document) { private fun addCurrentPositionToHistory(project: Project, document: Document) {
@@ -83,7 +179,298 @@ sealed class AceTagAction {
* On default action, places the caret at the first character of the search query. * On default action, places the caret at the first character of the search query.
* On shift action, adds the new caret to existing carets. * On shift action, adds the new caret to existing carets.
*/ */
object JumpToSearchStart : BaseJumpAction() 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)
}
}
}
/**
* 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)
}
}
}
/**
* On default action, selects a word according to [SelectWord], and then extends the selection in either or both directions based
* on the characters around the word. TODO
*
* On shift action, adds the new selection to existing selections.
*/
object SelectAroundWord : BaseSelectAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int) {
SelectWord(editor, searchProcessor, offset, shiftMode = false)
val text = editor.immutableText
var selectionStart = editor.selectionModel.selectionStart
var selectionEnd = editor.selectionModel.selectionEnd
val indentStart = EditorActionUtil.findFirstNonSpaceOffsetOnTheLine(editor.document, editor.caretModel.logicalPosition.line)
while (selectionStart > 0 && selectionStart > indentStart && text[selectionStart - 1].let { it == ' ' || it == ',' }) {
--selectionStart
}
while (selectionEnd < text.length && text[selectionEnd].let { it == ' ' || it == ',' }) {
++selectionEnd
}
if (selectionStart > 0 && text[selectionStart - 1] == '!') {
--selectionStart
}
selectRange(editor, selectionStart, selectionEnd)
}
}
/**
* 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)
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)
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_SELECT_WORD_AT_CARET)
repeat(extendCount) {
performAction(action)
}
}
}
/**
* 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)
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)
selectRange(editor, firstOffset, editor.caretModel.offset)
}
}
/**
* On default action, selects text based on the provided [selector] action, and deletes it without moving the existing carets.
* On shift action, moves caret to the position where deletion occurred.
*/
class Delete(private val selector: AceTagAction) : AceTagAction() {
override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
val oldCarets = editor.caretModel.caretsAndSelections
selector(editor, searchProcessor, offset, shiftMode = false)
WriteCommandAction.writeCommandAction(editor.project).withName("AceJump Delete").run<Throwable> {
editor.selectionModel.let { editor.document.deleteString(it.selectionStart, it.selectionEnd) }
}
if (!shiftMode) {
editor.caretModel.caretsAndSelections = oldCarets
}
}
}
/**
* 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)
}
}
}
/** /**
* On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`. * On default action, performs the Go To Declaration action, available via `Navigate | Declaration or Usages`.
@@ -91,9 +478,49 @@ sealed class AceTagAction {
* Always places the caret at the start of the word. * Always places the caret at the start of the word.
*/ */
object GoToDeclaration : AceTagAction() { object GoToDeclaration : AceTagAction() {
override fun invoke(tag: Tag, shiftMode: Boolean, isFinal: Boolean) { override fun invoke(editor: Editor, searchProcessor: SearchProcessor, offset: Int, shiftMode: Boolean) {
JumpToSearchStart.invoke(tag, shiftMode = false, isFinal = isFinal) JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
ApplicationManager.getApplication().invokeLater { performAction(if (shiftMode) IdeActions.ACTION_GOTO_TYPE_DECLARATION else IdeActions.ACTION_GOTO_DECLARATION) } performAction(if (shiftMode) GotoTypeDeclarationAction() else GotoDeclarationAction())
}
}
/**
* 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) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
performAction(if (shiftMode) FindUsagesAction() else ShowUsagesAction())
}
}
/**
* 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) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
performAction(ShowIntentionActionsAction())
}
}
/**
* 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) {
JumpToWordStart(editor, searchProcessor, offset, shiftMode = false)
if (shiftMode) {
ApplicationManager.getApplication().invokeLater { performAction(RenameElementAction()) }
}
else {
performAction(RefactoringQuickListPopupAction())
}
} }
} }
} }

View File

@@ -1,189 +0,0 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
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.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.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode.OP_PENDING
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.JumpMode
import org.acejump.search.Pattern
import org.acejump.search.Tag
import org.acejump.session.SessionManager
import org.acejump.session.SessionState
sealed class AceVimAction : DumbAwareAction() {
protected abstract val mode: AceVimMode
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
if (vim.mode is OP_PENDING) {
val keyHandler = KeyHandler.getInstance()
val key = keyHandler.keyHandlerState.commandBuilder.keys.singleOrNull()?.keyChar
keyHandler.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> {
keyHandler.keyHandlerState.commandBuilder.addAction(action)
val cmd = keyHandler.keyHandlerState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(vim.mode is OP_PENDING, cmd.rawCount, injector.vimState.mode)
injector.vimState.executingCommand = cmd
injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments)
}
keyHandler.reset(vim)
}
}
}
}
injector.scroll.scrollCaretIntoView(editor.vim)
mode.finishSession(editor, session)
}
}
}
mode.setupSession(editor, session)
}
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
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
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

@@ -1,64 +0,0 @@
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

@@ -18,11 +18,6 @@ sealed class EditorOffsetCache {
*/ */
abstract fun visibleArea(editor: Editor): Pair<Point, Point> abstract fun visibleArea(editor: Editor): Pair<Point, Point>
/**
* Returns whether the offset is in the visible area rectangle.
*/
abstract fun isVisible(editor: Editor, offset: Int): Boolean
/** /**
* Returns the editor offset at the provided pixel coordinate. * Returns the editor offset at the provided pixel coordinate.
*/ */
@@ -41,7 +36,6 @@ sealed class EditorOffsetCache {
private class Cache : EditorOffsetCache() { private class Cache : EditorOffsetCache() {
private var visibleArea: Pair<Point, Point>? = null private var visibleArea: Pair<Point, Point>? = null
private val lineToVisibleOffsetRange = Int2ObjectOpenHashMap<IntRange>()
private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) } private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
private val offsetToPoint = Int2ObjectOpenHashMap<Point>() private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
@@ -49,24 +43,6 @@ sealed class EditorOffsetCache {
return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it } return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
} }
override fun isVisible(editor: Editor, offset: Int): Boolean {
val visualLine = editor.offsetToVisualLine(offset, false)
var visibleRange = lineToVisibleOffsetRange.get(visualLine)
if (visibleRange == null) {
val (topLeft, bottomRight) = visibleArea(editor)
val lineY = editor.visualLineToY(visualLine)
val firstVisibleOffset = xyToOffset(editor, Point(topLeft.x, lineY))
val lastVisibleOffset = xyToOffset(editor, Point(bottomRight.x, lineY))
visibleRange = firstVisibleOffset..lastVisibleOffset
lineToVisibleOffsetRange.put(visualLine, visibleRange)
}
return offset in visibleRange
}
override fun xyToOffset(editor: Editor, pos: Point): Int { override fun xyToOffset(editor: Editor, pos: Point): Int {
val offset = pointToOffset.getInt(pos) val offset = pointToOffset.getInt(pos)
@@ -75,6 +51,7 @@ sealed class EditorOffsetCache {
} }
return Uncached.xyToOffset(editor, pos).also { return Uncached.xyToOffset(editor, pos).also {
@Suppress("ReplacePutWithAssignment")
pointToOffset.put(pos, it) pointToOffset.put(pos, it)
} }
} }
@@ -87,6 +64,7 @@ sealed class EditorOffsetCache {
} }
return Uncached.offsetToXY(editor, offset).also { return Uncached.offsetToXY(editor, offset).also {
@Suppress("ReplacePutWithAssignment")
offsetToPoint.put(offset, it) offsetToPoint.put(offset, it)
} }
} }
@@ -102,15 +80,6 @@ sealed class EditorOffsetCache {
) )
} }
override fun isVisible(editor: Editor, offset: Int): Boolean {
val (topLeft, bottomRight) = visibleArea(editor)
val pos = offsetToXY(editor, offset)
val x = pos.x
val y = pos.y
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
}
override fun xyToOffset(editor: Editor, pos: Point): Int { override fun xyToOffset(editor: Editor, pos: Point): Int {
return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos)) return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
} }

View File

@@ -13,40 +13,43 @@ enum class StandardBoundaries : Boundaries {
} }
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean { override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return cache.isVisible(editor, offset)
// If we are not using a cache, calling getOffsetRange will cause additional 1-2 pixel coordinate -> offset lookups, which is a lot
// more expensive than one lookup compared against the visible area.
// However, if we are using a cache, it's likely that the topmost and bottommost positions are already cached whereas the provided
// offset isn't, so we save a lookup for every offset outside the range.
if (cache !== EditorOffsetCache.Uncached && offset !in getOffsetRange(editor, cache)) {
return false
}
val (topLeft, bottomRight) = cache.visibleArea(editor)
val pos = cache.offsetToXY(editor, offset)
val x = pos.x
val y = pos.y
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
} }
}, },
BEFORE_CARET { BEFORE_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange { override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return 0 until editor.caretModel.offset return 0..(editor.caretModel.offset)
} }
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean { override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset < editor.caretModel.offset return offset <= editor.caretModel.offset
} }
}, },
AFTER_CARET { AFTER_CARET {
override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange { override fun getOffsetRange(editor: Editor, cache: EditorOffsetCache): IntRange {
return (editor.caretModel.offset + 1) until editor.document.textLength return editor.caretModel.offset until editor.document.textLength
} }
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean { override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
return offset > editor.caretModel.offset 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,12 +20,13 @@ class AceConfig : PersistentStateComponent<AceSettings> {
val layout get() = settings.layout val layout get() = settings.layout
val minQueryLength get() = settings.minQueryLength val minQueryLength get() = settings.minQueryLength
val invertUppercaseMode get() = settings.invertUppercaseMode
val editorFadeOpacity get() = settings.editorFadeOpacity
val jumpModeColor get() = settings.jumpModeColor val jumpModeColor get() = settings.jumpModeColor
val tagForegroundColor1 get() = settings.tagForegroundColor1 val fromCaretModeColor get() = settings.fromCaretModeColor
val tagForegroundColor2 get() = settings.tagForegroundColor2 val betweenPointsModeColor get() = settings.betweenPointsModeColor
val searchHighlightColor get() = settings.searchHighlightColor val textHighlightColor get() = settings.textHighlightColor
val tagForegroundColor get() = settings.tagForegroundColor
val tagBackgroundColor get() = settings.tagBackgroundColor
val acceptedTagColor get() = settings.acceptedTagColor
} }
override fun getState(): AceSettings { override fun getState(): AceSettings {

View File

@@ -15,23 +15,25 @@ class AceConfigurable : Configurable {
panel.allowedChars != settings.allowedChars || panel.allowedChars != settings.allowedChars ||
panel.keyboardLayout != settings.layout || panel.keyboardLayout != settings.layout ||
panel.minQueryLengthInt != settings.minQueryLength || panel.minQueryLengthInt != settings.minQueryLength ||
panel.invertUppercaseMode != settings.invertUppercaseMode ||
panel.editorFadeOpacityPercent != settings.editorFadeOpacity ||
panel.jumpModeColor != settings.jumpModeColor || panel.jumpModeColor != settings.jumpModeColor ||
panel.tagForegroundColor1 != settings.tagForegroundColor1 || panel.fromCaretModeColor != settings.fromCaretModeColor ||
panel.tagForegroundColor2 != settings.tagForegroundColor2 || panel.betweenPointsModeColor != settings.betweenPointsModeColor ||
panel.searchHighlightColor != settings.searchHighlightColor panel.textHighlightColor != settings.textHighlightColor ||
panel.tagForegroundColor != settings.tagForegroundColor ||
panel.tagBackgroundColor != settings.tagBackgroundColor ||
panel.acceptedTagColor != settings.acceptedTagColor
override fun apply() { override fun apply() {
settings.allowedChars = panel.allowedChars settings.allowedChars = panel.allowedChars
settings.layout = panel.keyboardLayout settings.layout = panel.keyboardLayout
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
settings.invertUppercaseMode = panel.invertUppercaseMode
settings.editorFadeOpacity = panel.editorFadeOpacityPercent
panel.jumpModeColor?.let { settings.jumpModeColor = it } panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.tagForegroundColor1?.let { settings.tagForegroundColor1 = it } panel.fromCaretModeColor?.let { settings.fromCaretModeColor = it }
panel.tagForegroundColor2?.let { settings.tagForegroundColor2 = it } panel.betweenPointsModeColor?.let { settings.betweenPointsModeColor = it }
panel.searchHighlightColor?.let { settings.searchHighlightColor = 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 }
KeyLayoutCache.reset(settings) KeyLayoutCache.reset(settings)
} }

View File

@@ -8,19 +8,26 @@ import java.awt.Color
data class AceSettings( data class AceSettings(
var layout: KeyLayout = QWERTY, var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars, var allowedChars: String = layout.allChars,
var invertUppercaseMode: Boolean = false,
var minQueryLength: Int = 1, var minQueryLength: Int = 1,
var editorFadeOpacity: Int = 70,
@OptionTag("jumpModeRGB", converter = ColorConverter::class) @OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF), var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("fromCaretModeRGB", converter = ColorConverter::class)
var fromCaretModeColor: 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) @OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var tagForegroundColor1: Color = Color(0xFFFFFF), var tagForegroundColor: Color = Color(0xFFFFFF),
@OptionTag("tagForeground2RGB", converter = ColorConverter::class) @OptionTag("tagBackgroundRGB", converter = ColorConverter::class)
var tagForegroundColor2: Color = Color(0xFFFFFF), var tagBackgroundColor: Color = Color(0x008299),
@OptionTag("searchHighlightRGB", converter = ColorConverter::class) @OptionTag("acceptedTagRGB", converter = ColorConverter::class)
var searchHighlightColor: Color = Color(0x008299), var acceptedTagColor: Color = Color(0x394B58)
) )

View File

@@ -2,23 +2,18 @@ package org.acejump.config
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.ColorPanel import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBSlider
import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.layout.Cell
import com.intellij.ui.dsl.builder.COLUMNS_SHORT import com.intellij.ui.layout.GrowPolicy.MEDIUM_TEXT
import com.intellij.ui.dsl.builder.columns import com.intellij.ui.layout.GrowPolicy.SHORT_TEXT
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.layout.panel
import org.acejump.input.KeyLayout import org.acejump.input.KeyLayout
import java.awt.Color import java.awt.Color
import java.awt.Dimension
import java.awt.Font import java.awt.Font
import java.util.Hashtable
import javax.swing.JCheckBox import javax.swing.JCheckBox
import javax.swing.JLabel import javax.swing.JComponent
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.JSlider
import javax.swing.text.JTextComponent import javax.swing.text.JTextComponent
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@@ -27,95 +22,82 @@ import kotlin.reflect.KProperty
*/ */
@Suppress("UsePropertyAccessSyntax") @Suppress("UsePropertyAccessSyntax")
internal class AceSettingsPanel { internal class AceSettingsPanel {
private val tagAllowedCharsField = JBTextField() private val tagCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>() private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false } private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val minQueryLengthField = JBTextField() private val minQueryLengthField = JBTextField()
private val invertUppercaseModeCheckBox = JBCheckBox("Invert uppercase mode")
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
minimumSize = Dimension(275, minimumSize.height)
}
private val jumpModeColorWheel = ColorPanel() private val jumpModeColorWheel = ColorPanel()
private val tagForeground1ColorWheel = ColorPanel() private val fromCaretModeColorWheel = ColorPanel()
private val tagForeground2ColorWheel = ColorPanel() private val betweenPointsModeColorWheel = ColorPanel()
private val searchHighlightColorWheel = ColorPanel() private val textHighlightColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel()
private val tagBackgroundColorWheel = ColorPanel()
private val acceptedTagColorWheel = ColorPanel()
init { init {
tagAllowedCharsField.apply { font = Font("monospaced", font.style, font.size) } tagCharsField.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) } keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") } keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") }
} }
internal val rootPanel: JPanel = panel { internal val rootPanel: JPanel = panel {
group("Characters and Layout") { fun Cell.short(component: JComponent) = component(growPolicy = SHORT_TEXT)
row("Allowed characters in tags:") { cell(tagAllowedCharsField).columns(COLUMNS_LARGE) } fun Cell.medium(component: JComponent) = component(growPolicy = MEDIUM_TEXT)
row("Keyboard layout:") { cell(keyboardLayoutCombo).columns(COLUMNS_SHORT) }
row("Keyboard design:") { cell(keyboardLayoutArea).columns(COLUMNS_SHORT) } titledRow("Characters and Layout") {
row("Allowed characters in tags:") { medium(tagCharsField) }
row("Keyboard layout:") { short(keyboardLayoutCombo) }
row("Keyboard design:") { short(keyboardLayoutArea) }
} }
group("Behavior") { titledRow("Behavior") {
row("Minimum typed characters (1-10):") { cell(minQueryLengthField).columns(COLUMNS_SHORT) } row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
row { cell(invertUppercaseModeCheckBox) }
} }
group("Colors") { titledRow("Colors") {
row("Caret background:") { row("Jump mode caret background:") { short(jumpModeColorWheel) }
cell(jumpModeColorWheel) row("From Caret mode caret background:") { short(fromCaretModeColorWheel) }
} row("Between Points mode caret background:") { short(betweenPointsModeColorWheel) }
row("Tag foreground:") { row("Searched text background:") { short(textHighlightColorWheel) }
cell(tagForeground1ColorWheel) row("Tag foreground:") { short(tagForegroundColorWheel) }
cell(tagForeground2ColorWheel) row("Tag background:") { short(tagBackgroundColorWheel) }
} row("Accepted tag position background:") { short(acceptedTagColorWheel) }
row("Search highlight:") {
cell(searchHighlightColorWheel)
}
row("Editor fade opacity (%):") {
cell(editorFadeOpacitySlider)
}
} }
} }
// Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342 // Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342
internal var allowedChars by tagAllowedCharsField internal var allowedChars by tagCharsField
internal var keyboardLayout by keyboardLayoutCombo internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea internal var keyChars by keyboardLayoutArea
internal var minQueryLength by minQueryLengthField internal var minQueryLength by minQueryLengthField
internal var invertUppercaseMode by invertUppercaseModeCheckBox
internal var editorFadeOpacity by editorFadeOpacitySlider
internal var jumpModeColor by jumpModeColorWheel internal var jumpModeColor by jumpModeColorWheel
internal var tagForegroundColor1 by tagForeground1ColorWheel internal var fromCaretModeColor by fromCaretModeColorWheel
internal var tagForegroundColor2 by tagForeground2ColorWheel internal var betweenPointsModeColor by betweenPointsModeColorWheel
internal var searchHighlightColor by searchHighlightColorWheel internal var textHighlightColor by textHighlightColorWheel
internal var tagForegroundColor by tagForegroundColorWheel
internal var tagBackgroundColor by tagBackgroundColorWheel
internal var acceptedTagColor by acceptedTagColorWheel
internal var minQueryLengthInt internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10) get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) { minQueryLength = value.toString() } set(value) { minQueryLength = value.toString() }
internal var editorFadeOpacityPercent
get() = editorFadeOpacity * 10
set(value) { editorFadeOpacity = value / 10 }
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars allowedChars = settings.allowedChars
keyboardLayout = settings.layout keyboardLayout = settings.layout
minQueryLength = settings.minQueryLength.toString() minQueryLength = settings.minQueryLength.toString()
invertUppercaseMode = settings.invertUppercaseMode
editorFadeOpacityPercent = settings.editorFadeOpacity
jumpModeColor = settings.jumpModeColor jumpModeColor = settings.jumpModeColor
tagForegroundColor1 = settings.tagForegroundColor1 fromCaretModeColor = settings.fromCaretModeColor
tagForegroundColor2 = settings.tagForegroundColor2 betweenPointsModeColor = settings.betweenPointsModeColor
searchHighlightColor = settings.searchHighlightColor textHighlightColor = settings.textHighlightColor
tagForegroundColor = settings.tagForegroundColor
tagBackgroundColor = settings.tagBackgroundColor
acceptedTagColor = settings.acceptedTagColor
} }
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575 // Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.lowercase() private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.toLowerCase()
private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s) private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s)
private operator fun ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor private operator fun ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor
@@ -124,9 +106,6 @@ internal class AceSettingsPanel {
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected 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 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>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item) private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)

View File

@@ -10,6 +10,7 @@ import com.intellij.openapi.editor.actionSystem.TypedActionHandler
* sessions' own handlers. * sessions' own handlers.
*/ */
internal object EditorKeyListener : TypedActionHandler { internal object EditorKeyListener : TypedActionHandler {
private val action = TypedAction.getInstance()
private val attached = mutableMapOf<Editor, TypedActionHandler>() private val attached = mutableMapOf<Editor, TypedActionHandler>()
private var originalHandler: TypedActionHandler? = null private var originalHandler: TypedActionHandler? = null
@@ -19,9 +20,8 @@ internal object EditorKeyListener : TypedActionHandler {
fun attach(editor: Editor, callback: TypedActionHandler) { fun attach(editor: Editor, callback: TypedActionHandler) {
if (attached.isEmpty()) { if (attached.isEmpty()) {
val typedAction = TypedAction.getInstance() originalHandler = action.rawHandler
originalHandler = typedAction.rawHandler action.setupRawHandler(this)
typedAction.setupRawHandler(this)
} }
attached[editor] = callback attached[editor] = callback
@@ -31,7 +31,7 @@ internal object EditorKeyListener : TypedActionHandler {
attached.remove(editor) attached.remove(editor)
if (attached.isEmpty()) { if (attached.isEmpty()) {
originalHandler?.let(TypedAction.getInstance()::setupRawHandler) originalHandler?.let(action::setupRawHandler)
originalHandler = null originalHandler = null
} }
} }

View File

@@ -4,49 +4,21 @@ 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 * 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. * ergonomically difficult they are to press.
*/ */
@Suppress("unused", "SpellCheckingInspection") @Suppress("unused")
enum class KeyLayout( enum class KeyLayout(internal val rows: Array<String>, priority: String) {
internal val rows: Array<String>,
priority: String,
private val characterSides: Pair<Set<Char>, Set<Char>> = Pair(emptySet(), emptySet()),
internal val characterRemapping: Map<Char, Char> = emptyMap(),
) {
COLEMK(arrayOf("1234567890", "qwfpgjluy", "arstdhneio", "zxcvbkm"), priority = "tndhseriaovkcmbxzgjplfuwyq5849673210"), COLEMK(arrayOf("1234567890", "qwfpgjluy", "arstdhneio", "zxcvbkm"), priority = "tndhseriaovkcmbxzgjplfuwyq5849673210"),
WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"), WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"),
DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"), DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"),
QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "zxcvb"), listOf("7890", "yuiop", "hjkl", "nm"))), QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210"),
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "yxcvb"), listOf("7890", "zuiop", "hjkl", "nm"))), QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QWERTZ_CZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "yxcvb"), listOf("7890", "zuiop", "hjkl", "nm")), 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"), QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"), QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"),
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210"); NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210");
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("") internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
private val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap() internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
fun priority(char: Char): Int { internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int? {
return allPriorities[char] ?: allChars.length return { allPriorities[tagToChar(it)] }
}
fun areOnSameSide(c1: Char, c2: Char): Boolean {
return (c1 in characterSides.first && c2 in characterSides.first) || (c1 in characterSides.second && c2 in characterSides.second)
} }
} }
private fun sides(left: List<String>, right: List<String>): Pair<Set<Char>, Set<Char>> {
return Pair(
left.flatMapTo(mutableSetOf()) { it.toCharArray().toSet() },
right.flatMapTo(mutableSetOf()) { it.toCharArray().toSet() }
)
}

View File

@@ -1,22 +1,72 @@
package org.acejump.input package org.acejump.input
import org.acejump.config.AceSettings import org.acejump.config.AceSettings
import kotlin.math.pow
import kotlin.math.roundToInt
/** /**
* Stores data specific to the selected keyboard layout. We want to assign tags with easily reachable keys first, and ideally have tags * Stores data specific to the selected keyboard layout. We want to assign tags with easily reachable keys first, and ideally have tags
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ). * with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/ */
internal object KeyLayoutCache { internal object KeyLayoutCache {
lateinit var allowedTagsSorted: List<String> /**
* 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>
private set private set
/** /**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing. * Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/ */
fun ensureInitialized(settings: AceSettings) { fun ensureInitialized(settings: AceSettings) {
if (!::allowedTagsSorted.isInitialized) { if (!::tagOrder.isInitialized) {
reset(settings) reset(settings)
} }
} }
@@ -25,38 +75,19 @@ internal object KeyLayoutCache {
* Re-initializes cached data according to updated settings. * Re-initializes cached data according to updated settings.
*/ */
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
val allowedChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars) } tagOrder = compareBy(
val allowedTags = mutableSetOf<String>() { it[0].isDigit() || it[1].isDigit() },
{ qwertyCharacterDistances.getValue(it[0]).getValue(it[1]) },
settings.layout.priority { it[0] }
)
for (c1 in allowedChars) { val allPossibleChars = settings.allowedChars
allowedTags.add("$c1") .toCharArray()
.filter(Char::isLetterOrDigit)
for (c2 in allowedChars) { .distinct()
if (c1 != c2) { .joinToString("")
allowedTags.add("$c1$c2") .ifEmpty(settings.layout::allChars)
}
}
}
allowedTagsSorted = allowedTags.sortedBy { rankPriority(settings.layout, it) } allPossibleTags = allPossibleChars.flatMap { a -> allPossibleChars.map { b -> "$a$b".intern() } }.sortedWith(tagOrder)
}
private fun processCharList(charList: String): List<Char> {
return charList.toCharArray().map(Char::lowercaseChar).distinct()
}
private fun rankPriority(layout: KeyLayout, tag: String): Int {
val c1 = tag.first()
val p1 = (1.0 + layout.priority(c1)).pow(3)
if (tag.length == 1) {
return p1.roundToInt()
}
val c2 = tag.last()
val p2 = (1.0 + layout.priority(c2)).pow(3)
val multiplier = if (layout.areOnSameSide(c1, c2)) 2 else 1
return (((p1 * 50) + p2 + 1000) * multiplier).roundToInt()
} }
} }

View File

@@ -0,0 +1,91 @@
package org.acejump.modes
import com.intellij.openapi.editor.CaretState
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
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>[S]</f>elect... / <f>[F]</f>rom Caret...",
"<f>[D]</f>elete...",
"<f>[C]</f>lone to Caret...",
"<f>[M]</f>ove to Caret..."
)
private const val ACTION_MODE_FROM_CARET = 'F'
private val ACTION_MODE_MAP = mapOf(
'S' to ({ action: AceTagAction.BaseSelectAction -> action }),
'D' to (AceTagAction::Delete),
'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: Int?): TypeResult {
val actionMode = actionMode
if (actionMode == null) {
if (charTyped.equals(ACTION_MODE_FROM_CARET, ignoreCase = true)) {
return TypeResult.ChangeMode(SelectFromCaretMode())
}
this.actionMode = ACTION_MODE_MAP[charTyped.toUpperCase()]
return TypeResult.Nothing
}
if (acceptedTag == null) {
return state.type(charTyped)
}
if (firstOffset == null) {
val selectAction = JumpMode.SELECT_ACTION_MAP[charTyped.toUpperCase()]
if (selectAction != null) {
state.act(actionMode(selectAction), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
}
val jumpAction = JumpMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
val firstOffset = firstOffset
if (firstOffset == null) {
val caretModel = state.editor.caretModel
this.originalCarets = caretModel.caretsAndSelections
state.act(jumpAction, acceptedTag, shiftMode = false)
this.firstOffset = caretModel.offset
return TypeResult.RestartSearch
}
originalCarets?.let { state.editor.caretModel.caretsAndSelections = it }
state.act(actionMode(AceTagAction.SelectBetweenPoints(firstOffset, jumpAction)), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
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 -> JumpMode.JUMP_ALT_HINT + JumpMode.SELECT_HINT
else -> JumpMode.JUMP_ALT_HINT
}
}
}

View File

@@ -2,23 +2,82 @@ package org.acejump.modes
import org.acejump.action.AceTagAction import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.search.Tag
import org.acejump.session.SessionState import org.acejump.session.SessionState
import org.acejump.session.TypeResult import org.acejump.session.TypeResult
open class JumpMode : SessionMode { class JumpMode : SessionMode {
companion object {
private val JUMP_HINT = arrayOf(
"<f>[J]</f>ump / <f>[L]</f> past Query / <f>[M]</f> Line End",
"Word <f>[S]</f>tart / <f>[E]</f>nd"
)
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,
'M' to AceTagAction.JumpToLineEnd,
'S' to AceTagAction.JumpToWordStart,
'E' to AceTagAction.JumpToWordEnd
)
val SELECT_HINT = arrayOf(
"Select <f>[W]</f>ord / <f>[H]</f>ump / <f>[A]</f>round",
"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,
'A' to AceTagAction.SelectAroundWord,
'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,
"Select <f>[P]</f>rogressively...",
"<f>[D]</f>eclaration / <f>[U]</f>sages",
"<f>[I]</f>ntentions / <f>[R]</f>efactor"
)
private const val ACTION_SELECT_PROGRESSIVELY = 'P'
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(),
'D' to AceTagAction.GoToDeclaration,
'U' to AceTagAction.ShowUsages,
'I' to AceTagAction.ShowIntentions,
'R' to AceTagAction.Refactor
)
}
override val caretColor override val caretColor
get() = AceConfig.jumpModeColor get() = AceConfig.jumpModeColor
protected var wasUpperCase = false override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
private set if (acceptedTag == null) {
return state.type(charTyped)
}
override fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult { val action = ALL_ACTION_MAP[charTyped.toUpperCase()]
wasUpperCase = charTyped.isUpperCase() if (action != null) {
return state.type(charTyped) state.act(action, acceptedTag, charTyped.isUpperCase())
return TypeResult.EndSession
}
else if (charTyped.equals(ACTION_SELECT_PROGRESSIVELY, ignoreCase = true)) {
state.act(AceTagAction.SelectQuery, acceptedTag, charTyped.isUpperCase())
return TypeResult.ChangeMode(ProgressiveSelectionMode())
}
return TypeResult.Nothing
} }
override fun accept(state: SessionState, acceptedTag: Tag) { override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
AceTagAction.JumpToSearchStart.invoke(acceptedTag, shiftMode = wasUpperCase, isFinal = true) return ALL_HINTS.takeIf { acceptedTag != null }
} }
} }

View File

@@ -0,0 +1,142 @@
package org.acejump.modes
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class ProgressiveSelectionMode : SessionMode {
private companion object {
private val EXPANSION_HINT = arrayOf(
"<f>[W]</f>ord / <f>[C]</f>har / <f>[L]</f>ine / <f>[S]</f>pace"
)
private val EXPANSION_MODES = mapOf(
'W' to SelectionMode.Word,
'C' to SelectionMode.Char,
'L' to SelectionMode.Line,
'S' to SelectionMode.Space
)
}
override val caretColor
get() = AceConfig.jumpModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
val editor = state.editor
val mode = EXPANSION_MODES[charTyped.toUpperCase()]
if (mode != null) {
val hintOffset = if (charTyped.isUpperCase()) {
editor.caretModel.runForEachCaret { mode.extendLeft(editor, it) }
editor.caretModel.allCarets.first().selectionStart
}
else {
editor.caretModel.runForEachCaret { mode.extendRight(editor, it); it.moveToOffset(it.selectionEnd) }
editor.caretModel.allCarets.last().selectionEnd
}
editor.scrollingModel.scrollTo(editor.offsetToLogicalPosition(hintOffset), ScrollType.RELATIVE)
return TypeResult.MoveHint(hintOffset)
}
return TypeResult.Nothing
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String> {
return EXPANSION_HINT
}
private sealed class SelectionMode {
abstract fun extendLeft(editor: Editor, caret: Caret)
abstract fun extendRight(editor: Editor, caret: Caret)
object Word : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
val text = editor.immutableText
val wordPart = when {
caret.selectionStart == 0 -> caret.selectionStart
text[caret.selectionStart - 1].isWordPart -> caret.selectionStart - 1
else -> (caret.selectionStart - 1 downTo 0).find { text[it].isWordPart } ?: return
}
caret.setSelection(caret.selectionEnd, AceTagAction.JumpToWordStart.getCaretOffset(editor, wordPart, wordPart, isInsideWord = true))
}
override fun extendRight(editor: Editor, caret: Caret) {
val text = editor.immutableText
val wordPart = when {
text[caret.selectionEnd].isWordPart -> caret.selectionEnd
else -> (caret.selectionEnd until text.length).find { text[it].isWordPart } ?: return
}
caret.setSelection(caret.selectionStart, AceTagAction.JumpToWordEnd.getCaretOffset(editor, wordPart, wordPart, isInsideWord = true))
}
}
object Char : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
caret.setSelection((caret.selectionStart - 1).coerceAtLeast(0), caret.selectionEnd)
}
override fun extendRight(editor: Editor, caret: Caret) {
caret.setSelection(caret.selectionStart, (caret.selectionEnd + 1).coerceAtMost(editor.immutableText.length))
}
}
object Line : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
val document = editor.document
val line = document.getLineNumber(caret.selectionStart)
val lineOffset = document.getLineStartOffset(line)
if (caret.selectionStart > lineOffset) {
caret.setSelection(lineOffset, caret.selectionEnd)
}
else if (line - 1 >= 0) {
caret.setSelection(document.getLineStartOffset(line - 1), caret.selectionEnd)
}
}
override fun extendRight(editor: Editor, caret: Caret) {
val document = editor.document
val line = document.getLineNumber(caret.selectionEnd)
val lineOffset = document.getLineEndOffset(line)
if (caret.selectionEnd < lineOffset) {
caret.setSelection(caret.selectionStart, lineOffset)
}
else if (line + 1 < document.lineCount) {
caret.setSelection(caret.selectionStart, document.getLineEndOffset(line + 1))
}
}
}
object Space : SelectionMode() {
override fun extendLeft(editor: Editor, caret: Caret) {
var offset = caret.selectionStart
while (offset > 0 && editor.immutableText[offset - 1].isWhitespace()) {
--offset
}
caret.setSelection(offset, caret.selectionEnd)
}
override fun extendRight(editor: Editor, caret: Caret) {
var offset = caret.selectionEnd
while (offset < editor.immutableText.length && editor.immutableText[offset].isWhitespace()) {
++offset
}
caret.setSelection(caret.selectionStart, offset)
}
}
}
}

View File

@@ -0,0 +1,29 @@
package org.acejump.modes
import org.acejump.action.AceTagAction
import org.acejump.config.AceConfig
import org.acejump.session.SessionState
import org.acejump.session.TypeResult
class SelectFromCaretMode : SessionMode {
override val caretColor
get() = AceConfig.fromCaretModeColor
override fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult {
if (acceptedTag == null) {
return state.type(charTyped)
}
val jumpAction = JumpMode.JUMP_ACTION_MAP[charTyped.toUpperCase()]
if (jumpAction == null) {
return TypeResult.Nothing
}
state.act(AceTagAction.SelectToCaret(jumpAction), acceptedTag, shiftMode = charTyped.isUpperCase())
return TypeResult.EndSession
}
override fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>? {
return JumpMode.JUMP_ALT_HINT.takeIf { acceptedTag != null }
}
}

View File

@@ -1,6 +1,5 @@
package org.acejump.modes package org.acejump.modes
import org.acejump.search.Tag
import org.acejump.session.SessionState import org.acejump.session.SessionState
import org.acejump.session.TypeResult import org.acejump.session.TypeResult
import java.awt.Color import java.awt.Color
@@ -8,6 +7,6 @@ import java.awt.Color
interface SessionMode { interface SessionMode {
val caretColor: Color val caretColor: Color
fun type(state: SessionState, charTyped: Char, acceptedTag: Tag?): TypeResult fun type(state: SessionState, charTyped: Char, acceptedTag: Int?): TypeResult
fun accept(state: SessionState, acceptedTag: Tag) fun getHint(acceptedTag: Int?, hasQuery: Boolean): Array<String>?
} }

View File

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

View File

@@ -3,42 +3,43 @@ package org.acejump.search
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.clone
import org.acejump.config.AceConfig
import org.acejump.immutableText import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.matchesAt import org.acejump.matchesAt
/** /**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [refineQuery]s a character. * Searches editor text for matches of a [SearchQuery], and updates previous results when the user [type]s a character.
*/ */
class SearchProcessor private constructor(query: SearchQuery, val boundaries: Boundaries, val invertUppercaseMode: Boolean, private val results: MutableMap<Editor, IntArrayList>) { class SearchProcessor private constructor(private val editor: Editor, query: SearchQuery) {
internal constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries, invertUppercaseMode: Boolean) : this(query, boundaries, invertUppercaseMode, mutableMapOf()) { companion object {
val regex = query.toRegex(invertUppercaseMode) fun fromChar(editor: Editor, char: Char, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.Literal(char.toString()), boundaries)
}
fun fromRegex(editor: Editor, pattern: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editor, SearchQuery.RegularExpression(pattern), boundaries)
}
}
private constructor(editor: Editor, query: SearchQuery, boundaries: Boundaries) : this(editor, query) {
val regex = query.toRegex()
if (regex != null) { if (regex != null) {
for (editor in editors) { val offsetRange = boundaries.getOffsetRange(editor)
val cache = EditorOffsetCache.new() var result = regex.find(editor.immutableText, offsetRange.first)
val offsets = IntArrayList()
while (result != null) {
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception.
val highlightEnd = index + query.getHighlightLength("", index)
val offsetRange = boundaries.getOffsetRange(editor, cache) if (highlightEnd > offsetRange.last) {
var result = regex.find(editor.immutableText, offsetRange.first) break
}
while (result != null) { else if (boundaries.isOffsetInside(editor, index)) {
val index = result.range.first // For some reason regex matches can be out of bounds, but boundary check prevents an exception. results.add(index)
val highlightEnd = index + query.getHighlightLength("", index)
if (highlightEnd > offsetRange.last) {
break
}
else if (boundaries.isOffsetInside(editor, index, cache) && !editor.foldingModel.isOffsetCollapsed(index)) {
offsets.add(index)
}
result = result.next()
} }
results[editor] = offsets result = result.next()
} }
} }
} }
@@ -46,40 +47,81 @@ class SearchProcessor private constructor(query: SearchQuery, val boundaries: Bo
internal var query = query internal var query = query
private set private set
val resultsCopy internal var results = IntArrayList(0)
get() = results.clone() private set
val isQueryFinished /**
get() = query.rawText.length >= AceConfig.minQueryLength * 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
fun refineQuery(char: Char): Boolean { * make sense because it would remove every result, the change is reverted and this function returns false.
if (char == '\n') { */
return true fun type(char: Char, tagger: Tagger): Boolean {
val newQuery = query.rawText + char
val chars = editor.immutableText
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 && results.none { chars.matchesAt(it, newQuery, ignoreCase = true) }) {
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()
val iter = results.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()
}
}
} }
else { else {
query = query.refine(char) removeObsoleteResults(newQuery, tagger)
removeObsoleteResults() query = SearchQuery.Literal(newQuery)
return isQueryFinished
} }
return true
} }
/** /**
* After updating the query, removes all results that no longer match the search query. * After updating the query, removes all results that no longer match the search query.
*/ */
private fun removeObsoleteResults() { private fun removeObsoleteResults(newQuery: String, tagger: Tagger) {
val query = query.rawText val lastCharOffset = newQuery.lastIndex
val lastChar = newQuery[lastCharOffset]
val ignoreCase = newQuery[0].isLowerCase()
val chars = editor.immutableText
for (entry in results) { val remaining = IntArrayList()
val editor = entry.key val iter = results.iterator()
val offsetIter = entry.value.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()) { if (lastTypedCharMatches || tagger.isQueryCompatibleWithTagAt(newQuery, offset)) {
val offset = offsetIter.nextInt() remaining.add(offset)
if (!editor.immutableText.matchesAt(offset, query, ignoreCase = true)) {
offsetIter.remove()
}
} }
} }
results = remaining
}
fun clone(): SearchProcessor {
return SearchProcessor(editor, query).also { it.results.addAll(results) }
} }
} }

View File

@@ -8,11 +8,6 @@ import org.acejump.countMatchingCharacters
internal sealed class SearchQuery { internal sealed class SearchQuery {
abstract val rawText: String abstract val rawText: String
/**
* Returns a new query with the given character appended.
*/
abstract fun refine(char: Char): SearchQuery
/** /**
* Returns how many characters the search occurrence highlight should cover. * Returns how many characters the search occurrence highlight should cover.
*/ */
@@ -21,40 +16,32 @@ internal sealed class SearchQuery {
/** /**
* Converts the query into a regular expression to find the initial matches. * Converts the query into a regular expression to find the initial matches.
*/ */
abstract fun toRegex(invertUppercaseMode: Boolean): Regex? abstract fun toRegex(): Regex?
/** /**
* Searches for all occurrences of a literal text query. * Searches for all occurrences of a literal text query. If the first character of the query is lowercase, then the entire query will be
* If the first character of the query is lowercase, then the entire query will be case-insensitive, * case-insensitive.
* and only beginnings of words and camel humps will be matched. *
* Each occurrence must either match the entire query, or match the query up to a point so that the rest of the query matches the
* beginning of a tag at the location of the occurrence.
*/ */
class Literal(override val rawText: String) : SearchQuery() { class Literal(override val rawText: String) : SearchQuery() {
init { init {
require(rawText.isNotEmpty()) require(rawText.isNotEmpty())
} }
override fun refine(char: Char): SearchQuery {
return Literal(rawText + char)
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int { override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return text.countMatchingCharacters(offset, rawText) return text.countMatchingCharacters(offset, rawText)
} }
override fun toRegex(invertUppercaseMode: Boolean): Regex { override fun toRegex(): Regex {
val firstChar = rawText.first() val options = mutableSetOf(RegexOption.MULTILINE)
val pattern = if (firstChar.isLowerCase() xor invertUppercaseMode) {
val fullPattern = Regex.escape(rawText) if (rawText.first().isLowerCase()) {
"(?i)$fullPattern" options.add(RegexOption.IGNORE_CASE)
}
else {
val firstCharUppercasePattern = Regex.escape(firstChar.uppercase())
val firstCharLowercasePattern = Regex.escape(firstChar.lowercase())
val remainingPattern = if (rawText.length > 1) Regex.escape(rawText.drop(1)) else ""
"(?:$firstCharUppercasePattern|(?<![a-zA-Z])$firstCharLowercasePattern)$remainingPattern"
} }
return Regex(pattern, setOf(RegexOption.MULTILINE)) return Regex(Regex.escape(rawText), options)
} }
} }
@@ -63,16 +50,12 @@ internal sealed class SearchQuery {
*/ */
class RegularExpression(private val pattern: String) : SearchQuery() { class RegularExpression(private val pattern: String) : SearchQuery() {
override val rawText = "" override val rawText = ""
override fun refine(char: Char): SearchQuery {
return Literal(char.toString())
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int { override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return 1 return 1
} }
override fun toRegex(invertUppercaseMode: Boolean): Regex { override fun toRegex(): Regex {
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)) return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
} }
} }

View File

@@ -0,0 +1,193 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.*
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
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.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
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 editor: Editor,
private val queryLength: Int,
private val newResults: IntList,
private val allResults: IntList
) {
companion object {
fun solve(
editor: Editor, query: SearchQuery, newResults: IntList, allResults: IntList, tags: List<String>, cache: EditorOffsetCache
): Map<String, Int> {
return Solver(editor, max(1, query.rawText.length), newResults, allResults).map(tags, cache)
}
}
private var newTags = Object2IntOpenHashMap<String>(KeyLayoutCache.allPossibleTags.size)
private val newTagIndices = IntOpenHashSet()
private var allWordFragments = HashSet<String>(allResults.size).apply {
val iter = allResults.iterator()
while (iter.hasNext()) {
forEachWordFragment(iter.nextInt()) { add(it) }
}
}
fun map(availableTags: List<String>, cache: EditorOffsetCache): Map<String, Int> {
val eligibleSitesByTag = HashMap<String, IntList>(100)
val tagsByFirstLetter = availableTags.groupBy { it[0] }
val iter = newResults.iterator()
while (iter.hasNext()) {
val site = iter.nextInt()
for ((firstLetter, tags) in tagsByFirstLetter.entries) {
if (canTagBeginWithChar(site, firstLetter)) {
for (tag in tags) {
eligibleSitesByTag.getOrPut(tag) { IntArrayList(10) }.add(site)
}
}
}
}
val matchingSites = HashMap<IntList, IntArray>()
val matchingSitesAsArrays = IdentityHashMap<String, IntArray>() // Keys are guaranteed to be from a single collection.
val siteOrder = siteOrder(cache)
val tagOrder = KeyLayoutCache.tagOrder
.thenComparingInt { eligibleSitesByTag.getValue(it).size }
.thenBy(AceConfig.layout.priority(String::last))
val sortedTags = eligibleSitesByTag.keys.toMutableList().apply {
sortWith(tagOrder)
}
for ((key, value) in eligibleSitesByTag.entries) {
matchingSitesAsArrays[key] = matchingSites.getOrPut(value) {
value.toIntArray().apply { IntArrays.mergeSort(this, siteOrder) }
}
}
var totalAssigned = 0
for (tag in sortedTags) {
if (totalAssigned == newResults.size) {
break
}
if (tryToAssignTag(tag, matchingSitesAsArrays.getValue(tag))) {
totalAssigned++
}
}
return newTags
}
private fun tryToAssignTag(tag: String, sites: IntArray): Boolean {
if (newTags.containsKey(tag)) {
return false
}
val index = sites.firstOrNull { it !in newTagIndices } ?: return false
@Suppress("ReplacePutWithAssignment")
newTags.put(tag, index)
newTagIndices.add(index)
return true
}
private fun siteOrder(cache: EditorOffsetCache) = IntComparator { a, b ->
val aIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, a, cache)
val bIsVisible = StandardBoundaries.VISIBLE_ON_SCREEN.isOffsetInside(editor, b, cache)
if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first.
return@IntComparator if (aIsVisible) -1 else 1
}
val chars = editor.immutableText
val aIsNotWordStart = chars[max(0, a - 1)].isWordPart
val bIsNotWordStart = chars[max(0, b - 1)].isWordPart
if (aIsNotWordStart != bIsNotWordStart) {
// Ensure that the first letter of a word is prioritized for tagging.
return@IntComparator if (bIsNotWordStart) -1 else 1
}
when {
a < b -> -1
a > b -> 1
else -> 0
}
}
private fun canTagBeginWithChar(site: Int, char: Char): Boolean {
if (char.toString() in allWordFragments) {
return false
}
forEachWordFragment(site) {
if (it + char in allWordFragments) {
return false
}
}
return true
}
private inline fun forEachWordFragment(site: Int, callback: (String) -> Unit) {
val chars = editor.immutableText
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,13 +0,0 @@
package org.acejump.search
import com.intellij.openapi.editor.Editor
data class Tag(val editor: Editor, val offset: Int) {
override fun equals(other: Any?): Boolean {
return other is Tag && other.offset == offset && other.editor === editor
}
override fun hashCode(): Int {
return (offset * 31) + editor.hashCode()
}
}

View File

@@ -1,174 +1,202 @@
package org.acejump.search package org.acejump.search
import com.google.common.collect.ArrayListMultimap import com.google.common.collect.HashBiMap
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import org.acejump.ExternalUsage
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN import org.acejump.boundaries.StandardBoundaries
import org.acejump.input.KeyLayoutCache import org.acejump.immutableText
import org.acejump.view.TagMarker import org.acejump.input.KeyLayoutCache.allPossibleTags
import org.acejump.isWordPart
import org.acejump.matchesAt
import org.acejump.view.Tag
import java.util.AbstractMap.SimpleImmutableEntry
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.math.min
/** /**
* Assigns tags to search occurrences. * Assigns tags to search occurrences, updates them when the search query changes, and requests a jump if the search query matches a tag.
* 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>, results: Map<Editor, IntList>) { class Tagger(private val editor: Editor) {
private var tagMap: Map<String, Tag> private var tagMap = HashBiMap.create<String, Int>()
private var typedTag = ""
internal val markers: Map<Editor, Collection<TagMarker>> val hasTags
get() { get() = tagMap.isNotEmpty()
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)
}
return markers.asMap()
}
init { @ExternalUsage
val caches = results.keys.associateWith { EditorOffsetCache.new() } internal val tags
get() = tagMap.map { SimpleImmutableEntry(it.key, it.value) }.sortedBy { it.value }
sortResults(results, caches) /**
* Removes all markers, allowing them to be regenerated from scratch.
val tagSites = results */
.flatMap { (editor, sites) -> sites.map { site -> Tag(editor, site) } } internal fun unmark() {
.sortedWith(siteOrder(editors, caches)) tagMap = HashBiMap.create()
tagMap = generateTags(tagSites.size).zip(tagSites).toMap()
} }
internal fun type(char: Char): TaggingResult { /**
val newTypedTag = typedTag + char.lowercaseChar() * Assigns tags to as many results as possible, keeping previously assigned tags. Returns a [TaggingResult.Accept] if the current search
val matchingTag = tagMap[newTypedTag] * query matches any existing tag and we should jump to it and end the session, or [TaggingResult.Mark] to continue the session with
if (matchingTag != null) { * updated tag markers.
return TaggingResult.Accept(matchingTag) *
* Note that the [results] collection will be mutated.
*/
internal fun update(query: SearchQuery, results: 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)
}
}
if (queryText.length == 1) {
removeResultsWithOverlappingTags(results)
}
} }
val newTagMap = tagMap.filter { it.key.startsWith(newTypedTag) } if (!isRegex || tagMap.isEmpty()) {
if (newTagMap.isEmpty()) { tagMap = assignTagsAndMerge(results, availableTags, query, queryText)
return TaggingResult.Nothing
} }
typedTag = newTypedTag return TaggingResult.Mark(createTagMarkers(results, query.rawText.ifEmpty { null }))
tagMap = newTagMap }
return TaggingResult.Mark(markers)
fun clone(): Tagger {
return Tagger(editor).also { it.tagMap.putAll(tagMap) }
}
/**
* Assigns as many unassigned tags as possible, and merges them with the existing compatible tags.
*/
private fun assignTagsAndMerge(results: IntList, availableTags: List<String>, query: SearchQuery, queryText: String): HashBiMap<String, Int> {
val cache = EditorOffsetCache.new()
results.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
}
}
val allAssignedTags = mutableMapOf<String, Int>()
val oldCompatibleTags = tagMap.filter { isTagCompatibleWithQuery(it.key, it.value, queryText) || it.value in results }
val vacantResults: IntList
if (oldCompatibleTags.isEmpty()) {
vacantResults = results
}
else {
vacantResults = IntArrayList()
val iter = results.iterator()
while (iter.hasNext()) {
val offset = iter.nextInt()
if (offset !in oldCompatibleTags.values) {
vacantResults.add(offset)
}
}
}
allAssignedTags.putAll(oldCompatibleTags)
allAssignedTags.putAll(Solver.solve(editor, query, vacantResults, results, availableTags, cache))
return allAssignedTags.mapKeysTo(HashBiMap.create(allAssignedTags.size)) { (tag, _) ->
// Avoid matching query - will trigger a jump.
// TODO: lift this constraint.
val queryEndsWith = queryText.endsWith(tag[0]) || queryText.endsWith(tag)
if (!queryEndsWith && canShortenTag(tag, allAssignedTags))
tag[0].toString()
else
tag
}
}
private infix fun Map.Entry<String, Int>.solves(query: String): Boolean {
return query.endsWith(key, true) && isTagCompatibleWithQuery(key, value, query)
}
private fun isTagCompatibleWithQuery(tag: String, offset: Int, query: String): Boolean {
return editor.immutableText.matchesAt(offset, getPlaintextPortion(query, tag), ignoreCase = true)
}
fun isQueryCompatibleWithTagAt(query: String, offset: Int): Boolean {
return tagMap.inverse()[offset].let { it != null && isTagCompatibleWithQuery(it, offset, 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(results: IntList) {
val iter = results.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(results: IntList, literalQueryText: String?): List<Tag> {
val tagMapInv = tagMap.inverse()
return results.mapNotNull { index -> tagMapInv[index]?.let { tag -> Tag.create(editor, tag, index, literalQueryText) } }
} }
private companion object { private companion object {
private fun generateTags(tagCount: Int): List<String> { private fun CharSequence.canTagWithoutOverlap(loc: Int) = when {
val allowedTagsSorted = KeyLayoutCache.allowedTagsSorted loc - 1 < 0 -> true
val tags = mutableListOf<String>() loc + 1 >= length -> true
this[loc] isUnlike this[loc - 1] -> true
val containedSingleCharTags = mutableSetOf<Char>() this[loc] isUnlike this[loc + 1] -> true
val blockedSingleCharTags = mutableSetOf<Char>() this[loc] != this[loc - 1] -> true
val doubleCharTagCountsByFirstChar = Object2IntOpenHashMap<Char>() this[loc] != this[loc + 1] -> true
this[loc + 1] == '\r' || this[loc + 1] == '\n' -> true
for (tag in allowedTagsSorted) { this[loc - 1] == this[loc] && this[loc] == this[loc + 1] -> false
val firstChar = tag.first() this[loc + 1].isWhitespace() && this[(loc + 2).coerceAtMost(length - 1)].isWhitespace() -> true
else -> false
if (tag.length == 1) {
if (firstChar in blockedSingleCharTags) {
continue
}
containedSingleCharTags.add(firstChar)
}
else {
if (containedSingleCharTags.remove(firstChar)) {
tags.remove(firstChar.toString())
}
blockedSingleCharTags.add(firstChar)
doubleCharTagCountsByFirstChar.addTo(firstChar, 1)
}
tags.add(tag)
if (tags.size >= tagCount) {
break
}
}
// In rare cases, the final tag list may contain a double character tag that is the only tag starting with its first character,
// so we replace it with the single character tag.
for (entry in doubleCharTagCountsByFirstChar.object2IntEntrySet()) {
if (entry.intValue != 1) {
continue
}
tags.removeAt(tags.indexOfFirst { it.first() == entry.key })
val tag = entry.key.toString()
var previousTagIndex = -1
// The implementation of searching where to place the single character tag is theoretically slow,
// but getting here is so rare it doesn't matter.
for (i in allowedTagsSorted.indexOf(tag) - 1 downTo 0) {
previousTagIndex = tags.indexOf(allowedTagsSorted[i])
if (previousTagIndex != -1) {
break
}
}
tags.add(previousTagIndex + 1, tag)
}
return tags
} }
private fun sortResults(results: Map<Editor, IntList>, caches: Map<Editor, EditorOffsetCache>) { private infix fun Char.isUnlike(other: Char): Boolean {
for ((editor, offsets) in results) { return this.isWordPart xor other.isWordPart || this.isWhitespace() xor other.isWhitespace()
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 fun siteOrder(editorPriority: List<Editor>, caches: Map<Editor, EditorOffsetCache>) = Comparator<Tag> { a, b -> private fun getPlaintextPortion(query: String, tag: String) = when {
val aEditor = a.editor query.endsWith(tag, true) -> query.dropLast(tag.length)
val bEditor = b.editor query.endsWith(tag.first(), true) -> query.dropLast(1)
else -> query
if (aEditor !== bEditor) { }
val aEditorIndex = editorPriority.indexOf(aEditor)
val bEditorIndex = editorPriority.indexOf(bEditor) private fun getTagPortion(query: String, tag: String) = when {
// For multiple editors, prioritize them based on the provided order. query.endsWith(tag, true) -> query.takeLast(tag.length)
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1 query.endsWith(tag.first(), true) -> query.takeLast(1)
else -> ""
}
private fun canShortenTag(tag: String, tagMap: Map<String, Int>): Boolean {
for (other in tagMap.keys) {
if (tag != other && tag[0] == other[0]) {
return false
}
} }
val aCaches = caches.getValue(aEditor) return true
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 firstEditor = editorPriority[0]
val caretPosition = caches.getValue(firstEditor).offsetToXY(firstEditor, firstEditor.caretModel.offset)
val aDistance = aCaches.offsetToXY(aEditor, a.offset).distanceSq(caretPosition)
val bDistance = bCaches.offsetToXY(bEditor, b.offset).distanceSq(caretPosition)
return@Comparator aDistance.compareTo(bDistance)
} }
} }
} }

View File

@@ -1,10 +1,8 @@
package org.acejump.search package org.acejump.search
import com.intellij.openapi.editor.Editor import org.acejump.view.Tag
import org.acejump.view.TagMarker
internal sealed class TaggingResult { internal sealed class TaggingResult {
object Nothing : TaggingResult() class Accept(val offset: Int) : TaggingResult()
class Accept(val tag: Tag) : TaggingResult() class Mark(val tags: List<Tag>) : TaggingResult()
class Mark(val markers: Map<Editor, Collection<TagMarker>>) : TaggingResult()
} }

View File

@@ -1,148 +1,190 @@
package org.acejump.session package org.acejump.session
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hint.HintManagerImpl 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.Editor
import com.intellij.openapi.editor.ScrollType 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.EditorColors
import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme import com.intellij.openapi.editor.colors.impl.AbstractColorsScheme
import it.unimi.dsi.fastutil.ints.IntList import com.intellij.ui.LightweightHint
import org.acejump.ExternalUsage
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
import org.acejump.immutableText
import org.acejump.input.EditorKeyListener import org.acejump.input.EditorKeyListener
import org.acejump.input.KeyLayoutCache import org.acejump.input.KeyLayoutCache
import org.acejump.modes.BetweenPointsMode
import org.acejump.modes.JumpMode import org.acejump.modes.JumpMode
import org.acejump.modes.SessionMode import org.acejump.modes.SessionMode
import org.acejump.search.Pattern import org.acejump.search.*
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.TagCanvas
import org.acejump.view.TagMarker
import org.acejump.view.TextHighlighter import org.acejump.view.TextHighlighter
/** /**
* Manages an AceJump session for a single [Editor]. * Manages an AceJump session for a single [Editor].
*/ */
class Session(private val mainEditor: Editor, private val jumpEditors: List<Editor>) { class Session(private val editor: Editor) {
private val editorSettings = EditorSettings.setup(mainEditor) private val editorSettings = EditorSettings.setup(editor)
private lateinit var mode: SessionMode private lateinit var mode: SessionMode
private var state: SessionState? = null private var state: SessionStateImpl? = null
private var tagger = Tagger(editor)
private var acceptedTag: Tag? = null private var acceptedTag: Int? = null
set(value) { set(value) {
field = value field = value
if (value != null) { if (value != null) {
tagCanvases.values.forEach(TagCanvas::removeMarkers) tagCanvas.removeMarkers()
editorSettings.onTagAccepted(mainEditor) editorSettings.onTagAccepted(editor)
} }
} }
private val textHighlighter = TextHighlighter() private val textHighlighter = TextHighlighter(editor)
private val tagCanvases = jumpEditors.associateWith(::TagCanvas) private val tagCanvas = TagCanvas(editor)
var defaultBoundary: Boundaries = StandardBoundaries.VISIBLE_ON_SCREEN @ExternalUsage
val tags
private val actions = object : SessionActions { get() = tagger.tags
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 { init {
KeyLayoutCache.ensureInitialized(AceConfig.settings) KeyLayoutCache.ensureInitialized(AceConfig.settings)
EditorKeyListener.attach(mainEditor) { editor, charTyped, _ -> typeCharacter(editor, charTyped) }
EditorKeyListener.attach(editor, 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()
TypeResult.RestartSearch -> restart().also { this@Session.state = SessionStateImpl(editor, tagger); updateHint() }
is TypeResult.UpdateResults -> updateSearch(result.processor, markImmediately = hadTags)
is TypeResult.MoveHint -> { textHighlighter.reset(); acceptedTag = result.offset; updateHint() }
is TypeResult.ChangeMode -> setMode(result.mode)
TypeResult.EndSession -> end()
}
}
})
} }
private fun typeCharacter(editor: Editor, charTyped: Char) { /**
val state = state ?: return * 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
editorSettings.startEditing(editor) if (!markImmediately && query.rawText.let { it.length < AceConfig.minQueryLength && it.all(Char::isLetterOrDigit) }) {
val result = mode.type(state, charTyped, acceptedTag) textHighlighter.renderOccurrences(results, query)
editorSettings.stopEditing(editor) return
}
when (result) { when (val result = tagger.update(query, results.clone())) {
Nothing -> return is TaggingResult.Accept -> {
is ChangeState -> this.state = result.state val offset = result.offset
is ChangeMode -> setMode(result.mode) acceptedTag = offset
textHighlighter.renderFinal(offset, processor.query)
is AcceptTag -> {
acceptedTag = result.tag
mode.accept(state, result.tag)
end()
} }
EndSession -> end() is TaggingResult.Mark -> {
val tags = result.tags
tagCanvas.setMarkers(tags)
textHighlighter.renderOccurrences(results, query)
}
} }
updateHint()
} }
private fun setMode(mode: SessionMode) { private fun setMode(mode: SessionMode) {
this.mode = mode this.mode = mode
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor) editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
updateHint()
} }
fun startJumpMode() { private fun updateHint() {
startJumpMode(::JumpMode) val hintArray = mode.getHint(acceptedTag, 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 = acceptedTag?.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(mode: () -> JumpMode) { fun cycleMode() {
if (this::mode.isInitialized && mode is JumpMode) { if (!this::mode.isInitialized) {
end() setMode(JumpMode())
state = SessionStateImpl(editor, tagger)
return return
} }
if (this::mode.isInitialized) { restart()
restart() setMode(when (mode) {
} is JumpMode -> BetweenPointsMode()
else -> JumpMode()
})
setMode(mode()) state = SessionStateImpl(editor, tagger)
state = SessionState.WaitForKey(actions, jumpEditors, defaultBoundary, AceConfig.invertUppercaseMode)
} }
/** /**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights. * Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/ */
fun startRegexSearch(pattern: Pattern) { fun startRegexSearch(pattern: String, boundaries: Boundaries) {
if (!this::mode.isInitialized) { if (!this::mode.isInitialized) {
setMode(JumpMode()) setMode(JumpMode())
} }
for (canvas in tagCanvases.values) { tagger = Tagger(editor)
canvas.setMarkers(emptyList()) tagCanvas.setMarkers(emptyList())
}
val processor = SearchProcessor(jumpEditors, SearchQuery.RegularExpression(pattern.regex), defaultBoundary, AceConfig.invertUppercaseMode) val processor = SearchProcessor.fromRegex(editor, pattern, boundaries).also { state = SessionStateImpl(editor, tagger, it) }
textHighlighter.renderOccurrences(processor.resultsCopy, processor.query) updateSearch(processor, markImmediately = true)
}
state = SessionState.SelectTag(actions, jumpEditors, processor)
/**
* Starts a regular expression search. If a search was already active, it will be reset alongside its tags and highlights.
*/
fun startRegexSearch(pattern: Pattern, boundaries: Boundaries) {
startRegexSearch(pattern.regex, boundaries)
} }
fun tagImmediately() { fun tagImmediately() {
typeCharacter(mainEditor, '\n') val state = state ?: return
val processor = state.currentProcessor
if (processor != null) {
updateSearch(processor, markImmediately = true)
}
else if (mode is JumpMode) {
val offset = editor.caretModel.offset
val result = editor.immutableText.getOrNull(offset)?.let(state::type)
if (result is TypeResult.UpdateResults) {
acceptedTag = offset
textHighlighter.renderFinal(offset, result.processor.query)
updateHint()
}
}
} }
/** /**
* Ends this session. * Ends this session.
*/ */
fun end() { fun end() {
SessionManager.end(mainEditor) SessionManager.end(editor)
} }
/** /**
@@ -150,31 +192,31 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
*/ */
fun restart() { fun restart() {
state = null state = null
tagger = Tagger(editor)
acceptedTag = null acceptedTag = null
tagCanvases.values.forEach(TagCanvas::removeMarkers) tagCanvas.removeMarkers()
textHighlighter.reset() textHighlighter.reset()
HintManagerImpl.getInstanceImpl().hideAllHints() HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.onTagUnaccepted(mainEditor) editorSettings.onTagUnaccepted(editor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor) editor.colorsScheme.setColor(EditorColors.CARET_COLOR, mode.caretColor)
jumpEditors.forEach { it.contentComponent.repaint() } editor.contentComponent.repaint()
} }
/** /**
* Should only be used from [SessionManager] to dispose a successfully ended session. * Should only be used from [SessionManager] to dispose a successfully ended session.
*/ */
internal fun dispose() { internal fun dispose() {
tagCanvases.values.forEach(TagCanvas::unbind) tagger = Tagger(editor)
tagCanvas.unbind()
textHighlighter.reset() textHighlighter.reset()
EditorKeyListener.detach(mainEditor) EditorKeyListener.detach(editor)
if (!mainEditor.isDisposed) { if (!editor.isDisposed) {
HintManagerImpl.getInstanceImpl().hideAllHints() HintManagerImpl.getInstanceImpl().hideAllHints()
editorSettings.restore(mainEditor) editorSettings.restore(editor)
mainEditor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER) editor.colorsScheme.setColor(EditorColors.CARET_COLOR, AbstractColorsScheme.INHERITED_COLOR_MARKER)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
val focusedEditor = acceptedTag?.editor ?: mainEditor
focusedEditor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
} }
} }
} }

View File

@@ -1,13 +0,0 @@
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

@@ -16,16 +16,7 @@ object SessionManager {
* Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one. * Starts a new [Session], or returns an existing [Session] if the specified [Editor] already has one.
*/ */
fun start(editor: Editor): Session { fun start(editor: Editor): Session {
return start(editor, listOf(editor)) return sessions.getOrPut(editor) { cleanup(); Session(editor) }
}
/**
* Starts a new multi-editor [Session], or returns an existing [Session] if the specified main [Editor] already has one.
* The [mainEditor] is used for typing the search query and tag.
* The [jumpEditors] are all editors that will be searched and tagged.
*/
fun start(mainEditor: Editor, jumpEditors: List<Editor>): Session {
return sessions.getOrPut(mainEditor) { cleanup(); Session(mainEditor, jumpEditors) }
} }
/** /**

View File

@@ -1,95 +1,10 @@
package org.acejump.session package org.acejump.session
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries import org.acejump.action.AceTagAction
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.search.SearchProcessor
import org.acejump.search.SearchQuery
import org.acejump.search.Tagger
import org.acejump.search.TaggingResult
sealed interface SessionState { interface SessionState {
val editor: Editor
fun type(char: Char): TypeResult fun type(char: Char): TypeResult
fun act(action: AceTagAction, offset: Int, shiftMode: Boolean)
class WaitForKey internal constructor(
private val actions: SessionActions,
private val jumpEditors: List<Editor>,
private val defaultBoundary: Boundaries,
private val invertUppercaseMode: Boolean,
) : SessionState {
override fun type(char: Char): TypeResult {
val searchProcessor = SearchProcessor(jumpEditors, SearchQuery.Literal(char.toString()), defaultBoundary, invertUppercaseMode)
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,
private val jumpEditors: List<Editor>,
private val searchProcessor: SearchProcessor,
) : SessionState {
private val tagger = Tagger(jumpEditors, searchProcessor.resultsCopy)
init {
actions.setTagMarkers(tagger.markers)
}
override fun type(char: Char): TypeResult {
if (char == ' ') {
val query = searchProcessor.query
if (query is SearchQuery.Literal) {
val newBoundaries = when (searchProcessor.boundaries) {
StandardBoundaries.VISIBLE_ON_SCREEN -> StandardBoundaries.AFTER_CARET
StandardBoundaries.AFTER_CARET -> StandardBoundaries.BEFORE_CARET
StandardBoundaries.BEFORE_CARET -> StandardBoundaries.VISIBLE_ON_SCREEN
else -> searchProcessor.boundaries
}
val newSearchProcessor = SearchProcessor(jumpEditors, query, newBoundaries, searchProcessor.invertUppercaseMode)
return TypeResult.ChangeState(SelectTag(actions, jumpEditors, newSearchProcessor))
}
}
else if (char == '\n') {
val newSearchProcessor = SearchProcessor(jumpEditors, searchProcessor.query, searchProcessor.boundaries, !searchProcessor.invertUppercaseMode)
return TypeResult.ChangeState(SelectTag(actions, jumpEditors, newSearchProcessor))
}
return when (val result = tagger.type(AceConfig.layout.characterRemapping.getOrDefault(char, char))) {
is TaggingResult.Nothing -> TypeResult.Nothing
is TaggingResult.Accept -> TypeResult.AcceptTag(result.tag)
is TaggingResult.Mark -> {
actions.setTagMarkers(result.markers)
TypeResult.Nothing
}
}
}
}
} }

View File

@@ -0,0 +1,30 @@
package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.action.AceTagAction
import org.acejump.boundaries.StandardBoundaries
import org.acejump.search.SearchProcessor
import org.acejump.search.Tagger
internal class SessionStateImpl(override val editor: Editor, private val tagger: Tagger, 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(editor, char, StandardBoundaries.VISIBLE_ON_SCREEN)
return TypeResult.UpdateResults(newProcessor.also { currentProcessor = it })
}
if (processor.type(char, tagger)) {
return TypeResult.UpdateResults(processor)
}
return TypeResult.Nothing
}
override fun act(action: AceTagAction, offset: Int, shiftMode: Boolean) {
currentProcessor?.let { action(editor, it, offset, shiftMode) }
}
}

View File

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

View File

@@ -0,0 +1,130 @@
package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil
import com.intellij.ui.scale.JBUIScale
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
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 Tag(
private val tag: String,
val offsetL: Int,
val offsetR: Int,
private val shiftR: Int,
private val hasSpaceRight: Boolean
) {
private val length = tag.length
companion object {
private const val ARC = 1
/**
* 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.
*/
fun create(editor: Editor, tag: String, offset: Int, literalQueryText: String?): Tag {
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 Tag(displayedTag, offset, offset + max(0, matching - 1), tag.length - displayedTag.length, hasSpaceRight)
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.fillRoundRect(rect.x, rect.y, rect.width, rect.height + 1, ARC, ARC)
}
/**
* 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 (!ColorUtil.isDark(AceConfig.tagForegroundColor)) {
g.color = Color(0F, 0F, 0F, 0.35F)
g.drawString(text, x + 1, y + 1)
}
g.color = AceConfig.tagForegroundColor
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
}
/**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
* Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap.
*/
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, AceConfig.tagBackgroundColor)
drawForeground(g, font, rect.location, tag)
occupied.add(JBUIScale.scale(2).let { Rectangle(rect.x - it, rect.y, rect.width + (2 * it), rect.height) })
return rect
}
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)
}
}

View File

@@ -2,10 +2,10 @@ package org.acejump.view
import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.Rectangle import java.awt.Rectangle
@@ -16,9 +16,8 @@ import javax.swing.SwingUtilities
/** /**
* Holds all active tag markers and renders them on top of the editor. * Holds all active tag markers and renders them on top of the editor.
*/ */
internal class TagCanvas(private val editor: Editor) : JComponent() { internal class TagCanvas(private val editor: Editor) : JComponent(), CaretListener {
private var markers: Collection<TagMarker> = emptyList() private var markers: List<Tag>? = null
private var visible = false
init { init {
val contentComponent = editor.contentComponent val contentComponent = editor.contentComponent
@@ -28,49 +27,63 @@ internal class TagCanvas(private val editor: Editor) : JComponent() {
if (ApplicationInfo.getInstance().build.components.first() < 173) { if (ApplicationInfo.getInstance().build.components.first() < 173) {
SwingUtilities.convertPoint(this, location, editor.component.rootPane).let { setLocation(-it.x, -it.y) } SwingUtilities.convertPoint(this, location, editor.component.rootPane).let { setLocation(-it.x, -it.y) }
} }
editor.caretModel.addCaretListener(this)
} }
fun unbind() { fun unbind() {
markers = null
editor.contentComponent.remove(this) editor.contentComponent.remove(this)
editor.contentComponent.repaint() editor.caretModel.removeCaretListener(this)
} }
fun setMarkers(markers: Collection<TagMarker>) { /**
* 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()
}
fun setMarkers(markers: List<Tag>) {
this.markers = markers this.markers = markers
this.visible = true
repaint() repaint()
} }
fun removeMarkers() { fun removeMarkers() {
this.markers = emptyList() this.markers = emptyList()
this.visible = false }
repaint()
override fun paint(g: Graphics) {
if (!markers.isNullOrEmpty()) {
super.paint(g)
}
} }
override fun paintChildren(g: Graphics) { override fun paintChildren(g: Graphics) {
super.paintChildren(g) super.paintChildren(g)
if (!visible) { val markers = markers ?: return
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)
val font = TagFont(editor) val font = TagFont(editor)
val cache = EditorOffsetCache.new() val cache = EditorOffsetCache.new()
val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache) val viewRange = StandardBoundaries.VISIBLE_ON_SCREEN.getOffsetRange(editor, cache)
val occupied = mutableListOf<Rectangle>() val occupied = mutableListOf<Rectangle>()
(g as Graphics2D).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
// 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) { for (marker in markers) {
if (marker.offset in viewRange) { if (marker.isOffsetInRange(viewRange) && marker !== caretMarker) {
marker.paint(g, editor, cache, font, occupied) marker.paint(g, editor, cache, font, occupied)
} }
} }

View File

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

View File

@@ -1,84 +0,0 @@
package org.acejump.view
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.SystemInfo
import org.acejump.boundaries.EditorOffsetCache
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
/**
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/
internal class TagMarker(
private val firstChar: String,
private val secondChar: String,
val offset: Int
) {
private constructor(tag: String, offset: Int) : this(tag.first().toString(), tag.drop(1), offset)
private val length = firstChar.length + secondChar.length
companion object {
/**
* TODO This might be due to DPI settings.
*/
private val HIGHLIGHT_OFFSET = if (SystemInfo.isMac) -0.5 else 0.0
/**
* Creates a new tag, precomputing some information about the nearby characters to reduce rendering overhead. If the last typed
* character ([typedTag]) matches the first [tag] character, only the second [tag] character is displayed.
*/
fun create(tag: String, offset: Int, typedTag: String): TagMarker {
return TagMarker(tag.drop(typedTag.length), offset)
}
}
/**
* Paints the tag, taking into consideration visual space around characters in the editor, as well as all other previously painted tags.
* Returns a rectangle indicating the area where the tag was rendered, or null if the tag could not be rendered due to overlap.
*/
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, editor.colorsScheme.defaultBackground)
drawForeground(g, font, rect.location)
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.drawString(firstChar, x, y)
if (secondChar.isNotEmpty()) {
g.color = font.foregroundColor2
g.drawString(secondChar, x + font.tagCharWidth, y)
}
}
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
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,7 +6,7 @@ import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.ui.ColorUtil import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.boundaries.EditorOffsetCache import org.acejump.boundaries.EditorOffsetCache
import org.acejump.config.AceConfig import org.acejump.config.AceConfig
@@ -18,61 +18,59 @@ import java.awt.Graphics
/** /**
* Renders highlights for search occurrences. * Renders highlights for search occurrences.
*/ */
internal class TextHighlighter { internal class TextHighlighter(private val editor: Editor) {
private var previousHighlights = mutableMapOf<Editor, Array<RangeHighlighter>>() private var previousHighlights: Array<RangeHighlighter>? = null
/** /**
* Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change. * Removes all current highlights and re-creates them from scratch. Must be called whenever any of the method parameters change.
*/ */
fun renderOccurrences(results: Map<Editor, IntList>, query: SearchQuery) { fun renderOccurrences(offsets: IntList, query: SearchQuery) {
render(results, when (query) { render(offsets, when (query) {
is SearchQuery.RegularExpression -> RegexRenderer is SearchQuery.RegularExpression -> RegexRenderer
else -> SearchedWordRenderer else -> SearchedWordRenderer
}, query::getHighlightLength) }, query::getHighlightLength)
} }
private inline fun render(results: Map<Editor, IntList>, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) { /**
for ((editor, offsets) in results) { * Removes all current highlights and re-adds a single highlight at the position of the accepted tag with a different color.
val highlights = previousHighlights[editor] */
fun renderFinal(offset: Int, query: SearchQuery) {
render(IntArrayList(intArrayOf(offset)), AcceptedTagRenderer, query::getHighlightLength)
}
private inline fun render(offsets: IntList, renderer: CustomHighlighterRenderer, getHighlightLength: (CharSequence, Int) -> Int) {
val markup = editor.markupModel
val chars = editor.immutableText
val modifications = (previousHighlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000
val document = editor.document
try {
if (enableBulkEditing) {
document.isInBulkUpdate = true
}
val markup = editor.markupModel previousHighlights?.forEach(markup::removeHighlighter)
val document = editor.document previousHighlights = Array(offsets.size) { index ->
val chars = editor.immutableText val start = offsets.getInt(index)
val end = start + getHighlightLength(chars, start)
val modifications = (highlights?.size ?: 0) + offsets.size
val enableBulkEditing = modifications > 1000
try {
if (enableBulkEditing) {
document.isInBulkUpdate = true
}
highlights?.forEach(markup::removeHighlighter) markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
previousHighlights[editor] = Array(offsets.size) { index -> customRenderer = renderer
val start = offsets.getInt(index)
val end = start + getHighlightLength(chars, start)
markup.addRangeHighlighter(start, end, LAYER, null, HighlighterTargetArea.EXACT_RANGE).apply {
customRenderer = renderer
}
}
} finally {
if (enableBulkEditing) {
document.isInBulkUpdate = false
} }
} }
} } finally {
if (enableBulkEditing) {
for (editor in previousHighlights.keys.toList()) { document.isInBulkUpdate = false
if (!results.containsKey(editor)) {
previousHighlights.remove(editor)?.forEach(editor.markupModel::removeHighlighter)
} }
} }
} }
fun reset() { fun reset() {
previousHighlights.keys.forEach { it.markupModel.removeAllHighlighters() } editor.markupModel.removeAllHighlighters()
previousHighlights.clear() previousHighlights = null
} }
/** /**
@@ -80,7 +78,7 @@ internal class TextHighlighter {
*/ */
private object SearchedWordRenderer : CustomHighlighterRenderer { private object SearchedWordRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) { override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.searchHighlightColor) drawFilled(g, editor, highlighter.startOffset, highlighter.endOffset, AceConfig.textHighlightColor)
} }
} }
@@ -89,7 +87,16 @@ internal class TextHighlighter {
*/ */
private object RegexRenderer : CustomHighlighterRenderer { private object RegexRenderer : CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) { override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
drawSingle(g, editor, highlighter.startOffset, AceConfig.searchHighlightColor) 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)
} }
} }
@@ -99,11 +106,11 @@ internal class TextHighlighter {
private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int, color: Color) { private fun drawFilled(g: Graphics, editor: Editor, startOffset: Int, endOffset: Int, color: Color) {
val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset) val start = EditorOffsetCache.Uncached.offsetToXY(editor, startOffset)
val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset) val end = EditorOffsetCache.Uncached.offsetToXY(editor, endOffset)
g.color = ColorUtil.withAlpha(AceConfig.searchHighlightColor, 0.2) g.color = color
g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1) g.fillRect(start.x, start.y + 1, end.x - start.x, editor.lineHeight - 1)
g.color = color g.color = AceConfig.tagBackgroundColor
g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight) g.drawRect(start.x, start.y, end.x - start.x, editor.lineHeight)
} }
@@ -113,10 +120,10 @@ internal class TextHighlighter {
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char) val lastCharWidth = editor.component.getFontMetrics(font).charWidth(char)
g.color = ColorUtil.withAlpha(AceConfig.searchHighlightColor, 0.2) g.color = color
g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1) g.fillRect(pos.x, pos.y + 1, lastCharWidth, editor.lineHeight - 1)
g.color = color g.color = AceConfig.tagBackgroundColor
g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight) g.drawRect(pos.x, pos.y, lastCharWidth, editor.lineHeight)
} }
} }

View File

@@ -1,6 +1,6 @@
<idea-plugin> <idea-plugin url="https://github.com/acejump/AceJump">
<name>AceJump</name> <name>AceJump</name>
<id>AceJump-chylex</id> <id>AceJump</id>
<description><![CDATA[ <description><![CDATA[
AceJump allows you to quickly navigate the caret to any position visible in the editor. AceJump allows you to quickly navigate the caret to any position visible in the editor.
@@ -9,7 +9,6 @@
</description> </description>
<depends>com.intellij.modules.platform</depends> <depends>com.intellij.modules.platform</depends>
<depends>IdeaVIM</depends>
<category>Navigation</category> <category>Navigation</category>
<vendor url="https://github.com/acejump/AceJump">AceJump</vendor> <vendor url="https://github.com/acejump/AceJump">AceJump</vendor>
@@ -24,28 +23,52 @@
implementationClass="org.acejump.action.AceEditorAction$Reset"/> implementationClass="org.acejump.action.AceEditorAction$Reset"/>
<editorActionHandler action="EditorBackSpace" order="first" <editorActionHandler action="EditorBackSpace" order="first"
implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/> implementationClass="org.acejump.action.AceEditorAction$ClearSearch"/>
<editorActionHandler action="EditorEnter" order="first, before terminalEnter" <editorActionHandler action="EditorEnter" order="first"
implementationClass="org.acejump.action.AceEditorAction$TagImmediately"/> 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> </extensions>
<actions> <actions>
<action id="AceVimAction_JumpAllEditors" class="org.acejump.action.AceVimAction$JumpAllEditors" text="AceJump Vim - Jump All Editors" /> <action id="AceAction"
<action id="AceVimAction_JumpAllEditors_GoToDeclaration" class="org.acejump.action.AceVimAction$JumpAllEditorsGoToDeclaration" text="AceJump Vim - Jump All Editors - Go To Declaration" /> class="org.acejump.action.AceKeyboardAction$ActivateAceJump"
<action id="AceVimAction_JumpForward" class="org.acejump.action.AceVimAction$JumpForward" text="AceJump Vim - Jump Forward" /> text="Activate AceJump Mode">
<action id="AceVimAction_JumpBackward" class="org.acejump.action.AceVimAction$JumpBackward" text="AceJump Vim - Jump Backward" /> <keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl SEMICOLON"/>
<action id="AceVimAction_JumpTillForward" class="org.acejump.action.AceVimAction$JumpTillForward" text="AceJump Vim - Jump Till Forward" /> <keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl SEMICOLON"/>
<action id="AceVimAction_JumpTillBackward" class="org.acejump.action.AceVimAction$JumpTillBackward" text="AceJump Vim - Jump Till Backward" /> <keyboard-shortcut keymap="$default" first-keystroke="ctrl SEMICOLON"/>
<action id="AceVimAction_JumpOnLineForward" class="org.acejump.action.AceVimAction$JumpOnLineForward" text="AceJump Vim - Jump On Line Forward" /> </action>
<action id="AceVimAction_JumpOnLineBackward" class="org.acejump.action.AceVimAction$JumpOnLineBackward" text="AceJump Vim - Jump On Line Backward" /> <action id="AceLineAction"
<action id="AceVimAction_JumpLineIndentsForward" class="org.acejump.action.AceVimAction$JumpLineIndentsForward" text="AceJump Vim - Jump Line Indents Forward" /> class="org.acejump.action.AceKeyboardAction$StartAllLineMarksMode"
<action id="AceVimAction_JumpLineIndentsBackward" class="org.acejump.action.AceVimAction$JumpLineIndentsBackward" text="AceJump Vim - Jump Line Indents Backward" /> text="Start AceJump in All Line Marks Mode">
<action id="AceVimAction_JumpLWordForward" class="org.acejump.action.AceVimAction$JumpLWordForward" text="AceJump Vim - Jump LWord Forward" /> <keyboard-shortcut keymap="Mac OS X" first-keystroke="ctrl shift SEMICOLON"/>
<action id="AceVimAction_JumpUWordForward" class="org.acejump.action.AceVimAction$JumpUWordForward" text="AceJump Vim - Jump UWord Forward" /> <keyboard-shortcut keymap="Mac OS X 10.5+" first-keystroke="ctrl shift SEMICOLON"/>
<action id="AceVimAction_JumpLWordBackward" class="org.acejump.action.AceVimAction$JumpLWordBackward" text="AceJump Vim - Jump LWord Backward" /> <keyboard-shortcut keymap="$default" first-keystroke="ctrl shift SEMICOLON"/>
<action id="AceVimAction_JumpUWordBackward" class="org.acejump.action.AceVimAction$JumpUWordBackward" text="AceJump Vim - Jump UWord Backward" /> </action>
<action id="AceVimAction_JumpLWordEndForward" class="org.acejump.action.AceVimAction$JumpLWordEndForward" text="AceJump Vim - Jump LWord End Forward" /> <action id="AceLineStartsAction"
<action id="AceVimAction_JumpUWordEndForward" class="org.acejump.action.AceVimAction$JumpUWordEndForward" text="AceJump Vim - Jump UWord End Forward" /> class="org.acejump.action.AceKeyboardAction$StartAllLineStartsMode"
<action id="AceVimAction_JumpLWordEndBackward" class="org.acejump.action.AceVimAction$JumpLWordEndBackward" text="AceJump Vim - Jump LWord End Backward" /> text="Start AceJump in All Line Starts Mode"/>
<action id="AceVimAction_JumpUWordEndBackward" class="org.acejump.action.AceVimAction$JumpUWordEndBackward" text="AceJump Vim - Jump UWord End Backward" /> <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"/>
</actions> </actions>
</idea-plugin> </idea-plugin>

118
src/test/kotlin/AceTest.kt Normal file
View File

@@ -0,0 +1,118 @@
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_ENTER
import com.intellij.openapi.actionSystem.IdeActions.ACTION_EDITOR_START_NEW_LINE
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 that jumping to first occurrence succeeds`() {
"<caret>testing 1234".search("1")
takeAction(ACTION_EDITOR_ENTER)
myFixture.checkResult("testing <caret>1234")
}
fun `test that jumping to second occurrence succeeds`() {
"<caret>testing 1234".search("ti")
takeAction(ACTION_EDITOR_ENTER)
myFixture.checkResult("tes<caret>ting 1234")
}
fun `test that jumping to previous occurrence succeeds`() {
"te<caret>sting 1234".search("t")
takeAction(ACTION_EDITOR_START_NEW_LINE)
myFixture.checkResult("<caret>testing 1234")
}
fun `test tag selection`() {
"<caret>testing 1234".search("g")
typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("j")
myFixture.checkResult("testin<caret>g 1234")
}
fun `test shift selection`() {
"<caret>testing 1234".search("4")
typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("J")
myFixture.checkResult("<selection>testing 123<caret></selection>4")
}
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)
typeAndWaitForResults("j")
myFixture.checkResult("test <caret>word action")
}
fun `test target mode`() {
"<caret>test target action".search("target")
typeAndWaitForResults(session.tags[0].key)
typeAndWaitForResults("s")
myFixture.checkResult("test <selection>target<caret></selection> action")
}
fun `test line mode`() {
makeEditor(" test\n three\n lines\n")
takeAction(AceKeyboardAction.StartAllLineMarksMode)
assertEquals(9, session.tags.size)
}
}

View File

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

View File

@@ -2,12 +2,11 @@ package org.acejump.test.util
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import org.acejump.action.AceVimAction import org.acejump.action.AceKeyboardAction
import org.acejump.session.SessionManager import org.acejump.session.SessionManager
abstract class BaseTest : BasePlatformTestCase() { abstract class BaseTest : BasePlatformTestCase() {
@@ -39,9 +38,7 @@ abstract class BaseTest : BasePlatformTestCase() {
fun takeAction(action: AnAction) = myFixture.testAction(action) fun takeAction(action: AnAction) = myFixture.testAction(action)
fun makeEditor(contents: String): PsiFile { fun makeEditor(contents: String): PsiFile {
val file = myFixture.configureByText(PlainTextFileType.INSTANCE, contents) return myFixture.configureByText(PlainTextFileType.INSTANCE, contents)
(myFixture.editor as EditorImpl).scrollPane.viewport.setSize(1000, 100)
return file
} }
fun resetEditor() { fun resetEditor() {
@@ -55,10 +52,10 @@ abstract class BaseTest : BasePlatformTestCase() {
UIUtil.dispatchAllInvocationEvents() UIUtil.dispatchAllInvocationEvents()
} }
private fun String.executeQuery(query: String) { fun String.executeQuery(query: String) {
myFixture.run { myFixture.run {
makeEditor(this@executeQuery) makeEditor(this@executeQuery)
testAction(AceVimAction.JumpAllEditors()) testAction(AceKeyboardAction.ActivateAceJump)
typeAndWaitForResults(query) typeAndWaitForResults(query)
} }
} }