sdedic commented on code in PR #6079:
URL: https://github.com/apache/netbeans/pull/6079#discussion_r1232027407


##########
java/java.lsp.server/vscode/src/extension.ts:
##########
@@ -21,10 +21,10 @@
 import { commands, window, workspace, ExtensionContext, ProgressLocation, 
TextEditorDecorationType } from 'vscode';
 
 import {
-       LanguageClient,
-       LanguageClientOptions,
-       ServerOptions,
-       StreamInfo
+    LanguageClient,

Review Comment:
   Please avoid unnecessary whitespace - this complicates searches in file's 
history. Redo this file's change without full file reformat.



##########
java/java.lsp.server/vscode/package.json:
##########
@@ -1025,29 +1038,32 @@
        },
        "scripts": {
                "vscode:prepublish": "npm run compile",
-               "compile": "tsc -p ./",
-               "watch": "tsc -watch -p ./",
+               "compile": "tsc -p ./; node ./esbuild.js",
+               "watch": "tsc -watch -p ./ | node ./esbuild.js --watch",

Review Comment:
   Why pipe (instead of `;`) from `tsc` to `esbuild` ?



##########
java/java.lsp.server/vscode/src/propertiesView/propertiesHtmlBuilder.ts:
##########
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import * as vscode from 'vscode';
+import { Properties, Property } from "./controlTypes";
+
+export function makeHtmlForProperties(name: string, nonce: string, scriptUri: 
vscode.Uri, properties: Properties): string {
+    return `<!DOCTYPE html>
+    <html lang="en">
+    
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="Content-Security-Policy" content="default-src 
'none'; script-src 'nonce-${nonce}';">
+        <title>${name}</title>
+    </head>
+    
+    <body>
+        <h1>${name} Properties</h1>
+        <vscode-divider></vscode-divider>
+        <table>
+            ${makePropertiesTable(properties)}
+            <tr>
+                <td colspan="2">
+                </td>
+            </tr>
+            <tr>
+                <td colspan="2" align="right" style="text-align: right;">
+                    <vscode-button id="save" 
appearance="primary">Save</vscode-button>
+                    <vscode-button id="cancel" 
appearance="secondary">Cancel</vscode-button>
+                </td>
+            </tr>
+        </table>
+        <script type="module" nonce="${nonce}" src="${scriptUri}"></script>
+    </body>
+    
+    </html>`
+};
+
+function wrapToTable(name: string, content: string, separator: string = ":"): 
string {
+    return `<tr><td align="right"><b>${name}${separator}</b></td><td 
align="left">${content}</td></tr>`;
+}
+
+function makePropertiesTable(properties: Properties): string {
+    let html = "";
+    for (const prop of properties.props) {
+        html += makePropAccess(prop);
+    }
+    return html;
+}
+
+function makePropAccess(prop: Property): string {
+    let out: string;
+    switch (prop.propType) {
+        case 'java.lang.String':

Review Comment:
   We have these String literals used in `PropTypes`, `PropTypeMap` and here 
... still there is not an enum/const that would give a compile-type symbol to 
these literals ?



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);
+        }
+        TreeNodeRegistry r = 
Lookup.getDefault().lookup(TreeNodeRegistry.class);
+        if (r == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        int nodeId = ((JsonPrimitive) arguments.get(0)).getAsInt();
+        TreeViewProvider nodeProvider = r.providerOf(nodeId);
+        Node node = null;
+        if (nodeProvider != null) {
+            node = nodeProvider.findNode(nodeId);
+        }
+        if (node == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        boolean getProps = COMMAND_GET_NODE_PROPERTIES.equals(command);
+        Node.PropertySet[] propertySets = node.getPropertySets();
+
+        if (getProps) {
+            return 
CompletableFuture.completedFuture(getAllPropertiesMap(propertySets));
+        }
+        if (arguments.size() == 2) {
+            Object propJson = arguments.get(1);
+            if (propJson instanceof JsonNull) {
+                return CompletableFuture.completedFuture(null);

Review Comment:
   What is the semantic of calling set(node, null) ?



##########
java/java.lsp.server/vscode/package.json:
##########
@@ -1025,29 +1038,32 @@
        },
        "scripts": {
                "vscode:prepublish": "npm run compile",
-               "compile": "tsc -p ./",
-               "watch": "tsc -watch -p ./",
+               "compile": "tsc -p ./; node ./esbuild.js",
+               "watch": "tsc -watch -p ./ | node ./esbuild.js --watch",
                "test": "node ./out/test/runTest.js",
                "nbcode": "node ./out/nbcode.js",
                "nbjavac": "node ./out/nbcode.js -J-Dnetbeans.close=true 
--modules --install .*nbjavac.*",
                "apisupport": "node ./out/nbcode.js -J-Dnetbeans.close=true 
--modules --install 
'(org.netbeans.libs.xerces|org.netbeans.modules.editor.structure|org.netbeans.modules.xml|org.netbeans.modules.xml.axi|org.netbeans.modules.xml.retriever|org.netbeans.modules.xml.schema.model|org.netbeans.modules.xml.tax|org.netbeans.modules.xml.text|org.netbeans.modules.ant.browsetask|.*apisupport.*|org.netbeans.modules.debugger.jpda.ant)'
 && node ./out/nbcode.js -J-Dnetbeans.close=true --modules --enable 
.*apisupport.ant"
        },
        "devDependencies": {
-               "@vscode/codicons": "0.0.29",
                "@types/glob": "^7.1.1",
                "@types/mocha": "^9.0.0",
                "@types/node": "^13.11.0",
                "@types/ps-node": "^0.1.0",
                "@types/vscode": "^1.76.0",
+               "@types/vscode-webview": "^1.57.1",
+               "@vscode/codicons": "0.0.29",

Review Comment:
   nitpick: unnecessary movement.



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);
+        }
+        TreeNodeRegistry r = 
Lookup.getDefault().lookup(TreeNodeRegistry.class);
+        if (r == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        int nodeId = ((JsonPrimitive) arguments.get(0)).getAsInt();
+        TreeViewProvider nodeProvider = r.providerOf(nodeId);
+        Node node = null;
+        if (nodeProvider != null) {
+            node = nodeProvider.findNode(nodeId);
+        }
+        if (node == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        boolean getProps = COMMAND_GET_NODE_PROPERTIES.equals(command);
+        Node.PropertySet[] propertySets = node.getPropertySets();
+
+        if (getProps) {
+            return 
CompletableFuture.completedFuture(getAllPropertiesMap(propertySets));
+        }
+        if (arguments.size() == 2) {
+            Object propJson = arguments.get(1);
+            if (propJson instanceof JsonNull) {
+                return CompletableFuture.completedFuture(null);
+            }
+            List m = gson.fromJson((JsonElement) propJson, List.class);

Review Comment:
   Hm ... could be more friendly if the property sets was a map (keyed by set 
name) instead of array, but the propset name is present in each array's item, 
so it's probably OK.
   
   Maybe if
   ```
   List<Map<String, Object>> m = ...
   ```
   the `setAllProperties` and others can use strongly typed Map instead of raw 
type(s) ?



##########
java/java.lsp.server/vscode/src/propertiesView/script.ts:
##########
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { provideVSCodeDesignSystem, vsCodeButton, vsCodeTextField, 
vsCodeDivider, vsCodeCheckbox, Button, TextField, Checkbox } from 
"@vscode/webview-ui-toolkit";
+import { isError, asClass, isClass } from "../typesUtil";
+import { CommandKey, Message, MessageProp } from "./controlTypes";
+
+provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeTextField(), 
vsCodeDivider(), vsCodeCheckbox());
+
+const vscode = acquireVsCodeApi();
+document.addEventListener("DOMContentLoaded", () => {
+    try {
+        asClass(Button, document.getElementById('save'), "Not save.")
+            .addEventListener('click', () => {
+                try {
+                    if (validate())
+                        sendMessage({ _type: CommandKey.Save, properties: 
getProperties() });
+                } catch (e: unknown) {
+                    handleError(e);
+                }
+            });
+        asClass(Button, document.getElementById('cancel'), "Not cancel.")
+            .addEventListener('click', () => {
+                sendMessage({ _type: CommandKey.Cancel });
+            });
+    } catch (e: unknown) {
+        handleError(e);
+    }
+});
+
+function handleError(error: unknown) {
+    if (isError(error))
+        sendMessage({ _type: CommandKey.Error, error: error.message, stack: 
error.stack });
+    else
+        sendMessage({ _type: CommandKey.Error, error: JSON.stringify(error) });
+}
+
+function sendMessage(message: Message) {
+    vscode.postMessage(message);
+}
+
+function getProperties(): MessageProp[] {
+    const out: MessageProp[] = [];
+    const elements = document.getElementsByName("input");
+    for (let i = 0; i < elements.length; ++i) {
+        const element = elements.item(i);
+        if (element)
+            out.push(getProperty(element));
+    }
+    return out;
+}
+
+function getProperty(element: HTMLElement): MessageProp {
+    if (isClass(TextField, element)) {
+        return makeProperty(element.value, element?.id);

Review Comment:
   what's the use-case for supporting properties from  id-less elements ?



##########
java/java.lsp.server/vscode/src/propertiesView/controlTypes.ts:
##########
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { EnumType, KeyOfArray, Typed } from "../typesUtil";
+
+export type ID = number;
+
+export const PropTypes: KeyOfArray<PropTypeMap> = [
+    "java.lang.String",
+    "java.lang.Boolean",
+    "java.util.Properties",

Review Comment:
   Integer (number) is not supported ?



##########
java/java.lsp.server/vscode/src/typesUtil.ts:
##########
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+
+export type IsType<T> = (obj: unknown) => obj is T;
+
+function assertType<T>(obj: unknown, isTypeTest: IsType<T>, errorMessage?: 
string): asserts obj is T {
+    if (!isTypeTest(obj))
+        throw new Error(errorMessage || "Object isn't of expected type.");
+}
+
+export type Constructor<T> = new (...args: any) => T;
+
+export function isClass<T>(cls: Constructor<T>, obj: unknown): obj is T {
+    return obj instanceof cls;
+}
+export function isClassTest<T>(cls: Constructor<T>): ((obj: unknown) => obj is 
T) {
+    return (obj: unknown): obj is T => isClass(cls, obj);
+}
+export function asClass<T>(cls: Constructor<T>, obj: unknown, errorMessage?: 
string): T {
+    assertType(obj, isClassTest(cls), errorMessage);
+    return obj;
+}
+
+export function isError(obj: unknown): obj is Error {
+    return obj instanceof Error;
+}
+
+export function assertNever(_obj: never, errorMessage?: string): never {
+    throw new Error(errorMessage || "Shouldn't reach here.");
+}
+
+export type KeyOfArray<T> = TupleUnion<keyof T>;
+export type TupleUnion<U extends PropertyKey, R extends PropertyKey[] = []> = {

Review Comment:
   I admit I did not parse (not even mention understanding) this declaration. 
Could it be explained in the comment somehow ?



##########
java/java.lsp.server/vscode/src/propertiesView/script.ts:
##########
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { provideVSCodeDesignSystem, vsCodeButton, vsCodeTextField, 
vsCodeDivider, vsCodeCheckbox, Button, TextField, Checkbox } from 
"@vscode/webview-ui-toolkit";
+import { isError, asClass, isClass } from "../typesUtil";
+import { CommandKey, Message, MessageProp } from "./controlTypes";
+
+provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeTextField(), 
vsCodeDivider(), vsCodeCheckbox());
+
+const vscode = acquireVsCodeApi();
+document.addEventListener("DOMContentLoaded", () => {
+    try {
+        asClass(Button, document.getElementById('save'), "Not save.")

Review Comment:
   Does the error message indicate that element identified by 'save' is not a 
Button ?



##########
java/java.lsp.server/vscode/src/propertiesView/controlTypes.ts:
##########
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { EnumType, KeyOfArray, Typed } from "../typesUtil";
+
+export type ID = number;
+
+export const PropTypes: KeyOfArray<PropTypeMap> = [
+    "java.lang.String",
+    "java.lang.Boolean",
+    "java.util.Properties",
+    "unknown"
+];// unfortunate but necessary duplication
+export type PropTypeMap = {
+    "java.lang.String": string;
+    "java.lang.Boolean": boolean;
+    "java.util.Properties": Record<string, string>;
+    "unknown": unknown
+};
+export type Property<T extends keyof PropTypeMap = keyof PropTypeMap> = T 
extends T ? {
+    propPref: boolean;

Review Comment:
   why all the `prop` prefixes ? Inconsistent naming: `dispName` vs. `shortName`



##########
java/java.lsp.server/vscode/src/propertiesView/controlTypes.ts:
##########
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { EnumType, KeyOfArray, Typed } from "../typesUtil";
+
+export type ID = number;
+
+export const PropTypes: KeyOfArray<PropTypeMap> = [
+    "java.lang.String",
+    "java.lang.Boolean",
+    "java.util.Properties",
+    "unknown"
+];// unfortunate but necessary duplication
+export type PropTypeMap = {
+    "java.lang.String": string;
+    "java.lang.Boolean": boolean;
+    "java.util.Properties": Record<string, string>;
+    "unknown": unknown
+};
+export type Property<T extends keyof PropTypeMap = keyof PropTypeMap> = T 
extends T ? {
+    propPref: boolean;
+    propDispName: string;
+    propShortName: string;
+    propWrite: boolean;
+    propHidden: boolean;
+    propExpert: boolean;
+    propType: T;

Review Comment:
   OK, I think I have deciphered the constructed type `PropTypeMap` ensures the 
type for `propValue` is determined by the value of `propType` ... but given 
that there's a `switch` over `propType` value anyway, with an imperative 
decision on `propValue` interpretation ....
   
   ... isn't it a little overkill, however fancy ? If this type bind relaxes a 
little, all the `KeyOfArray`, `TupleUnion` can go away.



##########
java/java.lsp.server/vscode/src/propertiesView/controlTypes.ts:
##########
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { EnumType, KeyOfArray, Typed } from "../typesUtil";
+
+export type ID = number;
+
+export const PropTypes: KeyOfArray<PropTypeMap> = [
+    "java.lang.String",
+    "java.lang.Boolean",
+    "java.util.Properties",
+    "unknown"
+];// unfortunate but necessary duplication
+export type PropTypeMap = {
+    "java.lang.String": string;
+    "java.lang.Boolean": boolean;
+    "java.util.Properties": Record<string, string>;
+    "unknown": unknown
+};
+export type Property<T extends keyof PropTypeMap = keyof PropTypeMap> = T 
extends T ? {
+    propPref: boolean;
+    propDispName: string;
+    propShortName: string;
+    propWrite: boolean;
+    propHidden: boolean;
+    propExpert: boolean;
+    propType: T;
+    propValue: PropTypeMap[T];
+    propName: string;
+} : never; // Distributive type
+export type Properties = {
+    propPref: boolean;
+    propDispName: string;
+    propShortName: string;
+    propHidden: boolean;
+    propExpert: boolean;
+    propName: string;
+    props: Property[];
+};
+
+export type MessageProp = {
+    name: string;
+    value: string | boolean | Record<string, string>;
+};
+
+export type Command = "Save" | "Cancel" | "Error" | "Info";

Review Comment:
   I've just tried a different approach: declare a string-valued enum-like with 
`Save`, 'Cancel` ... members, make an union `CommandType` type:
   ```
   export const CommandKey = {
       Save : "Save",
       Error : "Error",
       Info : "Info",
       Cancel : "Cancel"
   } as const;
   
   export type CommandType = (typeof CommandKey)[keyof typeof CommandKey];
   
   export type MessageCommon<T extends CommandType> = Typed<T>;
   ```
   (rest of code unchanged) ... but I am not entirely sure about benefits of 
such approach, it does not save much.
   But I'd at least suggest to make the union type should use an `CommandKey` 
enum's values, to save string duplication:
   
   ```
   export enum CommandKey {
       Save = "Save",
       Error = "Error",
       Info = "Info",
       Cancel = "Cancel"
   }
   
   export type Command = CommandKey.Save | CommandKey.Cancel | CommandKey.Error 
| CommandKey.Info;
   ```



##########
java/java.lsp.server/vscode/src/propertiesView/script.ts:
##########
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { provideVSCodeDesignSystem, vsCodeButton, vsCodeTextField, 
vsCodeDivider, vsCodeCheckbox, Button, TextField, Checkbox } from 
"@vscode/webview-ui-toolkit";
+import { isError, asClass, isClass } from "../typesUtil";
+import { CommandKey, Message, MessageProp } from "./controlTypes";
+
+provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeTextField(), 
vsCodeDivider(), vsCodeCheckbox());
+
+const vscode = acquireVsCodeApi();
+document.addEventListener("DOMContentLoaded", () => {
+    try {
+        asClass(Button, document.getElementById('save'), "Not save.")
+            .addEventListener('click', () => {
+                try {
+                    if (validate())
+                        sendMessage({ _type: CommandKey.Save, properties: 
getProperties() });
+                } catch (e: unknown) {
+                    handleError(e);
+                }
+            });
+        asClass(Button, document.getElementById('cancel'), "Not cancel.")
+            .addEventListener('click', () => {
+                sendMessage({ _type: CommandKey.Cancel });
+            });
+    } catch (e: unknown) {
+        handleError(e);
+    }
+});
+
+function handleError(error: unknown) {
+    if (isError(error))
+        sendMessage({ _type: CommandKey.Error, error: error.message, stack: 
error.stack });
+    else
+        sendMessage({ _type: CommandKey.Error, error: JSON.stringify(error) });
+}
+
+function sendMessage(message: Message) {
+    vscode.postMessage(message);
+}
+
+function getProperties(): MessageProp[] {
+    const out: MessageProp[] = [];
+    const elements = document.getElementsByName("input");
+    for (let i = 0; i < elements.length; ++i) {
+        const element = elements.item(i);
+        if (element)
+            out.push(getProperty(element));
+    }
+    return out;
+}
+
+function getProperty(element: HTMLElement): MessageProp {
+    if (isClass(TextField, element)) {
+        return makeProperty(element.value, element?.id);
+    } else if (isClass(Checkbox, element)) {
+        return makeProperty(element.checked, element?.id);
+    } else if (isClass(HTMLTableElement, element)) {
+        return makeProperty(parseProperties(element), element?.id);
+    }
+    return { name: element?.id || "", value: element?.nodeValue || "" };
+}
+
+function makeProperty(value: string | boolean | Record<string, string>, name?: 
string): MessageProp {
+    return { name: name || "NoID", value: value };

Review Comment:
   here, the default property name is not consistent with the default name on 
line 64. OK ?



##########
java/java.lsp.server/vscode/src/propertiesView/script.ts:
##########
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { provideVSCodeDesignSystem, vsCodeButton, vsCodeTextField, 
vsCodeDivider, vsCodeCheckbox, Button, TextField, Checkbox } from 
"@vscode/webview-ui-toolkit";
+import { isError, asClass, isClass } from "../typesUtil";
+import { CommandKey, Message, MessageProp } from "./controlTypes";
+
+provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeTextField(), 
vsCodeDivider(), vsCodeCheckbox());
+
+const vscode = acquireVsCodeApi();
+document.addEventListener("DOMContentLoaded", () => {
+    try {
+        asClass(Button, document.getElementById('save'), "Not save.")
+            .addEventListener('click', () => {
+                try {
+                    if (validate())
+                        sendMessage({ _type: CommandKey.Save, properties: 
getProperties() });
+                } catch (e: unknown) {
+                    handleError(e);
+                }
+            });
+        asClass(Button, document.getElementById('cancel'), "Not cancel.")
+            .addEventListener('click', () => {
+                sendMessage({ _type: CommandKey.Cancel });
+            });
+    } catch (e: unknown) {
+        handleError(e);
+    }
+});
+
+function handleError(error: unknown) {
+    if (isError(error))
+        sendMessage({ _type: CommandKey.Error, error: error.message, stack: 
error.stack });
+    else
+        sendMessage({ _type: CommandKey.Error, error: JSON.stringify(error) });
+}
+
+function sendMessage(message: Message) {
+    vscode.postMessage(message);
+}
+
+function getProperties(): MessageProp[] {
+    const out: MessageProp[] = [];
+    const elements = document.getElementsByName("input");
+    for (let i = 0; i < elements.length; ++i) {
+        const element = elements.item(i);
+        if (element)
+            out.push(getProperty(element));
+    }
+    return out;
+}
+
+function getProperty(element: HTMLElement): MessageProp {
+    if (isClass(TextField, element)) {
+        return makeProperty(element.value, element?.id);
+    } else if (isClass(Checkbox, element)) {
+        return makeProperty(element.checked, element?.id);
+    } else if (isClass(HTMLTableElement, element)) {
+        return makeProperty(parseProperties(element), element?.id);
+    }
+    return { name: element?.id || "", value: element?.nodeValue || "" };
+}
+
+function makeProperty(value: string | boolean | Record<string, string>, name?: 
string): MessageProp {
+    return { name: name || "NoID", value: value };
+}
+
+function parseProperties(table: HTMLTableElement): Record<string, string> {
+    const out: Record<string, string> = {};
+    for (let i = 0; i < table.rows.length; ++i) {
+        readProperty(out, table.rows.item(i)?.cells);
+    }
+    return out;
+}
+
+function readProperty(out: Record<string, string>, cells?: 
HTMLCollectionOf<HTMLTableCellElement> | null) {
+    out[asClass(TextField, 
cells?.item(0)?.getElementsByClassName("name").item(0)).value]

Review Comment:
   Q: Doesn't it make sense to guard with `if` when `cells` is undefined ? How 
would `asClass` behave of undefined/null ?



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);

Review Comment:
   This shouldn't happen (dispatch to a handler that did not declare the 
command): error or a log ?



##########
java/java.lsp.server/vscode/src/propertiesView/controlTypes.ts:
##########
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at 
https://oss.oracle.com/licenses/upl.
+ */
+import { EnumType, KeyOfArray, Typed } from "../typesUtil";
+
+export type ID = number;
+
+export const PropTypes: KeyOfArray<PropTypeMap> = [
+    "java.lang.String",
+    "java.lang.Boolean",
+    "java.util.Properties",
+    "unknown"
+];// unfortunate but necessary duplication
+export type PropTypeMap = {
+    "java.lang.String": string;
+    "java.lang.Boolean": boolean;
+    "java.util.Properties": Record<string, string>;
+    "unknown": unknown
+};
+export type Property<T extends keyof PropTypeMap = keyof PropTypeMap> = T 
extends T ? {
+    propPref: boolean;
+    propDispName: string;
+    propShortName: string;
+    propWrite: boolean;
+    propHidden: boolean;
+    propExpert: boolean;
+    propType: T;
+    propValue: PropTypeMap[T];
+    propName: string;
+} : never; // Distributive type
+export type Properties = {
+    propPref: boolean;
+    propDispName: string;
+    propShortName: string;
+    propHidden: boolean;
+    propExpert: boolean;
+    propName: string;
+    props: Property[];
+};
+
+export type MessageProp = {
+    name: string;
+    value: string | boolean | Record<string, string>;
+};
+
+export type Command = "Save" | "Cancel" | "Error" | "Info";
+export const CommandKey: EnumType<Command> = {
+    Save: "Save",
+    Error: "Error",
+    Info: "Info",
+    Cancel: "Cancel"
+};
+
+export type MessageCommon<T extends Command> = Typed<T>;

Review Comment:
   Why using type aliases and their unions is more desirable than using 
`interface`s and their inheritance ?



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);
+        }
+        TreeNodeRegistry r = 
Lookup.getDefault().lookup(TreeNodeRegistry.class);
+        if (r == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        int nodeId = ((JsonPrimitive) arguments.get(0)).getAsInt();
+        TreeViewProvider nodeProvider = r.providerOf(nodeId);
+        Node node = null;
+        if (nodeProvider != null) {
+            node = nodeProvider.findNode(nodeId);
+        }
+        if (node == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        boolean getProps = COMMAND_GET_NODE_PROPERTIES.equals(command);
+        Node.PropertySet[] propertySets = node.getPropertySets();
+
+        if (getProps) {
+            return 
CompletableFuture.completedFuture(getAllPropertiesMap(propertySets));
+        }
+        if (arguments.size() == 2) {
+            Object propJson = arguments.get(1);
+            if (propJson instanceof JsonNull) {
+                return CompletableFuture.completedFuture(null);
+            }
+            List m = gson.fromJson((JsonElement) propJson, List.class);
+            setAllProperties(propertySets, m);
+        }
+        return CompletableFuture.completedFuture(null);

Review Comment:
   Should the client really receive the same response for for malformed set 
command (no 2nd arg) and for successful set (and if other 
`completedFuture(null)`s are not fixed, for node-not-found, tree-not-supported 
conditions as well) ?



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);

Review Comment:
   Malformed command: shouldn't it throw or at least log ?



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);
+        }
+        TreeNodeRegistry r = 
Lookup.getDefault().lookup(TreeNodeRegistry.class);
+        if (r == null) {
+            return CompletableFuture.completedFuture(null);

Review Comment:
   Not sure if 'tree service unavailable' is a valid state of the system; if 
not, throw an error.



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);
+        }
+        TreeNodeRegistry r = 
Lookup.getDefault().lookup(TreeNodeRegistry.class);
+        if (r == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        int nodeId = ((JsonPrimitive) arguments.get(0)).getAsInt();
+        TreeViewProvider nodeProvider = r.providerOf(nodeId);
+        Node node = null;
+        if (nodeProvider != null) {
+            node = nodeProvider.findNode(nodeId);
+        }
+        if (node == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        boolean getProps = COMMAND_GET_NODE_PROPERTIES.equals(command);
+        Node.PropertySet[] propertySets = node.getPropertySets();
+
+        if (getProps) {
+            return 
CompletableFuture.completedFuture(getAllPropertiesMap(propertySets));
+        }
+        if (arguments.size() == 2) {
+            Object propJson = arguments.get(1);
+            if (propJson instanceof JsonNull) {
+                return CompletableFuture.completedFuture(null);
+            }
+            List m = gson.fromJson((JsonElement) propJson, List.class);
+            setAllProperties(propertySets, m);
+        }
+        return CompletableFuture.completedFuture(null);
+    }
+
+    Map<String, ?>[] getAllPropertiesMap(Node.PropertySet[] propertySets) {
+        Map<String, Object>[] allPropertiesMap = new Map[propertySets.length];
+
+        for (int i = 0; i < propertySets.length; i++) {
+            Map<String, Object> propertiesMap = new HashMap<>();
+            Node.PropertySet ps = propertySets[i];
+            propertiesMap.put(PROP_NAME, ps.getName());
+            propertiesMap.put(PROP_DNAME, ps.getDisplayName());
+            propertiesMap.put(PROP_HTML_NAME, ps.getHtmlDisplayName());
+            propertiesMap.put(PROP_SHORT_NAME, ps.getShortDescription());
+            propertiesMap.put(PROP_PREF, ps.isPreferred());
+            propertiesMap.put(PROP_EXPERT, ps.isExpert());
+            propertiesMap.put(PROP_HIDDEN, ps.isHidden());
+            propertiesMap.put(PROPS, getProperties(ps.getProperties()));
+            allPropertiesMap[i] = propertiesMap;
+        }
+        return allPropertiesMap;
+    }
+
+    private Map<String, Object>[] getProperties(Node.Property<?>[] properties) 
{
+        Map<String, Object>[] props = new Map[properties.length];
+        for (int i = 0; i < properties.length; i++) {
+            Map<String, Object> propMap = new HashMap<>();
+            Node.Property<?> property = properties[i];
+            if (property.canRead()) {
+                propMap.put(PROP_DNAME, property.getDisplayName());
+                propMap.put(PROP_HTML_NAME, property.getHtmlDisplayName());
+                propMap.put(PROP_SHORT_NAME, property.getShortDescription());
+                propMap.put(PROP_NAME, property.getName());
+                propMap.put(PROP_PREF, property.isPreferred());
+                propMap.put(PROP_EXPERT, property.isExpert());
+                propMap.put(PROP_HIDDEN, property.isHidden());
+                propMap.put(PROP_CAN_WRITE, property.canWrite());
+                propMap.put(PROP_VAL_TYPE, property.getValueType().getName());
+                propMap.put(PROP_VALUE, getPropertyValue(property));
+                props[i] = propMap;
+            }
+        }
+        return props;
+    }
+
+    private Object getPropertyValue(Node.Property<?> prop) {
+        try {
+            return prop.getValue();
+        } catch (IllegalAccessException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        return null;
+    }
+
+    private void setAllProperties(Node.PropertySet[] propertySets, List m) {
+        assert m.size() <= propertySets.length;
+
+        for (int i = 0; i < m.size(); i++) {
+            Map pm = (Map) m.get(i);
+            Node.PropertySet p = propertySets[i];
+            setProperties(p.getProperties(), (List<Map<String, Object>>) 
pm.get(PROPS));
+        }
+    }
+
+    private void setProperties(Node.Property[] properties, List<Map<String, 
Object>> props) {
+        Map<String, Node.Property> names = new HashMap<>();
+
+        for (Node.Property p : properties) {
+            names.put(p.getName(), p);
+        }
+        for (Map pm : props) {
+            Node.Property prop = names.get((String) pm.get(PROP_NAME));
+            if (prop != null && prop.canWrite()) {
+                try {
+                    Object val = pm.get(PROP_VALUE);
+                    Object oldVal = prop.getValue();
+
+                    if (!Objects.equals(val, oldVal)) {
+                        prop.setValue(val);
+                    }
+                } catch (IllegalAccessException ex) {
+                    Exceptions.printStackTrace(ex);

Review Comment:
   Maybe there could be a way how to report a failed set to the client.



##########
java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/explorer/NodePropertiesProvider.java:
##########
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.lsp.server.explorer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.CodeAction;
+import org.eclipse.lsp4j.CodeActionParams;
+import org.netbeans.modules.java.lsp.server.protocol.CodeActionsProvider;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.openide.nodes.Node;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Tomas Hurka
+ */
+@ServiceProvider(service = CodeActionsProvider.class)
+public class NodePropertiesProvider extends CodeActionsProvider {
+
+    private static final String COMMAND_GET_NODE_PROPERTIES = 
"java.node.properties.get";      // NOI18N
+    private static final String COMMAND_SET_NODE_PROPERTIES = 
"java.node.properties.set";      // NOI18N
+
+    private static final String PROP_NAME = "propName";      // NOI18N
+    private static final String PROP_DNAME = "propDispName";      // NOI18N
+    private static final String PROP_HTML_NAME = "propHtmlName";      // NOI18N
+    private static final String PROP_SHORT_NAME = "propShortName";      // 
NOI18N
+    private static final String PROP_PREF = "propPref";      // NOI18N
+    private static final String PROP_EXPERT = "propExpert";      // NOI18N
+    private static final String PROP_HIDDEN = "propHidden";      // NOI18N
+    private static final String PROP_CAN_READ = "propRead";      // NOI18N
+    private static final String PROP_CAN_WRITE = "propWrite";      // NOI18N
+    private static final String PROP_VAL_TYPE = "propType";      // NOI18N
+    private static final String PROP_VALUE = "propValue";      // NOI18N
+    private static final String PROPS = "props";      // NOI18N
+
+    private static final Set<String> COMMANDS = new HashSet<>(Arrays.asList(
+            COMMAND_GET_NODE_PROPERTIES, COMMAND_SET_NODE_PROPERTIES
+    ));
+
+    private final Gson gson = new Gson();
+
+    @Override
+    public List<CodeAction> getCodeActions(ResultIterator resultIterator, 
CodeActionParams params) throws Exception {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public CompletableFuture<Object> processCommand(NbCodeLanguageClient 
client, String command, List<Object> arguments) {
+        if (!COMMANDS.contains(command)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        if (arguments == null || arguments.isEmpty()) {
+            return CompletableFuture.completedFuture(null);
+        }
+        TreeNodeRegistry r = 
Lookup.getDefault().lookup(TreeNodeRegistry.class);
+        if (r == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        int nodeId = ((JsonPrimitive) arguments.get(0)).getAsInt();
+        TreeViewProvider nodeProvider = r.providerOf(nodeId);
+        Node node = null;
+        if (nodeProvider != null) {
+            node = nodeProvider.findNode(nodeId);
+        }
+        if (node == null) {
+            return CompletableFuture.completedFuture(null);
+        }
+        boolean getProps = COMMAND_GET_NODE_PROPERTIES.equals(command);
+        Node.PropertySet[] propertySets = node.getPropertySets();
+
+        if (getProps) {
+            return 
CompletableFuture.completedFuture(getAllPropertiesMap(propertySets));
+        }
+        if (arguments.size() == 2) {
+            Object propJson = arguments.get(1);
+            if (propJson instanceof JsonNull) {
+                return CompletableFuture.completedFuture(null);
+            }
+            List m = gson.fromJson((JsonElement) propJson, List.class);
+            setAllProperties(propertySets, m);
+        }
+        return CompletableFuture.completedFuture(null);
+    }
+
+    Map<String, ?>[] getAllPropertiesMap(Node.PropertySet[] propertySets) {
+        Map<String, Object>[] allPropertiesMap = new Map[propertySets.length];
+
+        for (int i = 0; i < propertySets.length; i++) {
+            Map<String, Object> propertiesMap = new HashMap<>();
+            Node.PropertySet ps = propertySets[i];
+            propertiesMap.put(PROP_NAME, ps.getName());
+            propertiesMap.put(PROP_DNAME, ps.getDisplayName());
+            propertiesMap.put(PROP_HTML_NAME, ps.getHtmlDisplayName());
+            propertiesMap.put(PROP_SHORT_NAME, ps.getShortDescription());
+            propertiesMap.put(PROP_PREF, ps.isPreferred());
+            propertiesMap.put(PROP_EXPERT, ps.isExpert());
+            propertiesMap.put(PROP_HIDDEN, ps.isHidden());
+            propertiesMap.put(PROPS, getProperties(ps.getProperties()));
+            allPropertiesMap[i] = propertiesMap;
+        }
+        return allPropertiesMap;
+    }
+
+    private Map<String, Object>[] getProperties(Node.Property<?>[] properties) 
{
+        Map<String, Object>[] props = new Map[properties.length];
+        for (int i = 0; i < properties.length; i++) {
+            Map<String, Object> propMap = new HashMap<>();
+            Node.Property<?> property = properties[i];
+            if (property.canRead()) {

Review Comment:
   This will make some entries of the returned array `null`, client having to 
check for null/undefined there. Wouldn't it be better to accumulate in a List 
and turn to array at the end ?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists

Reply via email to