diff --git a/nop-idea-plugin/build.gradle.kts b/nop-idea-plugin/build.gradle.kts index 27bb061e996440ac02b8a90368f8b8c52a8abf40..54e9d70ee183f4b9b584a95c64b05a4ea75f803b 100644 --- a/nop-idea-plugin/build.gradle.kts +++ b/nop-idea-plugin/build.gradle.kts @@ -22,7 +22,7 @@ intellij { //version.set("2022.3") type.set("IC") // Target IDE Platform - plugins.set(listOf("java", "org.jetbrains.plugins.yaml")) + plugins.set(listOf("java", "gradle", "org.jetbrains.plugins.yaml")) } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointHandler.java index 30ce82b315851978e7ecf7f40ab08deab5b47837..d4dff30663abda962e46ddff55736ac5e9a13b1f 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointHandler.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointHandler.java @@ -36,18 +36,18 @@ public class XLangBreakpointHandler extends XBreakpointHandler> breakpoints = new HashMap(); - private XLangDebugProcess debugProcess; + private final Map> breakpoints = new HashMap<>(); + private final XLangDebugProcess debugProcess; private boolean muted; - private Map bpMap = new HashMap<>(); + private final Map bpMap = new HashMap<>(); public XLangBreakpointHandler(final XLangDebugProcess debugProcess) { super(XLangBreakpointType.class); this.debugProcess = debugProcess; } - public void breakpointMuted(boolean muted) { + public void breakpointsMuted(boolean muted) { this.muted = muted; IDebuggerAsync debugger = debugProcess.getDebugger(); if (debugger != null) { @@ -55,6 +55,7 @@ public class XLangBreakpointHandler extends XBreakpointHandler breakpoint) { Breakpoint bp = makeBreakpoint(breakpoint); if (bp == null) @@ -62,7 +63,6 @@ public class XLangBreakpointHandler extends XBreakpointHandler breakpoint, final boolean temporary) { + Breakpoint bp = makeBreakpoint(breakpoint); + if (bp == null) + return; + + String key = getKey(bp); + breakpoints.remove(key); + bpMap.remove(key); + + sendBreakpoints(); + } + private String getKey(Breakpoint bp) { return bp.getSourcePath() + ':' + bp.getLine(); } @@ -102,34 +115,19 @@ public class XLangBreakpointHandler extends XBreakpointHandler breakpoint, final boolean temporary) { - Breakpoint bp = makeBreakpoint(breakpoint); - if (bp == null) - return; - - String key = getKey(bp); - breakpoints.remove(key); - - bpMap.remove(key); - sendBreakpoints(); - - } - public void sendBreakpoints() { IDebuggerAsync debugger = debugProcess.getDebugger(); if (debugger != null) { List bps = new ArrayList<>(bpMap.values()); debugger.updateBreakpointsAsync(bps, muted) .exceptionally(e -> { - debugProcess.getSession().reportMessage("update breakpoints fail", MessageType.ERROR); + debugProcess.getSession().reportMessage("Update breakpoints fail", MessageType.ERROR); return null; }); } } public XBreakpoint findBreakPoint(@NotNull StackTraceElement elm) { - if (elm == null) - return null; String path = elm.getSourcePath(); int lineNumber = elm.getLine(); return breakpoints.get(path + ':' + lineNumber); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointType.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointType.java index 5010d280f96d04975c360848cf2d59a7c007f8e4..e9617d28fe9836c38d963c7578ba9dc4dacb9e17 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointType.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangBreakpointType.java @@ -27,7 +27,7 @@ import org.jetbrains.annotations.Nullable; public class XLangBreakpointType extends XLineBreakpointType { public XLangBreakpointType() { - super("xlang-line", NopPluginBundle.message("line.breakpoints.tab.title")); + super("xlang-line", NopPluginBundle.message("xlang.debugger.line.breakpoints.tab.title")); } @Override diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugConnector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugConnector.java index 7acb1a63e4812e6d0ebf706c257cefc8f511860a..1dbb02e601bf0fc072ba6b90fc332763e9d4b46a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugConnector.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugConnector.java @@ -21,16 +21,18 @@ import java.util.function.Consumer; * 通过socket连接到远程XLang服务器 */ public class XLangDebugConnector implements IDestroyable { - private SimpleRpcClientFactory clientFactory = new SimpleRpcClientFactory<>(); + private final SimpleRpcClientFactory clientFactory = new SimpleRpcClientFactory<>(); public XLangDebugConnector(int debugPort, Consumer> action, Runnable onChannelOpen) { ClientConfig config = new ClientConfig(); config.setReadTimeout(0); config.setPort(debugPort); - RetryPolicy policy = new RetryPolicy(); + + RetryPolicy policy = new RetryPolicy<>(); policy.setRetryDelay(200); policy.setExponentialDelay(true); policy.setMaxRetryDelay(1000); + config.setReconnectPolicy(policy); clientFactory.setClientConfig(config); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugProcess.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugProcess.java index e80b1575717bb269f9c38299c4ca487ad60df9dd..5cd70f61316f47be90ceefd4e9f052450e411de8 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugProcess.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebugProcess.java @@ -7,11 +7,12 @@ */ package io.nop.idea.plugin.debugger; +import javax.swing.event.HyperlinkListener; + +import com.intellij.debugger.DebugEnvironment; import com.intellij.debugger.engine.JavaDebugProcess; import com.intellij.debugger.impl.DebuggerSession; import com.intellij.debugger.impl.PrioritizedTask; -import com.intellij.execution.configurations.JavaCommandLineState; -import com.intellij.execution.configurations.RunProfileState; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.progress.ProgressIndicator; @@ -34,15 +35,15 @@ import io.nop.api.debugger.Breakpoint; import io.nop.api.debugger.BreakpointHitMessage; import io.nop.api.debugger.IDebuggerAsync; import io.nop.api.debugger.LineLocation; +import io.nop.commons.lang.impl.Cancellable; import io.nop.core.reflect.bean.BeanTool; +import io.nop.idea.plugin.messages.NopPluginBundle; import io.nop.idea.plugin.utils.ProjectFileHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.event.HyperlinkListener; - import static com.intellij.xdebugger.impl.ui.DebuggerUIUtil.invokeLater; /** @@ -51,7 +52,6 @@ import static com.intellij.xdebugger.impl.ui.DebuggerUIUtil.invokeLater; public class XLangDebugProcess extends JavaDebugProcess { static final Logger LOG = LoggerFactory.getLogger(XLangDebugProcess.class); private final XLangBreakpointHandler myBreakPointHandler; - private final JavaCommandLineState state; private final XLangDebuggerEditorsProvider myEditorsProvider; boolean isDisconnected = false; @@ -60,24 +60,23 @@ public class XLangDebugProcess extends JavaDebugProcess { //private final AtomicBoolean breakpointsInitiated = new AtomicBoolean(); private final XLangDebugConnector connector; + private final Cancellable cleanup = new Cancellable(); - public XLangDebugProcess(@NotNull final XDebugSession session, - @NotNull final JavaCommandLineState state, - final DebuggerSession javaSession, - int debugPort + public XLangDebugProcess( + @NotNull final XDebugSession session, final DebuggerSession javaSession, int debugPort ) { super(session, javaSession); - this.state = state; this.myEditorsProvider = new XLangDebuggerEditorsProvider(); this.myBreakPointHandler = new XLangBreakpointHandler(this); + this.connector = new XLangDebugConnector(debugPort, this::debuggerNotification, this::onSocketConnected); javaSession.getProcess().setXDebugProcess(this); session.addSessionListener(new XDebugSessionListener() { @Override public void breakpointsMuted(boolean muted) { - myBreakPointHandler.breakpointMuted(muted); + myBreakPointHandler.breakpointsMuted(muted); } public void sessionResumed() { @@ -106,16 +105,17 @@ public class XLangDebugProcess extends JavaDebugProcess { public void sessionInitialized() { super.sessionInitialized(); - ProgressManager.getInstance().run(new Task.Backgroundable(null, "XLang debugger", true) { + ProgressManager.getInstance().run(new Task.Backgroundable(null, "XLang debugger connector", true) { + @Override public void run(@NotNull final ProgressIndicator indicator) { - indicator.setText("XLang Debugger Connecting..."); + indicator.setText("XLang debugger connecting..."); indicator.setIndeterminate(true); try { - if (!connect()) - return; - startDebugSession(); - } catch (final Exception e) { + if (connect()) { + startDebugSession(); + } + } catch (Exception e) { onConnectFail(e.getMessage()); } } @@ -126,16 +126,19 @@ public class XLangDebugProcess extends JavaDebugProcess { while (true) { try { debugger = connector.connect(); - System.out.println("connected"); + cleanup.appendOnCancelTask(connector::destroy); + + LOG.info("xlang.debugger.connector-connected"); return true; - } catch (final Exception e) { + } catch (Exception e) { try { Thread.sleep(200); - } catch (Exception e2) { - + } catch (Exception ignored) { } - if (getProcessHandler().isProcessTerminated()) + + if (getProcessHandler().isProcessTerminated()) { return false; + } } } } @@ -147,16 +150,16 @@ public class XLangDebugProcess extends JavaDebugProcess { private void onConnectFail(final String msg) { getProcessHandler().destroyProcess(); invokeLater(() -> { - String text = "XLangDebugger can't connect to DebuggerServer on port " + connector.getDebugPort();//myDebuggerProxy.getPort(); - Messages.showErrorDialog(msg != null ? text + ":\r\n" + msg : text, "XLang debugger"); + String text = NopPluginBundle.message("xlang.debugger.connect-fail", + String.valueOf(connector.getDebugPort())); + + Messages.showErrorDialog(msg != null ? text + ":\r\n" + msg : text, "XLang Debugger"); }); } private void startDebugSession() { //initBreakpointHandlersAndSetBreakpoints(); - ReadAction.run(() -> { - myBreakPointHandler.sendBreakpoints(); - }); + ReadAction.run(myBreakPointHandler::sendBreakpoints); } @@ -177,8 +180,9 @@ public class XLangDebugProcess extends JavaDebugProcess { @Override public void resume(@Nullable XSuspendContext context) { if (context instanceof XLangSuspendContext) { - if (debugger != null) + if (debugger != null) { debugger.resumeAsync(); + } } else { super.resume(context); } @@ -243,22 +247,22 @@ public class XLangDebugProcess extends JavaDebugProcess { } public void stop() { - connector.destroy(); + cleanup.cancel(); super.stop(); isDisconnected = true; - System.out.println("end debug process"); + LOG.info("xlang.debugger.process-stopped"); } @Override - public XBreakpointHandler[] getBreakpointHandlers() { + public XBreakpointHandler @NotNull [] getBreakpointHandlers() { XBreakpointHandler[] handlers = super.getBreakpointHandlers(); XBreakpointHandler[] ret = new XBreakpointHandler[handlers.length + 1]; - for (int i = 0; i < handlers.length; i++) { - ret[i] = handlers[i]; - } + + System.arraycopy(handlers, 0, ret, 0, handlers.length); ret[ret.length - 1] = myBreakPointHandler; + return ret; } @@ -292,8 +296,8 @@ public class XLangDebugProcess extends JavaDebugProcess { this.getDebuggerSession().getProcess().getManagerThread().schedule(PrioritizedTask.Priority.LOW, () -> { BreakpointHitMessage hit = BeanTool.buildBean(response.getData(), BreakpointHitMessage.class); - XBreakpoint breakpoint = myBreakPointHandler.findBreakPoint( - hit.getStackInfo().getTopElement()); + XBreakpoint breakpoint = myBreakPointHandler.findBreakPoint(hit.getStackInfo() + .getTopElement()); XDebugSession session = getSession(); XSuspendContext context = session.getSuspendContext(); @@ -342,9 +346,10 @@ public class XLangDebugProcess extends JavaDebugProcess { } @Override - public void registerAdditionalActions(@NotNull DefaultActionGroup leftToolbar, - @NotNull DefaultActionGroup topToolbar, - @NotNull DefaultActionGroup settings) { + public void registerAdditionalActions( + @NotNull DefaultActionGroup leftToolbar, @NotNull DefaultActionGroup topToolbar, + @NotNull DefaultActionGroup settings + ) { super.registerAdditionalActions(leftToolbar, topToolbar, settings); //topToolbar.remove(ActionManager.getInstance().getAction(XDebuggerActions.RUN_TO_CURSOR)); } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebuggerRunner.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebuggerRunner.java index 3162fd5def25b51257f8d1c1c15ed4f82015d219..00c080ecf63075325dfc8300aaebb55e3a7e6497 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebuggerRunner.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangDebuggerRunner.java @@ -7,22 +7,26 @@ */ package io.nop.idea.plugin.debugger; +import java.awt.*; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; + import com.intellij.debugger.DebugEnvironment; import com.intellij.debugger.DebuggerManagerEx; -import com.intellij.debugger.DefaultDebugEnvironment; import com.intellij.debugger.impl.DebuggerSession; import com.intellij.debugger.impl.GenericDebuggerRunner; import com.intellij.execution.ExecutionException; import com.intellij.execution.JavaRunConfigurationBase; -import com.intellij.execution.configurations.JavaCommandLineState; -import com.intellij.execution.configurations.JavaParameters; -import com.intellij.execution.configurations.RemoteConnection; +import com.intellij.execution.RunConfigurationExtension; import com.intellij.execution.configurations.RunProfile; import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.runners.ExecutionEnvironment; -import com.intellij.execution.runners.JavaProgramPatcher; import com.intellij.execution.ui.RunContentDescriptor; -import com.intellij.util.net.NetUtils; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Computable; +import com.intellij.psi.search.GlobalSearchScope; import com.intellij.xdebugger.XDebugProcess; import com.intellij.xdebugger.XDebugProcessStarter; import com.intellij.xdebugger.XDebugSession; @@ -30,30 +34,18 @@ import com.intellij.xdebugger.XDebuggerManager; import io.nop.api.core.exceptions.NopException; import io.nop.api.core.util.FutureHelper; import io.nop.idea.plugin.execution.XLangDebugExecutor; +import io.nop.idea.plugin.messages.NopPluginBundle; +import io.nop.idea.plugin.utils.PsiClassHelper; +import io.nop.xlang.debugger.XLangDebugger; import org.jetbrains.annotations.NotNull; - -import java.awt.*; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; +import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration; /** * 选择XLang Runner来执行java代码时自动启动XLang调试器 */ public class XLangDebuggerRunner extends GenericDebuggerRunner { - private static final String XDEBUG = "-Xdebug"; - private static final String JDWP = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address="; - private static final String XLANG_DEBUG_ENABLED = "-Dnop.xlang.debugger.enabled=true"; - private static final String XLANG_DEBUG_PORT = "-Dnop.xlang.debugger.port="; - - private static final String LOCALHOST = "127.0.0.1"; - - private static final String XLANG_DEBUG_RUNNER_ID = "XLangDebugRunner"; - public XLangDebuggerRunner() { - super(); - } - @Override public @NotNull String getRunnerId() { return XLANG_DEBUG_RUNNER_ID; @@ -62,64 +54,69 @@ public class XLangDebuggerRunner extends GenericDebuggerRunner { @Override public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) { return executorId.equals(XLangDebugExecutor.EXECUTOR_ID) // 与 XLangDebugExecutor 绑定 - // 同时支持 Application 和 JUnit 类型的 Run Configuration, - // 即,可对 main() 函数和单元测试进行 XLang 调试 - && profile instanceof JavaRunConfigurationBase; + && ( // 同时支持 Application 和 JUnit 类型的 Run Configuration, + // 即,可对 main() 函数和单元测试进行 XLang 调试 + profile instanceof JavaRunConfigurationBase + // 支持 Gradle 类型的 Run Configuration + || profile instanceof GradleRunConfiguration); } @Override - protected RunContentDescriptor createContentDescriptor(RunProfileState state, ExecutionEnvironment environment) throws ExecutionException { - // FileDocumentManager.getInstance().saveAllDocuments(); - // String debuggerPort = DebuggerUtils.getInstance().findAvailableDebugAddress(true); - int[] ports; - try { - ports = NetUtils.findAvailableSocketPorts(2); - } catch (Exception e) { - throw NopException.adapt(e); + protected RunContentDescriptor createContentDescriptor( + @NotNull RunProfileState state, @NotNull ExecutionEnvironment environment + ) throws ExecutionException { + XLangRunConfigurationExtension extension = // + RunConfigurationExtension.EP_NAME.findExtensionOrFail(XLangRunConfigurationExtension.class); + + int xlangDebugPort = extension.getXLangDebugPort(); + DebugEnvironment debugEnvironment = extension.createDebugEnvironment(state, environment); + + if (notIncludeXLangDebugger(environment.getProject(), debugEnvironment)) { + // Note: 确保 Messages#showErrorDialog 在事件分发线程 (EDT) 中调用, + // 避免出现异常 "Access is allowed from Event Dispatch Thread (EDT) only" + ApplicationManager.getApplication().invokeLater(() -> { + Messages.showErrorDialog(NopPluginBundle.message("xlang.debugger.not-exist"), "XLang Debugger"); + }); + return null; } - JavaCommandLineState javaCommandLineState = (JavaCommandLineState) state; - - JavaParameters javaParameters = javaCommandLineState.getJavaParameters(); - // Making the assumption that it's JVM 7 onwards - javaParameters.getVMParametersList().addParametersString(XDEBUG); - // Debugger port - - String remotePort = JDWP + ports[0]; - javaParameters.getVMParametersList().addParametersString(remotePort); - - // 传入调试开关和调试端口 - javaParameters.getVMParametersList().addParametersString(XLANG_DEBUG_ENABLED); - javaParameters.getVMParametersList().addParametersString(XLANG_DEBUG_PORT + ports[1]); - - JavaProgramPatcher.runCustomPatchers(javaParameters, environment.getExecutor(), environment.getRunProfile()); - - //final ProcessHandler handler = state.execute(environment.getExecutor(),this).getProcessHandler(); - RemoteConnection connection = new RemoteConnection(true, LOCALHOST, String.valueOf(ports[0]), false); - DebugEnvironment debugEnvironment = new DefaultDebugEnvironment(environment, state, connection, 3000L); - - - return dispatch(() -> { - final DebuggerSession debuggerSession = DebuggerManagerEx.getInstanceEx(environment.getProject()).attachVirtualMachine(debugEnvironment); - + return dispatch(()-> { // 实际上同时启动了java调试器和XLang调试器 - final XDebugSession session = XDebuggerManager.getInstance(environment.getProject()).startSession(environment, new XDebugProcessStarter() { + XDebugSession session = XDebuggerManager.getInstance(environment.getProject()).startSession(environment, new XDebugProcessStarter() { @NotNull @Override - public XDebugProcess start(@NotNull XDebugSession xDebugSession) throws ExecutionException { - debuggerSession.getContextManager().addListener(new XLangDebugContextListener(xDebugSession, debuggerSession)); - XLangDebugProcess process = new XLangDebugProcess(xDebugSession, javaCommandLineState, debuggerSession, - ports[1]); - return process; + public XDebugProcess start(@NotNull XDebugSession debugSession) throws ExecutionException { + DebuggerSession debuggerSession = // + DebuggerManagerEx.getInstanceEx(environment.getProject()) // + .attachVirtualMachine(debugEnvironment); + assert debuggerSession != null; + + debuggerSession.getContextManager() // + .addListener(new XLangDebugContextListener(debugSession, debuggerSession)); + + return new XLangDebugProcess(debugSession, debuggerSession, xlangDebugPort); } }); return session.getRunContentDescriptor(); }); + } + /** 当前调试环境中是否未引入 {@link XLangDebugger} */ + private boolean notIncludeXLangDebugger(@NotNull Project project, @NotNull DebugEnvironment environment) { + return ApplicationManager.getApplication().runReadAction((Computable) () -> { + // Note: 避免出现异常 "Read access is allowed from inside read-action only" + GlobalSearchScope scope = environment.getSearchScope(); + try { + return PsiClassHelper.findClass(project, XLangDebugger.class.getName(), scope) == null; + } catch (Exception ignore) { + // Note: 可能存在索引还未创建完毕的异常,这里直接忽略即可 + return false; + } + }); } - T dispatch(Callable task) { + private T dispatch(Callable task) { if (EventQueue.isDispatchThread()) { try { return task.call(); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangRunConfigurationExtension.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangRunConfigurationExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..1d2e6e6a8eeeb69f729d93db5dacdf877898105d --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/debugger/XLangRunConfigurationExtension.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.debugger; + +import com.intellij.debugger.DebugEnvironment; +import com.intellij.debugger.DefaultDebugEnvironment; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.Executor; +import com.intellij.execution.JavaRunConfigurationBase; +import com.intellij.execution.RunConfigurationExtension; +import com.intellij.execution.configurations.JavaParameters; +import com.intellij.execution.configurations.ParametersList; +import com.intellij.execution.configurations.RemoteConnection; +import com.intellij.execution.configurations.RunConfigurationBase; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.configurations.RunnerSettings; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.JavaProgramPatcher; +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunnableState; +import com.intellij.util.net.NetUtils; +import io.nop.api.core.exceptions.NopException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration; + +import static io.nop.xlang.XLangConfigs.CFG_XLANG_DEBUGGER_ENABLED; +import static io.nop.xlang.XLangConfigs.CFG_XLANG_DEBUGGER_PORT; + +/** + * 通过单独的 {@link RunConfigurationExtension} 向 + * {@link ExternalSystemRunnableState#getJvmParametersSetup()} + * 注入 jvm 参数,以支持对 Gradle 项目的调试 + * + * @author flytreeleft + * @date 2025-07-27 + */ +public class XLangRunConfigurationExtension extends RunConfigurationExtension { + private static final String LOCALHOST = "127.0.0.1"; + + private static final String XDEBUG = "-Xdebug"; + private static final String JDWP = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address="; + + private int[] ports; + + public DebugEnvironment createDebugEnvironment( + @NotNull RunProfileState state, @NotNull ExecutionEnvironment environment + ) { + int javaDebugPort = getJavaDebugPort(); + RemoteConnection connection = new RemoteConnection(true, LOCALHOST, String.valueOf(javaDebugPort), false); + + // Note: gradle 的启动时间较长,因此,需设置更长的等待超时时间 + return new DefaultDebugEnvironment(environment, state, connection, DebugEnvironment.LOCAL_START_TIMEOUT); + } + + @Override + public boolean isApplicableFor(@NotNull RunConfigurationBase configuration) { + return configuration instanceof JavaRunConfigurationBase // + || configuration instanceof GradleRunConfiguration; + } + + @Override + public > void updateJavaParameters( + @NotNull T configuration, @NotNull JavaParameters params, RunnerSettings runnerSettings, + @NotNull Executor executor + ) throws ExecutionException { + updateJavaParameters(configuration, params, runnerSettings); + + JavaProgramPatcher.runCustomPatchers(params, executor, configuration); + } + + @Override + public > void updateJavaParameters( + @NotNull T configuration, @NotNull JavaParameters params, @Nullable RunnerSettings runnerSettings + ) throws ExecutionException { + int javaDebugPort = getJavaDebugPort(); + int xlangDebugPort = getXLangDebugPort(); + ParametersList paramsList = params.getVMParametersList(); + + // Making the assumption that it's JVM 7 onwards + paramsList.addParametersString(XDEBUG); + + // Debugger port + String remotePort = JDWP + LOCALHOST + ':' + javaDebugPort; + paramsList.addParametersString(remotePort); + + // 传入调试开关和调试端口 + paramsList.addParametersString("-D" + CFG_XLANG_DEBUGGER_ENABLED.getName() + "=true"); + paramsList.addParametersString("-D" + CFG_XLANG_DEBUGGER_PORT.getName() + '=' + xlangDebugPort); + } + + public int getJavaDebugPort() { + return getPorts()[0]; + } + + public int getXLangDebugPort() { + return getPorts()[1]; + } + + private int[] getPorts() { + if (ports == null) { + try { + ports = NetUtils.findAvailableSocketPorts(2); + } catch (Exception e) { + throw NopException.adapt(e); + } + } + return ports; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/execution/XLangDebugExecutor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/execution/XLangDebugExecutor.java index be6a4fa391cfe9f1f53c3f8466a4da5dcc466f44..9601b2b420f62fe486e4ed71eafa42c7724cef7d 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/execution/XLangDebugExecutor.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/execution/XLangDebugExecutor.java @@ -7,6 +7,8 @@ */ package io.nop.idea.plugin.execution; +import javax.swing.*; + import com.intellij.execution.Executor; import com.intellij.execution.ExecutorRegistry; import com.intellij.openapi.util.IconLoader; @@ -16,8 +18,6 @@ import io.nop.idea.plugin.icons.NopIcons; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import javax.swing.Icon; - public class XLangDebugExecutor extends Executor { @NonNls public static final String EXECUTOR_ID = "XLangDebug"; @@ -31,13 +31,13 @@ public class XLangDebugExecutor extends Executor { @NotNull @Override public Icon getToolWindowIcon() { - return NopIcons.XLangDebug; + return NopIcons.Tool_XLangDebug; } @Override @NotNull public Icon getIcon() { - return NopIcons.StartXLangDebugger; + return NopIcons.Action_XLangDebug; } @Override diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/icons/NopIcons.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/icons/NopIcons.java index f6d54974997b559b653376b1738f4d36cb17e143..a1c3d203949852aaae03c3740f39c71c5bd1be29 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/icons/NopIcons.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/icons/NopIcons.java @@ -24,11 +24,13 @@ import static com.intellij.openapi.util.IconLoader.getIcon; * Font : Gotham */ public class NopIcons { + /** 文件类型图标: 16x16 */ + public static final Icon XLangFileType = getIcon("/icons/xlang.svg", NopIcons.class); - public static final Icon XLangFileType = getIcon("/icons/type.svg", NopIcons.class); - - public static final Icon XLangDebug = getIcon("/actions/xlangDebug.svg", NopIcons.class); - public static final Icon StartXLangDebugger = XLangDebug; + /** 底部工具栏的图标: 13x13 */ + public static final Icon Tool_XLangDebug = getIcon("/icons/xlangDebug-small.svg", NopIcons.class); + /** 顶部调试启动按钮图标: 16x16 */ + public static final Icon Action_XLangDebug = getIcon("/icons/xlangDebug.svg", NopIcons.class); private NopIcons() { } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileHelper.java deleted file mode 100644 index 6572e07873b79d41a0b1e0c2ad534530c122a0ac..0000000000000000000000000000000000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileHelper.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.lang; - -import io.nop.commons.io.stream.CharSequenceReader; -import io.nop.commons.util.CharSequenceHelper; -import io.nop.core.lang.xml.XNode; -import io.nop.core.lang.xml.parse.XRootNodeParser; -import io.nop.xlang.xdsl.XDslKeys; - -public class XLangFileHelper { - public static String getSchemaFromContent(CharSequence content) { - try { - if (!CharSequenceHelper.startsWith(content, "<")) - return null; - - XNode node = new XRootNodeParser().parseFromReader(null, new CharSequenceReader(content)); - XDslKeys keys = XDslKeys.of(node); - return (String) node.getAttr(keys.SCHEMA); - } catch (Exception e) { - return null; - } - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java deleted file mode 100644 index 5cbbf292c41fe376c8c01d9825195e4b63d02abd..0000000000000000000000000000000000000000 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangFileTypeDetector.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2017-2024 Nop Platform. All rights reserved. - * Author: canonical_entropy@163.com - * Blog: https://www.zhihu.com/people/canonical-entropy - * Gitee: https://gitee.com/canonical-entropy/nop-entropy - * Github: https://github.com/entropy-cloud/nop-entropy - */ -package io.nop.idea.plugin.lang; - -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.fileTypes.FileTypeRegistry; -import com.intellij.openapi.util.io.ByteSequence; -import com.intellij.openapi.vfs.VirtualFile; -import io.nop.commons.util.StringHelper; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class XLangFileTypeDetector implements FileTypeRegistry.FileTypeDetector { - - @Override - public @Nullable FileType detect( - @NotNull VirtualFile file, @NotNull ByteSequence firstBytes, @Nullable CharSequence firstCharsIfText - ) { - if (firstCharsIfText == null) { - return null; - } - - String ext = file.getExtension(); - if (ext == null) { - return null; - } - - if (ext.equals("xdef") || ext.equals("xpl") || ext.equals("xgen") || ext.equals("xrun")) { - return XLangFileType.INSTANCE; - } - - String schema = XLangFileHelper.getSchemaFromContent(firstCharsIfText); - if (!StringHelper.isEmpty(schema)) { - return XLangFileType.INSTANCE; - } - - return null; - } -} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangIconProvider.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangIconProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..929be82afa6960a12471129d1e67494518792466 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangIconProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang; + +import javax.swing.*; + +import com.intellij.ide.IconProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 根据实际的文件语言类型获取文件图标 + *

+ * 适用于对 {@link XLangLanguageSubstitutor} 从 xml 中识别出的 XLang + * 的文件图标进行修改 + * + * @author flytreeleft + * @date 2025-08-02 + */ +public class XLangIconProvider extends IconProvider { + + @Override + public @Nullable Icon getIcon(@NotNull PsiElement element, int flags) { + if (element instanceof PsiFile f && f.getLanguage() == XLangLanguage.INSTANCE) { + return f.getLanguage().getAssociatedFileType().getIcon(); + } + return null; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguageSubstitutor.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguageSubstitutor.java new file mode 100644 index 0000000000000000000000000000000000000000..957beb0e0dd088ad989e890097dfea1018c6f5ca --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/lang/XLangLanguageSubstitutor.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.lang; + +import java.io.InputStreamReader; +import java.io.Reader; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.intellij.lang.Language; +import com.intellij.lang.xml.XMLLanguage; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem; +import com.intellij.psi.LanguageSubstitutor; +import io.nop.commons.io.stream.FastBufferedReader; +import io.nop.core.lang.xml.XNode; +import io.nop.core.lang.xml.parse.XRootNodeParser; +import io.nop.xlang.xdsl.XDslKeys; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 根据 XML 中的特定结构(即包含 xmlns:x 和 x:schema 属性)动态识别其是否为 XLang + * + * @author flytreeleft + * @date 2025-08-02 + */ +public class XLangLanguageSubstitutor extends LanguageSubstitutor { + private static final Logger LOG = LoggerFactory.getLogger(XLangLanguageSubstitutor.class); + + private static final Language LANG_NULL = new Language("NULL") {}; + + private final Cache cached = Caffeine.newBuilder().maximumSize(500).build(); + + @Override + public @Nullable Language getLanguage(@NotNull VirtualFile file, @NotNull Project project) { + String ext = file.getExtension(); + if ("xsd".equalsIgnoreCase(ext) // + || file.getName().equals("pom.xml") // + ) { + return null; + } + + Language lang; + // Note: 对于只读文件,缓存结果,避免重复读取 + if (file.getFileSystem() instanceof ArchiveFileSystem) { + lang = cached.get(file.hashCode(), (k) -> getLanguage(file)); + } else { + lang = getLanguage(file); + } + + return lang == LANG_NULL ? null : lang; + } + + public Language getLanguage(@NotNull VirtualFile file) { + LOG.debug("Try to detect XLang from file {}", file); + + try (InputStreamReader reader = new InputStreamReader(file.getInputStream(), file.getCharset())) { + return isXLangFile(reader) // + ? XLangLanguage.INSTANCE + // Note: 由于是按内容动态确定,故而,需在非 XLang 时,返回原始语言 + : XMLLanguage.INSTANCE; + } catch (Exception ignore) { + return LANG_NULL; + } + } + + /** 根据 xml 内容做精确判断 */ + private boolean isXLangFile(Reader reader) { + // Note: 仅分析根节点 + XNode node = parseRootNode(reader); + XDslKeys keys = XDslKeys.of(node); + + return node.hasAttr(keys.SCHEMA); + } + + public static XNode parseRootNode(Reader reader) { + return new XRootNodeParser().parseFromReader(null, new FastBufferedReader(reader)); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/messages/NopPluginBundle.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/messages/NopPluginBundle.java index 506b9f728aa6d826b6217220173ad77cad197846..e06a67b4b09ae95e43f3400d73df29c87d5339f9 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/messages/NopPluginBundle.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/messages/NopPluginBundle.java @@ -7,55 +7,28 @@ */ package io.nop.idea.plugin.messages; -import com.intellij.BundleBase; -import com.intellij.reference.SoftReference; +import java.util.function.Supplier; + +import com.intellij.DynamicBundle; +import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.PropertyKey; -import java.lang.ref.Reference; -import java.util.ResourceBundle; - public class NopPluginBundle { - @NonNls - private static final String BUNDLE = "io.nop.idea.plugin.messages.NopPluginBundle"; - private static Reference ourBundle; - - public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) { - return message(getBundle(), key, params); - } - - public static String key(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key) { - return getBundle().getString(key); - } + public static final @NonNls String BUNDLE = "messages.NopPluginBundle"; - private NopPluginBundle() { - } - - @NotNull - private static ResourceBundle getBundle() { - ResourceBundle bundle = SoftReference.dereference(ourBundle); - if (bundle == null) { - bundle = ResourceBundle.getBundle(BUNDLE); - ourBundle = new java.lang.ref.SoftReference(bundle); - } - - return bundle; - } - - public static String messageOrDefault(@Nullable ResourceBundle bundle, @NotNull String key, @Nullable String defaultValue, @NotNull Object... params) { - return BundleBase.messageOrDefault(bundle, key, defaultValue, params); - } + private static final DynamicBundle INSTANCE = new DynamicBundle(NopPluginBundle.class, BUNDLE); - @NotNull - public static String message(@NotNull ResourceBundle bundle, @NotNull String key, @NotNull Object... params) { - return BundleBase.message(bundle, key, params); + public static @NotNull @Nls String message( + @NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params + ) { + return INSTANCE.getMessage(key, params); } - @Nullable - public static String messageOfNull(@NotNull ResourceBundle bundle, @NotNull String key, @NotNull Object... params) { - String value = messageOrDefault(bundle, key, key, params); - return key.equals(value) ? null : value; + public static @NotNull Supplier<@Nls String> messagePointer( + @NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params + ) { + return INSTANCE.getLazyMessage(key, params); } } diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java index e5fb2f64ecdfe080dbb7f6697d6e1993b8c48b4e..a599e507e72037db64f7bc630744b8bce87dccb5 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/services/NopProjectService.java @@ -30,7 +30,8 @@ public final class NopProjectService implements Disposable { private final Cancellable cleanup = new Cancellable(); private final ResourceLoadingCache dictCache = new ResourceLoadingCache<>("project-dict-cache", - ProjectDictProvider::loadDictModel, null); + ProjectDictProvider::loadDictModel, + null); public NopProjectService() { } @@ -41,8 +42,9 @@ public final class NopProjectService implements Disposable { } private synchronized void init(Project project) { - if (inited) + if (inited) { return; + } inited = true; ProjectEnv.withProject(project, () -> { @@ -57,8 +59,9 @@ public final class NopProjectService implements Disposable { public static NopProjectService get() { Project project = ProjectEnv.currentProject(); - if (project == null) - throw new IllegalStateException("not in project env"); + if (project == null) { + throw new IllegalStateException("Not in project environment, please call with ProjectEnv#withProject"); + } NopProjectService service = project.getService(NopProjectService.class); service.init(project); diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/CreateXLangFileFromTemplateAction.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/CreateXLangFileFromTemplateAction.java new file mode 100644 index 0000000000000000000000000000000000000000..2652dfc222b365b7851047f1442a98d5113c5cca --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/CreateXLangFileFromTemplateAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.template; + +import com.intellij.ide.actions.CreateFileFromTemplateAction; +import com.intellij.ide.actions.CreateFileFromTemplateDialog; +import com.intellij.ide.actions.CreateTemplateInPackageAction; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.NlsContexts; +import com.intellij.psi.PsiDirectory; +import io.nop.idea.plugin.lang.XLangFileType; +import io.nop.idea.plugin.messages.NopPluginBundle; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes; + +/** + * 配置 XLang 文件创建窗口 + * + * @author flytreeleft + * @date 2025-08-03 + */ +public class CreateXLangFileFromTemplateAction extends CreateFileFromTemplateAction { + // Note: 模板名不包含后缀 .xdef.ft + public static final String TEMPLATE_XDEF_NAME = "New_XLang_XDef"; + public static final String TEMPLATE_XDSL_NAME = "New_XLang_XDSL"; + + @Override + protected void buildDialog( + @NotNull Project project, @NotNull PsiDirectory directory, + @NotNull CreateFileFromTemplateDialog.Builder builder + ) { + builder.setTitle(NopPluginBundle.message("action.NopEntropy.NewXLang.title")); + + builder.addKind("XLang xdef", XLangFileType.INSTANCE.getIcon(), TEMPLATE_XDEF_NAME); + builder.addKind("XLang DSL", XLangFileType.INSTANCE.getIcon(), TEMPLATE_XDSL_NAME); + } + + @Override + protected @NlsContexts.Command String getActionName( + PsiDirectory directory, @NonNls @NotNull String newName, @NonNls String templateName + ) { + return NopPluginBundle.message("action.NopEntropy.NewXLang.text"); + } + + @Override + protected boolean isAvailable(final DataContext dataContext) { + return CreateTemplateInPackageAction.isAvailable(dataContext, JavaModuleSourceRootTypes.RESOURCES, (d) -> true); + } + + @Override + protected @NotNull PsiDirectory adjustDirectory(@NotNull PsiDirectory directory) { + return CreateTemplateInPackageAction.adjustDirectory(directory, JavaModuleSourceRootTypes.RESOURCES); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/CreateXLangFileFromTemplateHandler.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/CreateXLangFileFromTemplateHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..9ab0ec89908615e147b0deb907ea1473ebb7cff6 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/CreateXLangFileFromTemplateHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.template; + +import java.util.Map; + +import com.intellij.ide.fileTemplates.DefaultCreateFromTemplateHandler; +import com.intellij.ide.fileTemplates.FileTemplate; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypeManager; +import com.intellij.openapi.fileTypes.FileTypeRegistry; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.util.ArrayUtil; +import com.intellij.util.IncorrectOperationException; +import io.nop.commons.util.StringHelper; +import io.nop.idea.plugin.lang.XLangFileType; +import io.nop.idea.plugin.messages.NopPluginBundle; +import org.jetbrains.annotations.NotNull; + +/** + * 用于为模板准备变量,并可控制从模板内容到 {@link com.intellij.psi.PsiElement} 的转换过程 + * + * @author flytreeleft + * @date 2025-08-03 + */ +public class CreateXLangFileFromTemplateHandler extends DefaultCreateFromTemplateHandler { + + @Override + public boolean handlesTemplate(@NotNull FileTemplate template) { + return template.isTemplateOfType(XLangFileType.INSTANCE) // + && ArrayUtil.contains(template.getName(), + CreateXLangFileFromTemplateAction.TEMPLATE_XDSL_NAME, + CreateXLangFileFromTemplateAction.TEMPLATE_XDEF_NAME); + } + + @Override + public @NotNull PsiElement createFromTemplate( + @NotNull Project project, @NotNull PsiDirectory directory, String fileName, @NotNull FileTemplate template, + @NotNull String templateText, @NotNull Map props + ) throws IncorrectOperationException { + FileType type = FileTypeRegistry.getInstance().getFileTypeByFileName(fileName); + if (type != XLangFileType.INSTANCE) { + String ext = StringHelper.fileExt(fileName); + FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, ext); + } + + return super.createFromTemplate(project, directory, fileName, template, templateText, props); + } + + @Override + public void prepareProperties( + @NotNull Map props, String filename, // + @NotNull FileTemplate template, @NotNull Project project + ) { + if (template.getExtension().equals("xdef")) { + String name = StringHelper.removeLastPart(filename, '.'); + if (name.isEmpty()) { + name = filename; + } + name = name.replace('.', '-'); + + props.put("NODE_ROOT_NAME", name); + } + + super.prepareProperties(props, filename, template, project); + } + + @Override + protected String checkAppendExtension(String fileName, @NotNull FileTemplate template) { + if (!StringHelper.isValidFileName(fileName)) { + throw new IncorrectOperationException(NopPluginBundle.message("action.error.invalid-filename")); + } + + if (template.getExtension().equals("xdsl")) { + String ext = StringHelper.fileExt(fileName); + + if (ext.isEmpty()) { + throw new IncorrectOperationException(NopPluginBundle.message("action.error.no-xlang-file-extension")); + } + return fileName; + } + return super.checkAppendExtension(fileName, template); + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangFileLiveTemplateContextType.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangFileLiveTemplateContextType.java new file mode 100644 index 0000000000000000000000000000000000000000..8677323d79f1ce0fab2f3bf48baf1819fa023702 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangFileLiveTemplateContextType.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.template; + +import com.intellij.codeInsight.template.TemplateActionContext; +import com.intellij.codeInsight.template.TemplateContextType; +import com.intellij.ide.highlighter.XmlFileType; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiErrorElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.xml.XmlDocument; +import io.nop.idea.plugin.lang.XLangFileType; +import org.jetbrains.annotations.NotNull; + +/** + * @author flytreeleft + * @date 2025-08-06 + */ +public class XLangFileLiveTemplateContextType extends TemplateContextType { + + protected XLangFileLiveTemplateContextType() { + super("XLang"); + } + + @Override + public boolean isInContext(@NotNull TemplateActionContext templateActionContext) { + PsiFile file = templateActionContext.getFile(); + int startOffset = templateActionContext.getStartOffset(); + + if (file.getFileType() != XLangFileType.INSTANCE && file.getFileType() != XmlFileType.INSTANCE) { + return false; + } + + PsiElement element = file.findElementAt(startOffset); + if (element != null && element.getParent() instanceof PsiErrorElement e) { + if (e.getParent() instanceof XmlDocument doc) { + return doc.getRootTag() == null // + || (doc.getRootTag().getName().isEmpty() // + && doc.getRootTag().getAttributes().length == 0); + } + } + return false; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangRootTagNameMacro.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangRootTagNameMacro.java new file mode 100644 index 0000000000000000000000000000000000000000..84c5b6b27eae600aefaadbe91b470668f45b4d78 --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangRootTagNameMacro.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.template; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.intellij.codeInsight.template.Expression; +import com.intellij.codeInsight.template.ExpressionContext; +import com.intellij.codeInsight.template.Macro; +import com.intellij.codeInsight.template.Result; +import com.intellij.codeInsight.template.TemplateContextType; +import com.intellij.codeInsight.template.TextResult; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.vfs.VirtualFile; +import io.nop.commons.util.StringHelper; +import io.nop.core.lang.xml.XNode; +import io.nop.core.resource.IResource; +import io.nop.core.resource.VirtualFileSystem; +import io.nop.idea.plugin.lang.XLangFileType; +import io.nop.idea.plugin.lang.XLangLanguageSubstitutor; +import io.nop.idea.plugin.resource.ProjectEnv; +import io.nop.idea.plugin.utils.XmlPsiHelper; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-08-06 + */ +public class XLangRootTagNameMacro extends Macro { + private static final Pattern REGEX_SCHEMA = Pattern.compile(".+ \\S+:schema=\"([^\"]+)\".+", + Pattern.DOTALL | Pattern.MULTILINE); + + @Override + public @NonNls String getName() { + return "rootTagName"; + } + + @Override + public @Nullable Result calculateResult(Expression @NotNull [] params, ExpressionContext context) { + Editor editor = context.getEditor(); + VirtualFile file = editor != null ? editor.getVirtualFile() : null; + if (file == null) { + return null; + } + + String text = editor.getDocument().getText(); + Matcher matcher = REGEX_SCHEMA.matcher(text); + if (!matcher.matches()) { + return null; + } + + String vfsPath = matcher.group(1); + if (StringHelper.isBlank(vfsPath) || !vfsPath.endsWith(".xdef")) { + return null; + } + + String resourcePath = XmlPsiHelper.getNopVfsAbsolutePath(vfsPath, file); + String charset = XLangFileType.INSTANCE.getCharset(file, text.getBytes()); + + String tagName = ProjectEnv.withProject(context.getProject(), () -> { + IResource resource = VirtualFileSystem.instance().getResource(resourcePath); + XNode node = XLangLanguageSubstitutor.parseRootNode(resource.getReader(charset)); + if (node == null) { + return null; + } + + String name = node.getTagName(); + if (name.endsWith(":unknown-tag")) { + name = file.getName(); + + int index = name.indexOf('.'); + name = index <= 0 ? name : name.substring(0, index); + } + return name; + }); + + return tagName != null ? new TextResult(tagName) : null; + } + + @Override + public boolean isAcceptableInContext(TemplateContextType context) { + return context instanceof XLangFileLiveTemplateContextType; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangSchemaPathMacro.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangSchemaPathMacro.java new file mode 100644 index 0000000000000000000000000000000000000000..ca9613f8c025011a1e4c388c2e6cde9c62297dcb --- /dev/null +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/template/XLangSchemaPathMacro.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017-2025 Nop Platform. All rights reserved. + * Author: canonical_entropy@163.com + * Blog: https://www.zhihu.com/people/canonical-entropy + * Gitee: https://gitee.com/canonical-entropy/nop-entropy + * Github: https://github.com/entropy-cloud/nop-entropy + */ + +package io.nop.idea.plugin.template; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.template.Expression; +import com.intellij.codeInsight.template.ExpressionContext; +import com.intellij.codeInsight.template.Macro; +import com.intellij.codeInsight.template.Result; +import com.intellij.codeInsight.template.TemplateContextType; +import com.intellij.openapi.project.Project; +import io.nop.idea.plugin.utils.LookupElementHelper; +import io.nop.idea.plugin.utils.ProjectFileHelper; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author flytreeleft + * @date 2025-08-06 + */ +public class XLangSchemaPathMacro extends Macro { + + @Override + public @NonNls String getName() { + return "schemaPath"; + } + + @Override + public @Nullable Result calculateResult(Expression @NotNull [] params, ExpressionContext context) { + return null; + } + + @Override + public LookupElement @Nullable [] calculateLookupItems(Expression @NotNull [] params, ExpressionContext context) { + // Note: TODO 全项目搜索的性能极差,暂时不启用补全 +// Project project = context.getProject(); +// +// return ProjectFileHelper.findAllNopVfsPaths(project) +// .stream() +// .sorted() +// .filter(path -> path.endsWith(".xdef")) +// .map(LookupElementHelper::lookupString) +// .toArray(LookupElement[]::new); + return LookupElement.EMPTY_ARRAY; + } + + @Override + public boolean isAcceptableInContext(TemplateContextType context) { + return context instanceof XLangFileLiveTemplateContextType; + } +} diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java index 3db8a015f33bd72501533644e771436bbae0a51a..fd3443d2cc9193aa9c57af5e614bce30fed2ad52 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/LookupElementHelper.java @@ -59,6 +59,10 @@ public class LookupElementHelper { ; } + public static LookupElement lookupString(String s) { + return LookupElementBuilder.create(s).withIcon(PlatformIcons.ENUM_ICON); + } + public static StreamEx lookupPsiPackagesStream(StreamEx stream) { return lookupPsiPackagesStream(stream, (p) -> LookupElementBuilder.create(p) diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java index 1f4043c8a5e73ab420c4e1ccd88e67d520b34920..d738e455502bb51d44dfcd7fa68361e4fd744a01 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/ProjectFileHelper.java @@ -135,7 +135,8 @@ public class ProjectFileHelper { GlobalSearchScope scope = GlobalSearchScope.allScope(project); FilenameIndex.processFilesByNames(names, true, scope, null, (file) -> { String vfsPath = getNopVfsPath(file); - if (vfsPath != null) { + + if (!file.isDirectory() && vfsPath != null) { vfsPaths.add(vfsPath); } return true; diff --git a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java index 1247ff901ef2c6ec4b12305d37ff80c6ea28c2cf..e2719c7fe59515362f197a1a736605a3cabacd2a 100644 --- a/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java +++ b/nop-idea-plugin/src/main/java/io/nop/idea/plugin/utils/XmlPsiHelper.java @@ -54,9 +54,13 @@ public class XmlPsiHelper { } VirtualFile vf = file.getVirtualFile(); + return getNopVfsPath(vf, file); + } + + private static String getNopVfsPath(VirtualFile vf, PsiFile file) { // Note: 在编辑过程中得到的 VirtualFile 可能为 null,需尝试通过 // PsiFile#getOriginalFile 获得 VirtualFile - if (vf == null && file.getOriginalFile() != file) { + if (vf == null && file != null && file.getOriginalFile() != file) { vf = file.getOriginalFile().getVirtualFile(); } @@ -64,8 +68,8 @@ public class XmlPsiHelper { } /** - * 获取 path 的 vfs 绝对路径。 - * 若 path 为相对路径,则视为其相对于 element 所在文件的目录 + * 获取 {@code path} 的 vfs 绝对路径。 + * 若 {@code path} 为相对路径,则视为其相对于 {@code element} 所在文件的目录 */ public static String getNopVfsAbsolutePath(String path, PsiElement element) { String filePath = getNopVfsPath(element); @@ -73,6 +77,16 @@ public class XmlPsiHelper { return StringHelper.absolutePath(filePath, path); } + /** + * 获取 {@code path} 的 vfs 绝对路径。 + * 若 {@code path} 为相对路径,则视为其相对于 {@code vf} 所在文件的目录 + */ + public static String getNopVfsAbsolutePath(String path, VirtualFile vf) { + String filePath = getNopVfsPath(vf, null); + + return StringHelper.absolutePath(filePath, path); + } + public static List findPsiFileList(Project project, String path) { String fileName = StringHelper.fileFullName(path); Collection vfList = FilenameIndex.getVirtualFilesByName(fileName, diff --git a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml index caaaf1c54f4c186dcc3a22918b8b94f9bdd46ba8..aa49b33061c64bee64c7ee566708f9ae1e1ad99b 100644 --- a/nop-idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/nop-idea-plugin/src/main/resources/META-INF/plugin.xml @@ -5,8 +5,10 @@ Canonical +

  • 根据 xdef 元模型定义来校验 dsl 文件格式;
  • +
  • 为 Xpl 模板语言提供调试功能;
  • + ]]> com.intellij.modules.lang com.intellij.java + com.intellij.gradle org.jetbrains.plugins.yaml + messages.NopPluginBundle + @@ -43,6 +48,11 @@ + + + + @@ -74,25 +84,40 @@ + + + + + + + + + + - - + + extensions="xdef;xdsl;xpl;xgen;xui;xlib;xrun;xwf;xmeta;xpage;xrule"/> + + + + - + - + - + @@ -105,6 +130,8 @@ + diff --git a/nop-idea-plugin/src/main/resources/fileTemplates/internal/New_XLang_XDSL.xdsl.ft b/nop-idea-plugin/src/main/resources/fileTemplates/internal/New_XLang_XDSL.xdsl.ft new file mode 100644 index 0000000000000000000000000000000000000000..e08a008b4335d144379aec19bc70e4768b3ac3d1 --- /dev/null +++ b/nop-idea-plugin/src/main/resources/fileTemplates/internal/New_XLang_XDSL.xdsl.ft @@ -0,0 +1,6 @@ + + + diff --git a/nop-idea-plugin/src/main/resources/fileTemplates/internal/New_XLang_XDef.xdef.ft b/nop-idea-plugin/src/main/resources/fileTemplates/internal/New_XLang_XDef.xdef.ft new file mode 100644 index 0000000000000000000000000000000000000000..e0581ddfb528d515137debd05b27252aae0f40c0 --- /dev/null +++ b/nop-idea-plugin/src/main/resources/fileTemplates/internal/New_XLang_XDef.xdef.ft @@ -0,0 +1,11 @@ + + + +<${NODE_ROOT_NAME} xmlns:x="/nop/schema/xdsl.xdef" xmlns:xdef="/nop/schema/xdef.xdef" + x:schema="/nop/schema/xdef.xdef" +> + + diff --git a/nop-idea-plugin/src/main/resources/icons/type.svg b/nop-idea-plugin/src/main/resources/icons/xlang.svg similarity index 100% rename from nop-idea-plugin/src/main/resources/icons/type.svg rename to nop-idea-plugin/src/main/resources/icons/xlang.svg diff --git a/nop-idea-plugin/src/main/resources/icons/xlangDebug-small.svg b/nop-idea-plugin/src/main/resources/icons/xlangDebug-small.svg new file mode 100644 index 0000000000000000000000000000000000000000..219a33b16a96f69a0e639591ebd3a6c9b943afb2 --- /dev/null +++ b/nop-idea-plugin/src/main/resources/icons/xlangDebug-small.svg @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/nop-idea-plugin/src/main/resources/actions/xlangDebug.svg b/nop-idea-plugin/src/main/resources/icons/xlangDebug.svg similarity index 100% rename from nop-idea-plugin/src/main/resources/actions/xlangDebug.svg rename to nop-idea-plugin/src/main/resources/icons/xlangDebug.svg diff --git a/nop-idea-plugin/src/main/resources/liveTemplates/XLang_File.xml b/nop-idea-plugin/src/main/resources/liveTemplates/XLang_File.xml new file mode 100644 index 0000000000000000000000000000000000000000..5ba75a5e968b038791e2bc2174289a303186727d --- /dev/null +++ b/nop-idea-plugin/src/main/resources/liveTemplates/XLang_File.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties b/nop-idea-plugin/src/main/resources/messages/NopPluginBundle.properties similarity index 82% rename from nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties rename to nop-idea-plugin/src/main/resources/messages/NopPluginBundle.properties index 5d0fde4402d1816988a346ba44d8ce2da2fd182c..943976c3849f5674ed117a83fa8b9cced8ae802c 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle.properties +++ b/nop-idea-plugin/src/main/resources/messages/NopPluginBundle.properties @@ -1,4 +1,8 @@ -line.breakpoints.tab.title = XLang line breakpoints +action.NopEntropy.NewXLang.text = XLang File +action.NopEntropy.NewXLang.title = New XLang File +action.error.invalid-filename = Invalid file name +action.error.no-xlang-file-extension = XLang DSL file must have extension name, such as 'example.xlib' + xlang.annotation.attr.not-defined = Undefined attribute ''{0}'' xlang.annotation.attr.value-required = Attribute ''{0}'' can''t have empty value xlang.annotation.tag.not-defined = Undefined tag ''{0}'' @@ -35,3 +39,7 @@ xlang.doc.flag.internal = [Internal] xlang.doc.flag.allow-cp-expr = [#{var}] xlang.doc.markdown.link-title = '{'Link'}'{0} xlang.doc.markdown.image-title = '{'Image'}'{0} + +xlang.debugger.line.breakpoints.tab.title = XLang Line Breakpoints +xlang.debugger.not-exist = Current module doesn't depend on 'nop-xlang-debugger', please add dependency 'io.github.entropy-cloud:nop-xlang-debugger' to pom.xml or build.gradle.kts with 'test' scope +xlang.debugger.connect-fail = Can''t connect to XLangDebugger on port {0} diff --git a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties b/nop-idea-plugin/src/main/resources/messages/NopPluginBundle_zh.properties similarity index 82% rename from nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties rename to nop-idea-plugin/src/main/resources/messages/NopPluginBundle_zh.properties index cf1366f2c7d0a503b4365492611aefe3d646af3a..306feadc2d43b288f17d8ce9c61b95365d6d04bf 100644 --- a/nop-idea-plugin/src/main/resources/io/nop/idea/plugin/messages/NopPluginBundle_zh.properties +++ b/nop-idea-plugin/src/main/resources/messages/NopPluginBundle_zh.properties @@ -1,4 +1,8 @@ -line.breakpoints.tab.title = XLang line breakpoints +action.NopEntropy.NewXLang.text = XLang File +action.NopEntropy.NewXLang.title = \u65B0\u5EFA XLang File +action.error.invalid-filename = \u6587\u4EF6\u540D\u65E0\u6548 +action.error.no-xlang-file-extension = XLang DSL \u6587\u4EF6\u5FC5\u987B\u6307\u5B9A\u540E\u7F00\u540D\uFF0C\u4F8B\u5982 'example.xlib' + xlang.annotation.attr.not-defined = \u5C5E\u6027 ''{0}'' \u672A\u5B9A\u4E49 xlang.annotation.attr.value-required = \u5C5E\u6027 ''{0}'' \u7684\u503C\u4E0D\u5141\u8BB8\u4E3A\u7A7A xlang.annotation.tag.not-defined = \u6807\u7B7E ''{0}'' \u672A\u5B9A\u4E49 @@ -35,3 +39,7 @@ xlang.doc.flag.internal = [\u5185\u90E8] xlang.doc.flag.allow-cp-expr = [#{var}] xlang.doc.markdown.link-title = '{'\u94FE\u63A5'}'{0} xlang.doc.markdown.image-title = '{'\u56FE\u7247'}'{0} + +xlang.debugger.line.breakpoints.tab.title = XLang \u884C\u65AD\u70B9 +xlang.debugger.not-exist = \u5F53\u524D\u6A21\u5757\u672A\u4F9D\u8D56 'nop-xlang-debugger'\uFF0C\u8BF7\u6DFB\u52A0\u4F9D\u8D56 'io.github.entropy-cloud:nop-xlang-debugger' \u5230 pom.xml \u6216 build.gradle.kts \u6587\u4EF6\u4E2D\uFF0C\u5E76\u5C06 scope \u8BBE\u7F6E\u4E3A test +xlang.debugger.connect-fail = \u65E0\u6CD5\u4E0E XLangDebugger \u7684\u670D\u52A1\u7AEF\u53E3 {0} \u5EFA\u7ACB\u8FDE\u63A5 diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java index 4865213580ae1642753efed2144b7daa5120aac5..7144f58c40c4725e5329d19060d665e25f13489a 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/BaseXLangPluginTestCase.java @@ -38,15 +38,19 @@ import com.intellij.psi.impl.DebugUtil; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.testFramework.fixtures.CodeInsightTestFixture; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; +import io.nop.api.core.util.SourceLocation; import io.nop.commons.lang.impl.Cancellable; import io.nop.commons.util.FileHelper; import io.nop.commons.util.IoHelper; +import io.nop.commons.util.StringHelper; import io.nop.core.resource.IResource; import io.nop.core.resource.ResourceHelper; import io.nop.core.resource.impl.ClassPathResource; import io.nop.idea.plugin.lang.XLangFileType; import io.nop.idea.plugin.lang.reference.XLangReference; import io.nop.idea.plugin.services.NopAppListener; +import io.nop.xlang.debugger.XLangDebugger; +import io.nop.xlang.debugger.initialize.XLangDebuggerInitializer; /** * @author flytreeleft @@ -77,10 +81,10 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur // Note: *.xdef 等需显式注册,否则,这类文件会被视为二进制文件, // 在通过 PsiDocumentManager 获取 Document 时,将返回 null FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, "xdef"); - FileTypeManager.getInstance().associateExtension(XLangFileType.INSTANCE, XLANG_EXT); new NopAppListener().appFrameCreated(new ArrayList<>()); + initXLangDebugger(); // Note: 提前将被引用的 vfs 资源添加到 Project 中 addAllNopXDefsToProject(); @@ -98,6 +102,41 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur super.tearDown(); } + private void initXLangDebugger() { + // Note: 在单元测试中,vfs 资源是针对 Project 被复制到单独的 src 目录下的, + // 通过 ProjectVirtualFileSystem 得到的 vfs 资源路径与调试断点的文件路径是不一致的, + // 因此,需要针对测试资源做路径转换,以匹配断点所在的文件路径 + XLangDebuggerInitializer debugger = new XLangDebuggerInitializer() { + @Override + protected XLangDebugger createDebugger() { + return new XLangDebugger() { + @Override + protected String toSourcePath(SourceLocation loc) { + String prefix = "/src/_vfs/"; + String path = super.toSourcePath(loc); + + if (path.startsWith(prefix)) { + String vfsFileName = path.substring(prefix.length()); + File rootDir = new File(getVfsDir(), "../../../.."); + File vfsSrcFile = new File(new File(rootDir, "src/test/resources/_vfs"), vfsFileName); + + if (vfsSrcFile.isFile()) { + path = vfsSrcFile.toURI().toString(); + path = StringHelper.normalizePath(path); + } + } + return path; + } + }; + } + }; + + if (debugger.isEnabled()) { + debugger.initialize(); + cleanup.appendOnCancelTask(debugger::destroy); + } + } + protected PsiFile configureByXLangText(String text) { return myFixture.configureByText("unit." + XLANG_EXT, text); } @@ -123,9 +162,13 @@ public abstract class BaseXLangPluginTestCase extends LightJavaCodeInsightFixtur } } + protected File getVfsDir() { + return new File(getClass().getResource("/_vfs").getFile()); + } + /** 将 vfs 测试资源全部复制到 Project 中 */ protected void addAllTestVfsResourcesToProject() { - File vfsDir = new File(getClass().getResource("/_vfs").getFile()); + File vfsDir = getVfsDir(); FileHelper.walk(vfsDir, (file) -> { if (file.isFile()) { diff --git a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangFileTypeDetector.java b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangFileTypeDetector.java index 1a96e09e17c5ee6bac510138691031474d44de05..26759d3fbfe6164a027e6109ed88e33d6a660c04 100644 --- a/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangFileTypeDetector.java +++ b/nop-idea-plugin/src/test/java/io/nop/idea/plugin/lang/TestXLangFileTypeDetector.java @@ -15,28 +15,72 @@ import org.junit.Assert; public class TestXLangFileTypeDetector extends BaseXLangPluginTestCase { - public void testXLangFileType() { - assertFileType("my.wf.xml", // - """ + public void testDetectXLangByExtension() { + String[] extensions = "xdef;xpl;xgen;xui;xlib;xrun;xwf;xmeta;xpage;xrule".split(";"); + + for (String ext : extensions) { + assertFileType("a." + ext, "", XLangFileType.INSTANCE); + } + } + + public void testDetectXLangFromXml() { + String[] samples = new String[] { + "", // + """ + + """, // + """ + + """, // + """ + + """, // + """ + + """, // + """ + + """, // + """ + + """, // + """ + + """, // + }; + for (String text : samples) { + assertFileType("a.wf.xml", text, XmlFileType.INSTANCE); + } + + samples = new String[] { + """ + + """, // + """ + + """, // + """ """, // - XmlFileType.INSTANCE // - ); - - assertFileType("xlib.register-model.xml", // - """ - - - - - - """, // - XLangFileType.INSTANCE // - ); + """ + + + """, // + """ + + """, // + """ + + + """, // + """ + + """, // + }; + for (String text : samples) { + assertFileType("b.wf.xml", text, XLangFileType.INSTANCE); + } } private void assertFileType(String fileName, String text, LanguageFileType expectedFileType) { diff --git a/nop-xlang-debugger/src/main/java/io/nop/xlang/debugger/initialize/XLangDebuggerInitializer.java b/nop-xlang-debugger/src/main/java/io/nop/xlang/debugger/initialize/XLangDebuggerInitializer.java index f18909130c5f6acae8e4c442f6f1b77f0f8e5a55..6d8a449552a49d8adb98db6cebc9522ce2475f03 100644 --- a/nop-xlang-debugger/src/main/java/io/nop/xlang/debugger/initialize/XLangDebuggerInitializer.java +++ b/nop-xlang-debugger/src/main/java/io/nop/xlang/debugger/initialize/XLangDebuggerInitializer.java @@ -61,7 +61,7 @@ public class XLangDebuggerInitializer implements ICoreInitializer { config.setIdleTimeout(0); server.setServerConfig(config); - debugger = new XLangDebugger(); + debugger = createDebugger(); debugger.setNotifier(new DebugNotifier()); server.addServiceImpl(IDebugger.class, debugger); server.setOnChannelOpen(this::sendBreakpointNotice); @@ -76,6 +76,10 @@ public class XLangDebuggerInitializer implements ICoreInitializer { EvalExprProvider.registerGlobalExecutor(new DebugExpressionExecutor(debugger)); } + protected XLangDebugger createDebugger() { + return new XLangDebugger(); + } + // 通知调试器客户端当前正在处理的断点情况 private void sendBreakpointNotice(String addr) { XLangDebugger debugger = this.debugger; diff --git a/nop-xlang/src/main/java/io/nop/xlang/XLangConfigs.java b/nop-xlang/src/main/java/io/nop/xlang/XLangConfigs.java index 65611bd319de2cd22ec15f461f9acb4515287150..529930fe71289f70dd88be86a3a031c7bf768712 100644 --- a/nop-xlang/src/main/java/io/nop/xlang/XLangConfigs.java +++ b/nop-xlang/src/main/java/io/nop/xlang/XLangConfigs.java @@ -33,7 +33,7 @@ public interface XLangConfigs { @Description("XLang调试器端口") IConfigReference CFG_XLANG_DEBUGGER_PORT = varRef(s_loc,"nop.xlang.debugger.port", Integer.class, 12345); - @Description("XLang调试器端口") + @Description("XLang调试服务传输的最大数据长度") IConfigReference CFG_XLANG_DEBUGGER_MAX_DATA_LEN = varRef(s_loc,"nop.xlang.debugger.max-data-len", Integer.class, 1024 * 1024 * 2);