Skip to content

Commit

Permalink
[XML+HTML] Support for autoCloseTags
Browse files Browse the repository at this point in the history
Fixes eclipse-wildwebdeveloper#143

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Oct 18, 2022
1 parent d70dbb0 commit cdc2b13
Show file tree
Hide file tree
Showing 29 changed files with 872 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.lsp4e.operations.completion.LSContentAssistProcessor;
Expand Down Expand Up @@ -140,4 +141,20 @@ public void testComplexXML() throws Exception {
DisplayHelper.sleep(editor.getSite().getShell().getDisplay(), 2000);
assertTrue(proposals.length > 1);
}

@Test
public void autoCloseTags() throws Exception {
final IFile file = project.getFile("autoCloseTags.xml");
file.create(new ByteArrayInputStream("<foo".getBytes()), true, null);
ITextEditor editor = (ITextEditor) IDE
.openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), file);
IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput());
document.replace(4, 0, ">");
assertTrue(new DisplayHelper() {
@Override
protected boolean condition() {
return "<foo></foo>".equals(document.get());
}
}.waitForCondition(PlatformUI.getWorkbench().getDisplay(), 5000), "Autoclose not done");
}
}
3 changes: 2 additions & 1 deletion org.eclipse.wildwebdeveloper.xml/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Require-Bundle: org.eclipse.tm4e.registry;bundle-version="0.3.0",
org.eclipse.ui.genericeditor;bundle-version="1.0.0",
org.eclipse.core.net;bundle-version="1.3.0",
org.eclipse.lsp4j.jsonrpc,
org.eclipse.text
org.eclipse.text,
org.eclipse.jface.text;bundle-version="3.20.100"
Bundle-ActivationPolicy: lazy
Bundle-Activator: org.eclipse.wildwebdeveloper.xml.internal.Activator
Export-Package: org.eclipse.wildwebdeveloper.xml;x-friends:="org.eclipse.m2e.editor.lemminx"
8 changes: 8 additions & 0 deletions org.eclipse.wildwebdeveloper.xml/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
</presentationReconciler>
</extension>

<extension
point="org.eclipse.ui.genericeditor.reconcilers">
<reconciler
class="org.eclipse.wildwebdeveloper.xml.internal.autoclose.XMLAutoCloseTagReconciler"
contentType="org.eclipse.core.runtime.xml">
</reconciler>
</extension>

<!-- XML Language -->
<extension
Expand Down Expand Up @@ -49,6 +56,7 @@
point="org.eclipse.lsp4e.languageServer">
<server
class="org.eclipse.wildwebdeveloper.xml.internal.XMLLanguageServer"
serverInterface="org.eclipse.wildwebdeveloper.xml.internal.XMLLanguageServerAPI"
clientImpl="org.eclipse.wildwebdeveloper.xml.internal.XmlLanguageClientImpl"
id="org.eclipse.wildwebdeveloper.xml"
label="XML Language Server"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.wildwebdeveloper.xml.internal.ui.preferences.XMLPreferenceConstants;
import org.eclipse.wildwebdeveloper.xml.internal.ui.preferences.XMLPreferenceServerConstants;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;

Expand All @@ -53,7 +53,7 @@ public class XMLLanguageServer extends ProcessStreamConnectionProvider {
private static final LanguageServerDefinition lemminxDefinition = LanguageServersRegistry.getInstance()
.getDefinition("org.eclipse.wildwebdeveloper.xml");
private static final IPropertyChangeListener psListener = event -> {
XMLPreferenceConstants.getLemminxPreference(event).ifPresent(pref -> {
XMLPreferenceServerConstants.getLemminxPreference(event).ifPresent(pref -> {
Map<String, Object> config = mergeCustomInitializationOptions(
extensionJarRegistry.getInitiatizationOptions());

Expand Down Expand Up @@ -165,7 +165,7 @@ public Object getInitializationOptions(URI rootUri) {

private static Map<String, Object> mergeCustomInitializationOptions(Map<String, Object> defaults) {
Map<String, Object> xmlOpts = new HashMap<>(defaults);
XMLPreferenceConstants.storePreferencesToLemminxOptions(store, xmlOpts);
XMLPreferenceServerConstants.storePreferencesToLemminxOptions(store, xmlOpts);
return Map.of(SETTINGS_KEY, Map.of(XML_KEY, xmlOpts));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Victor Rubezhny (Red Hat Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.wildwebdeveloper.xml.internal;

import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.wildwebdeveloper.xml.internal.autoclose.AutoCloseTagResponse;

/**
* XML language server API which defines custom LSP commands.
*
*/
public interface XMLLanguageServerAPI extends LanguageServer {

@JsonRequest("xml/closeTag")
CompletableFuture<AutoCloseTagResponse> closeTag(TextDocumentPositionParams params);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*******************************************************************************
* Copyright (c) 2020 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Victor Rubezhny (Red Hat Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.wildwebdeveloper.xml.internal.autoclose;

import org.eclipse.lsp4j.Range;

/**
* Auto close tag LSP response.
*
*/
public class AutoCloseTagResponse {

public String snippet;
public Range range;

public AutoCloseTagResponse(String snippet, Range range) {
this.snippet = snippet;
this.range = range;
}

public AutoCloseTagResponse(String snippet) {
this.snippet = snippet;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*******************************************************************************
* Copyright (c) 2022 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Angelo ZERR (Red Hat Inc.) - initial implementation
*/
package org.eclipse.wildwebdeveloper.xml.internal.autoclose;

import static org.eclipse.wildwebdeveloper.xml.internal.ui.preferences.XMLPreferenceClientConstants.XML_PREFERENCES_COMPLETION_AUTO_CLOSE_TAGS;

import java.net.URI;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.LanguageServiceAccessor.LSPDocumentInfo;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.swt.widgets.Display;
import org.eclipse.wildwebdeveloper.xml.internal.Activator;
import org.eclipse.wildwebdeveloper.xml.internal.XMLLanguageServerAPI;

/**
* {@link IReconciler} implementation used to support auto close tags , features
* provides by the LemMinx XML language server with the custom 'xml/closeTag'
* LSP request.
*
*/
public class XMLAutoCloseTagReconciler implements IReconciler {

private IDocument document;

private ITextViewer viewer;

private Listener listener;

private void autoInsert(DocumentEvent event) {
if (!isEnabled()) {
return;
}
if (event == null || viewer == null) {
return;
}
IDocument document = event.getDocument();
if (document == null || event == null || event.getLength() != 0 || event.getText().length() != 1) {
return;
}

int offset = event.getOffset() + 1;
char c = event.getText().charAt(0);
if (c != '>' && c != '/') {
return;
}
URI uri = LSPEclipseUtils.toUri(document);
if (uri == null) {
return;
}

TextDocumentIdentifier identifier = new TextDocumentIdentifier(uri.toString());
Optional<LSPDocumentInfo> info = LanguageServiceAccessor
.getLSPDocumentInfosFor(document, (capabilities) -> true).stream()
.filter(doc -> (doc.getLanguageClient() instanceof XMLLanguageServerAPI)).findAny();
if (!info.isEmpty()) {
// The document is bound with XML language server, consumes the xml/closeTag
final Display display = viewer.getTextWidget().getDisplay();
CompletableFuture.supplyAsync(() -> {
try {
// Wait for textDocument/didChange
Thread.sleep(100);
} catch (InterruptedException ex) {
Thread.interrupted();
}
try {
TextDocumentPositionParams params = LSPEclipseUtils.toTextDocumentPosistionParams(uri, offset,
document);
// consumes xml/closeTag from XML language server
((XMLLanguageServerAPI) info.get().getLanguageClient()).closeTag(params).thenAccept(r -> {
if (r != null) {
display.asyncExec(() -> {
try {
// we receive a text like
// $0</foo>
// $0 should be used for set the cursor.
String text = r.snippet.replace("$0", "");
int replaceLength = getReplaceLength(r.range, document);
document.replace(offset, replaceLength, text);
} catch (BadLocationException e) {
// Do nothing
}
});

}
});
} catch (BadLocationException e) {
// Do nothing
}
return null;
});
}
}

private boolean isEnabled() {
return Activator.getDefault().getPreferenceStore().getBoolean(XML_PREFERENCES_COMPLETION_AUTO_CLOSE_TAGS);
}

private static int getReplaceLength(Range range, IDocument document) throws BadLocationException {
if (range == null) {
return 0;
}
Position start = range.getStart();
Position end = range.getEnd();
if (start.getLine() == end.getLine()) {
return end.getCharacter() - start.getCharacter();
}
int startOffset = LSPEclipseUtils.toOffset(start, document);
int endOffset = LSPEclipseUtils.toOffset(end, document);
return endOffset - startOffset;
}

/**
* Internal document listener and text input listener.
*/
class Listener implements IDocumentListener, ITextInputListener {

/*
* @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
*/
@Override
public void documentAboutToBeChanged(DocumentEvent e) {
}

/*
* @see IDocumentListener#documentChanged(DocumentEvent)
*/
@Override
public void documentChanged(DocumentEvent e) {
autoInsert(e);
}

/*
* @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument)
*/
@Override
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
if (oldInput == document) {
if (document != null) {
document.removeDocumentListener(this);
}
document = null;
}
}

/*
* @see ITextInputListener#inputDocumentChanged(IDocument, IDocument)
*/
@Override
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
document = newInput;
if (document == null) {
return;
}
document.addDocumentListener(this);
}

}

@Override
public void install(ITextViewer viewer) {
this.viewer = viewer;
listener = new Listener();
viewer.addTextInputListener(listener);
}

@Override
public void uninstall() {
if (listener != null) {
viewer.removeTextInputListener(listener);
if (document != null) {
document.removeDocumentListener(listener);
}
listener = null;
}
this.viewer = null;
}

@Override
public IReconcilingStrategy getReconcilingStrategy(String contentType) {
return null;
}

}
Loading

0 comments on commit cdc2b13

Please sign in to comment.