diff --git a/ide/lsp.client/apichanges.xml b/ide/lsp.client/apichanges.xml index b01e979d9c92..b92e84280a35 100644 --- a/ide/lsp.client/apichanges.xml +++ b/ide/lsp.client/apichanges.xml @@ -25,6 +25,34 @@ LSP Client API + + + Adding LanguageIdProvider + + + + + +

+ While NetBeans uses mimetypes as primary identifier for source types, + LSP uses languageId as a carrier to inform what language/type of file + is being edited. Even a single language server can provide support + for multiple languages. One such example is the typescript language + server, which defaults to typescript, but also supports the + typescript variant that embeds react templates (TSX). +

+

+ To support this an implementation can provide a resolver. The + resolver has to implement the interface + org.netbeans.modules.lsp.client.spi.LanguageIdResolver + and will be provided by a lookup provided to the + LanguageServerDescription. For each opened file + the resolver will be called with the file object and will return + the language id if it can resolve it for the given file. +

+
+ +
Adding MultiMimeLanguageServerProvider diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java index 836f11345151..3308bfb20ced 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LSPBindings.java @@ -92,6 +92,7 @@ import org.netbeans.modules.lsp.client.bindings.LanguageClientImpl; import org.netbeans.modules.lsp.client.bindings.TextDocumentSyncServerCapabilityHandler; import org.netbeans.modules.lsp.client.options.MimeTypeInfo; +import org.netbeans.modules.lsp.client.spi.LanguageIdResolver; import org.netbeans.modules.lsp.client.spi.ServerRestarter; import org.netbeans.modules.lsp.client.spi.LanguageServerProvider; import org.netbeans.modules.lsp.client.spi.LanguageServerProvider.LanguageServerDescription; @@ -372,7 +373,9 @@ public void close() throws IOException { } InitializeResult result = initServer(process, server, dir); //XXX: what if a different root is expected???? server.initialized(new InitializedParams()); - b = new LSPBindings(server, result, LanguageServerProviderAccessor.getINSTANCE().getProcess(desc)); + process = LanguageServerProviderAccessor.getINSTANCE().getProcess(desc); + Lookup serverLookup = LanguageServerProviderAccessor.getINSTANCE().getLookup(desc); + b = new LSPBindings(server, result, process, serverLookup.lookup(LanguageIdResolver.class)); // Register cleanup via LSPReference#run new LSPReference(b, Utilities.activeReferenceQueue()); lci.setBindings(b); @@ -426,7 +429,7 @@ public void write(int w) throws IOException { LanguageServer server = launcher.getRemoteProxy(); InitializeResult result = initServer(null, server, root); server.initialized(new InitializedParams()); - LSPBindings bindings = new LSPBindings(server, result, null); + LSPBindings bindings = new LSPBindings(server, result, null, null); lc.setBindings(bindings); @@ -521,11 +524,13 @@ public static synchronized Set getAllBindings() { private final LanguageServer server; private final InitializeResult initResult; private final Process process; + private final LanguageIdResolver languageIdResolver; - private LSPBindings(LanguageServer server, InitializeResult initResult, Process process) { + private LSPBindings(LanguageServer server, InitializeResult initResult, Process process, LanguageIdResolver languageIdResolver) { this.server = server; this.initResult = initResult; this.process = process; + this.languageIdResolver = languageIdResolver; } public TextDocumentService getTextDocumentService() { @@ -541,6 +546,16 @@ public InitializeResult getInitResult() { return initResult; } + public String resolveLanguageId(FileObject fileObject) { + if(languageIdResolver != null) { + String languageId = languageIdResolver.resolveLanguageId(fileObject); + if (languageId != null) { + return languageId; + } + } + return FileUtil.getMIMEType(fileObject); + } + @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject") public static synchronized void addBackgroundTask(FileObject file, BackgroundTask task) { RequestProcessor.Task req = WORKER.create(() -> { diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java index 9fab198d7a19..c9f14607c9c8 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/LanguageServerProviderAccessor.java @@ -24,6 +24,7 @@ import org.netbeans.api.annotations.common.NonNull; import org.netbeans.modules.lsp.client.spi.LanguageServerProvider.LanguageServerDescription; import org.openide.util.Exceptions; +import org.openide.util.Lookup; /** * @@ -57,4 +58,5 @@ public static void setINSTANCE (LanguageServerProviderAccessor instance) { public abstract LSPBindings getBindings(LanguageServerDescription desc); public abstract void setBindings(LanguageServerDescription desc, LSPBindings bindings); public abstract LanguageServerDescription createLanguageServerDescription(@NonNull LanguageServer server); + public abstract Lookup getLookup(LanguageServerDescription desc); } 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 6b38f9494365..e4b621af3275 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 @@ -315,14 +315,13 @@ private void ensureDidOpenSent(Document doc, boolean sync) { } }); - // @todo: the mimetype is not the language ID - TextDocumentItem textDocumentItem = new TextDocumentItem(uri, - FileUtil.getMIMEType(file), - 0, - text[0]); for (LSPBindings server : servers) { if (server.getOpenedFiles().add(file)) { + TextDocumentItem textDocumentItem = new TextDocumentItem(uri, + server.resolveLanguageId(file), + 0, + text[0]); server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(textDocumentItem)); } } diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties index 9e6229d88267..c5e9b4baa886 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties @@ -41,3 +41,5 @@ ACSD_Marks_CB=Keep Marks Checkbox text/x-generic-lsp=Language Server Protocol Client (generic) LanguageDescriptionPanel.debugger.text=Enable &Breakpoints +LanguageDescriptionPanel.languageIdLabel.text=Language Identifier: +LanguageDescriptionPanel.languageId.text= diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericLanguageServer.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericLanguageServer.java index e39e8b97f908..c0d5c6738579 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericLanguageServer.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericLanguageServer.java @@ -25,6 +25,7 @@ import org.netbeans.api.io.InputOutput; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectUtils; +import org.netbeans.modules.lsp.client.spi.LanguageIdResolver; import org.netbeans.modules.lsp.client.spi.LanguageServerProvider; import org.openide.filesystems.FileAttributeEvent; import org.openide.filesystems.FileChangeListener; @@ -35,6 +36,7 @@ import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.RequestProcessor; +import org.openide.util.lookup.Lookups; /** * @@ -57,6 +59,7 @@ public LanguageServerDescription startServer(Lookup lookup) { FileObject server = FileUtil.getConfigFile("Editors/" + mti.mimeType + "/org-netbeans-modules-lsp-client-options-GenericLanguageServer.instance"); String[] command = (String[]) server.getAttribute("command"); String name = (String) server.getAttribute("name"); + String languageId = (String) server.getAttribute("languageId"); if (name == null) { name = command[0]; @@ -130,7 +133,16 @@ private void update() { io.show(); } }); - return LanguageServerDescription.create(process.getInputStream(), process.getOutputStream(), process); + Lookup serverLookup = null; + if(languageId != null && ! languageId.isBlank()) { + serverLookup = Lookups.fixed(new LanguageIdResolver() { + @Override + public String resolveLanguageId(FileObject fileObject) { + return languageId; + } + }); + } + return LanguageServerDescription.create(process.getInputStream(), process.getOutputStream(), process, serverLookup); } catch (Throwable t) { t.printStackTrace(); //TODO return null; diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form index c081107add5a..76e4a5f9c182 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form @@ -32,105 +32,10 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -145,6 +50,11 @@ + + + + + @@ -159,6 +69,11 @@ + + + + + @@ -173,6 +88,11 @@ + + + + + @@ -187,34 +107,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -229,6 +178,11 @@ + + + + + @@ -243,12 +197,22 @@ + + + + + + + + + + @@ -260,6 +224,11 @@ + + + + + @@ -274,13 +243,24 @@ + + + + + + + + + + + @@ -295,6 +275,11 @@ + + + + + @@ -302,6 +287,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java index d33b2d76bbcc..981b87e40e89 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java @@ -18,17 +18,20 @@ */ package org.netbeans.modules.lsp.client.options; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; import java.io.File; import java.util.Locale; import java.util.Set; -import javax.swing.GroupLayout; +import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JSeparator; import javax.swing.JTextField; -import javax.swing.LayoutStyle; import javax.swing.filechooser.FileFilter; import org.netbeans.modules.lsp.client.options.LanguageStorage.LanguageDescription; import org.openide.awt.Mnemonics; @@ -63,11 +66,21 @@ public LanguageDescriptionPanel(LanguageDescription desc, Set usedIds) { this.name.setText(desc.name); this.icon.setText(desc.icon); this.debugger.setSelected(desc.debugger); + this.languageId.setText(desc.languageId); } } public LanguageDescription getDescription() { - return new LanguageDescription(id, this.extensions.getText(), this.syntax.getText(), this.server.getText(), this.name.getText(), this.icon.getText(), this.debugger.isSelected()); + return new LanguageDescription( + id, + this.extensions.getText(), + this.syntax.getText(), + this.server.getText(), + this.name.getText(), + this.icon.getText(), + this.debugger.isSelected(), + this.languageId.getText() + ); } /** @@ -78,6 +91,7 @@ public LanguageDescription getDescription() { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + GridBagConstraints gridBagConstraints; JLabel nameLabel = new JLabel(); JLabel extensionsLabel = new JLabel(); @@ -95,119 +109,188 @@ private void initComponents() { icon = new JTextField(); JButton browseIcon = new JButton(); debugger = new JCheckBox(); + languageIdLabel = new JLabel(); + languageId = new JTextField(); + filler1 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(0, 32767)); + + setLayout(new GridBagLayout()); nameLabel.setLabelFor(name); Mnemonics.setLocalizedText(nameLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.nameLabel.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(nameLabel, gridBagConstraints); extensionsLabel.setLabelFor(extensions); Mnemonics.setLocalizedText(extensionsLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.extensionsLabel.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.ipadx = 32; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(extensionsLabel, gridBagConstraints); grammarLabel.setLabelFor(syntax); Mnemonics.setLocalizedText(grammarLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.grammarLabel.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(grammarLabel, gridBagConstraints); serverLabel.setLabelFor(server); Mnemonics.setLocalizedText(serverLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.serverLabel.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(serverLabel, gridBagConstraints); + name.setColumns(40); name.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.name.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 5; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(name, gridBagConstraints); + extensions.setColumns(40); extensions.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.extensions.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(extensions, gridBagConstraints); + syntax.setColumns(40); syntax.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.syntax.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(syntax, gridBagConstraints); + server.setColumns(40); server.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.server.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(server, gridBagConstraints); Mnemonics.setLocalizedText(browseGrammar, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.browseGrammar.text")); // NOI18N browseGrammar.addActionListener(this::browseGrammarActionPerformed); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(browseGrammar, gridBagConstraints); Mnemonics.setLocalizedText(browseServer, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.browseServer.text")); // NOI18N browseServer.addActionListener(this::browseServerActionPerformed); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(browseServer, gridBagConstraints); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(extraOptionsSeparator, gridBagConstraints); Mnemonics.setLocalizedText(optionalParams, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.optionalParams.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(optionalParams, gridBagConstraints); iconLabel.setLabelFor(icon); Mnemonics.setLocalizedText(iconLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.iconLabel.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 6; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(iconLabel, gridBagConstraints); + icon.setColumns(40); icon.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.icon.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 6; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(icon, gridBagConstraints); Mnemonics.setLocalizedText(browseIcon, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.browseIcon.text")); // NOI18N browseIcon.addActionListener(this::browseIconActionPerformed); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 7; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(browseIcon, gridBagConstraints); Mnemonics.setLocalizedText(debugger, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.debugger.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(debugger, gridBagConstraints); - GroupLayout layout = new GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(optionalParams) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addComponent(extraOptionsSeparator)) - .addGroup(GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addComponent(nameLabel) - .addComponent(iconLabel)) - .addGap(68, 68, 68) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING) - .addComponent(icon, GroupLayout.DEFAULT_SIZE, 273, Short.MAX_VALUE) - .addComponent(name)) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addComponent(browseIcon)) - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addComponent(grammarLabel) - .addComponent(serverLabel) - .addComponent(extensionsLabel, GroupLayout.PREFERRED_SIZE, 101, GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addComponent(extensions) - .addComponent(server) - .addComponent(syntax)) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addComponent(browseGrammar, GroupLayout.Alignment.TRAILING) - .addComponent(browseServer, GroupLayout.Alignment.TRAILING))) - .addGroup(layout.createSequentialGroup() - .addComponent(debugger) - .addGap(0, 0, Short.MAX_VALUE))) - .addContainerGap()) - ); - layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) - .addComponent(extensionsLabel) - .addComponent(extensions, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) - .addGap(18, 18, 18) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) - .addComponent(grammarLabel) - .addComponent(syntax, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) - .addComponent(browseGrammar)) - .addGap(18, 18, 18) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) - .addComponent(server, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) - .addComponent(serverLabel) - .addComponent(browseServer)) - .addGap(18, 18, 18) - .addComponent(debugger) - .addGap(18, 18, 18) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addComponent(optionalParams) - .addGroup(layout.createSequentialGroup() - .addGap(7, 7, 7) - .addComponent(extraOptionsSeparator, GroupLayout.PREFERRED_SIZE, 11, GroupLayout.PREFERRED_SIZE))) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) - .addComponent(nameLabel) - .addComponent(name, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) - .addGap(18, 18, 18) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) - .addComponent(icon, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) - .addComponent(iconLabel) - .addComponent(browseIcon)) - .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); + Mnemonics.setLocalizedText(languageIdLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.languageIdLabel.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 7; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(languageIdLabel, gridBagConstraints); + + languageId.setColumns(40); + languageId.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.languageId.text")); // NOI18N + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 7; + gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new Insets(5, 5, 5, 5); + add(languageId, gridBagConstraints); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 8; + gridBagConstraints.weighty = 1.0; + add(filler1, gridBagConstraints); }// //GEN-END:initComponents @Messages("DESC_JSONFilter=Grammars (.json, .xml, .tmLanguage)") @@ -271,7 +354,10 @@ private void browseServerActionPerformed(java.awt.event.ActionEvent evt) {//GEN- // Variables declaration - do not modify//GEN-BEGIN:variables private JCheckBox debugger; private JTextField extensions; + private Box.Filler filler1; private JTextField icon; + private JTextField languageId; + private JLabel languageIdLabel; private JTextField name; private JTextField server; private JTextField syntax; diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java index 8572d2807897..741d08cc439c 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java @@ -38,7 +38,7 @@ final class LanguageServersPanel extends javax.swing.JPanel { - private static final LanguageDescription PROTOTYPE = new LanguageDescription(null, null, null, null, "MMMMMMMMMMMMMMMMM", null, false); + private static final LanguageDescription PROTOTYPE = new LanguageDescription(null, null, null, null, "MMMMMMMMMMMMMMMMM", null, false, null); private final LanguageServersOptionsPanelController controller; private final DefaultListModel languages; private final Set usedIds; diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java index 31d61b38fce0..7d167a92d239 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java @@ -187,6 +187,9 @@ static void store(List languages) { if (description.name != null) { langServer.setAttribute("name", description.name); } + if (description.languageId != null && !description.languageId.isBlank()) { + langServer.setAttribute("languageId", description.languageId); + } } deleteConfigFileIfExists("Editors/" + description.mimeType + "/generic-breakpoints.instance"); @@ -282,6 +285,7 @@ public static class LanguageDescription { public String icon; public String mimeType; public boolean debugger; + public String languageId; public LanguageDescription() { this.id = null; @@ -292,9 +296,10 @@ public LanguageDescription() { this.icon = null; this.debugger = false; this.mimeType = null; + this.languageId = null; } - public LanguageDescription(String id, String extensions, String syntaxGrammar, String languageServer, String name, String icon, boolean debugger) { + public LanguageDescription(String id, String extensions, String syntaxGrammar, String languageServer, String name, String icon, boolean debugger, String languageId) { this.id = id; this.extensions = extensions; this.syntaxGrammar = syntaxGrammar; @@ -303,6 +308,7 @@ public LanguageDescription(String id, String extensions, String syntaxGrammar, S this.icon = icon; this.debugger = debugger; this.mimeType = "text/x-ext-" + id; + this.languageId = languageId; } } diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageIdResolver.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageIdResolver.java new file mode 100644 index 000000000000..f315fe4c897e --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageIdResolver.java @@ -0,0 +1,43 @@ +/* + * 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.spi; + +import org.openide.filesystems.FileObject; + +/** + * LSP uses languageId as a carrier to inform what language/type of file is + * being edited. Even a single language server can provide support for multiple + * languages. One such example is the typescript language server, which defaults + * to typescript, but also supports the typescript variant that embeds react + * templates (TSX). + * + *

Language servers can provide a language id mapper to allow customization + * of the resolving process.

+ * + * @since 1.33.0 + */ +public interface LanguageIdResolver { + /** + * Resolve the language id for the given file object. + * + * @param fileObject target to resolve the langeuge id for + * @return the determined language id or {@code null} if that can't be found. + */ + public String resolveLanguageId(FileObject fileObject); +} diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java index eea786f3fe34..48fb06d3a0bf 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/LanguageServerProvider.java @@ -57,25 +57,47 @@ public static final class LanguageServerDescription { * @param process the process of the running language server, or null if none. * @return an instance of LanguageServerDescription */ - public static @NonNull LanguageServerDescription create(@NonNull InputStream in, @NonNull OutputStream out, @NullAllowed Process process) { - return new LanguageServerDescription(in, out, process, null); + public static @NonNull LanguageServerDescription create( + @NonNull InputStream in, + @NonNull OutputStream out, + @NullAllowed Process process) { + return create(in, out, process, null); + } + + /** + * Create the description of a running language server. + * + * @param in the InputStream that should be used to communicate with the server + * @param out the OutputStream that should be used to communicate with the server + * @param process the process of the running language server, or null if none. + * @param lookup lookup to be provided by the server + * @return an instance of LanguageServerDescription + */ + public static @NonNull LanguageServerDescription create( + @NonNull InputStream in, + @NonNull OutputStream out, + @NullAllowed Process process, + @NullAllowed Lookup lookup) { + return new LanguageServerDescription(in, out, process, null, lookup == null ? Lookup.EMPTY : lookup); } static @NonNull LanguageServerDescription create(@NonNull LanguageServer server) { - return new LanguageServerDescription(null, null, null, server); + return new LanguageServerDescription(null, null, null, server, Lookup.EMPTY); } private final InputStream in; private final OutputStream out; private final Process process; private final LanguageServer server; + private final Lookup lookup; private LSPBindings bindings; - private LanguageServerDescription(InputStream in, OutputStream out, Process process, LanguageServer server) { + private LanguageServerDescription(InputStream in, OutputStream out, Process process, LanguageServer server, Lookup lookup) { this.in = in; this.out = out; this.process = process; this.server = server; + this.lookup = lookup; } static { @@ -110,6 +132,11 @@ public void setBindings(LanguageServerDescription desc, LSPBindings bindings) { desc.bindings = bindings; } + @Override + public Lookup getLookup(LanguageServerDescription desc) { + return desc.lookup; + } + @Override public LanguageServerDescription createLanguageServerDescription(LanguageServer server) { return LanguageServerDescription.create(server); diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java index 8cd5eea1b9c3..89b4a020c995 100644 --- a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java @@ -93,7 +93,7 @@ public FileObject findResource(String name) { DataObject testDO = DataObject.find(testFO); assertEquals("org.openide.loaders.DefaultDataObject", testDO.getClass().getName()); - LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false))); + LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false, null))); assertEquals("text/x-ext-t", FileUtil.getMIMEType(testFO)); DataObject recognized = DataObject.find(testFO); @@ -107,7 +107,7 @@ public FileObject findResource(String name) { Language l = MimeLookup.getLookup("text/x-ext-t").lookup(Language.class); assertNotNull(l); - LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false))); + LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false, null))); LanguageStorage.store(Collections.emptyList()); diff --git a/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptDataObjectDataObject.java b/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptDataObjectDataObject.java index a3db16474ea2..7f2c633a5a1c 100644 --- a/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptDataObjectDataObject.java +++ b/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptDataObjectDataObject.java @@ -44,7 +44,7 @@ @MIMEResolver.ExtensionRegistration( displayName = "#LBL_TypeScriptDataObject_LOADER", mimeType = TYPESCRIPT_MIME_TYPE, - extension = {"ts"}, + extension = {"ts", "tsx"}, position = 193 // lower than 218 as CND also recognizes .ts file ) @DataObject.Registration( diff --git a/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptLSP.java b/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptLSP.java index c54c6f33ccbf..16d140dd5ea0 100644 --- a/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptLSP.java +++ b/webcommon/typescript.editor/src/org/netbeans/modules/typescript/editor/TypeScriptLSP.java @@ -26,12 +26,14 @@ import org.netbeans.api.editor.mimelookup.MimeRegistration; import org.netbeans.api.options.OptionsDisplayer; import org.netbeans.modules.javascript.nodejs.api.NodeJsSupport; +import org.netbeans.modules.lsp.client.spi.LanguageIdResolver; import org.netbeans.modules.lsp.client.spi.LanguageServerProvider; import org.openide.awt.NotificationDisplayer; import org.openide.modules.InstalledFileLocator; import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; +import org.openide.util.lookup.Lookups; import static org.netbeans.modules.typescript.editor.TypeScriptEditorKit.TYPESCRIPT_ICON; import static org.netbeans.modules.typescript.editor.TypeScriptEditorKit.TYPESCRIPT_MIME_TYPE; @@ -45,6 +47,14 @@ public class TypeScriptLSP implements LanguageServerProvider { private static final Logger LOG = Logger.getLogger(TypeScriptLSP.class.getName()); + private static final LanguageIdResolver LANGUAGE_ID_RESOLVER = fo -> { + if("tsx".equalsIgnoreCase(fo.getExt())) { + return "typescriptreact"; + } else { + return "typescript"; + } + }; + private final AtomicBoolean missingNodeWarningIssued = new AtomicBoolean(); @Override @@ -64,7 +74,12 @@ public LanguageServerDescription startServer(Lookup lookup) { File server = InstalledFileLocator.getDefault().locate("typescript-lsp/node_modules/typescript-language-server/lib/cli.mjs", "org.netbeans.modules.typescript.editor", false); try { Process p = new ProcessBuilder(new String[] {node, server.getAbsolutePath(), "--stdio"}).directory(server.getParentFile().getParentFile()).redirectError(ProcessBuilder.Redirect.INHERIT).start(); - return LanguageServerDescription.create(p.getInputStream(), p.getOutputStream(), p); + return LanguageServerDescription.create( + p.getInputStream(), + p.getOutputStream(), + p, + Lookups.fixed(LANGUAGE_ID_RESOLVER) + ); } catch (IOException ex) { LOG.log(Level.FINE, null, ex); return null;