Skip to content

Commit

Permalink
[DRAFT 1426] Add support for optional text completion on textfield wi…
Browse files Browse the repository at this point in the history
…dgets

Bug: #1426
Signed-off-by: Pierre-Charles David <pierre-charles.david@obeo.fr>
  • Loading branch information
pcdavid committed Oct 26, 2022
1 parent 8547f95 commit 772488f
Show file tree
Hide file tree
Showing 24 changed files with 920 additions and 41 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- [ADR-072] Add support for the edition of routing points
- [ADR-073] Add support for intermodel references (inside the same project)
- [ADR-074] Add support for edge reconnection

=== Breaking changes

- https://github.com/eclipse-sirius/sirius-components/issues/1300[#1300] [core] Rename SiriusWebJSONResourceFactoryImpl to JSONResourceFactoryImpl
Expand All @@ -36,6 +36,8 @@
- https://github.com/eclipse-sirius/sirius-components/issues/1387[#1387] [releng] Migrate VSCode Extension to Sirius Components.
- https://github.com/eclipse-sirius/sirius-components/issues/1423[#1423] [forms] In-browser spell-checking is now disabled on textfields.

- https://github.com/eclipse-sirius/sirius-components/issues/1426[#1426] [forms] Add support for optional text completion on textfield widgets

=== New Features

- https://github.com/eclipse-sirius/sirius-components/issues/1316[#1316] [diagram] Support parametric svg node style
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,31 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import org.eclipse.acceleo.query.runtime.EvaluationResult;
import org.eclipse.acceleo.query.runtime.ICompletionProposal;
import org.eclipse.acceleo.query.runtime.ICompletionResult;
import org.eclipse.acceleo.query.runtime.IQueryBuilderEngine;
import org.eclipse.acceleo.query.runtime.IQueryBuilderEngine.AstResult;
import org.eclipse.acceleo.query.runtime.IQueryCompletionEngine;
import org.eclipse.acceleo.query.runtime.IQueryEnvironment;
import org.eclipse.acceleo.query.runtime.IQueryEvaluationEngine;
import org.eclipse.acceleo.query.runtime.IService;
import org.eclipse.acceleo.query.runtime.Query;
import org.eclipse.acceleo.query.runtime.QueryCompletion;
import org.eclipse.acceleo.query.runtime.QueryEvaluation;
import org.eclipse.acceleo.query.runtime.QueryParsing;
import org.eclipse.acceleo.query.runtime.ServiceUtils;
import org.eclipse.acceleo.query.validation.type.EClassifierType;
import org.eclipse.acceleo.query.validation.type.IType;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.ecore.EPackage;
Expand Down Expand Up @@ -161,4 +170,16 @@ private void log(String expression, Diagnostic diagnostic) {

diagnostic.getChildren().forEach(childDiagnostic -> this.log(expression, childDiagnostic));
}

public List<ICompletionProposal> complete(String expression, int offset) {
IQueryCompletionEngine engine = QueryCompletion.newEngine(this.queryEnvironment);
Map<String, Set<IType>> variableTypes = new LinkedHashMap<>();
final Set<IType> potentialTypes = new LinkedHashSet<>(1);
potentialTypes.add(new EClassifierType(this.queryEnvironment, EcorePackage.Literals.EOBJECT));
variableTypes.put("self", potentialTypes); //$NON-NLS-1$

ICompletionResult completionResult = engine.getCompletion(expression, offset, variableTypes);
Set<ICompletionProposal> aqlProposals = new LinkedHashSet<>(completionResult.getProposals(QueryCompletion.createBasicFilter(completionResult)));
return aqlProposals.stream().collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms.dto;

import java.text.MessageFormat;
import java.util.Objects;

/**
* Represents a text completion proposal that can be inserted into a text field which supports it.
*
* @author pcdavid
*/
public final class CompletionProposal {
private String label;

private String textToInsert;

private int charsToReplace;

public CompletionProposal(String label, String textToInsert, int charsToReplace) {
this.label = Objects.requireNonNull(label);
this.textToInsert = Objects.requireNonNull(textToInsert);
this.charsToReplace = Objects.requireNonNull(charsToReplace);
}

public String getLabel() {
return this.label;
}

public String getTextToInsert() {
return this.textToInsert;
}

public int getCharsToReplace() {
return this.charsToReplace;
}

@Override
public String toString() {
String pattern = "{0} '{'label: {1}, textToInsert: {2}, charsToReplace: {3}'}'"; //$NON-NLS-1$
return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.label, this.textToInsert, this.charsToReplace);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms.dto;

import java.text.MessageFormat;
import java.util.Objects;
import java.util.UUID;

import org.eclipse.sirius.components.collaborative.forms.api.IFormInput;

/**
* Represents a request for code completion inside a text field which supports it.
*
* @author pcdavid
*/
public class CompletionRequestInput implements IFormInput {
private UUID id;

private String editingContextId;

private String representationId;

private String widgetId;

private String currentText;

private int cursorPosition;

public CompletionRequestInput() {
// Used by Jackson
}

public CompletionRequestInput(UUID id, String editingContextId, String representationId, String widgetId, String currentText, int cursorPosition) {
this.id = Objects.requireNonNull(id);
this.editingContextId = Objects.requireNonNull(editingContextId);
this.representationId = Objects.requireNonNull(representationId);
this.widgetId = Objects.requireNonNull(widgetId);
this.currentText = Objects.requireNonNull(currentText);
this.cursorPosition = cursorPosition;
}

@Override
public UUID getId() {
return this.id;
}

public String getEditingContextId() {
return this.editingContextId;
}

@Override
public String getRepresentationId() {
return this.representationId;
}

public String getWidgetId() {
return this.widgetId;
}

public String getCurrentText() {
return this.currentText;
}

public int getCursorPosition() {
return this.cursorPosition;
}

@Override
public String toString() {
String pattern = "{0} '{'id: {1}, editingContextId: {2}, representationId: {3}, widgetId: {4}, currentText: {5}, cursorPosition: {6}'}'"; //$NON-NLS-1$
return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.id, this.editingContextId, this.representationId, this.widgetId, this.currentText, this.cursorPosition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms.dto;

import java.text.MessageFormat;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

import org.eclipse.sirius.components.core.api.IPayload;

/**
* The payload for the "completionProposals" query on success.
*
* @author pcdavid
*/
public final class CompletionRequestSuccessPayload implements IPayload {
private final UUID id;

private final List<CompletionProposal> proposals;

public CompletionRequestSuccessPayload(UUID id, List<CompletionProposal> proposals) {
this.id = Objects.requireNonNull(id);
this.proposals = List.copyOf(proposals);
}

@Override
public UUID getId() {
return this.id;
}

public List<CompletionProposal> getProposals() {
return this.proposals;
}

@Override
public String toString() {
String pattern = "{0} '{'id: {1}, proposals: {2}'}'"; //$NON-NLS-1$
return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.id, this.proposals);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms.handlers;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.sirius.components.collaborative.api.ChangeDescription;
import org.eclipse.sirius.components.collaborative.api.ChangeKind;
import org.eclipse.sirius.components.collaborative.api.Monitoring;
import org.eclipse.sirius.components.collaborative.forms.api.IFormEventHandler;
import org.eclipse.sirius.components.collaborative.forms.api.IFormInput;
import org.eclipse.sirius.components.collaborative.forms.api.IFormQueryService;
import org.eclipse.sirius.components.collaborative.forms.dto.CompletionRequestInput;
import org.eclipse.sirius.components.collaborative.forms.dto.CompletionRequestSuccessPayload;
import org.eclipse.sirius.components.collaborative.forms.messages.ICollaborativeFormMessageService;
import org.eclipse.sirius.components.core.api.ErrorPayload;
import org.eclipse.sirius.components.core.api.IEditingContext;
import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.components.forms.AbstractWidget;
import org.eclipse.sirius.components.forms.CompletionProposal;
import org.eclipse.sirius.components.forms.CompletionRequest;
import org.eclipse.sirius.components.forms.Form;
import org.eclipse.sirius.components.forms.Textarea;
import org.eclipse.sirius.components.forms.Textfield;
import org.springframework.stereotype.Service;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import reactor.core.publisher.Sinks.Many;
import reactor.core.publisher.Sinks.One;

/**
* The handler for computing completion proposals on text fields.
*
* @author pcdavid
*/
@Service
public class CompletionProposalEventHandler implements IFormEventHandler {
private final IFormQueryService formQueryService;

private final ICollaborativeFormMessageService messageService;

private final Counter counter;

public CompletionProposalEventHandler(IFormQueryService formQueryService, ICollaborativeFormMessageService messageService, MeterRegistry meterRegistry) {
this.formQueryService = Objects.requireNonNull(formQueryService);
this.messageService = Objects.requireNonNull(messageService);

// @formatter:off
this.counter = Counter.builder(Monitoring.EVENT_HANDLER)
.tag(Monitoring.NAME, this.getClass().getSimpleName())
.register(meterRegistry);
// @formatter:on
}

@Override
public boolean canHandle(IFormInput formInput) {
return formInput instanceof CompletionRequestInput;
}

@Override
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink, IEditingContext editingContext, Form form, IFormInput formInput) {
this.counter.increment();
String message = this.messageService.invalidInput(formInput.getClass().getSimpleName(), CompletionRequestInput.class.getSimpleName());
IPayload payload = new ErrorPayload(formInput.getId(), message);
ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, formInput.getRepresentationId(), formInput);

if (formInput instanceof CompletionRequestInput) {
CompletionRequestInput input = (CompletionRequestInput) formInput;
CompletionRequest request = new CompletionRequest(input.getCurrentText(), input.getCursorPosition());
// @formatter:off
List<CompletionProposal> proposals = this.formQueryService.findWidget(form, input.getWidgetId())
.flatMap(this::getProposalProvider)
.map(proposalsProvider -> proposalsProvider.apply(request))
.orElse(List.of());
// @formatter:on

// @formatter:off
payload = new CompletionRequestSuccessPayload(
formInput.getId(),
proposals.stream()
.map(prop -> new org.eclipse.sirius.components.collaborative.forms.dto.CompletionProposal(prop.getLabel(), prop.getTextToInsert(), prop.getCharsToReplace()))
.collect(Collectors.toList()));

// @formatter:on

}

changeDescriptionSink.tryEmitNext(changeDescription);
payloadSink.tryEmitValue(payload);
}

private Optional<Function<CompletionRequest, List<CompletionProposal>>> getProposalProvider(AbstractWidget widget) {
Optional<Function<CompletionRequest, List<CompletionProposal>>> proposalsProvider = Optional.empty();
if (widget instanceof Textfield) {
proposalsProvider = Optional.of(((Textfield) widget).getCompletionProposalsProvider());
} else if (widget instanceof Textarea) {
proposalsProvider = Optional.of(((Textarea) widget).getCompletionProposalsProvider());
}
return proposalsProvider;
}
}
Loading

0 comments on commit 772488f

Please sign in to comment.