Skip to content

Commit

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

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr authored and mickaelistria committed Oct 19, 2022
1 parent d70dbb0 commit 5aba9af
Show file tree
Hide file tree
Showing 25 changed files with 868 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
Expand Down Expand Up @@ -71,8 +72,8 @@ public void testFormat() throws Exception {
editor.setFocus();
editor.getSelectionProvider().setSelection(new TextSelection(0, 0));
IHandlerService handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class);
assertTrue(
PlatformUI.getWorkbench().getService(ICommandService.class).getCommand("org.eclipse.lsp4e.format").isEnabled());
assertTrue(PlatformUI.getWorkbench().getService(ICommandService.class).getCommand("org.eclipse.lsp4e.format")
.isEnabled());
AtomicReference<Exception> ex = new AtomicReference<>();
new DisplayHelper() {
@Override
Expand All @@ -95,4 +96,24 @@ protected boolean condition() {
}
}.waitForCondition(editor.getSite().getShell().getDisplay(), 3000);
}

@Test
public void autoCloseTags() throws Exception {
final IProject project = ResourcesPlugin.getWorkspace().getRoot()
.getProject("testHTMLFile" + System.currentTimeMillis());
project.create(null);
project.open(null);
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");
}
}
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
@@ -0,0 +1,30 @@
/*******************************************************************************
* 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;

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) 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 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 5aba9af

Please sign in to comment.