diff --git a/pom.xml b/pom.xml index d9d0872c2f..d46068e026 100644 --- a/pom.xml +++ b/pom.xml @@ -651,8 +651,8 @@ - 3.1.0 - 2.3.1 + 3.1.1 + 2.3.2 2.0.9 1.4.14 4.7.2 diff --git a/server-core/src/main/java/io/onedev/server/model/support/AIModelSetting.java b/server-core/src/main/java/io/onedev/server/model/support/AIModelSetting.java index 7f7362033b..40a7780bdc 100644 --- a/server-core/src/main/java/io/onedev/server/model/support/AIModelSetting.java +++ b/server-core/src/main/java/io/onedev/server/model/support/AIModelSetting.java @@ -61,7 +61,7 @@ public class AIModelSetting implements Serializable { this.apiKey = apiKey; } - @Editable(order=400) + @Editable(order=400, name="Name") @ChoiceProvider("getModels") @NotEmpty public String getName() { diff --git a/server-core/src/main/java/io/onedev/server/model/support/administration/AISetting.java b/server-core/src/main/java/io/onedev/server/model/support/administration/AISetting.java index accc56867a..a60e2656fb 100644 --- a/server-core/src/main/java/io/onedev/server/model/support/administration/AISetting.java +++ b/server-core/src/main/java/io/onedev/server/model/support/administration/AISetting.java @@ -12,30 +12,24 @@ import io.onedev.server.model.support.AIModelSetting; public class AISetting implements Serializable { private static final long serialVersionUID = 1L; + + public static final String PROP_LITE_MODEL_SETTING = "liteModelSetting"; - private AIModelSetting naturalLanguageQueryModelSetting; + private AIModelSetting liteModelSetting; - @Editable(order=100, name="Natural Language Query Model", description= - """ - If specified, one will be able to query issues, pull requests and builds via natural language. Suggested models in terms of performance and cost for this task: - - """) + @Editable(order=100) @Nullable - public AIModelSetting getNaturalLanguageQueryModelSetting() { - return naturalLanguageQueryModelSetting; + public AIModelSetting getLiteModelSetting() { + return liteModelSetting; } - public void setNaturalLanguageQueryModelSetting(AIModelSetting naturalLanguageQueryModelSetting) { - this.naturalLanguageQueryModelSetting = naturalLanguageQueryModelSetting; + public void setLiteModelSetting(AIModelSetting liteModelSetting) { + this.liteModelSetting = liteModelSetting; } @Nullable - public ChatModel getNaturalLanguageQueryModel() { - return naturalLanguageQueryModelSetting != null ? naturalLanguageQueryModelSetting.getChatModel() : null; + public ChatModel getLiteModel() { + return liteModelSetting != null ? liteModelSetting.getChatModel() : null; } } diff --git a/server-core/src/main/java/io/onedev/server/search/code/hit/QueryHit.java b/server-core/src/main/java/io/onedev/server/search/code/hit/QueryHit.java index c2dc5404dc..98191feac5 100644 --- a/server-core/src/main/java/io/onedev/server/search/code/hit/QueryHit.java +++ b/server-core/src/main/java/io/onedev/server/search/code/hit/QueryHit.java @@ -2,10 +2,9 @@ package io.onedev.server.search.code.hit; import java.io.Serializable; -import org.jspecify.annotations.Nullable; - import org.apache.wicket.Component; import org.apache.wicket.markup.html.image.Image; +import org.jspecify.annotations.Nullable; import io.onedev.commons.utils.PlanarRange; diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java index 993ad78c8e..018b1b2bb0 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java @@ -258,16 +258,16 @@ public class BuildQueryBehavior extends ANTLRAssistBehavior { } } } - if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null) - hints.add(_T("Set up AI to use natural language query")); + if (getSettingService().getAISetting().getLiteModelSetting() == null) + hints.add(_T("Set up AI to use natural language query")); return hints; } @Override protected NaturalLanguageTranslator getNaturalLanguageTranslator() { - var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel(); - if (naturalLanguageQueryModel != null) { - return new NaturalLanguageTranslator(naturalLanguageQueryModel) { + var liteModel = getSettingService().getAISetting().getLiteModel(); + if (liteModel != null) { + return new NaturalLanguageTranslator(liteModel) { @Override public String getQueryDescription() { diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java index aa5642c769..e8855612f6 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java @@ -432,8 +432,8 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior { } } } - if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null) - hints.add(_T("Set up AI to use natural language query")); + if (getSettingService().getAISetting().getLiteModelSetting() == null) + hints.add(_T("Set up AI to use natural language query")); return hints; } @@ -444,9 +444,9 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior { @Override protected NaturalLanguageTranslator getNaturalLanguageTranslator() { - var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel(); - if (naturalLanguageQueryModel != null) { - return new NaturalLanguageTranslator(naturalLanguageQueryModel) { + var liteModel = getSettingService().getAISetting().getLiteModel(); + if (liteModel != null) { + return new NaturalLanguageTranslator(liteModel) { @Override public String getQueryDescription() { diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java index 8ed998012c..3b78abd13d 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java @@ -263,8 +263,8 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior { } } } - if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null) - hints.add(_T("Set up AI to use natural language query")); + if (getSettingService().getAISetting().getLiteModelSetting() == null) + hints.add(_T("Set up AI to use natural language query")); return hints; } @@ -276,9 +276,9 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior { @Override protected NaturalLanguageTranslator getNaturalLanguageTranslator() { - var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel(); - if (naturalLanguageQueryModel != null) { - return new NaturalLanguageTranslator(naturalLanguageQueryModel) { + var liteModel = getSettingService().getAISetting().getLiteModel(); + if (liteModel != null) { + return new NaturalLanguageTranslator(liteModel) { @Override public String getQueryDescription() { diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/inputassist/InputAssistBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/inputassist/InputAssistBehavior.java index df2cccd6c5..85eab83410 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/inputassist/InputAssistBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/inputassist/InputAssistBehavior.java @@ -127,7 +127,7 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior { String toTranslate = params.getParameterValue("input").toString(); String translated; try { - translated = Preconditions.checkNotNull(getNaturalLanguageTranslator()).translate(toTranslate); + translated = getNaturalLanguageTranslator().translate(toTranslate); } catch (Exception e) { translated = toTranslate; var explicitException = ExceptionUtils.find(e, ExplicitException.class); diff --git a/server-core/src/main/java/io/onedev/server/web/component/diff/blob/text/BlobTextDiffPanel.java b/server-core/src/main/java/io/onedev/server/web/component/diff/blob/text/BlobTextDiffPanel.java index 50e8714431..4a5eb7900b 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/diff/blob/text/BlobTextDiffPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/component/diff/blob/text/BlobTextDiffPanel.java @@ -14,8 +14,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.jspecify.annotations.Nullable; - import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AttributeAppender; @@ -32,6 +30,7 @@ import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.jspecify.annotations.Nullable; import org.unbescape.html.HtmlEscape; import com.fasterxml.jackson.core.JsonProcessingException; @@ -43,7 +42,6 @@ import io.onedev.commons.utils.LinearRange; import io.onedev.commons.utils.StringUtils; import io.onedev.server.OneDev; import io.onedev.server.codequality.CodeProblem; -import io.onedev.server.service.CodeCommentService; import io.onedev.server.git.BlameBlock; import io.onedev.server.git.BlameCommit; import io.onedev.server.git.BlobChange; @@ -54,6 +52,7 @@ import io.onedev.server.model.Project; import io.onedev.server.model.PullRequest; import io.onedev.server.search.code.hit.QueryHit; import io.onedev.server.security.SecurityUtils; +import io.onedev.server.service.CodeCommentService; import io.onedev.server.util.DateUtils; import io.onedev.server.util.Pair; import io.onedev.server.util.diff.DiffBlock; @@ -66,6 +65,7 @@ import io.onedev.server.web.behavior.blamemessage.BlameMessageBehavior; import io.onedev.server.web.component.diff.blob.BlobAnnotationSupport; import io.onedev.server.web.component.diff.revision.DiffViewMode; import io.onedev.server.web.component.svg.SpriteImage; +import io.onedev.server.web.component.symboltooltip.SymbolContext; import io.onedev.server.web.component.symboltooltip.SymbolTooltipPanel; import io.onedev.server.web.page.base.BasePage; import io.onedev.server.web.page.project.blob.ProjectBlobPage; @@ -352,7 +352,62 @@ public class BlobTextDiffPanel extends Panel { protected Project getProject() { return change.getProject(); } - + + @Override + protected String getSymbolPositionCalcFunction() { + return + """ + function(symbolEl) { + var $td = $(symbolEl).closest('td.content'); + if ($td.hasClass('old')) + return 'old:' + $td.data('old'); + else + return 'new:' + $td.data('new'); + }"""; + } + + @Override + protected SymbolContext getSymbolContext(String symbolPosition, int beforeContextSize, + int afterContextSize, int atStartContextSize) { + String[] parts = symbolPosition.split(":"); + boolean isNew = parts[0].equals("new"); + int lineNo = Integer.parseInt(parts[1]); + + List lines; + String path; + if (isNew) { + lines = change.getNewText().getLines(); + path = change.getNewBlobIdent().path; + } else { + lines = change.getOldText().getLines(); + path = change.getOldBlobIdent().path; + } + + List linesBefore = new ArrayList<>(); + List linesAfter = new ArrayList<>(); + String symbolLine = lines.get(lineNo); + + for (int i = Math.max(0, lineNo - beforeContextSize); i < lineNo; i++) { + linesBefore.add(lines.get(i)); + } + for (int i = lineNo + 1; i <= Math.min(lineNo + afterContextSize, lines.size() - 1); i++) { + linesAfter.add(lines.get(i)); + } + + List linesAtStart = new ArrayList<>(); + for (int i = 0; i < Math.min(atStartContextSize, lines.size()); i++) { + linesAtStart.add(lines.get(i)); + } + + return new SymbolContext( + path, + symbolLine, + linesBefore, + linesAfter, + linesAtStart + ); + } + }; add(symbolTooltip); diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java new file mode 100644 index 0000000000..bc74fcee0b --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java @@ -0,0 +1,49 @@ +package io.onedev.server.web.component.symboltooltip; + +import java.io.Serializable; +import java.util.List; + +public class SymbolContext implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String blobPath; + + private final String symbolLine; + + private final List linesBeforeSymbolLine; + + private final List linesAfterSymbolLine; + + private final List linesAtStart; + + public SymbolContext(String fileName, String symbolLine, + List linesBeforeSymbolLine, List linesAfterSymbolLine, List linesAtStart) { + this.blobPath = fileName; + this.symbolLine = symbolLine; + this.linesBeforeSymbolLine = linesBeforeSymbolLine; + this.linesAfterSymbolLine = linesAfterSymbolLine; + this.linesAtStart = linesAtStart; + } + + public String getBlobPath() { + return blobPath; + } + + public String getSymbolLine() { + return symbolLine; + } + + public List getLinesBeforeSymbolLine() { + return linesBeforeSymbolLine; + } + + public List getLinesAfterSymbolLine() { + return linesAfterSymbolLine; + } + + public List getLinesAtStart() { + return linesAtStart; + } + +} diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.html b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.html index e4582f6ef7..436d724c0f 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.html +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.html @@ -1,9 +1,13 @@
- -
Possible declarations
-
\ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java index ef308420dc..142629b479 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java @@ -1,26 +1,15 @@ package io.onedev.server.web.component.symboltooltip; -import io.onedev.commons.jsymbol.Symbol; -import io.onedev.server.OneDev; -import io.onedev.server.service.SettingService; -import io.onedev.server.git.Blob; -import io.onedev.server.git.BlobIdent; -import io.onedev.server.model.Project; -import io.onedev.server.search.code.CodeSearchService; -import io.onedev.server.search.code.IndexConstants; -import io.onedev.server.search.code.hit.QueryHit; -import io.onedev.server.search.code.hit.SymbolHit; -import io.onedev.server.search.code.query.BlobQuery; -import io.onedev.server.search.code.query.SymbolQuery; -import io.onedev.server.search.code.query.TextQuery; -import io.onedev.server.web.behavior.AbstractPostAjaxBehavior; -import io.onedev.server.web.behavior.CtrlClickBehavior; -import io.onedev.server.web.behavior.RunTaskBehavior; -import io.onedev.server.web.component.link.ViewStateAwareAjaxLink; -import io.onedev.server.web.page.project.blob.ProjectBlobPage; -import io.onedev.server.web.page.project.blob.render.BlobRenderer; +import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + import org.apache.commons.lang.StringUtils; import org.apache.wicket.Component; +import org.apache.wicket.Session; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxLink; import org.apache.wicket.behavior.AttributeAppender; @@ -39,22 +28,64 @@ import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreType; +import com.fasterxml.jackson.databind.ObjectMapper; -import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import io.onedev.commons.jsymbol.Symbol; +import io.onedev.commons.utils.LinearRange; +import io.onedev.commons.utils.PlanarRange; +import io.onedev.server.git.Blob; +import io.onedev.server.git.BlobIdent; +import io.onedev.server.model.Project; +import io.onedev.server.search.code.CodeSearchService; +import io.onedev.server.search.code.IndexConstants; +import io.onedev.server.search.code.hit.QueryHit; +import io.onedev.server.search.code.hit.SymbolHit; +import io.onedev.server.search.code.query.BlobQuery; +import io.onedev.server.search.code.query.SymbolQuery; +import io.onedev.server.search.code.query.TextQuery; +import io.onedev.server.service.SettingService; +import io.onedev.server.web.behavior.AbstractPostAjaxBehavior; +import io.onedev.server.web.behavior.CtrlClickBehavior; +import io.onedev.server.web.behavior.RunTaskBehavior; +import io.onedev.server.web.component.link.ViewStateAwareAjaxLink; +import io.onedev.server.web.page.project.blob.ProjectBlobPage; +import io.onedev.server.web.page.project.blob.render.BlobRenderer; public abstract class SymbolTooltipPanel extends Panel { private static final int QUERY_ENTRIES = 20; + + private static final int BEFORE_CONTEXT_SIZE = 5; + + private static final int AFTER_CONTEXT_SIZE = 5; + + private static final int AT_START_CONTEXT_SIZE = 200; + + private static final Logger logger = LoggerFactory.getLogger(SymbolTooltipPanel.class); private String revision = ""; private String symbolName = ""; + + private String symbolPosition = ""; private List symbolHits = new ArrayList<>(); + @Inject + private CodeSearchService searchService; + + @Inject + private SettingService settingService; + + @Inject + private ObjectMapper objectMapper; + public SymbolTooltipPanel(String id) { super(id); } @@ -69,7 +100,7 @@ public abstract class SymbolTooltipPanel extends Panel { content.setOutputMarkupId(true); add(content); - content.add(new ListView("declarations", new AbstractReadOnlyModel<>() { + content.add(new ListView("definitions", new AbstractReadOnlyModel<>() { @Override public List getObject() { @@ -80,7 +111,7 @@ public abstract class SymbolTooltipPanel extends Panel { @Override protected void populateItem(ListItem item) { - final QueryHit hit = item.getModelObject(); + var hit = item.getModelObject(); item.add(hit.renderIcon("icon")); AjaxLink delegateLink = new ViewStateAwareAjaxLink("delegateLink") { @@ -105,6 +136,7 @@ public abstract class SymbolTooltipPanel extends Panel { link.add(new Label("scope", hit.getNamespace()).setVisible(hit.getNamespace()!=null)); item.add(link); + item.setOutputMarkupId(true); } @Override @@ -113,6 +145,15 @@ public abstract class SymbolTooltipPanel extends Panel { setVisible(!symbolHits.isEmpty()); } + }); + content.add(new WebMarkupContainer("definitionInferHint") { + + @Override + protected void onConfigure() { + super.onConfigure(); + setVisible(settingService.getAISetting().getLiteModelSetting() == null && symbolHits.size() > 1); + } + }); content.add(new ViewStateAwareAjaxLink("findOccurrences") { @@ -144,14 +185,12 @@ public abstract class SymbolTooltipPanel extends Panel { List hits; // do this check to avoid TooGeneralQueryException if (symbolName.length() >= IndexConstants.NGRAM_SIZE) { - int maxQueryEntries = OneDev.getInstance(SettingService.class) - .getPerformanceSetting().getMaxCodeSearchEntries(); + int maxQueryEntries = settingService.getPerformanceSetting().getMaxCodeSearchEntries(); var query = new TextQuery.Builder(symbolName) .wholeWord(true) .caseSensitive(true) .count(maxQueryEntries) .build(); - CodeSearchService searchService = OneDev.getInstance(CodeSearchService.class); ObjectId commit = getProject().getRevCommit(revision, true); hits = searchService.search(getProject(), commit, query); } else { @@ -178,83 +217,147 @@ public abstract class SymbolTooltipPanel extends Panel { } }); + + var definitionInferBehavior = new AbstractPostAjaxBehavior() { + + @Override + protected void respond(AjaxRequestTarget target) { + } + + }; + add(definitionInferBehavior); add(new AbstractPostAjaxBehavior() { @Override protected void respond(AjaxRequestTarget target) { IRequestParameters params = RequestCycle.get().getRequest().getPostParameters(); - revision = params.getParameterValue("revision").toString(); - symbolName = params.getParameterValue("symbol").toString(); + var action = params.getParameterValue("action").toString(); + if (action.equals("query")) { + revision = params.getParameterValue("revision").toString(); + symbolName = params.getParameterValue("symbolName").toString(); + symbolPosition = params.getParameterValue("symbolPosition").toString(); - if (symbolName.startsWith("#include")) { - // handle c/c++ include directive as CodeMirror return the whole line as a meta - symbolName = symbolName.substring("#include".length()).trim(); - } + if (symbolName.startsWith("#include")) { + // handle c/c++ include directive as CodeMirror return the whole line as a meta + symbolName = symbolName.substring("#include".length()).trim(); + } - String charsToStrip = "@#'\"./\\"; - symbolName = StringUtils.stripEnd(StringUtils.stripStart(symbolName, charsToStrip), charsToStrip); - symbolHits.clear(); - - // do this check to avoid TooGeneralQueryException - if (symbolName.length() != 0 && symbolName.indexOf('?') == -1 && symbolName.indexOf('*') == -1) { - BlobIdent blobIdent = new BlobIdent(revision, getBlobPath(), FileMode.TYPE_FILE); - Blob blob = getProject().getBlob(blobIdent, true); + String charsToStrip = "@#'\"./\\"; + symbolName = StringUtils.stripEnd(StringUtils.stripStart(symbolName, charsToStrip), charsToStrip); + symbolHits.clear(); - if (symbolHits.size() < QUERY_ENTRIES) { - // first find in current file for matched symbols - List symbols = OneDev.getInstance(CodeSearchService.class).getSymbols(getProject(), - blob.getBlobId(), getBlobPath()); - if (symbols != null) { - for (Symbol symbol: symbols) { - if (symbolHits.size() < QUERY_ENTRIES - && symbol.isSearchable() - && symbolName.equals(symbol.getName()) - && symbol.isPrimary()) { - symbolHits.add(new SymbolHit(getBlobPath(), symbol, null)); + // do this check to avoid TooGeneralQueryException + if (symbolName.length() != 0 && symbolName.indexOf('?') == -1 && symbolName.indexOf('*') == -1) { + BlobIdent blobIdent = new BlobIdent(revision, getBlobPath(), FileMode.TYPE_FILE); + Blob blob = getProject().getBlob(blobIdent, true); + + if (symbolHits.size() < QUERY_ENTRIES) { + // first find in current file for matched symbols + List symbols = searchService.getSymbols(getProject(), + blob.getBlobId(), getBlobPath()); + if (symbols != null) { + for (Symbol symbol: symbols) { + if (symbolHits.size() < QUERY_ENTRIES + && symbol.isSearchable() + && symbolName.equals(symbol.getName()) + && symbol.isPrimary()) { + symbolHits.add(new SymbolHit(getBlobPath(), symbol, null)); + } + } + for (Symbol symbol: symbols) { + if (symbolHits.size() < QUERY_ENTRIES + && symbol.isSearchable() + && symbolName.equals(symbol.getName()) + && !symbol.isPrimary()) { + symbolHits.add(new SymbolHit(getBlobPath(), symbol, null)); + } } } - for (Symbol symbol: symbols) { - if (symbolHits.size() < QUERY_ENTRIES - && symbol.isSearchable() - && symbolName.equals(symbol.getName()) - && !symbol.isPrimary()) { - symbolHits.add(new SymbolHit(getBlobPath(), symbol, null)); - } + } + + if (symbolHits.size() < QUERY_ENTRIES) { + // then find in other files for public symbols + ObjectId commit = getProject().getRevCommit(revision, true); + BlobQuery query; + if (symbolHits.size() < QUERY_ENTRIES) { + query = new SymbolQuery.Builder(symbolName) + .caseSensitive(true) + .excludeBlobPath(blobIdent.path) + .primary(true) + .local(false) + .count(QUERY_ENTRIES) + .build(); + symbolHits.addAll(searchService.search(getProject(), commit, query)); + } + if (symbolHits.size() < QUERY_ENTRIES) { + query = new SymbolQuery.Builder(symbolName) + .caseSensitive(true) + .excludeBlobPath(blobIdent.path) + .primary(false) + .local(false) + .count(QUERY_ENTRIES - symbolHits.size()) + .build(); + symbolHits.addAll(searchService.search(getProject(), commit, query)); } + } + } + + target.add(content); + + CharSequence callback; + if (settingService.getAISetting().getLiteModelSetting() != null && symbolHits.size() > 1) + callback = getCallbackFunction(explicit("action")); + else + callback = "undefined"; + String script = String.format("onedev.server.symboltooltip.doneQuery('%s', %s);", + content.getMarkupId(), callback); + target.appendJavaScript(script); + } else { + var liteModel = settingService.getAISetting().getLiteModel(); + try { + ObjectMapper mapperCopy = objectMapper.copy(); + mapperCopy.addMixIn(PlanarRange.class, IgnorePlanarRangeMixin.class); + mapperCopy.addMixIn(LinearRange.class, IgnoreLinearRangeMixin.class); + var jsonOfSymbolHits = mapperCopy.writeValueAsString(symbolHits); + var symbolContext = getSymbolContext(symbolPosition, BEFORE_CONTEXT_SIZE, + AFTER_CONTEXT_SIZE, AT_START_CONTEXT_SIZE); + var jsonOfSymbolContext = mapperCopy.writeValueAsString(symbolContext); + var systemMessage = new SystemMessage(""" + You are familiar with various programming languages. Given a symbol name, a json object of + symbol context, and a json array of possible symbol definitions, please determine the most + likely symbol definition and return its index in the array. Symbol definition may contain + parent symbol, and this is where the symbol is defined inside (namespace, package etc). + The @type property in symbol definition means category/kind of the symbol (type, method, + variable etc). + + IMPORTANT: only return index of the definition, no other text or comments. + """); + + var userMessage = new UserMessage(String.format(""" + Symbol name: + %s + + Symbol context json: + %s + + Possible symbol definitions json: + %s + """, symbolName, jsonOfSymbolContext, jsonOfSymbolHits)); + var lineNo = Integer.parseInt(liteModel.chat(systemMessage, userMessage).aiMessage().text()); + if (lineNo >= 0 && lineNo < symbolHits.size()) { + @SuppressWarnings("unchecked") + ListView definitionsView = (ListView) content.get("definitions"); + @SuppressWarnings("deprecation") + var script = String.format("onedev.server.symboltooltip.doneInfer('%s');", + definitionsView.get(lineNo).getMarkupId()); + target.appendJavaScript(script); } - } - - if (symbolHits.size() < QUERY_ENTRIES) { - // then find in other files for public symbols - CodeSearchService searchService = OneDev.getInstance(CodeSearchService.class); - ObjectId commit = getProject().getRevCommit(revision, true); - BlobQuery query; - if (symbolHits.size() < QUERY_ENTRIES) { - query = new SymbolQuery.Builder(symbolName) - .caseSensitive(true) - .excludeBlobPath(blobIdent.path) - .primary(true) - .local(false) - .count(QUERY_ENTRIES) - .build(); - symbolHits.addAll(searchService.search(getProject(), commit, query)); - } - if (symbolHits.size() < QUERY_ENTRIES) { - query = new SymbolQuery.Builder(symbolName) - .caseSensitive(true) - .excludeBlobPath(blobIdent.path) - .primary(false) - .local(false) - .count(QUERY_ENTRIES - symbolHits.size()) - .build(); - symbolHits.addAll(searchService.search(getProject(), commit, query)); - } + } catch (Exception e) { + logger.error("Error inferring most likely symbol definition", e); + Session.get().error("Error inferring most likely symbol definition, check server log for details"); } } - target.add(content); - String script = String.format("onedev.server.symboltooltip.doneQuery('%s');", content.getMarkupId()); - target.appendJavaScript(script); } @Override @@ -263,8 +366,10 @@ public abstract class SymbolTooltipPanel extends Panel { response.render(JavaScriptHeaderItem.forReference(new SymbolTooltipResourceReference())); - String script = String.format("onedev.server.symboltooltip.init('%s', %s);", - getMarkupId(), getCallbackFunction(explicit("revision"), explicit("symbol"))); + var callback = getCallbackFunction(explicit("action"), explicit("revision"), + explicit("symbolName"), explicit("symbolPosition")); + String script = String.format("onedev.server.symboltooltip.init('%s', %s, %s);", + getMarkupId(), callback, getSymbolPositionCalcFunction()); response.render(OnDomReadyHeaderItem.forScript(script)); } @@ -302,5 +407,18 @@ public abstract class SymbolTooltipPanel extends Panel { protected abstract void onSelect(AjaxRequestTarget target, QueryHit hit); protected abstract void onOccurrencesQueried(AjaxRequestTarget target, List hits); - + + protected abstract String getSymbolPositionCalcFunction(); + + protected abstract SymbolContext getSymbolContext(String symbolPosition, int beforeContextSize, + int afterContextSize, int atStartContextSize); + } + +@JsonIgnoreType +interface IgnorePlanarRangeMixin { +} + +@JsonIgnoreType +interface IgnoreLinearRangeMixin { +} \ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.css b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.css index 6e6c04eaba..18dfbe4ec3 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.css +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.css @@ -9,6 +9,10 @@ box-shadow: 0px 0px 8px 0px rgba(0,0,0,0.1); max-width: 720px; } +.symbol-tooltip .hint { + font-size: 11px; + opacity: 0.7; +} .dark-mode .symbol-tooltip { background-color: var(--dark-mode-light-info); color: var(--info); @@ -21,15 +25,28 @@ color: #0073e9; } -.symbol-tooltip ul.declarations li { +.symbol-tooltip .definition-inferring-indicator img { + width: 14px; + height: 14px; +} +.symbol-tooltip .definitions.no-definition-infer>li .most-probable-indicator { + display: none; +} +.symbol-tooltip .definitions>li .most-probable-indicator { + visibility: hidden; +} +.symbol-tooltip .definitions>li.most-probable .most-probable-indicator { + visibility: visible; +} +.symbol-tooltip ul.definitions li { vertical-align: top; padding: 2px 4px; white-space: nowrap; } -.symbol-tooltip ul.declarations .scope { +.symbol-tooltip ul.definitions .scope { color: var(--gray); } -.dark-mode .symbol-tooltip ul.declarations .scope { +.dark-mode .symbol-tooltip ul.definitions .scope { color: var(--dark-mode-gray); } .symbol-tooltip a.find-occurrences[disabled] { diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.js b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.js index f8e7553334..2f3f3d6496 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.js +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/symbol-tooltip.js @@ -1,5 +1,5 @@ onedev.server.symboltooltip = { - init: function(containerId, queryCallback) { + init: function(containerId, callback, symbolPositionCalcFunction) { var container = document.getElementById(containerId); var showTimer; @@ -69,21 +69,41 @@ onedev.server.symboltooltip = { }); $tooltip.data("alignment", {placement: {x: 0, y:0, offset:2, targetX: 0, targetY: 100}, target: {element: symbolEl}}); - $tooltip.align($tooltip.data("alignment")); - - queryCallback(revision, $symbol.text()); + $tooltip.align($tooltip.data("alignment")); + callback("query", revision, $symbol.text(), symbolPositionCalcFunction($symbol[0])); showTimer = null; }, 500); }; }, - doneQuery: function(contentId) { + doneQuery: function(contentId, callback) { var $content = $("#" + contentId); var $container = $content.parent(); var $tooltip = $("#" + $container.attr("id") + "-symbol-tooltip"); $tooltip.removeClass("d-none"); - if ($tooltip.length != 0) + var $definitions = $content.children(".definitions"); + if (callback) { + var $indicator; + if (onedev.server.isDarkMode()) + $indicator = $('
Inferring the most probable...
'); + else + $indicator = $('
Inferring the most probable...
'); + $indicator.insertBefore($definitions); + callback("infer"); + } else { + $definitions.addClass("no-definition-infer"); + } + if ($tooltip.length != 0) { $tooltip.html($content.children()).align($tooltip.data("alignment")); + } + }, + + doneInfer: function(definitionId) { + var $definition = $("#" + definitionId); + $definition.addClass("most-probable"); + var $tooltip = $definition.closest(".symbol-tooltip"); + $tooltip.find(".definition-inferring-indicator").remove(); + $tooltip.align($tooltip.data("alignment")); }, // this is public API which can be called from other components using this component diff --git a/server-core/src/main/java/io/onedev/server/web/mapper/BaseUrlMapper.java b/server-core/src/main/java/io/onedev/server/web/mapper/BaseUrlMapper.java index 736045b405..1ef51f86c5 100644 --- a/server-core/src/main/java/io/onedev/server/web/mapper/BaseUrlMapper.java +++ b/server-core/src/main/java/io/onedev/server/web/mapper/BaseUrlMapper.java @@ -8,7 +8,7 @@ import org.apache.wicket.request.mapper.CompoundRequestMapper; import io.onedev.commons.utils.ExplicitException; import io.onedev.server.web.asset.icon.IconScope; -import io.onedev.server.web.page.admin.aisetting.AISettingPage; +import io.onedev.server.web.page.admin.aisetting.LiteModelPage; import io.onedev.server.web.page.admin.alertsettings.AlertSettingPage; import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage; import io.onedev.server.web.page.admin.brandingsetting.BrandingSettingPage; @@ -347,7 +347,7 @@ public class BaseUrlMapper extends CompoundRequestMapper { add(new BasePageMapper("~administration/labels", LabelManagementPage.class)); add(new BasePageMapper("~administration/settings/alert", AlertSettingPage.class)); add(new BasePageMapper("~administration/settings/performance", PerformanceSettingPage.class)); - add(new BasePageMapper("~administration/settings/ai", AISettingPage.class)); + add(new BasePageMapper("~administration/settings/lite-ai-model", LiteModelPage.class)); add(new BasePageMapper("~administration/settings/backup", DatabaseBackupPage.class)); add(new BasePageMapper("~administration/settings/authenticator", AuthenticatorPage.class)); add(new BasePageMapper("~administration/settings/sso-providers", SsoProviderListPage.class)); diff --git a/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.html b/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.html deleted file mode 100644 index 07b08bdb37..0000000000 --- a/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.html +++ /dev/null @@ -1,10 +0,0 @@ - -
-
-
-
- -
-
-
-
diff --git a/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/LiteModelPage.html b/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/LiteModelPage.html new file mode 100644 index 0000000000..9e6d5a2514 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/LiteModelPage.html @@ -0,0 +1,21 @@ + +
+
+
+ This model will be used to perform lite tasks including: +
    +
  • Query issues, builds, and pull requests with natural language
  • +
  • Mark the most probable symbol definition in symbol navigation
  • +
+ It should be fast and cost-effective. Some suggested models: + Google/gemini-2.5-flash, + OpenAI/gpt-4.1-mini, + Qwen/Qwen-2.5-72B-instruct +
+
+
+ +
+
+
+
diff --git a/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.java b/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/LiteModelPage.java similarity index 75% rename from server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.java rename to server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/LiteModelPage.java index 5241e2073b..679fce7fc0 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/LiteModelPage.java @@ -12,15 +12,15 @@ import org.apache.wicket.request.mapper.parameter.PageParameters; import io.onedev.server.data.migration.VersionedXmlDoc; import io.onedev.server.model.support.administration.AISetting; import io.onedev.server.service.SettingService; -import io.onedev.server.web.editable.BeanContext; +import io.onedev.server.web.editable.PropertyContext; import io.onedev.server.web.page.admin.AdministrationPage; -public class AISettingPage extends AdministrationPage { +public class LiteModelPage extends AdministrationPage { @Inject private SettingService settingService; - public AISettingPage(PageParameters params) { + public LiteModelPage(PageParameters params) { super(params); } @@ -39,20 +39,20 @@ public class AISettingPage extends AdministrationPage { var newAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML(); settingService.saveAISetting(aiSetting); auditService.audit(null, "changed AI settings", oldAuditContent, newAuditContent); - getSession().success(_T("AI settings have been saved")); + getSession().success(_T("Lite AI model settings have been saved")); - setResponsePage(AISettingPage.class); + setResponsePage(LiteModelPage.class); } }; - form.add(BeanContext.edit("editor", aiSetting)); + form.add(PropertyContext.edit("editor", aiSetting, AISetting.PROP_LITE_MODEL_SETTING)); add(form); } @Override protected Component newTopbarTitle(String componentId) { - return new Label(componentId, _T("AI Settings")); + return new Label(componentId, _T("Lite AI Model")); } } diff --git a/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java b/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java index 8760776b28..4ce1611e8a 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java @@ -91,7 +91,7 @@ import io.onedev.server.web.component.svg.SpriteImage; import io.onedev.server.web.component.user.UserAvatar; import io.onedev.server.web.editable.EditableUtils; import io.onedev.server.web.page.HomePage; -import io.onedev.server.web.page.admin.aisetting.AISettingPage; +import io.onedev.server.web.page.admin.aisetting.LiteModelPage; import io.onedev.server.web.page.admin.alertsettings.AlertSettingPage; import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage; import io.onedev.server.web.page.admin.brandingsetting.BrandingSettingPage; @@ -339,8 +339,12 @@ public abstract class LayoutPage extends BasePage { administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("Groovy Scripts"), GroovyScriptListPage.class, new PageParameters())); - administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("AI Settings"), - AISettingPage.class, new PageParameters())); + List aiMenuItems = new ArrayList<>(); + + aiMenuItems.add(new SidebarMenuItem.Page(null, _T("Lite Model"), + LiteModelPage.class, new PageParameters())); + + administrationMenuItems.add(new SidebarMenuItem.SubMenu(null, _T("AI Settings"), aiMenuItems)); administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("Branding"), BrandingSettingPage.class, new PageParameters())); diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/source/SourceViewPanel.java b/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/source/SourceViewPanel.java index 9a737478fe..eff93273f7 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/source/SourceViewPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/blob/render/source/SourceViewPanel.java @@ -13,7 +13,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import org.jspecify.annotations.Nullable; import javax.servlet.http.Cookie; import org.apache.wicket.Component; @@ -51,6 +50,7 @@ import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.revwalk.RevCommit; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.unbescape.html.HtmlEscape; @@ -73,10 +73,6 @@ import io.onedev.server.codequality.CodeProblem; import io.onedev.server.codequality.CodeProblemContribution; import io.onedev.server.codequality.CoverageStatus; import io.onedev.server.codequality.LineCoverageContribution; -import io.onedev.server.service.BuildService; -import io.onedev.server.service.CodeCommentService; -import io.onedev.server.service.CodeCommentReplyService; -import io.onedev.server.service.CodeCommentStatusChangeService; import io.onedev.server.git.BlameBlock; import io.onedev.server.git.Blob; import io.onedev.server.git.BlobIdent; @@ -92,6 +88,10 @@ import io.onedev.server.model.support.Mark; import io.onedev.server.search.code.CodeSearchService; import io.onedev.server.search.code.hit.QueryHit; import io.onedev.server.security.SecurityUtils; +import io.onedev.server.service.BuildService; +import io.onedev.server.service.CodeCommentReplyService; +import io.onedev.server.service.CodeCommentService; +import io.onedev.server.service.CodeCommentStatusChangeService; import io.onedev.server.util.DateUtils; import io.onedev.server.util.Similarities; import io.onedev.server.util.diff.DiffUtils; @@ -115,6 +115,7 @@ import io.onedev.server.web.component.sourceformat.SourceFormatPanel; import io.onedev.server.web.component.suggestionapply.SuggestionApplyBean; import io.onedev.server.web.component.suggestionapply.SuggestionApplyModalPanel; import io.onedev.server.web.component.svg.SpriteImage; +import io.onedev.server.web.component.symboltooltip.SymbolContext; import io.onedev.server.web.component.symboltooltip.SymbolTooltipPanel; import io.onedev.server.web.page.project.blob.render.BlobRenderContext; import io.onedev.server.web.page.project.blob.render.BlobRenderContext.Mode; @@ -135,7 +136,7 @@ import io.onedev.server.web.util.WicketUtils; */ public class SourceViewPanel extends BlobViewPanel implements Positionable, SearchMenuContributor { - private static final Logger logger = LoggerFactory.getLogger(SourceViewPanel.class); + private static final Logger logger = LoggerFactory.getLogger(SourceViewPanel.class); private static final String COOKIE_OUTLINE = "sourceView.outline"; @@ -835,6 +836,51 @@ public class SourceViewPanel extends BlobViewPanel implements Positionable, Sear protected Project getProject() { return context.getProject(); } + + @Override + protected String getSymbolPositionCalcFunction() { + return + """ + function(symbolEl) { + var cm = symbolEl.closest(".CodeMirror").CodeMirror; + if (cm) { + var lineElement = symbolEl.closest('.CodeMirror-line').parentElement; + for (var view of cm.display.view) { + if (view.node === lineElement) { + return cm.getLineNumber(view.line); + } + } + } + return -1; + }"""; + } + + @Override + protected SymbolContext getSymbolContext(String symbolPosition, int beforeContextSize, + int afterContextSize, int atStartContextSize) { + var lineNo = Integer.parseInt(symbolPosition); + var lines = context.getProject().getBlob(context.getBlobIdent(), true).getText().getLines(); + List linesBefore = new ArrayList<>(); + List linesAfter = new ArrayList<>(); + String symbolLine = lines.get(lineNo); + for (int i = Math.max(0, lineNo - beforeContextSize); i < lineNo; i++) { + linesBefore.add(lines.get(i)); + } + for (int i = lineNo + 1; i <= Math.min(lineNo + afterContextSize, lines.size() - 1); i++) { + linesAfter.add(lines.get(i)); + } + List linesAtStart = new ArrayList<>(); + for (int i = 0; i < Math.min(atStartContextSize, lines.size()); i++) { + linesAtStart.add(lines.get(i)); + } + return new SymbolContext( + context.getBlobIdent().path, + symbolLine, + linesBefore, + linesAfter, + linesAtStart + ); + } }); }