This is an automated email from the ASF dual-hosted git repository. lkishalmi pushed a commit to branch release122 in repository https://gitbox.apache.org/repos/asf/netbeans.git
commit af35c99def8e74b2cc7d47043e0c98e053ade829 Author: Jan Lahoda <jlah...@netbeans.org> AuthorDate: Sat Oct 17 21:45:57 2020 +0200 [NETBEANS-4910] Correcting open and close events sent from the LSP client to the LSP server. --- .../org/netbeans/api/editor/EditorRegistry.java | 5 + .../editor/lib2/EditorApiPackageAccessor.java | 3 + .../lsp/client/bindings/BreadcrumbsImpl.java | 3 + .../TextDocumentSyncServerCapabilityHandler.java | 141 ++++++--- ...extDocumentSyncServerCapabilityHandlerTest.java | 349 +++++++++++++++++++++ 5 files changed, 457 insertions(+), 44 deletions(-) diff --git a/ide/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java b/ide/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java index ac3ea6c..8ca9bea 100644 --- a/ide/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java +++ b/ide/editor.lib2/src/org/netbeans/api/editor/EditorRegistry.java @@ -781,6 +781,11 @@ public final class EditorRegistry { } @Override + public void forceRelease(JTextComponent c) { + EditorRegistry.releasedByCloneableEditor(c); + } + + @Override public void setIgnoredAncestorClass(Class ignoredAncestorClass) { EditorRegistry.setIgnoredAncestorClass(ignoredAncestorClass); } diff --git a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorApiPackageAccessor.java b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorApiPackageAccessor.java index a1606a1..0ca1aee 100644 --- a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorApiPackageAccessor.java +++ b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/EditorApiPackageAccessor.java @@ -51,6 +51,9 @@ public abstract class EditorApiPackageAccessor { /** Register text component to registry. */ public abstract void register(JTextComponent c); + + /**Forcibly release from the registry - useful for tests.*/ + public abstract void forceRelease(JTextComponent c); public abstract void setIgnoredAncestorClass(Class ignoredAncestorClass); diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java index a19f481..fe9841c 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/BreadcrumbsImpl.java @@ -278,6 +278,9 @@ public class BreadcrumbsImpl implements BackgroundTask { } public static List<BreadcrumbsElement> create(BreadcrumbsElement parent, List<DocumentSymbol> symbols, FileObject file, Document doc) { + if (symbols == null) { + return Collections.emptyList(); + } return symbols.stream() .map(c -> create(parent, file, doc, c)) .filter(e -> e != null) diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java index 60e38d3..81d98c0 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandler.java @@ -21,8 +21,10 @@ package org.netbeans.modules.lsp.client.bindings; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; @@ -32,10 +34,12 @@ import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.text.StyledDocument; import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.TextDocumentSyncOptions; @@ -55,8 +59,7 @@ import org.openide.text.NbDocument; import org.openide.util.Exceptions; import org.openide.util.RequestProcessor; -/** TODO: follow the synchronization options - * TODO: close +/** * * @author lahvac */ @@ -79,15 +82,57 @@ public class TextDocumentSyncServerCapabilityHandler { lastOpened.addAll(currentOpened); for (JTextComponent opened : newOpened) { - FileObject file = NbEditorUtilities.getFileObject(opened.getDocument()); + editorOpened(opened); + } - if (file == null) - continue; //ignore + for (JTextComponent closed : newClosed) { + editorClosed(closed); + } + } - Document doc = opened.getDocument(); + private void ensureOpenedInServer(JTextComponent opened) { + FileObject file = NbEditorUtilities.getFileObject(opened.getDocument()); - ensureOpenedInServer(opened); + if (file == null) + return; //ignore + + Document doc = opened.getDocument(); + ensureDidOpenSent(doc); + } + + public static void refreshOpenedFilesInServers() { + SwingUtilities.invokeLater(() -> { + assert SwingUtilities.isEventDispatchThread(); + for (JTextComponent c : EditorRegistry.componentList()) { + h.ensureOpenedInServer(c); + } + }); + } + private static final TextDocumentSyncServerCapabilityHandler h = new TextDocumentSyncServerCapabilityHandler(); + @OnStart + public static class Init implements Runnable { + + @Override + public void run() { + EditorRegistry.addPropertyChangeListener(evt -> h.handleChange()); + SwingUtilities.invokeLater(() -> h.handleChange()); + } + + } + + private final Map<Document, Integer> openDocument2PanesCount = new HashMap<>(); + + private void documentOpened(Document doc) { + FileObject file = NbEditorUtilities.getFileObject(doc); + + if (file == null) + return; //ignore + + openDocument2PanesCount.computeIfAbsent(doc, d -> { + doc.putProperty(HyperlinkProviderImpl.class, true); + doc.putProperty(TextDocumentSyncServerCapabilityHandler.class, true); + ensureDidOpenSent(doc); doc.addDocumentListener(new DocumentListener() { //XXX: listener int version; //XXX: proper versioning! @Override @@ -180,17 +225,58 @@ public class TextDocumentSyncServerCapabilityHandler { @Override public void changedUpdate(DocumentEvent e) {} }); - } + return 0; + }); } - private void ensureOpenedInServer(JTextComponent opened) { - FileObject file = NbEditorUtilities.getFileObject(opened.getDocument()); + private synchronized void editorOpened(JTextComponent c) { + Document doc = c.getDocument(); + FileObject file = NbEditorUtilities.getFileObject(c.getDocument()); if (file == null) return; //ignore - Document doc = opened.getDocument(); + documentOpened(doc); + openDocument2PanesCount.compute(doc, (d, count) -> count + 1); + } + + private synchronized void editorClosed(JTextComponent c) { + Document doc = c.getDocument(); + Integer count = openDocument2PanesCount.getOrDefault(doc, -1); + if (count > 0) { + openDocument2PanesCount.put(doc, --count); + } + if (count == 0) { + //TODO modified! + WORKER.post(() -> { + FileObject file = NbEditorUtilities.getFileObject(doc); + + if (file == null) + return; //ignore + + LSPBindings server = LSPBindings.getBindings(file); + + if (server == null) + return ; //ignore + + TextDocumentIdentifier di = new TextDocumentIdentifier(); + di.setUri(Utils.toURI(file)); + DidCloseTextDocumentParams params = new DidCloseTextDocumentParams(di); + + server.getTextDocumentService().didClose(params); + server.getOpenedFiles().remove(file); + }); + openDocument2PanesCount.remove(doc); + } + } + + private void ensureDidOpenSent(Document doc) { WORKER.post(() -> { + FileObject file = NbEditorUtilities.getFileObject(doc); + + if (file == null) + return; //ignore + LSPBindings server = LSPBindings.getBindings(file); if (server == null) @@ -201,8 +287,6 @@ public class TextDocumentSyncServerCapabilityHandler { return ; } - doc.putProperty(HyperlinkProviderImpl.class, Boolean.TRUE); - String uri = Utils.toURI(file); String[] text = new String[1]; @@ -221,38 +305,7 @@ public class TextDocumentSyncServerCapabilityHandler { text[0]); server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(textDocumentItem)); - if (opened.getClientProperty(MarkOccurrences.class) == null) { - MarkOccurrences mo = new MarkOccurrences(opened); - LSPBindings.addBackgroundTask(file, mo); - opened.putClientProperty(MarkOccurrences.class, mo); - } - if (opened.getClientProperty(BreadcrumbsImpl.class) == null) { - BreadcrumbsImpl bi = new BreadcrumbsImpl(opened); - LSPBindings.addBackgroundTask(file, bi); - opened.putClientProperty(BreadcrumbsImpl.class, bi); - } server.scheduleBackgroundTasks(file); }); } - - public static void refreshOpenedFilesInServers() { - SwingUtilities.invokeLater(() -> { - assert SwingUtilities.isEventDispatchThread(); - for (JTextComponent c : EditorRegistry.componentList()) { - h.ensureOpenedInServer(c); - } - }); - } - - private static final TextDocumentSyncServerCapabilityHandler h = new TextDocumentSyncServerCapabilityHandler(); - @OnStart - public static class Init implements Runnable { - - @Override - public void run() { - EditorRegistry.addPropertyChangeListener(evt -> h.handleChange()); - SwingUtilities.invokeLater(() -> h.handleChange()); - } - - } } diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandlerTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandlerTest.java new file mode 100644 index 0000000..d7fe5a7 --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/TextDocumentSyncServerCapabilityHandlerTest.java @@ -0,0 +1,349 @@ +/* + * 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.lsp.client.bindings; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.swing.JEditorPane; +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.StyledDocument; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.launch.LSPLauncher; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; +import org.junit.Test; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.junit.MockServices; +import org.netbeans.modules.editor.NbEditorKit; +import org.netbeans.modules.editor.lib2.EditorApiPackageAccessor; +import org.netbeans.modules.lsp.client.Utils; +import org.netbeans.modules.lsp.client.spi.LanguageServerProvider; +import org.netbeans.spi.editor.mimelookup.MimeDataProvider; +import org.openide.cookies.EditorCookie; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.filesystems.MIMEResolver; +import org.openide.loaders.DataObject; +import org.openide.text.CloneableEditorSupport; +import org.openide.text.NbDocument; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import static org.junit.Assert.*; + +/** + * + * @author lahvac + */ +public class TextDocumentSyncServerCapabilityHandlerTest { + + private static final String MIME_TYPE = "application/mock-txt"; + private static final List<String> eventLog = new ArrayList<>(); + + @Test + public void testOpenClose() throws Exception { + MockServices.setServices(MimeDataProviderImpl.class, MockMimeResolver.class); + + new TextDocumentSyncServerCapabilityHandler.Init().run(); + + FileObject folder = FileUtil.createMemoryFileSystem().getRoot().createFolder("myfolder"); + FileObject file = folder.createData("data.mock-txt"); + EditorCookie ec = file.getLookup().lookup(EditorCookie.class); + ((CloneableEditorSupport) ec).setMIMEType(MIME_TYPE); + Document doc = ec.openDocument(); + JEditorPane pane = new JEditorPane() { + @Override + public boolean isFocusOwner() { + return true; + } + }; + + pane.setDocument(doc); + + String uri = Utils.toURI(file); + + SwingUtilities.invokeLater(() -> { + EditorApiPackageAccessor.get().register(pane); + }); + + assertEvents("didOpen: " + uri + "/" + MIME_TYPE + "/0/"); + + NbDocument.runAtomic((StyledDocument) doc, () -> { + try { + doc.insertString(0, "text", null); + } catch (BadLocationException ex) { + throw new IllegalStateException(ex); + } + }); + + assertEvents("didChange: " + uri + "/1/[0:0-0:0 => text]"); + + NbDocument.runAtomic((StyledDocument) doc, () -> { + try { + doc.remove(2, 1); + } catch (BadLocationException ex) { + throw new IllegalStateException(ex); + } + }); + + assertEvents("didChange: " + uri + "/2/[0:2-0:3 => ]"); + + assertTrue(DataObject.getRegistry().getModifiedSet().contains(DataObject.find(file))); + + //TODO: send save event: +// LifecycleManager.getDefault().saveAll(); +// +// assertEvents("didSave: " + uri); + + SwingUtilities.invokeLater(() -> { + EditorApiPackageAccessor.get().forceRelease(pane); + }); + + assertEvents("didClose: " + uri); + + SwingUtilities.invokeLater(() -> { + EditorApiPackageAccessor.get().register(pane); + }); + + assertEvents("didOpen: " + uri + "/" + MIME_TYPE + "/0/tet"); + + SwingUtilities.invokeLater(() -> { + EditorApiPackageAccessor.get().forceRelease(pane); + }); + + assertEvents("didClose: " + uri); + } + + private void assertEvents(String... events) { + synchronized (eventLog) { + long timeout = System.currentTimeMillis() + 10000000; + + while (System.currentTimeMillis() < timeout && eventLog.size() < events.length) { + try { + eventLog.wait(timeout - System.currentTimeMillis()); + } catch (InterruptedException ex) { + } + } + assertEquals(Arrays.asList(events), eventLog); + eventLog.clear(); + } + } + + public static final class MimeDataProviderImpl implements MimeDataProvider { + @Override + public Lookup getLookup(MimePath mp) { + assertEquals("application/mock-txt", mp.getPath()); + return Lookups.fixed(new MockLSP(), new NbEditorKit() { + @Override + public String getContentType() { + return "application/mock-txt"; + } + }); + } + } + + public static final class MockLSP implements LanguageServerProvider { + @Override + public LanguageServerProvider.LanguageServerDescription startServer(Lookup lookup) { + try { + final MockProcess process = new MockProcess(); + ServerSocket srv = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); + Thread serverThread = new Thread(() -> { + try { + Socket server = srv.accept(); + + LSPLauncher.createServerLauncher(new TestLanguageServer(), server.getInputStream(), server.getOutputStream()).startListening().get(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + }); + serverThread.start(); + Socket client = new Socket(srv.getInetAddress(), srv.getLocalPort()); + + return LanguageServerProvider.LanguageServerDescription.create(client.getInputStream(), client.getOutputStream(), process); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + + public final static class MockMimeResolver extends MIMEResolver { + + public MockMimeResolver() { + } + + @Override + public String findMIMEType(FileObject fo) { + return fo.hasExt("mock-txt") ? "application/mock-txt" : null; + } + } + + static final class MockProcess extends Process { + final ByteArrayInputStream in; + final ByteArrayOutputStream out; + + public MockProcess() { + this.in = new ByteArrayInputStream(new byte[0]); + this.out = new ByteArrayOutputStream(); + } + + @Override + public OutputStream getOutputStream() { + return out; + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public InputStream getErrorStream() { + return in; + } + + @Override + public int waitFor() throws InterruptedException { + throw new InterruptedException(); + } + + @Override + public boolean isAlive() { + return true; + } + + @Override + public int exitValue() { + return 0; + } + + @Override + public void destroy() { + } + } + + private static final class TestLanguageServer implements LanguageServer { + + @Override + public CompletableFuture<InitializeResult> initialize(InitializeParams params) { + ServerCapabilities caps = new ServerCapabilities(); + caps.setTextDocumentSync(TextDocumentSyncKind.Incremental); + InitializeResult initResult = new InitializeResult(caps); + return CompletableFuture.completedFuture(initResult); + } + + @Override + public CompletableFuture<Object> shutdown() { + return CompletableFuture.completedFuture(null); + } + + @Override + public void exit() { + } + + @Override + public TextDocumentService getTextDocumentService() { + return new TextDocumentService() { + @Override + public void didOpen(DidOpenTextDocumentParams params) { + TextDocumentItem td = params.getTextDocument(); + synchronized (eventLog) { + eventLog.add("didOpen: " + td.getUri() + "/" + td.getLanguageId() + "/" + td.getVersion() + "/" + td.getText()); + eventLog.notifyAll(); + } + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + VersionedTextDocumentIdentifier td = params.getTextDocument(); + List<TextDocumentContentChangeEvent> changes = params.getContentChanges(); + synchronized (eventLog) { + eventLog.add("didChange: " + td.getUri() + "/" + td.getVersion() + "/" + changes.stream().map(c -> range2String(c.getRange()) + " => " + c.getText()).collect(Collectors.joining(", ", "[", "]"))); + eventLog.notifyAll(); + } + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + TextDocumentIdentifier td = params.getTextDocument(); + synchronized (eventLog) { + eventLog.add("didClose: " + td.getUri()); + eventLog.notifyAll(); + } + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + TextDocumentIdentifier td = params.getTextDocument(); + synchronized (eventLog) { + eventLog.add("didSave: " + td.getUri() + "/" + params.getText()); + eventLog.notifyAll(); + } + } + }; + } + + @Override + public WorkspaceService getWorkspaceService() { + return new WorkspaceService() { + @Override + public void didChangeConfiguration(DidChangeConfigurationParams params) { + throw new IllegalStateException("Should not be called."); + } + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + throw new IllegalStateException("Should not be called."); + } + }; + } + + } + + private static String range2String(Range range) { + return range.getStart().getLine() + ":" + range.getStart().getCharacter() + + "-" + range.getEnd().getLine() + ":" + range.getEnd().getCharacter(); + } +} --------------------------------------------------------------------- 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