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