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

@ -13,29 +13,23 @@ public class AISetting implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private AIModelSetting naturalLanguageQueryModelSetting; public static final String PROP_LITE_MODEL_SETTING = "liteModelSetting";
@Editable(order=100, name="Natural Language Query Model", description= private AIModelSetting liteModelSetting;
"""
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)
<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;
@ -353,6 +353,61 @@ public class BlobTextDiffPanel extends Panel {
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 {
@ -179,82 +218,146 @@ 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 // do this check to avoid TooGeneralQueryException
if (symbolName.length() != 0 && symbolName.indexOf('?') == -1 && symbolName.indexOf('*') == -1) { if (symbolName.length() != 0 && symbolName.indexOf('?') == -1 && symbolName.indexOf('*') == -1) {
BlobIdent blobIdent = new BlobIdent(revision, getBlobPath(), FileMode.TYPE_FILE); BlobIdent blobIdent = new BlobIdent(revision, getBlobPath(), FileMode.TYPE_FILE);
Blob blob = getProject().getBlob(blobIdent, true); Blob blob = getProject().getBlob(blobIdent, true);
if (symbolHits.size() < QUERY_ENTRIES) { if (symbolHits.size() < QUERY_ENTRIES) {
// first find in current file for matched symbols // first find in current file for matched symbols
List<Symbol> symbols = OneDev.getInstance(CodeSearchService.class).getSymbols(getProject(), List<Symbol> symbols = searchService.getSymbols(getProject(),
blob.getBlobId(), getBlobPath()); blob.getBlobId(), getBlobPath());
if (symbols != null) { if (symbols != null) {
for (Symbol symbol: symbols) { for (Symbol symbol: symbols) {
if (symbolHits.size() < QUERY_ENTRIES if (symbolHits.size() < QUERY_ENTRIES
&& symbol.isSearchable() && symbol.isSearchable()
&& symbolName.equals(symbol.getName()) && symbolName.equals(symbol.getName())
&& symbol.isPrimary()) { && symbol.isPrimary()) {
symbolHits.add(new SymbolHit(getBlobPath(), symbol, 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() 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));
} }
} }
} }
if (symbolHits.size() < QUERY_ENTRIES) { target.add(content);
// then find in other files for public symbols
CodeSearchService searchService = OneDev.getInstance(CodeSearchService.class); CharSequence callback;
ObjectId commit = getProject().getRevCommit(revision, true); if (settingService.getAISetting().getLiteModelSetting() != null && symbolHits.size() > 1)
BlobQuery query; callback = getCallbackFunction(explicit("action"));
if (symbolHits.size() < QUERY_ENTRIES) { else
query = new SymbolQuery.Builder(symbolName) callback = "undefined";
.caseSensitive(true) String script = String.format("onedev.server.symboltooltip.doneQuery('%s', %s);",
.excludeBlobPath(blobIdent.path) content.getMarkupId(), callback);
.primary(true) target.appendJavaScript(script);
.local(false) } else {
.count(QUERY_ENTRIES) var liteModel = settingService.getAISetting().getLiteModel();
.build(); try {
symbolHits.addAll(searchService.search(getProject(), commit, query)); ObjectMapper mapperCopy = objectMapper.copy();
} mapperCopy.addMixIn(PlanarRange.class, IgnorePlanarRangeMixin.class);
if (symbolHits.size() < QUERY_ENTRIES) { mapperCopy.addMixIn(LinearRange.class, IgnoreLinearRangeMixin.class);
query = new SymbolQuery.Builder(symbolName) var jsonOfSymbolHits = mapperCopy.writeValueAsString(symbolHits);
.caseSensitive(true) var symbolContext = getSymbolContext(symbolPosition, BEFORE_CONTEXT_SIZE,
.excludeBlobPath(blobIdent.path) AFTER_CONTEXT_SIZE, AT_START_CONTEXT_SIZE);
.primary(false) var jsonOfSymbolContext = mapperCopy.writeValueAsString(symbolContext);
.local(false) var systemMessage = new SystemMessage("""
.count(QUERY_ENTRIES - symbolHits.size()) You are familiar with various programming languages. Given a symbol name, a json object of
.build(); symbol context, and a json array of possible symbol definitions, please determine the most
symbolHits.addAll(searchService.search(getProject(), commit, query)); 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);
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 @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));
} }
@ -303,4 +408,17 @@ public abstract class SymbolTooltipPanel extends Panel {
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;
@ -70,20 +70,40 @@ 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;
@ -836,6 +837,51 @@ public class SourceViewPanel extends BlobViewPanel implements Positionable, Sear
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
);
}
}); });
} }