From 78f3fc404d8920b354e5c981f06348292195bef5 Mon Sep 17 00:00:00 2001 From: io42630 Date: Fri, 9 May 2025 17:37:52 +0200 Subject: [PATCH] paste "borrowed/edited" code --- .gitignore | 4 + .run/dev.run.xml | 24 ++ .tool-versions | 2 + build.gradle.kts | 66 +++++ gradle.properties | 8 + gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 234 ++++++++++++++++++ settings.gradle.kts | 8 + .../plexworlds/l3/CaretPositionAnalyzer.kt | 73 ++++++ .../l3/L3InlineCompletionProvider.kt | 62 +++++ .../com/plexworlds/l3/config/L3Config.kt | 51 ++++ .../plexworlds/l3/config/L3PersistentState.kt | 39 +++ .../plexworlds/l3/config/L3SettingsPanel.form | 66 +++++ .../plexworlds/l3/config/L3SettingsPanel.kt | 21 ++ .../kotlin/com/plexworlds/l3/llm/Dummy.kt | 18 ++ src/main/kotlin/com/plexworlds/l3/llm/LLM.kt | 23 ++ src/main/kotlin/com/plexworlds/l3/llm/LLMs.kt | 7 + .../kotlin/com/plexworlds/l3/llm/Ollama.kt | 65 +++++ src/main/resources/META-INF/plugin.xml | 24 ++ src/main/resources/META-INF/pluginIcon.svg | 7 + .../l3/L3CaretPositionAnalyzerTest.kt | 80 ++++++ 21 files changed, 889 insertions(+) create mode 100644 .gitignore create mode 100644 .run/dev.run.xml create mode 100644 .tool-versions create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 settings.gradle.kts create mode 100644 src/main/kotlin/com/plexworlds/l3/CaretPositionAnalyzer.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/L3InlineCompletionProvider.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/config/L3Config.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/config/L3PersistentState.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.form create mode 100644 src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/llm/Dummy.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/llm/LLM.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/llm/LLMs.kt create mode 100644 src/main/kotlin/com/plexworlds/l3/llm/Ollama.kt create mode 100644 src/main/resources/META-INF/plugin.xml create mode 100644 src/main/resources/META-INF/pluginIcon.svg create mode 100644 src/test/kotlin/com/plexworlds/l3/L3CaretPositionAnalyzerTest.kt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4eddce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.gradle/ +/.idea/ +/build/ +**.jar diff --git a/.run/dev.run.xml b/.run/dev.run.xml new file mode 100644 index 0000000..7c482cd --- /dev/null +++ b/.run/dev.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..ab1dd61 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +java temurin-21.0.4+7.0.LTS + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b500806 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + id("java") + id("org.jetbrains.kotlin.jvm") version "1.9.24" + id("org.jetbrains.intellij") version "1.17.3" +} + +group = "com.plexworlds" +version = "1.1" + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.github.amithkoujalgi:ollama4j:1.0.70") + implementation("org.slf4j:slf4j-jdk14:2.1.0-alpha1") + implementation("com.google.guava:guava:33.4.8-jre") + testImplementation(kotlin("test")) + testImplementation(kotlin("test-junit")) +} + +// Configure Gradle IntelliJ Plugin +// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html +intellij { + version.set("2024.1.7") + type.set("IC") // Target IDE Platform + + plugins.set(listOf(/* Plugin Dependencies */)) +} + +tasks { + // Set the JVM compatibility versions + withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + withType { + kotlinOptions.jvmTarget = "17" + } + + patchPluginXml { + sinceBuild.set("233") + untilBuild.set("242.*") + } + + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } + + jar { + manifest { + attributes["Implementation-Title"] = "Local Llama Link" + attributes["Implementation-Version"] = version + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from({ + configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) } + }) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6c35a1d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,8 @@ +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency = false + +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache = true + +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching = true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9355b41 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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" "$@" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ab53cfc --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "local-llama-link" diff --git a/src/main/kotlin/com/plexworlds/l3/CaretPositionAnalyzer.kt b/src/main/kotlin/com/plexworlds/l3/CaretPositionAnalyzer.kt new file mode 100644 index 0000000..1558425 --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/CaretPositionAnalyzer.kt @@ -0,0 +1,73 @@ +package com.plexworlds.l3 + + +fun String.shouldBeSkippedOnPosition(offset: Int) = checkElementUnderCaret(this, offset) { + afterSemicolon() + || afterLBrace() + || afterRBrace() + || beforeLParenthesis() + || afterRParenthesis() + || insideIdentifier() + || atTheBoundaryOfStringLiteral() + || atTheBoundaryOfCharLiteral() + || atTheEndOfDigitalLiteral() +} + + +private fun Pair.afterSemicolon(): Boolean { + return first == ';' +} + + +private fun Pair.afterLBrace(): Boolean { + return first == '{' +} + + +private fun Pair.afterRBrace(): Boolean { + return first == '}' +} + + +private fun Pair.beforeLParenthesis(): Boolean { + return second == '(' +} + + +private fun Pair.afterRParenthesis(): Boolean { + return first == ')' +} + + +private fun Pair.insideIdentifier(): Boolean { + return first.isLetterOrDigit() && second.isLetterOrDigit() +} + + +private fun Pair.atTheBoundaryOfStringLiteral(): Boolean { + return first == '"' || second == '"' +} + + +private fun Pair.atTheBoundaryOfCharLiteral(): Boolean { + return first == '\'' || second == '\'' +} + + +private fun Pair.atTheEndOfDigitalLiteral(): Boolean { + fun Char.isArithmeticOperator() = this == '+' || this == '-' || this == '*' || this == '/' + fun Char.isLogicalOperator() = this == '&' || this == '|' || this == '!' || this == '=' || this == '<' || this == '>' + return (first.isDigit() || first == 'L' || first == 'f') + && (second == ';' || second == ' ' || second.isArithmeticOperator() || second.isLogicalOperator()) +} + + +private fun checkElementUnderCaret( + text: String, + offset: Int, + satisfySkippedCriteria: Pair.() -> Boolean +): Boolean { + val beforeCaretChar = if (offset in 0..text.lastIndex) text[offset] else '\n' + val afterCaretChar = if (offset + 1 in 0..text.lastIndex) text[offset + 1] else '\n' + return (beforeCaretChar to afterCaretChar).satisfySkippedCriteria() +} diff --git a/src/main/kotlin/com/plexworlds/l3/L3InlineCompletionProvider.kt b/src/main/kotlin/com/plexworlds/l3/L3InlineCompletionProvider.kt new file mode 100644 index 0000000..f2fbe17 --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/L3InlineCompletionProvider.kt @@ -0,0 +1,62 @@ +package com.plexworlds.l3 + +import com.intellij.codeInsight.inline.completion.* +import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement +import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement +import com.plexworlds.l3.config.L3PersistentState +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.launch + + +class L3InlineCompletionProvider : InlineCompletionProvider { + + override val id = InlineCompletionProviderID("L3InlineCompletionProvider") + + + override suspend fun getSuggestion(request: InlineCompletionRequest): InlineCompletionSuggestion { + val startTime = System.nanoTime() + return InlineCompletionSuggestion.Default( + channelFlow { + val (prefix, suffix) = request.document.text.splitUsingOffset(request.startOffset) + val lastPrefixLine = prefix.lines().last() + + val llm = L3PersistentState.getInstance().provider + val suggestion = llm.call(prefix, suffix) + launch { + try { + trySend(InlineCompletionGrayTextElement(suggestion)) + } catch (e: Exception) { + println("Inline completion suggestion dispatch failed") + } + } + }.onCompletion { + val endTime = System.nanoTime() + val duration = (endTime - startTime) / 1_000_000 // Convert to milliseconds + } + ) + } + + + override val insertHandler: InlineCompletionInsertHandler + get() = object : InlineCompletionInsertHandler { + override fun afterInsertion( + environment: InlineCompletionInsertEnvironment, + elements: List + ) { + } + } + + + override fun isEnabled(event: InlineCompletionEvent): Boolean { + return event is InlineCompletionEvent.DocumentChange && with(event.editor) { + !document.text.shouldBeSkippedOnPosition(caretModel.offset) + } + } + + + private fun String.splitUsingOffset(offset: Int): Pair { + return substring(0, offset + 1) to substring(offset + 1) + } + +} diff --git a/src/main/kotlin/com/plexworlds/l3/config/L3Config.kt b/src/main/kotlin/com/plexworlds/l3/config/L3Config.kt new file mode 100644 index 0000000..09d9efd --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/config/L3Config.kt @@ -0,0 +1,51 @@ +package com.plexworlds.l3.config + +import com.plexworlds.l3.llm.Ollama +import com.intellij.openapi.options.SearchableConfigurable +import com.plexworlds.l3.llm.LLMs +import javax.swing.JComponent + +class L3Config : SearchableConfigurable { + + private var panel: L3SettingsPanel? = null + + override fun createComponent(): JComponent { + return L3SettingsPanel().also { panel = it }.mainPanel + } + + override fun isModified(): Boolean { + val panel = this.panel ?: return false + val l3PersistentState = L3PersistentState.getInstance() + return panel.modelField.text != l3PersistentState.model +// || panel.urlField.text != l3State.url + } + + override fun reset() { + val panel = this.panel ?: return + val l3PersistentState = L3PersistentState.getInstance() + panel.modelField.text = l3PersistentState.model + Ollama.changeModel(l3PersistentState.model) + } + + override fun apply() { + val panel = this.panel ?: return + val state = L3PersistentState.getInstance() + + val llm = LLMs.valueOf(panel.providerComboBox.selectedItem as String).llm + val model = panel.modelField.text + + llm.changeModel(model) + + state.provider = llm + state.model = model + state.url = panel.urlField.text + + } + + override fun disposeUIResources() { + this.panel = null + } + + override fun getDisplayName() = "AI code completion idea" + override fun getId() = "com.plexworlds.l3.config.L3Config" +} diff --git a/src/main/kotlin/com/plexworlds/l3/config/L3PersistentState.kt b/src/main/kotlin/com/plexworlds/l3/config/L3PersistentState.kt new file mode 100644 index 0000000..b9b596d --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/config/L3PersistentState.kt @@ -0,0 +1,39 @@ +package com.plexworlds.l3.config + +import com.plexworlds.l3.llm.Ollama +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.State +import com.intellij.util.xmlb.XmlSerializerUtil +import com.plexworlds.l3.llm.Dummy +import com.plexworlds.l3.llm.LLM + +@State( + name = "com.plexworlds.l3.config.L3PersistentState", + storages = [Storage("local-llama-link-plugin.xml")] +) +class L3PersistentState : PersistentStateComponent { + + @JvmField + var model: String = "codellama:7b-code" + + @JvmField + var url: String = "http://localhost:11434" + + @JvmField + var provider: LLM = Dummy + + override fun getState(): L3PersistentState = this + + override fun loadState(l3PersistentState: L3PersistentState) { + XmlSerializerUtil.copyBean(l3PersistentState, this) + Ollama.changeModel(l3PersistentState.model) + } + + companion object { + fun getInstance(): L3PersistentState { + return ApplicationManager.getApplication().getService(L3PersistentState::class.java) + } + } +} diff --git a/src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.form b/src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.form new file mode 100644 index 0000000..6d603ba --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.form @@ -0,0 +1,66 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.kt b/src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.kt new file mode 100644 index 0000000..00af61f --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/config/L3SettingsPanel.kt @@ -0,0 +1,21 @@ +package com.plexworlds.l3.config; + +import com.intellij.ui.IdeBorderFactory +import com.plexworlds.l3.llm.LLMs +import javax.swing.JComboBox +import javax.swing.JPanel +import javax.swing.JTextField + +class L3SettingsPanel { + + lateinit var mainPanel: JPanel + lateinit var providerComboBox: JComboBox + lateinit var modelField: JTextField + lateinit var urlField: JTextField + + init { + mainPanel.border = IdeBorderFactory.createTitledBorder("Plugin Settings") + providerComboBox.addItem(LLMs.OLLAMA.name) + providerComboBox.addItem(LLMs.DUMMY.name) + } +} diff --git a/src/main/kotlin/com/plexworlds/l3/llm/Dummy.kt b/src/main/kotlin/com/plexworlds/l3/llm/Dummy.kt new file mode 100644 index 0000000..21ff6ff --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/llm/Dummy.kt @@ -0,0 +1,18 @@ +package com.plexworlds.l3.llm + + +object Dummy : LLM { + + private var model = "" + + + override fun call(prefix: String, suffix: String): String { + return "Hello!" + } + + + override fun changeModel(model: String) { + Dummy.model = model + } + +} diff --git a/src/main/kotlin/com/plexworlds/l3/llm/LLM.kt b/src/main/kotlin/com/plexworlds/l3/llm/LLM.kt new file mode 100644 index 0000000..d2ded8f --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/llm/LLM.kt @@ -0,0 +1,23 @@ +package com.plexworlds.l3.llm + +/** + * This is an interface for a Large Language Model (LLM). + * It provides a method to generate a completion suggestion based on a given prefix and suffix. + */ +interface LLM { + /** + * This method generates a completion suggestion based on a given prefix and suffix. + * + * @param prefix The part of the code before the cursor. + * @param suffix The part of the code after the cursor. + * @return The generated completion suggestion, or null if no suggestion could be generated. + */ + fun call(prefix: String, suffix: String): String + + /** + * This method changes the current model used by the LLM. + * + * @param model The name of the model to be used. + */ + fun changeModel(model: String) +} diff --git a/src/main/kotlin/com/plexworlds/l3/llm/LLMs.kt b/src/main/kotlin/com/plexworlds/l3/llm/LLMs.kt new file mode 100644 index 0000000..5fa3842 --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/llm/LLMs.kt @@ -0,0 +1,7 @@ +package com.plexworlds.l3.llm + +enum class LLMs(val llm: LLM ) { + + OLLAMA(Ollama), + DUMMY(Dummy) +} diff --git a/src/main/kotlin/com/plexworlds/l3/llm/Ollama.kt b/src/main/kotlin/com/plexworlds/l3/llm/Ollama.kt new file mode 100644 index 0000000..d9b16c7 --- /dev/null +++ b/src/main/kotlin/com/plexworlds/l3/llm/Ollama.kt @@ -0,0 +1,65 @@ +package com.plexworlds.l3.llm + +import io.github.amithkoujalgi.ollama4j.core.OllamaAPI +import io.github.amithkoujalgi.ollama4j.core.utils.Options +import io.github.amithkoujalgi.ollama4j.core.utils.OptionsBuilder +import java.net.http.HttpTimeoutException + +object Ollama : LLM { + + private var model = "codellama:7b-code" + + + override fun call(prefix: String, suffix: String): String { + if (!isPingOk) { + return "" + } + + for (i in 0.. $prefix $suffix ", options).response.let { + if (it.endsWith(END)) it.substring(0, it.length - END.length).trim(' ', '\t', '\n') else it + } + } catch (e: HttpTimeoutException) { + continue + } + if (suggestion.isNotBlank()) { + return suggestion + } + } + + return "" + } + + + override fun changeModel(model: String) { + Ollama.model = model + } + + + private val options: Options by lazy { + OptionsBuilder() + .setTemperature(0.4f) + .build() + } + + + private val isPingOk: Boolean + get() { + return try { + OllamaAPI(HOST).ping() + } catch (e: Exception) { + false + } + } + + + private const val HOST = "http://localhost:44000/" + + + private const val END = "" + + private const val RETRY_COUNT = 4 +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..4bd2ccf --- /dev/null +++ b/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,24 @@ + + com.plexworlds.l3 + + Local Llama Link + + Plexworlds + + Connect your local Ollama to IDEA for inline completions. + + + com.intellij.modules.platform + + + + + + + + + diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..e314d13 --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/test/kotlin/com/plexworlds/l3/L3CaretPositionAnalyzerTest.kt b/src/test/kotlin/com/plexworlds/l3/L3CaretPositionAnalyzerTest.kt new file mode 100644 index 0000000..5faf495 --- /dev/null +++ b/src/test/kotlin/com/plexworlds/l3/L3CaretPositionAnalyzerTest.kt @@ -0,0 +1,80 @@ +package com.plexworlds.l3 + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class L3CaretPositionAnalyzerTest { + @Test + fun `test inside literal`() { + assertTrue("int x = 123;".shouldBeSkippedOnPosition(9)) + assertTrue("String str = \"Hello, world!\";".shouldBeSkippedOnPosition(16)) + } + + @Test + fun `test inside identifier or keyword`() { + assertTrue("int myAmazingVariable = 123;".shouldBeSkippedOnPosition(7)) + assertTrue("System.out.println();".shouldBeSkippedOnPosition(15)) + assertTrue("boolean f = true;".shouldBeSkippedOnPosition(4)) + } + + @Test + fun `test after semicolon`() { + assertTrue("int x = 123;".shouldBeSkippedOnPosition(11)) + } + + @Test + fun `test after left brace`() { + assertTrue("if (condition) {".shouldBeSkippedOnPosition(15)) + } + + @Test + fun `test after right brace`() { + assertTrue("if (condition) {}".shouldBeSkippedOnPosition(16)) + } + + @Test + fun `test before left parenthesis`() { + assertTrue("System.out.println();".shouldBeSkippedOnPosition(17)) + } + + @Test + fun `test after right parenthesis`() { + assertTrue("static void myMethod()".shouldBeSkippedOnPosition(21)) + } + + @Test + fun `test outside any specific context`() { + assertFalse("int x = 123;".shouldBeSkippedOnPosition(4)) + } + + @Test + fun `test at the boundary of string literal`() { + val str = "String str = \"Hello!\"" + assertTrue(str.shouldBeSkippedOnPosition(14)) + assertTrue(str.shouldBeSkippedOnPosition(20)) + } + + @Test + fun `test at the boundary of char literal`() { + val str = "char c = 'a'" + assertTrue(str.shouldBeSkippedOnPosition(9)) + assertTrue(str.shouldBeSkippedOnPosition(10)) + } + + @Test + fun `test at the end of digital literal`() { + val beforeSemicolon = "int x = 123;" + val afterLong = "long y = 123L;" + val afterFloat = "float z = 123.45f;" + val beforeSpace = "int a = 123 + 456;" + val beforeArithmetic = "int b = 123*456;" + val beforeLogical = "int c = 123<456;" + assertTrue(beforeSemicolon.shouldBeSkippedOnPosition(11)) + assertTrue(afterLong.shouldBeSkippedOnPosition(12)) + assertTrue(afterFloat.shouldBeSkippedOnPosition(16)) + assertTrue(beforeSpace.shouldBeSkippedOnPosition(10)) + assertTrue(beforeArithmetic.shouldBeSkippedOnPosition(10)) + assertTrue(beforeLogical.shouldBeSkippedOnPosition(10)) + } +}