This is an automated email from the ASF dual-hosted git repository.

sdedic pushed a commit to branch sdedic/feature/project-dependency-add_base2
in repository https://gitbox.apache.org/repos/asf/netbeans.git

commit e72a5b8ee52166e2042082414f486fbe2cd74687
Author: Svata Dedic <svatopluk.de...@oracle.com>
AuthorDate: Thu Dec 14 20:17:15 2023 +0100

    LSP change dependency command added.
---
 .../dependency/ProjectModificationResult.java      |  25 ++-
 .../impl/ProjectModificationResultImpl.java        |  21 ++-
 .../dependency/impl/WorkspaceEditAdapter.java      |  20 +++
 .../dependency/spi/ProjectDependencyModifier.java  |  13 ++
 .../spi/ProjectReloadImplementation.java           |  65 ++++++++
 .../nbcode/integration/nbproject/project.xml       |   8 +
 .../modules/nbcode/integration/ExtraGsonSetup.java | 172 +++++++++++++++++++--
 .../commands/LspDependencyChangeRequest.java       |  61 ++++++++
 .../commands/LspDependencyChangeResult.java        |  36 +++++
 .../commands/ProjectDependenciesCommand.java       |  97 +++++++++++-
 .../netbeans/modules/java/lsp/server/Utils.java    |  43 ++++++
 11 files changed, 539 insertions(+), 22 deletions(-)

diff --git 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectModificationResult.java
 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectModificationResult.java
index 6f1197c529..32440e8bc4 100644
--- 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectModificationResult.java
+++ 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/ProjectModificationResult.java
@@ -20,9 +20,12 @@ package org.netbeans.modules.project.dependency;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
+import org.netbeans.api.actions.Savable;
 import org.netbeans.api.lsp.WorkspaceEdit;
 import 
org.netbeans.modules.project.dependency.impl.ProjectModificationResultImpl;
 import org.netbeans.modules.project.dependency.impl.WorkspaceEditAdapter;
@@ -42,6 +45,13 @@ public final class ProjectModificationResult implements 
ModificationResult {
         this.impl = impl;
     }
     
+    /**
+     * @return files that should be save in order so that build system can 
recognize changes.
+     */
+    public Collection<FileObject> getFilesToSave() {
+        return impl.getFilesToSave();
+    }
+    
     /**
      * Describes the details of the workspace edit.
      * @return details of the edit
@@ -62,9 +72,9 @@ public final class ProjectModificationResult implements 
ModificationResult {
         return wrapEdits().getResultingSource(file);
     }
     
-    private ModificationResult wrapEdits;
+    private WorkspaceEditAdapter wrapEdits;
     
-    ModificationResult wrapEdits() {
+    WorkspaceEditAdapter wrapEdits() {
         if (wrapEdits == null) {
             wrapEdits = new WorkspaceEditAdapter(impl);
         }
@@ -95,9 +105,18 @@ public final class ProjectModificationResult implements 
ModificationResult {
 
     @Override
     public void commit() throws IOException {
-        wrapEdits().commit();
+        WorkspaceEditAdapter r = wrapEdits();
+        r.commit();
         if (impl.getCustomEdit() != null) {
             impl.getCustomEdit().commit();
         }
+        // save the modified files, so project system will pick things up.
+        // PENDING: make optional, at the discretion of 
ProjectDependencyModifier.
+        for (FileObject f : r.getFilesToSave()) {
+            Savable s = f.getLookup().lookup(Savable.class);
+            if (s != null) {
+                s.save();
+            }
+        }
     }
 }
diff --git 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/ProjectModificationResultImpl.java
 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/ProjectModificationResultImpl.java
index 7e762d9b47..09f231e943 100644
--- 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/ProjectModificationResultImpl.java
+++ 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/ProjectModificationResultImpl.java
@@ -23,12 +23,15 @@ import java.net.URL;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.netbeans.api.lsp.ResourceOperation;
 import org.netbeans.api.lsp.TextDocumentEdit;
 import org.netbeans.api.lsp.TextEdit;
@@ -50,6 +53,7 @@ import org.openide.util.Union2;
 public class ProjectModificationResultImpl {
     private final Project project;
     
+    private Set<FileObject> toSave = new LinkedHashSet<>();
     private List<ModificationResult>    customModifications = new 
ArrayList<>();
     private List<Union2<TextDocumentEdit, ResourceOperation>> edits;
     private ModificationResult combinedResult;
@@ -90,11 +94,16 @@ public class ProjectModificationResultImpl {
         if (r.getWorkspaceEdit() == null) {
             return;
         }
+        Collection<FileObject> save = r.requiresSave();
+        boolean saveAll = save == ProjectDependencyModifier.Result.SAVE_ALL;
+        if (save != null && !saveAll) {
+            toSave.addAll(save);
+        }
         for (Union2<TextDocumentEdit, ResourceOperation> op : 
r.getWorkspaceEdit().getDocumentChanges()) {
             if (op.hasSecond()) {
                 addResourceOperation(op.second());
             } else if (op.hasFirst()) {
-                addTextOperation(op.first());
+                addTextOperation(op.first(), saveAll);
             }
         }
     }
@@ -190,7 +199,11 @@ public class ProjectModificationResultImpl {
         };
     }
     
-    private void addTextOperation(TextDocumentEdit edit) {
+    public Collection<FileObject> getFilesToSave() {
+        return toSave;
+    }
+    
+    private void addTextOperation(TextDocumentEdit edit, boolean saveAll) {
         FileObject fo = fromString(edit.getDocument());
         if (fo == null) {
             throw new ProjectOperationException(project, 
ProjectOperationException.State.ERROR, 
@@ -203,7 +216,9 @@ public class ProjectModificationResultImpl {
                         Bundle.ERR_WritingToMissingFile(edit.getDocument()), 
Collections.emptySet());
             }
         }
-        
+        if (saveAll) {
+            toSave.add(fo);
+        }
         TextDocumentEdit tde = fileModifications.get(fo);
         List<TextEdit> newEdits = new ArrayList<>(edit.getEdits());
         Collections.sort(newEdits,  textEditComparator(edit.getEdits()));
diff --git 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/WorkspaceEditAdapter.java
 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/WorkspaceEditAdapter.java
index d758910add..1b0b08d3f3 100644
--- 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/WorkspaceEditAdapter.java
+++ 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/impl/WorkspaceEditAdapter.java
@@ -20,8 +20,10 @@ package org.netbeans.modules.project.dependency.impl;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.netbeans.api.lsp.ResourceOperation;
@@ -30,6 +32,7 @@ import org.netbeans.api.lsp.WorkspaceEdit;
 import org.netbeans.modules.refactoring.spi.ModificationResult;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.FileUtil;
+import org.openide.filesystems.URLMapper;
 import org.openide.util.NbBundle;
 import org.openide.util.Union2;
 
@@ -44,6 +47,23 @@ public final class WorkspaceEditAdapter implements 
ModificationResult {
     public WorkspaceEditAdapter(ProjectModificationResultImpl impl) {
         this.impl = impl;
     }
+    
+    public Collection<FileObject> getFilesToSave() {
+        List<FileObject> processed = new ArrayList<>();
+        for (FileObject f : impl.getFilesToSave()) {
+            if (f.isVirtual()) {
+                FileObject changed = URLMapper.findFileObject(f.toURL());
+                if (changed == null) {
+                    continue;
+                }
+                f = changed;
+            }
+            if (f.isValid()) {
+                processed.add(f);
+            }
+        }
+        return processed;
+    }
 
     @Override
     public String getResultingSource(FileObject file) throws IOException, 
IllegalArgumentException {
diff --git 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectDependencyModifier.java
 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectDependencyModifier.java
index e9520e2253..5c1182eeee 100644
--- 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectDependencyModifier.java
+++ 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectDependencyModifier.java
@@ -18,10 +18,13 @@
  */
 package org.netbeans.modules.project.dependency.spi;
 
+import java.util.Collection;
+import java.util.Collections;
 import org.netbeans.api.lsp.WorkspaceEdit;
 import org.netbeans.modules.project.dependency.DependencyChangeException;
 import org.netbeans.modules.project.dependency.DependencyChangeRequest;
 import org.netbeans.modules.project.dependency.ProjectOperationException;
+import org.openide.filesystems.FileObject;
 
 /**
  * Computes dependency modifications to project files. Must be registered in 
the project's
@@ -45,6 +48,16 @@ public interface ProjectDependencyModifier {
      * Result of dependency modification change.
      */
     public interface Result {
+        public static final Collection<FileObject> SAVE_ALL = 
Collections.singleton(null);
+        
+        /**
+         * Returns list of files that require save.
+         * @return files to save.
+         */
+        public default Collection<FileObject> requiresSave() {
+            return SAVE_ALL;
+        }
+        
         /**
          * ID of the partial result. Mainly used to override / suppress 
unwanted changes by
          * more specific Modified implementations.
diff --git 
a/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectReloadImplementation.java
 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectReloadImplementation.java
new file mode 100644
index 0000000000..d3cdbbc830
--- /dev/null
+++ 
b/ide/project.dependency/src/org/netbeans/modules/project/dependency/spi/ProjectReloadImplementation.java
@@ -0,0 +1,65 @@
+/*
+ * 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.project.dependency.spi;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import org.netbeans.api.project.Project;
+import org.openide.filesystems.FileObject;
+
+/**
+ * Provides information on files affecting the project reload, and allows to 
reload project metadata.
+ * The project infrastructure usually monitors the on-disk file changes and 
manages background project reloads.
+ * But with programmatic changes to project files, it may be necessary to wait 
for the project reload to pick 
+ * the new project's metadata. The project infrastructure may not be able to 
pick in-memory document changes
+ * to the project settings; especially when invokes external tools such as 
Maven, Gradle etc. This interface
+ * also allows to collect project files, that should be saved before project 
reload can pick fresh data.
+ * @since 1.7
+ * @author sdedic
+ */
+public interface ProjectReloadImplementation {
+    /**
+     * Attempts to find the set of files. It will return FileObjects 
representing
+     * files that contain project's definition. The implementation may also 
indicate
+     * that it needs to sync project to disk in order to do project reload. If 
+     * `forProjectLoad` is true, then reported files should be saved before 
reloading
+     * the project, otherwise the project metadata can still contain obsolete 
info. Note
+     * that the set of files is computed from the current project's metadata, 
so if the
+     * unsaved change contains gross changes, such pas parent POM change, the 
reported set
+     * of files may not be complete. The report for project load may also 
contain
+     * files from other projects.
+     * <p/>
+     * Implementations, that can analyze in-memory state may return an empty 
set for this
+     * case.
+     * @param forProjectLoad if true, implementation should report files that 
must be
+     * saved before project load could load fresh information
+     * @return set of project files.
+     */
+    public Set<FileObject>  findProjectFiles(boolean forProjectLoad);
+    
+    /**
+     * Attempts to reload project metadata, to reflect the current project 
state. Note that
+     * the resulting Future may report an {@link IOException} instead of a 
Project instance in
+     * the case that the project loading fails.
+     * 
+     * @return a Future that will be completed when the project reloads.
+     */
+    public CompletableFuture<Project> reloadProject();
+}
diff --git a/java/java.lsp.server/nbcode/integration/nbproject/project.xml 
b/java/java.lsp.server/nbcode/integration/nbproject/project.xml
index debf5ec044..085c288f8f 100644
--- a/java/java.lsp.server/nbcode/integration/nbproject/project.xml
+++ b/java/java.lsp.server/nbcode/integration/nbproject/project.xml
@@ -145,6 +145,14 @@
                         
<specification-version>1.104.0.8</specification-version>
                     </run-dependency>
                 </dependency>
+                <dependency>
+                    
<code-name-base>org.netbeans.modules.refactoring.api</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.70.0.1</specification-version>
+                    </run-dependency>
+                </dependency>
                 <dependency>
                     
<code-name-base>org.netbeans.modules.updatecenters</code-name-base>
                     <run-dependency>
diff --git 
a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/ExtraGsonSetup.java
 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/ExtraGsonSetup.java
index 56ebb6e78a..04332e87f5 100644
--- 
a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/ExtraGsonSetup.java
+++ 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/ExtraGsonSetup.java
@@ -20,7 +20,9 @@ package org.netbeans.modules.nbcode.integration;
 
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
@@ -28,23 +30,40 @@ import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
 import java.lang.reflect.Type;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import org.netbeans.modules.java.lsp.server.LspGsonSetup;
 import org.netbeans.modules.project.dependency.ArtifactSpec;
 import org.netbeans.modules.project.dependency.Dependency;
+import org.netbeans.modules.project.dependency.DependencyChange;
+import org.netbeans.modules.project.dependency.DependencyChangeRequest;
 import org.netbeans.modules.project.dependency.Scope;
+import org.netbeans.modules.project.dependency.Scopes;
 import org.openide.util.lookup.ServiceProvider;
 
 /**
  * Adds some more type adapters for ArtifactSpec.
- * 
+ *
  * @author sdedic
  */
 @ServiceProvider(service = LspGsonSetup.class)
-public class ExtraGsonSetup implements LspGsonSetup{
+public class ExtraGsonSetup implements LspGsonSetup {
 
     private static final Set<String> ARTIFACT_BLOCK_FIELDS = new 
HashSet<>(Arrays.asList(
             "data" // NOI18N
@@ -67,7 +86,7 @@ public class ExtraGsonSetup implements LspGsonSetup{
                 } else if 
(Throwable.class.isAssignableFrom(fa.getDeclaredClass())) {
                     return DEPENDENCY_BLOCK_FIELDS.contains(fa.getName());
                 } else if (fa.getDeclaringClass() == Dependency.class) {
-                    
+
                 }
                 return false;
             }
@@ -79,29 +98,30 @@ public class ExtraGsonSetup implements LspGsonSetup{
         });
         b.registerTypeAdapter(ArtifactSpec.class, new ArtifactDeserializer());
         b.registerTypeAdapter(Scope.class, new ScopeSerializer());
+        b.registerTypeAdapter(Dependency.class, new DependencySerializer());
+        b.registerTypeAdapter(DependencyChangeRequest.class, 
(InstanceCreator)(t) -> new DependencyChangeRequest(Collections.emptyList()));
+        b.registerTypeAdapter(DependencyChange.class, (InstanceCreator)(t) -> 
DependencyChange.builder(DependencyChange.Kind.ADD).create());
+        b.registerTypeAdapterFactory(new LowercaseEnumTypeAdapterFactory());
     }
-    
-    class ScopeSerializer implements JsonSerializer<Scope> {
 
-        @Override
-        public JsonElement serialize(Scope t, Type type, 
JsonSerializationContext jsc) {
-            return jsc.serialize(t.name());
-        }
-    }
-    
-    
     class ArtifactDeserializer implements JsonDeserializer<ArtifactSpec> {
 
         @Override
         public ArtifactSpec deserialize(JsonElement je, Type type, 
JsonDeserializationContext jdc) throws JsonParseException {
+            if (je.isJsonNull()) {
+                return null;
+            } else if (je.isJsonPrimitive()) {
+                return deserializeArtifactFromString(je.getAsString());
+            } else if (!je.isJsonObject()) {
+                throw new JsonParseException("Expected artifact string or 
structure");
+            }
             JsonObject obj = je.getAsJsonObject();
             String g = obj.has("groupId") ? 
obj.getAsJsonPrimitive("groupId").getAsString() : null;
             String a = obj.has("artifactId") ? 
obj.getAsJsonPrimitive("artifactId").getAsString() : null;
             String v = obj.has("versionSpec") ? 
obj.getAsJsonPrimitive("versionSpec").getAsString() : null;
             String c = obj.has("classifier") ? 
obj.getAsJsonPrimitive("classifier").getAsString() : null;
             String t = obj.has("type") ? 
obj.getAsJsonPrimitive("type").getAsString() : null;
-            
-            
+
             ArtifactSpec.Builder b = ArtifactSpec.builder(g, a, v, 
null).classifier(c).type(t);
             if (v != null && v.contains("-SNAPSHOT")) {
                 b.versionKind(ArtifactSpec.VersionKind.SNAPSHOT);
@@ -109,4 +129,128 @@ public class ExtraGsonSetup implements LspGsonSetup{
             return b.build();
         }
     }
+    
+    class DependencyChangeDeserializer implements 
JsonDeserializer<DependencyChange> {
+        private final Type OPTION_SET_TYPE = new 
TypeToken<EnumSet<DependencyChange.Options>>() {}.getType(); 
+        @Override
+        public DependencyChange deserialize(JsonElement je, Type type, 
JsonDeserializationContext jdc) throws JsonParseException {
+            if (je.isJsonNull()) {
+                return null;
+            }
+            if (!je.isJsonObject()) {
+                throw new JsonParseException("Expected DependencyChange 
structure");
+            }
+            JsonObject o = je.getAsJsonObject();
+            
+            DependencyChange.Kind kind = 
jdc.deserialize(o.getAsJsonPrimitive("kind"), DependencyChange.Kind.class);
+            EnumSet<DependencyChange.Options> opts = 
jdc.deserialize(o.getAsJsonPrimitive("kind"), OPTION_SET_TYPE);
+            return null;
+        }
+    }
+    
+    private static ArtifactSpec deserializeArtifactFromString(String s) {
+        int scopeIndex = s.lastIndexOf('[');
+        if (scopeIndex > -1) {
+            s = s.substring(0, scopeIndex);
+        }
+        String[] parts = s.split(":");
+        boolean snap = parts.length > 2 && parts[2].endsWith("-SNAPSHOT");
+        ArtifactSpec spec;
+        if (snap) {
+            return ArtifactSpec.createSnapshotSpec(parts[0], parts[1], null, 
parts.length > 3 ? parts[3] : null, parts.length > 2 ? parts[2] : null, false, 
null, null);
+        } else {
+            return ArtifactSpec.createVersionSpec(parts[0], parts[1], null, 
parts.length > 3 ? parts[3] : null, parts.length > 2 ? parts[2] : null, false, 
null, null);
+        }
+    }
+
+    static class DependencySerializer implements JsonDeserializer<Dependency> {
+        private static final Type DEPENDENCY_LIST_TYPE = new 
TypeToken<List<Dependency>>() {}.getType();
+        @Override
+        public Dependency deserialize(JsonElement je, Type type, 
JsonDeserializationContext jdc) throws JsonParseException {
+            Scope scope = Scopes.COMPILE;
+            ArtifactSpec a;
+            
+            if (je.isJsonNull()) {
+                return null;
+            } else if (je.isJsonPrimitive()) {
+                // attempt to interpret the dependency as a string
+                String s = je.getAsString();
+                int scopeIndex = s.lastIndexOf('[');
+                if (scopeIndex > -1) {
+                    int end = s.indexOf(']', scopeIndex);
+                    if (end == -1) {
+                        end = s.length();
+                    }
+                    scope = Scope.named(s.substring(scopeIndex, end));
+                    s = s.substring(0, scopeIndex);
+                }
+                a = deserializeArtifactFromString(s);
+                return Dependency.make(a, scope);
+            } else if (!je.isJsonObject()) {
+                throw new JsonParseException("Expected dependency string or 
structure");
+            }
+            JsonObject o = je.getAsJsonObject();
+            a = jdc.deserialize(o.get("artifact"), ArtifactSpec.class);
+            List<Dependency> children = new ArrayList<>();
+            if (o.has("scope")) {
+                scope = jdc.deserialize(o.get("scope"), Scope.class);
+            }
+            if (o.has("children")) {
+                children = jdc.deserialize(o.getAsJsonArray("children"), 
DEPENDENCY_LIST_TYPE);
+            }
+            return Dependency.create(a, scope, children, null);
+        }
+    }
+
+    public class LowercaseEnumTypeAdapterFactory implements TypeAdapterFactory 
{
+
+        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+            Class<T> rawType = (Class<T>) type.getRawType();
+            if (!rawType.isEnum() || 
!type.getType().getTypeName().startsWith("org.netbeans.modules.project.dependency"))
 {
+                return null;
+            }
+
+            final Map<String, T> lowercaseToConstant = new HashMap<String, 
T>();
+            for (T constant : rawType.getEnumConstants()) {
+                lowercaseToConstant.put(toLowercase(constant), constant);
+            }
+
+            return new TypeAdapter<T>() {
+                public void write(JsonWriter out, T value) throws IOException {
+                    if (value == null) {
+                        out.nullValue();
+                    } else {
+                        out.value(toLowercase(value));
+                    }
+                }
+
+                public T read(JsonReader reader) throws IOException {
+                    if (reader.peek() == JsonToken.NULL) {
+                        reader.nextNull();
+                        return null;
+                    } else {
+                        return lowercaseToConstant.get(reader.nextString());
+                    }
+                }
+            };
+        }
+
+        private String toLowercase(Object o) {
+            return o.toString().toLowerCase(Locale.US);
+        }
+    }
+
+    public class ScopeSerializer implements JsonDeserializer<Scope>, 
JsonSerializer<Scope> {
+
+        @Override
+        public Scope deserialize(JsonElement je, Type type, 
JsonDeserializationContext jdc) throws JsonParseException {
+            return Scope.named(je.getAsString());
+        }
+
+        @Override
+        public JsonElement serialize(Scope t, Type type, 
JsonSerializationContext jsc) {
+            return jsc.serialize(t.name());
+        }
+    }
+
 }
diff --git 
a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/LspDependencyChangeRequest.java
 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/LspDependencyChangeRequest.java
new file mode 100644
index 0000000000..64f8e9e69a
--- /dev/null
+++ 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/LspDependencyChangeRequest.java
@@ -0,0 +1,61 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt 
to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit 
this template
+ */
+package org.netbeans.modules.nbcode.integration.commands;
+
+import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
+import org.eclipse.xtext.xbase.lib.Pure;
+import org.netbeans.modules.project.dependency.DependencyChangeRequest;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LspDependencyChangeRequest {
+    private String uri;
+    private boolean applyChanges;
+    private boolean saveFromServer = true;
+    private DependencyChangeRequest changes;
+    
+    public LspDependencyChangeRequest() {
+    }
+
+    @Pure
+    public boolean isSaveFromServer() {
+        return saveFromServer;
+    }
+
+    public void setSaveFromServer(boolean saveFromServer) {
+        this.saveFromServer = saveFromServer;
+    }
+    
+    @Pure
+    public boolean isApplyChanges() {
+        return applyChanges;
+    }
+
+    public void setApplyChanges(boolean applyChanges) {
+        this.applyChanges = applyChanges;
+    }
+
+    @Pure
+    @NonNull
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+
+    @Pure
+    @NonNull
+    public DependencyChangeRequest getChanges() {
+        return changes;
+    }
+
+    public void setChanges(DependencyChangeRequest changes) {
+        this.changes = changes;
+    }
+}
diff --git 
a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/LspDependencyChangeResult.java
 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/LspDependencyChangeResult.java
new file mode 100644
index 0000000000..51cf2052c6
--- /dev/null
+++ 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/LspDependencyChangeResult.java
@@ -0,0 +1,36 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt 
to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit 
this template
+ */
+package org.netbeans.modules.nbcode.integration.commands;
+
+import java.util.List;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.eclipse.xtext.xbase.lib.Pure;
+
+/**
+ *
+ * @author sdedic
+ */
+public class LspDependencyChangeResult {
+    private WorkspaceEdit   edit;
+    private List<String> modifiedUris;
+
+    @Pure
+    public WorkspaceEdit getEdit() {
+        return edit;
+    }
+
+    public void setEdit(WorkspaceEdit edit) {
+        this.edit = edit;
+    }
+
+    @Pure
+    public List<String> getModifiedUris() {
+        return modifiedUris;
+    }
+
+    public void setModifiedUris(List<String> modifiedUris) {
+        this.modifiedUris = modifiedUris;
+    }
+}
diff --git 
a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectDependenciesCommand.java
 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectDependenciesCommand.java
index 6a6594f096..d1b12742fa 100644
--- 
a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectDependenciesCommand.java
+++ 
b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/commands/ProjectDependenciesCommand.java
@@ -19,7 +19,9 @@
 package org.netbeans.modules.nbcode.integration.commands;
 
 import com.google.gson.Gson;
+import java.io.IOException;
 import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -29,19 +31,31 @@ import java.util.List;
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.MessageType;
+import org.netbeans.api.lsp.WorkspaceEdit;
 import org.netbeans.api.project.FileOwnerQuery;
 import org.netbeans.api.project.Project;
+import org.netbeans.modules.java.lsp.server.LspServerState;
+import org.netbeans.modules.java.lsp.server.LspServerUtils;
 import org.netbeans.modules.java.lsp.server.Utils;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
+import org.netbeans.modules.java.lsp.server.protocol.SaveDocumentRequestParams;
 import org.netbeans.modules.project.dependency.ArtifactSpec;
 import org.netbeans.modules.project.dependency.Dependency;
+import org.netbeans.modules.project.dependency.DependencyChangeException;
 import org.netbeans.modules.project.dependency.DependencyResult;
 import org.netbeans.modules.project.dependency.ProjectDependencies;
+import org.netbeans.modules.project.dependency.ProjectModificationResult;
 import org.netbeans.modules.project.dependency.ProjectOperationException;
 import org.netbeans.modules.project.dependency.Scope;
 import org.netbeans.spi.lsp.CommandProvider;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.URLMapper;
+import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
+import org.openide.util.NbBundle;
 import org.openide.util.RequestProcessor;
 import org.openide.util.lookup.ServiceProvider;
 
@@ -53,7 +67,11 @@ import org.openide.util.lookup.ServiceProvider;
 public class ProjectDependenciesCommand implements CommandProvider {
     
     private static final RequestProcessor RP = new 
RequestProcessor(ProjectDependenciesCommand.class.getName(), 5);
-                                                            
+                                      
+    /**
+     * Finds dependencies in a project. The command expects {@link 
DependencyFindRequest} as a sole input, and produces
+     * {@link DependencyFindResult} as the output. Throws an exception if the 
operation fails.
+     */
     private static final String COMMAND_GET_DEPENDENCIES = 
"nbls.project.dependencies.find";
     private static final String COMMAND_CHANGE_DEPENDENCIES = 
"nbls.project.dependencies.change";
     
@@ -94,10 +112,18 @@ public class ProjectDependenciesCommand implements 
CommandProvider {
         return inst != null ? inst : gson;
     }
 
+    @NbBundle.Messages({
+        "# {0} - file uri",
+        "ERR_FileNotInProject=File {0} is not in any project.",
+        "# {0} - file uri",
+        "ERR_InvalidFileUri=Malformed URI: {0}"
+    })
+
     @Override
     public CompletableFuture<Object> runCommand(String command, List<Object> 
arguments) {
         switch (command) {
             case COMMAND_GET_DEPENDENCIES: {
+                // Finds dependencies in a project.
                 DependencyFindRequest request = 
gson().fromJson(gson().toJson(arguments.get(0)), DependencyFindRequest.class);
                 FileObject dir;
                 try {
@@ -192,7 +218,74 @@ public class ProjectDependenciesCommand implements 
CommandProvider {
                 return future;
             }
 
-            case COMMAND_CHANGE_DEPENDENCIES:
+            case COMMAND_CHANGE_DEPENDENCIES: {
+                // Finds dependencies in a project.
+                LspDependencyChangeRequest request = 
gson().fromJson(gson().toJson(arguments.get(0)), 
LspDependencyChangeRequest.class);
+                FileObject dir;
+                Project p;
+                
+                try {
+                    dir = Utils.fromUri(request.getUri());
+                    p = FileOwnerQuery.getOwner(dir);
+                    if (p == null) {
+                        throw new 
IllegalArgumentException(Bundle.ERR_FileNotInProject(request.getUri()));
+                    }
+                } catch (MalformedURLException ex) {
+                    throw new 
IllegalArgumentException(Bundle.ERR_InvalidFileUri(request.getUri()));
+                }
+                
+                CompletableFuture future = new CompletableFuture();
+                RP.post(() -> {
+                    LspDependencyChangeResult res = new 
LspDependencyChangeResult();
+                    ProjectModificationResult mod;
+                    try {
+                        mod = ProjectDependencies.modifyDependencies(p, 
request.getChanges());
+                    } catch (DependencyChangeException ex) {
+                        future.completeExceptionally(ex);
+                        return;
+                    }
+                    if (mod == null) {
+                        future.complete(null);
+                        return;
+                    }
+                    NbCodeLanguageClient client = 
LspServerUtils.requireLspClient(Lookup.getDefault());
+                    WorkspaceEdit wEdit = mod.getWorkspaceEdit();
+                    org.eclipse.lsp4j.WorkspaceEdit lspEdit = 
Utils.workspaceEditFromApi(wEdit, null, client);
+                    res.setEdit(lspEdit);
+                    if (request.isApplyChanges()) {
+                        if (request.isSaveFromServer()) {
+                            try {
+                                mod.commit();
+                            } catch (IOException ex) {
+                                future.completeExceptionally(ex);
+                                return;
+                            }
+                        } else {
+                            client.applyEdit(new 
ApplyWorkspaceEditParams(lspEdit)).thenAccept((x) -> {
+                                String[] uris = new 
String[mod.getFilesToSave().size()];
+                                int index = 0;
+                                
+                                for (FileObject f : mod.getFilesToSave()) {
+                                    URL u = URLMapper.findURL(f, 
URLMapper.EXTERNAL);
+                                    if (u != null) {
+                                        String s = u.toString();
+                                        if (s.indexOf(f.getPath()) == 5) {
+                                            s = "file://" + s.substring(5);
+                                        }
+                                        uris[index++] = s;
+                                    }
+                                }
+                                client.requestDocumentSave(new 
SaveDocumentRequestParams(Arrays.asList(uris)));
+                                future.complete(res);
+                            });
+                        }
+                    } else {
+                        future.complete(res);
+                    }
+                    // must broadcast instructions to the client
+                });
+                return future;
+            }
         }
         return null;
     }
diff --git 
a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java 
b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
index 74321bec92..28b296d599 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
@@ -26,6 +26,7 @@ import java.io.IOException;
 import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
@@ -44,22 +45,34 @@ import javax.lang.model.type.ArrayType;
 import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
 import javax.swing.text.StyledDocument;
+import org.eclipse.lsp4j.CreateFile;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.MessageType;
 import org.eclipse.lsp4j.Position;
 import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.ResourceOperation;
 import org.eclipse.lsp4j.SymbolKind;
 import org.eclipse.lsp4j.SymbolTag;
+import org.eclipse.lsp4j.TextDocumentEdit;
+import org.eclipse.lsp4j.TextEdit;
+import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.eclipse.lsp4j.jsonrpc.messages.Either;
 import org.netbeans.api.annotations.common.NonNull;
 import org.netbeans.api.java.source.CompilationInfo;
 import org.netbeans.api.lsp.StructureElement;
 import org.netbeans.modules.editor.java.Utilities;
 import org.netbeans.modules.java.lsp.server.protocol.NbCodeClientCapabilities;
+import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient;
 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
 import org.netbeans.spi.jumpto.type.SearchType;
 import org.openide.cookies.EditorCookie;
+import org.openide.cookies.SaveCookie;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.URLMapper;
 import org.openide.text.NbDocument;
 import org.openide.util.Exceptions;
+import org.openide.util.Union2;
 
 /**
  *
@@ -528,4 +541,34 @@ public class Utils {
             throw new IllegalStateException("Some commands are not properly 
prefixed: " + wrongCommands);
         }
     }
+    
+    public static WorkspaceEdit 
workspaceEditFromApi(org.netbeans.api.lsp.WorkspaceEdit edit, String uri, 
NbCodeLanguageClient client) {
+        List<Either<TextDocumentEdit, ResourceOperation>> documentChanges = 
new ArrayList<>();
+        for (Union2<org.netbeans.api.lsp.TextDocumentEdit, 
org.netbeans.api.lsp.ResourceOperation> parts : edit.getDocumentChanges()) {
+            if (parts.hasFirst()) {
+                String docUri = parts.first().getDocument();
+                try {
+                    FileObject file = Utils.fromUri(docUri);
+                    if (file == null) {
+                        file = Utils.fromUri(uri);
+                    }
+                    FileObject fo = file;
+                    if (fo != null) {
+                        List<TextEdit> edits = 
parts.first().getEdits().stream().map(te -> new TextEdit(new 
Range(Utils.createPosition(fo, te.getStartOffset()), Utils.createPosition(fo, 
te.getEndOffset())), te.getNewText())).collect(Collectors.toList());
+                        TextDocumentEdit tde = new TextDocumentEdit(new 
VersionedTextDocumentIdentifier(docUri, -1), edits);
+                        documentChanges.add(Either.forLeft(tde));
+                    }
+                } catch (Exception ex) {
+                    client.logMessage(new MessageParams(MessageType.Error, 
ex.getMessage()));
+                }
+            } else {
+                if (parts.second() instanceof 
org.netbeans.api.lsp.ResourceOperation.CreateFile) {
+                    documentChanges.add(Either.forRight(new 
CreateFile(((org.netbeans.api.lsp.ResourceOperation.CreateFile) 
parts.second()).getNewFile())));
+                } else {
+                    throw new 
IllegalStateException(String.valueOf(parts.second()));
+                }
+            }
+        }
+        return new WorkspaceEdit(documentChanges);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org
For additional commands, e-mail: commits-h...@netbeans.apache.org

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


Reply via email to