feat: Accurate symbol navigation with help of AI (OD-2601)

This commit is contained in:
Robin Shen 2025-11-12 15:18:09 +08:00
parent b465a1fd78
commit 1eb8bda97e
20 changed files with 492 additions and 175 deletions

View File

@ -651,8 +651,8 @@
</repository> </repository>
</repositories> </repositories>
<properties> <properties>
<commons.version>3.1.0</commons.version> <commons.version>3.1.1</commons.version>
<agent.version>2.3.1</agent.version> <agent.version>2.3.2</agent.version>
<slf4j.version>2.0.9</slf4j.version> <slf4j.version>2.0.9</slf4j.version>
<logback.version>1.4.14</logback.version> <logback.version>1.4.14</logback.version>
<antlr.version>4.7.2</antlr.version> <antlr.version>4.7.2</antlr.version>

View File

@ -61,7 +61,7 @@ public class AIModelSetting implements Serializable {
this.apiKey = apiKey; this.apiKey = apiKey;
} }
@Editable(order=400) @Editable(order=400, name="Name")
@ChoiceProvider("getModels") @ChoiceProvider("getModels")
@NotEmpty @NotEmpty
public String getName() { public String getName() {

View File

@ -12,30 +12,24 @@ import io.onedev.server.model.support.AIModelSetting;
public class AISetting implements Serializable { public class AISetting implements Serializable {
private static final long serialVersionUID = 1L; 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= @Editable(order=100)
"""
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:
<ul>
<li>Google/gemini-2.5-flash</li>
<li>OpenAI/gpt-4.1-mini</li>
<li>Qwen/Qwen-2.5-72B-instruct</li>
</ul>
""")
@Nullable @Nullable
public AIModelSetting getNaturalLanguageQueryModelSetting() { public AIModelSetting getLiteModelSetting() {
return naturalLanguageQueryModelSetting; return liteModelSetting;
} }
public void setNaturalLanguageQueryModelSetting(AIModelSetting naturalLanguageQueryModelSetting) { public void setLiteModelSetting(AIModelSetting liteModelSetting) {
this.naturalLanguageQueryModelSetting = naturalLanguageQueryModelSetting; this.liteModelSetting = liteModelSetting;
} }
@Nullable @Nullable
public ChatModel getNaturalLanguageQueryModel() { public ChatModel getLiteModel() {
return naturalLanguageQueryModelSetting != null ? naturalLanguageQueryModelSetting.getChatModel() : null; return liteModelSetting != null ? liteModelSetting.getChatModel() : null;
} }
} }

View File

@ -2,10 +2,9 @@ package io.onedev.server.search.code.hit;
import java.io.Serializable; import java.io.Serializable;
import org.jspecify.annotations.Nullable;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.markup.html.image.Image; import org.apache.wicket.markup.html.image.Image;
import org.jspecify.annotations.Nullable;
import io.onedev.commons.utils.PlanarRange; import io.onedev.commons.utils.PlanarRange;

View File

@ -258,16 +258,16 @@ public class BuildQueryBehavior extends ANTLRAssistBehavior {
} }
} }
} }
if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null) if (getSettingService().getAISetting().getLiteModelSetting() == null)
hints.add(_T("Set up AI to use natural language query")); hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to use natural language query"));
return hints; return hints;
} }
@Override @Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() { protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel(); var liteModel = getSettingService().getAISetting().getLiteModel();
if (naturalLanguageQueryModel != null) { if (liteModel != null) {
return new NaturalLanguageTranslator(naturalLanguageQueryModel) { return new NaturalLanguageTranslator(liteModel) {
@Override @Override
public String getQueryDescription() { public String getQueryDescription() {

View File

@ -432,8 +432,8 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
} }
} }
} }
if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null) if (getSettingService().getAISetting().getLiteModelSetting() == null)
hints.add(_T("Set up AI to use natural language query")); hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to use natural language query</a>"));
return hints; return hints;
} }
@ -444,9 +444,9 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
@Override @Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() { protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel(); var liteModel = getSettingService().getAISetting().getLiteModel();
if (naturalLanguageQueryModel != null) { if (liteModel != null) {
return new NaturalLanguageTranslator(naturalLanguageQueryModel) { return new NaturalLanguageTranslator(liteModel) {
@Override @Override
public String getQueryDescription() { public String getQueryDescription() {

View File

@ -263,8 +263,8 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
} }
} }
} }
if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null) if (getSettingService().getAISetting().getLiteModelSetting() == null)
hints.add(_T("Set up AI to use natural language query")); hints.add(_T("<a href='/~administration/settings/lite-ai-model' target='_blank'>Set up AI</a> to use natural language query</a>"));
return hints; return hints;
} }
@ -276,9 +276,9 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
@Override @Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() { protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel(); var liteModel = getSettingService().getAISetting().getLiteModel();
if (naturalLanguageQueryModel != null) { if (liteModel != null) {
return new NaturalLanguageTranslator(naturalLanguageQueryModel) { return new NaturalLanguageTranslator(liteModel) {
@Override @Override
public String getQueryDescription() { public String getQueryDescription() {

View File

@ -127,7 +127,7 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
String toTranslate = params.getParameterValue("input").toString(); String toTranslate = params.getParameterValue("input").toString();
String translated; String translated;
try { try {
translated = Preconditions.checkNotNull(getNaturalLanguageTranslator()).translate(toTranslate); translated = getNaturalLanguageTranslator().translate(toTranslate);
} catch (Exception e) { } catch (Exception e) {
translated = toTranslate; translated = toTranslate;
var explicitException = ExceptionUtils.find(e, ExplicitException.class); var explicitException = ExceptionUtils.find(e, ExplicitException.class);

View File

@ -14,8 +14,6 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.AttributeAppender; 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.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.jspecify.annotations.Nullable;
import org.unbescape.html.HtmlEscape; import org.unbescape.html.HtmlEscape;
import com.fasterxml.jackson.core.JsonProcessingException; 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.commons.utils.StringUtils;
import io.onedev.server.OneDev; import io.onedev.server.OneDev;
import io.onedev.server.codequality.CodeProblem; import io.onedev.server.codequality.CodeProblem;
import io.onedev.server.service.CodeCommentService;
import io.onedev.server.git.BlameBlock; import io.onedev.server.git.BlameBlock;
import io.onedev.server.git.BlameCommit; import io.onedev.server.git.BlameCommit;
import io.onedev.server.git.BlobChange; 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.model.PullRequest;
import io.onedev.server.search.code.hit.QueryHit; import io.onedev.server.search.code.hit.QueryHit;
import io.onedev.server.security.SecurityUtils; import io.onedev.server.security.SecurityUtils;
import io.onedev.server.service.CodeCommentService;
import io.onedev.server.util.DateUtils; import io.onedev.server.util.DateUtils;
import io.onedev.server.util.Pair; import io.onedev.server.util.Pair;
import io.onedev.server.util.diff.DiffBlock; 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.blob.BlobAnnotationSupport;
import io.onedev.server.web.component.diff.revision.DiffViewMode; import io.onedev.server.web.component.diff.revision.DiffViewMode;
import io.onedev.server.web.component.svg.SpriteImage; 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.component.symboltooltip.SymbolTooltipPanel;
import io.onedev.server.web.page.base.BasePage; import io.onedev.server.web.page.base.BasePage;
import io.onedev.server.web.page.project.blob.ProjectBlobPage; import io.onedev.server.web.page.project.blob.ProjectBlobPage;
@ -352,7 +352,62 @@ public class BlobTextDiffPanel extends Panel {
protected Project getProject() { protected Project getProject() {
return change.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<String> lines;
String path;
if (isNew) {
lines = change.getNewText().getLines();
path = change.getNewBlobIdent().path;
} else {
lines = change.getOldText().getLines();
path = change.getOldBlobIdent().path;
}
List<String> linesBefore = new ArrayList<>();
List<String> 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<String> 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); add(symbolTooltip);

View File

@ -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<String> linesBeforeSymbolLine;
private final List<String> linesAfterSymbolLine;
private final List<String> linesAtStart;
public SymbolContext(String fileName, String symbolLine,
List<String> linesBeforeSymbolLine, List<String> linesAfterSymbolLine, List<String> 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<String> getLinesBeforeSymbolLine() {
return linesBeforeSymbolLine;
}
public List<String> getLinesAfterSymbolLine() {
return linesAfterSymbolLine;
}
public List<String> getLinesAtStart() {
return linesAtStart;
}
}

View File

@ -1,9 +1,13 @@
<wicket:panel> <wicket:panel>
<div wicket:id="content"> <div wicket:id="content">
<wicket:enclosure child="declarations"> <wicket:enclosure child="definitions">
<div class="mb-2"><b>Possible declarations</b></div> <div class="mb-2"><b><wicket:t>Possible definitions</wicket:t></b></div>
<ul class="declarations list-unstyled"> <div wicket:id="definitionInferHint" class="mb-2 hint">
<li wicket:id="declarations"> <wicket:svg href="bulb" class="icon icon-sm"/><wicket:t><a href="/~administration/settings/lite-ai-model" target="_blank">Set up AI</a> to mark the most probable</wicket:t>
</div>
<ul class="definitions list-unstyled">
<li wicket:id="definitions">
<span class="most-probable-indicator"><wicket:svg href="tick" class="icon icon-sm"/></span>
<img wicket:id="icon"> <img wicket:id="icon">
<a wicket:id="delegateLink"></a> <a wicket:id="delegateLink"></a>
<a wicket:id="link"> <a wicket:id="link">
@ -18,7 +22,7 @@
</ul> </ul>
</wicket:enclosure> </wicket:enclosure>
<div> <div>
<a wicket:id="findOccurrences" class="find-occurrences"><b>All occurrences</b></a> <a wicket:id="findOccurrences" class="find-occurrences"><b><wicket:t>All occurrences</wicket:t></b></a>
</div> </div>
</div> </div>
</wicket:panel> </wicket:panel>

View File

@ -1,26 +1,15 @@
package io.onedev.server.web.component.symboltooltip; package io.onedev.server.web.component.symboltooltip;
import io.onedev.commons.jsymbol.Symbol; import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
import io.onedev.server.OneDev;
import io.onedev.server.service.SettingService; import java.util.ArrayList;
import io.onedev.server.git.Blob; import java.util.List;
import io.onedev.server.git.BlobIdent;
import io.onedev.server.model.Project; import javax.inject.Inject;
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 org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink; import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.behavior.AttributeAppender; 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.apache.wicket.request.mapper.parameter.PageParameters;
import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import com.fasterxml.jackson.annotation.JsonIgnoreType;
import java.util.List; 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 { public abstract class SymbolTooltipPanel extends Panel {
private static final int QUERY_ENTRIES = 20; 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 revision = "";
private String symbolName = ""; private String symbolName = "";
private String symbolPosition = "";
private List<QueryHit> symbolHits = new ArrayList<>(); private List<QueryHit> symbolHits = new ArrayList<>();
@Inject
private CodeSearchService searchService;
@Inject
private SettingService settingService;
@Inject
private ObjectMapper objectMapper;
public SymbolTooltipPanel(String id) { public SymbolTooltipPanel(String id) {
super(id); super(id);
} }
@ -69,7 +100,7 @@ public abstract class SymbolTooltipPanel extends Panel {
content.setOutputMarkupId(true); content.setOutputMarkupId(true);
add(content); add(content);
content.add(new ListView<QueryHit>("declarations", new AbstractReadOnlyModel<>() { content.add(new ListView<QueryHit>("definitions", new AbstractReadOnlyModel<>() {
@Override @Override
public List<QueryHit> getObject() { public List<QueryHit> getObject() {
@ -80,7 +111,7 @@ public abstract class SymbolTooltipPanel extends Panel {
@Override @Override
protected void populateItem(ListItem<QueryHit> item) { protected void populateItem(ListItem<QueryHit> item) {
final QueryHit hit = item.getModelObject(); var hit = item.getModelObject();
item.add(hit.renderIcon("icon")); item.add(hit.renderIcon("icon"));
AjaxLink<Void> delegateLink = new ViewStateAwareAjaxLink<Void>("delegateLink") { AjaxLink<Void> delegateLink = new ViewStateAwareAjaxLink<Void>("delegateLink") {
@ -105,6 +136,7 @@ public abstract class SymbolTooltipPanel extends Panel {
link.add(new Label("scope", hit.getNamespace()).setVisible(hit.getNamespace()!=null)); link.add(new Label("scope", hit.getNamespace()).setVisible(hit.getNamespace()!=null));
item.add(link); item.add(link);
item.setOutputMarkupId(true);
} }
@Override @Override
@ -113,6 +145,15 @@ public abstract class SymbolTooltipPanel extends Panel {
setVisible(!symbolHits.isEmpty()); 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<Void>("findOccurrences") { content.add(new ViewStateAwareAjaxLink<Void>("findOccurrences") {
@ -144,14 +185,12 @@ public abstract class SymbolTooltipPanel extends Panel {
List<QueryHit> hits; List<QueryHit> hits;
// do this check to avoid TooGeneralQueryException // do this check to avoid TooGeneralQueryException
if (symbolName.length() >= IndexConstants.NGRAM_SIZE) { if (symbolName.length() >= IndexConstants.NGRAM_SIZE) {
int maxQueryEntries = OneDev.getInstance(SettingService.class) int maxQueryEntries = settingService.getPerformanceSetting().getMaxCodeSearchEntries();
.getPerformanceSetting().getMaxCodeSearchEntries();
var query = new TextQuery.Builder(symbolName) var query = new TextQuery.Builder(symbolName)
.wholeWord(true) .wholeWord(true)
.caseSensitive(true) .caseSensitive(true)
.count(maxQueryEntries) .count(maxQueryEntries)
.build(); .build();
CodeSearchService searchService = OneDev.getInstance(CodeSearchService.class);
ObjectId commit = getProject().getRevCommit(revision, true); ObjectId commit = getProject().getRevCommit(revision, true);
hits = searchService.search(getProject(), commit, query); hits = searchService.search(getProject(), commit, query);
} else { } 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() { add(new AbstractPostAjaxBehavior() {
@Override @Override
protected void respond(AjaxRequestTarget target) { protected void respond(AjaxRequestTarget target) {
IRequestParameters params = RequestCycle.get().getRequest().getPostParameters(); IRequestParameters params = RequestCycle.get().getRequest().getPostParameters();
revision = params.getParameterValue("revision").toString(); var action = params.getParameterValue("action").toString();
symbolName = params.getParameterValue("symbol").toString(); if (action.equals("query")) {
revision = params.getParameterValue("revision").toString();
symbolName = params.getParameterValue("symbolName").toString();
symbolPosition = params.getParameterValue("symbolPosition").toString();
if (symbolName.startsWith("#include")) { if (symbolName.startsWith("#include")) {
// handle c/c++ include directive as CodeMirror return the whole line as a meta // handle c/c++ include directive as CodeMirror return the whole line as a meta
symbolName = symbolName.substring("#include".length()).trim(); symbolName = symbolName.substring("#include".length()).trim();
} }
String charsToStrip = "@#'\"./\\"; String charsToStrip = "@#'\"./\\";
symbolName = StringUtils.stripEnd(StringUtils.stripStart(symbolName, charsToStrip), charsToStrip); symbolName = StringUtils.stripEnd(StringUtils.stripStart(symbolName, charsToStrip), charsToStrip);
symbolHits.clear(); 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);
if (symbolHits.size() < QUERY_ENTRIES) { // do this check to avoid TooGeneralQueryException
// first find in current file for matched symbols if (symbolName.length() != 0 && symbolName.indexOf('?') == -1 && symbolName.indexOf('*') == -1) {
List<Symbol> symbols = OneDev.getInstance(CodeSearchService.class).getSymbols(getProject(), BlobIdent blobIdent = new BlobIdent(revision, getBlobPath(), FileMode.TYPE_FILE);
blob.getBlobId(), getBlobPath()); Blob blob = getProject().getBlob(blobIdent, true);
if (symbols != null) {
for (Symbol symbol: symbols) { if (symbolHits.size() < QUERY_ENTRIES) {
if (symbolHits.size() < QUERY_ENTRIES // first find in current file for matched symbols
&& symbol.isSearchable() List<Symbol> symbols = searchService.getSymbols(getProject(),
&& symbolName.equals(symbol.getName()) blob.getBlobId(), getBlobPath());
&& symbol.isPrimary()) { if (symbols != null) {
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));
}
} }
} }
for (Symbol symbol: symbols) { }
if (symbolHits.size() < QUERY_ENTRIES
&& symbol.isSearchable() if (symbolHits.size() < QUERY_ENTRIES) {
&& symbolName.equals(symbol.getName()) // then find in other files for public symbols
&& !symbol.isPrimary()) { ObjectId commit = getProject().getRevCommit(revision, true);
symbolHits.add(new SymbolHit(getBlobPath(), symbol, null)); 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<QueryHit> definitionsView = (ListView<QueryHit>) content.get("definitions");
@SuppressWarnings("deprecation")
var script = String.format("onedev.server.symboltooltip.doneInfer('%s');",
definitionsView.get(lineNo).getMarkupId());
target.appendJavaScript(script);
} }
} } catch (Exception e) {
logger.error("Error inferring most likely symbol definition", e);
if (symbolHits.size() < QUERY_ENTRIES) { Session.get().error("Error inferring most likely symbol definition, check server log for details");
// 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));
}
} }
} }
target.add(content);
String script = String.format("onedev.server.symboltooltip.doneQuery('%s');", content.getMarkupId());
target.appendJavaScript(script);
} }
@Override @Override
@ -263,8 +366,10 @@ public abstract class SymbolTooltipPanel extends Panel {
response.render(JavaScriptHeaderItem.forReference(new SymbolTooltipResourceReference())); response.render(JavaScriptHeaderItem.forReference(new SymbolTooltipResourceReference()));
String script = String.format("onedev.server.symboltooltip.init('%s', %s);", var callback = getCallbackFunction(explicit("action"), explicit("revision"),
getMarkupId(), getCallbackFunction(explicit("revision"), explicit("symbol"))); explicit("symbolName"), explicit("symbolPosition"));
String script = String.format("onedev.server.symboltooltip.init('%s', %s, %s);",
getMarkupId(), callback, getSymbolPositionCalcFunction());
response.render(OnDomReadyHeaderItem.forScript(script)); 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 onSelect(AjaxRequestTarget target, QueryHit hit);
protected abstract void onOccurrencesQueried(AjaxRequestTarget target, List<QueryHit> hits); protected abstract void onOccurrencesQueried(AjaxRequestTarget target, List<QueryHit> hits);
protected abstract String getSymbolPositionCalcFunction();
protected abstract SymbolContext getSymbolContext(String symbolPosition, int beforeContextSize,
int afterContextSize, int atStartContextSize);
} }
@JsonIgnoreType
interface IgnorePlanarRangeMixin {
}
@JsonIgnoreType
interface IgnoreLinearRangeMixin {
}

View File

@ -9,6 +9,10 @@
box-shadow: 0px 0px 8px 0px rgba(0,0,0,0.1); box-shadow: 0px 0px 8px 0px rgba(0,0,0,0.1);
max-width: 720px; max-width: 720px;
} }
.symbol-tooltip .hint {
font-size: 11px;
opacity: 0.7;
}
.dark-mode .symbol-tooltip { .dark-mode .symbol-tooltip {
background-color: var(--dark-mode-light-info); background-color: var(--dark-mode-light-info);
color: var(--info); color: var(--info);
@ -21,15 +25,28 @@
color: #0073e9; 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; vertical-align: top;
padding: 2px 4px; padding: 2px 4px;
white-space: nowrap; white-space: nowrap;
} }
.symbol-tooltip ul.declarations .scope { .symbol-tooltip ul.definitions .scope {
color: var(--gray); color: var(--gray);
} }
.dark-mode .symbol-tooltip ul.declarations .scope { .dark-mode .symbol-tooltip ul.definitions .scope {
color: var(--dark-mode-gray); color: var(--dark-mode-gray);
} }
.symbol-tooltip a.find-occurrences[disabled] { .symbol-tooltip a.find-occurrences[disabled] {

View File

@ -1,5 +1,5 @@
onedev.server.symboltooltip = { onedev.server.symboltooltip = {
init: function(containerId, queryCallback) { init: function(containerId, callback, symbolPositionCalcFunction) {
var container = document.getElementById(containerId); var container = document.getElementById(containerId);
var showTimer; 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.data("alignment", {placement: {x: 0, y:0, offset:2, targetX: 0, targetY: 100}, target: {element: symbolEl}});
$tooltip.align($tooltip.data("alignment")); $tooltip.align($tooltip.data("alignment"));
callback("query", revision, $symbol.text(), symbolPositionCalcFunction($symbol[0]));
queryCallback(revision, $symbol.text());
showTimer = null; showTimer = null;
}, 500); }, 500);
}; };
}, },
doneQuery: function(contentId) { doneQuery: function(contentId, callback) {
var $content = $("#" + contentId); var $content = $("#" + contentId);
var $container = $content.parent(); var $container = $content.parent();
var $tooltip = $("#" + $container.attr("id") + "-symbol-tooltip"); var $tooltip = $("#" + $container.attr("id") + "-symbol-tooltip");
$tooltip.removeClass("d-none"); $tooltip.removeClass("d-none");
if ($tooltip.length != 0) var $definitions = $content.children(".definitions");
if (callback) {
var $indicator;
if (onedev.server.isDarkMode())
$indicator = $('<div class="definition-inferring-indicator mb-2 ajax-loading-indicator"><img src="/~img/dark-ajax-indicator.gif"/> <wicket:t>Inferring the most probable...</wicket:t></div>');
else
$indicator = $('<div class="definition-inferring-indicator mb-2 ajax-loading-indicator"><img src="/~img/ajax-indicator.gif"/> <wicket:t>Inferring the most probable...</wicket:t></div>');
$indicator.insertBefore($definitions);
callback("infer");
} else {
$definitions.addClass("no-definition-infer");
}
if ($tooltip.length != 0) {
$tooltip.html($content.children()).align($tooltip.data("alignment")); $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 // this is public API which can be called from other components using this component

View File

@ -8,7 +8,7 @@ import org.apache.wicket.request.mapper.CompoundRequestMapper;
import io.onedev.commons.utils.ExplicitException; import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.web.asset.icon.IconScope; 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.alertsettings.AlertSettingPage;
import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage; import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage;
import io.onedev.server.web.page.admin.brandingsetting.BrandingSettingPage; 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/labels", LabelManagementPage.class));
add(new BasePageMapper("~administration/settings/alert", AlertSettingPage.class)); add(new BasePageMapper("~administration/settings/alert", AlertSettingPage.class));
add(new BasePageMapper("~administration/settings/performance", PerformanceSettingPage.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/backup", DatabaseBackupPage.class));
add(new BasePageMapper("~administration/settings/authenticator", AuthenticatorPage.class)); add(new BasePageMapper("~administration/settings/authenticator", AuthenticatorPage.class));
add(new BasePageMapper("~administration/settings/sso-providers", SsoProviderListPage.class)); add(new BasePageMapper("~administration/settings/sso-providers", SsoProviderListPage.class));

View File

@ -1,10 +0,0 @@
<wicket:extend>
<div class="card">
<div class="card-body">
<form wicket:id="form" class="leave-confirm">
<div wicket:id="editor" class="mb-4"></div>
<input type="submit" class="btn btn-primary dirty-aware" t:value="Save Settings">
</form>
</div>
</div>
</wicket:extend>

View File

@ -0,0 +1,21 @@
<wicket:extend>
<div class="card">
<div class="card-body">
<div class="alert alert-notice alert-light">
This model will be used to perform lite tasks including:
<ul class="mb-0">
<li>Query issues, builds, and pull requests with natural language</li>
<li>Mark the most probable symbol definition in symbol navigation</li>
</ul>
It should be fast and cost-effective. Some suggested models:
<code>Google/gemini-2.5-flash</code>,
<code>OpenAI/gpt-4.1-mini</code>,
<code>Qwen/Qwen-2.5-72B-instruct</code>
</div>
<form wicket:id="form" class="leave-confirm">
<div wicket:id="editor" class="mb-4"></div>
<input type="submit" class="btn btn-primary dirty-aware" t:value="Save Settings">
</form>
</div>
</div>
</wicket:extend>

View File

@ -12,15 +12,15 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
import io.onedev.server.data.migration.VersionedXmlDoc; import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.model.support.administration.AISetting; import io.onedev.server.model.support.administration.AISetting;
import io.onedev.server.service.SettingService; 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; import io.onedev.server.web.page.admin.AdministrationPage;
public class AISettingPage extends AdministrationPage { public class LiteModelPage extends AdministrationPage {
@Inject @Inject
private SettingService settingService; private SettingService settingService;
public AISettingPage(PageParameters params) { public LiteModelPage(PageParameters params) {
super(params); super(params);
} }
@ -39,20 +39,20 @@ public class AISettingPage extends AdministrationPage {
var newAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML(); var newAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML();
settingService.saveAISetting(aiSetting); settingService.saveAISetting(aiSetting);
auditService.audit(null, "changed AI settings", oldAuditContent, newAuditContent); 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); add(form);
} }
@Override @Override
protected Component newTopbarTitle(String componentId) { protected Component newTopbarTitle(String componentId) {
return new Label(componentId, _T("AI Settings")); return new Label(componentId, _T("Lite AI Model"));
} }
} }

View File

@ -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.component.user.UserAvatar;
import io.onedev.server.web.editable.EditableUtils; import io.onedev.server.web.editable.EditableUtils;
import io.onedev.server.web.page.HomePage; 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.alertsettings.AlertSettingPage;
import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage; import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage;
import io.onedev.server.web.page.admin.brandingsetting.BrandingSettingPage; 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"), administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("Groovy Scripts"),
GroovyScriptListPage.class, new PageParameters())); GroovyScriptListPage.class, new PageParameters()));
administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("AI Settings"), List<SidebarMenuItem> aiMenuItems = new ArrayList<>();
AISettingPage.class, new PageParameters()));
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"), administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("Branding"),
BrandingSettingPage.class, new PageParameters())); BrandingSettingPage.class, new PageParameters()));

View File

@ -13,7 +13,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.jspecify.annotations.Nullable;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import org.apache.wicket.Component; 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.apache.wicket.request.mapper.parameter.PageParameters;
import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.unbescape.html.HtmlEscape; 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.CodeProblemContribution;
import io.onedev.server.codequality.CoverageStatus; import io.onedev.server.codequality.CoverageStatus;
import io.onedev.server.codequality.LineCoverageContribution; 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.BlameBlock;
import io.onedev.server.git.Blob; import io.onedev.server.git.Blob;
import io.onedev.server.git.BlobIdent; 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.CodeSearchService;
import io.onedev.server.search.code.hit.QueryHit; import io.onedev.server.search.code.hit.QueryHit;
import io.onedev.server.security.SecurityUtils; 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.DateUtils;
import io.onedev.server.util.Similarities; import io.onedev.server.util.Similarities;
import io.onedev.server.util.diff.DiffUtils; 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.SuggestionApplyBean;
import io.onedev.server.web.component.suggestionapply.SuggestionApplyModalPanel; import io.onedev.server.web.component.suggestionapply.SuggestionApplyModalPanel;
import io.onedev.server.web.component.svg.SpriteImage; 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.component.symboltooltip.SymbolTooltipPanel;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext; import io.onedev.server.web.page.project.blob.render.BlobRenderContext;
import io.onedev.server.web.page.project.blob.render.BlobRenderContext.Mode; 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 { 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"; private static final String COOKIE_OUTLINE = "sourceView.outline";
@ -835,6 +836,51 @@ public class SourceViewPanel extends BlobViewPanel implements Positionable, Sear
protected Project getProject() { protected Project getProject() {
return context.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<String> linesBefore = new ArrayList<>();
List<String> 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<String> 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
);
}
}); });
} }