RSTALanguageSupport icon indicating copy to clipboard operation
RSTALanguageSupport copied to clipboard

Context-support for runtime-only variables in global script scope

Open Rolleander opened this issue 1 year ago • 1 comments

Hey together,

trying to get context-assist working in RSTA for my JavaScripts:

Im using the JavaScriptLanguageSupport together with the Rhino Engine and adding my java project-jar with JarManager::addClassFileSource, so that it gets to know my classes. Everything works so far, I can use importPackage() in the script to load one of my packages and then get context-assist in the editor for my own classes.

My problem is the following: The scripts im writing are not self-contained, instead I am using them in different contexts in my application. In the application I add specific Java-objects to the Rhino runtime with ScriptEngine::put(). Of course, the editor does not know about these runtime-only objects that are added to the global scope in execution.

In this scenario i have a class "MyRuntime" in my jar, from which I create an instance "runtime" and add it to the Rhino execution with ScriptEngine.put("runtime", new MyRuntime());

Now to make the editor recognize the object, I was thinking about the following (which should work in theory):

  • Get the class MyRuntime from the JarManager
  • Get the VariableResolver from the JavaScript SourceCompletionProvider
  • Tell it to add a variable (named runtime of type MyRuntime) to the system scope

Im not sure however how to do the last step, VariableResolver::addSystemVariable requires a JavaScriptVariableDeclaration instance, which needs a codeblock or AstNode. Maybe someone can guide me in the right direction how to continue from there.

Im very thankful to any help in this issue

Rolleander avatar Apr 20 '24 13:04 Rolleander

I made it work now, probably not the cleanest way, but this is my working solution now:

First, just the container for global variables to add to the script:

public class GlobalScriptVariable {

    private final String name;
    private final String packagePath;
    private final String className;

    public GlobalScriptVariable(String name, String packagePath, String className){
        this.name = name;
        this.packagePath = packagePath;
        this.className = className;
    }

    public String getClassName() {
        return className;
    }

    public String getName() {
        return name;
    }

    public String getPackagePath() {
        return packagePath;
    }

    public String getQualifiedClassName(){
        return packagePath+"."+className;
    }
}

Then my custom JavaScriptLanguageSupport

public class RhinoJavaScriptLanguageSupport extends JavaScriptLanguageSupport {

    private ScriptingCompletionProvider provider;


    public RhinoJavaScriptLanguageSupport(ScriptEnvironments.Type type) {
        JavaScriptTokenMaker.setJavaScriptVersion("1.7");
        setECMAVersion(TypeDeclarationsECMAv5.class.getName(), getJarManager());
        try {
            getJarManager().addClassFileSource(GameDebugger.debugPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        provider.setJarManager(getJarManager());
        provider.initSystemVariables(type);
    }

    @Override
    protected JavaScriptCompletionProvider createJavaScriptCompletionProvider() {
        provider = new ScriptingCompletionProvider();
        return new JavaScriptCompletionProvider(provider, getJarManager(), this);
    }

    public void install(RSyntaxTextArea textArea) {
        //remove javascript support and replace with our custom rhino support
        LanguageSupport support = (LanguageSupport) textArea.getClientProperty("org.fife.rsta.ac.LanguageSupport");
        if (support != null) {
            support.uninstall(textArea);
        }
        super.install(textArea);
    }

    private static class ScriptingCompletionProvider extends SourceCompletionProvider {

        private final static String GLOBAL_SCOPE_DETECTION = "Infinity";
        private final List<Completion> globalVariableCompletions = new ArrayList<>();
        private ShorthandCompletionCache shorthandCache;

        public ScriptingCompletionProvider() {
            super(RhinoJavaScriptEngine.RHINO_ENGINE, false);
        }

       public void initSystemVariables(ScriptEnvironments.Type type) {
            try {
                Field field = SourceCompletionProvider.class.getDeclaredField("shorthandCache");
                field.setAccessible(true);
                shorthandCache = (ShorthandCompletionCache) field.get(this);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
            for (GlobalScriptVariable variable : type.globalVariables) {
                addSystemVariable(variable);
            }
        }

        public void addSystemVariable(GlobalScriptVariable variable) {
            RhinoJavaScriptTypesFactory typeFactory = (RhinoJavaScriptTypesFactory) getJavaScriptTypesFactory();
            typeFactory.addImportPackage(variable.getPackagePath());
            JavaScriptVariableDeclaration variableDeclaration = new JavaScriptVariableDeclaration(variable.getName(),
                    Integer.MAX_VALUE, this, new GlobalCodeBlock());
            variableDeclaration.setTypeDeclaration(getTypeDeclaration(variable));
            getVariableResolver().addSystemVariable(variableDeclaration);
            JSVariableCompletion completion = new JSVariableCompletion(this, variableDeclaration);
            globalVariableCompletions.add(completion);
            shorthandCache.addShorthandCompletion(completion);
        }


        private TypeDeclaration getTypeDeclaration(GlobalScriptVariable variable) {
            ClassFile classFile = getJarManager().getClassEntry(variable.getQualifiedClassName());
            return getJavaScriptTypesFactory().createNewTypeDeclaration(classFile, false, true);
        }

        @Override
        protected List<Completion> getCompletionsImpl(JTextComponent comp) {
            List<Completion> completions = super.getCompletionsImpl(comp);
            if (completions.stream().anyMatch(it -> GLOBAL_SCOPE_DETECTION.equals(it.getReplacementText()))) {
                completions.addAll(globalVariableCompletions);
            }
            return completions;
        }
    }

    private static class GlobalCodeBlock extends CodeBlock {

        public GlobalCodeBlock() {
            super(0);
        }

        @Override
        public boolean contains(int offset) {
            return true;
        }
    }
}

(ScriptEnvironments.Type is just an enum that has a list of predefined GlobalScriptVariable instances)

This is how this (probably hacky) solution works:

  • Gets the RhinoJavaScriptTypesFactory and adds import packages for all the global script variables (because in the script we dont import them manually and expect them to be known)
  • Adds system variables to the VariableResolver for each global script variable (simulating a code block that covers the whole file, so it tricks the system into always showing the variable in the completions). Now the completions will appear after typing variable-name and a dot.
  • Extends the completions for the JSGlobal object (which are shown when nothing is typed yet and represent always available completions). Now the variables will be shown if nothing has been typed yet.
  • Adds the completions for the global variables to the shorthandCache (only way to access this was through reflection sadly). Now the matching variables will still be shown after typing has started

Adding my own completion options for the JSGlobal seems to be the most hacky part of this solution, I was not sure where to add my custom completions exactly (most of the stuff of the base classes is private, I didnt want to re-do private code or do private access via reflection). Hence it is implemented somewhat stupid, but works: We check the resolved completions if they contain a completion for "Infinity" (which is part of the JSGlobal) and then we can add our global variable completions to the list.

Rolleander avatar Apr 21 '24 09:04 Rolleander