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