chore(lsp): Pull in lsp4intellij into our codebase to make fixing stuff faster

This commit is contained in:
FalsePattern 2023-08-16 13:13:04 +02:00 committed by FalsePattern
parent 2979e53bd9
commit f77975406c
Signed by: falsepattern
GPG key ID: FDF7126A9E124447
82 changed files with 10698 additions and 49 deletions

View file

@ -56,14 +56,31 @@ proper caret placements with go to usages, and so on.
Thus, this project will still use PSI trees and the IntelliJ lexer/parser system, but with heavy moderation, and any
sort of "smart inspection" *shall not* be implemented in the PSI, but instead retrieved from the language server.
<!-- Plugin description end -->
## Licenses
```
All code in this project, unless specified differently, is licensed under the Apache 2.0 license.
```
```
<p>
All code in this project, unless specified differently, is licensed under the `Apache 2.0` license.
</p>
<p>
The code inside the `lsp` package is derived from the LSP4IntelliJ project, with various modifications, fixes, and
additions to fix any outstanding issues i was having with the original code. (https://github.com/ballerina-platform/lsp4intellij)
The original code is Copyright WSO2 Inc., licensed under the `Apache 2.0` license.
</p>
<p>
The art assets inside src/art/zig, and all copies of them, are derived from the official Zig Programming Language logo,
which are property of the Zig Software Foundation. (https://github.com/ziglang/logo).
These art assets are licensed under `Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).`
```
</p>
<!-- Plugin description end -->

View file

@ -42,7 +42,9 @@ repositories {
dependencies {
compileOnly(libs.annotations)
implementation("com.github.FalsePattern:lsp4intellij-zigbrains:c6b5059039")
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.21.0")
implementation("com.vladsch.flexmark:flexmark:0.34.60")
implementation("org.apache.commons:commons-lang3:3.12.0")
}
intellij {

View file

@ -0,0 +1,383 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp;
import com.falsepattern.zigbrains.lsp.requests.Timeout;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import static com.falsepattern.zigbrains.lsp.utils.ApplicationUtils.pool;
public class IntellijLanguageClient {
private static Logger LOG = Logger.getInstance(IntellijLanguageClient.class);
private static final Map<Pair<String, String>, LanguageServerWrapper> extToLanguageWrapper = new ConcurrentHashMap<>();
private static Map<String, Set<LanguageServerWrapper>> projectToLanguageWrappers = new ConcurrentHashMap<>();
private static Map<Pair<String, String>, LanguageServerDefinition> extToServerDefinition = new ConcurrentHashMap<>();
private static Map<String, LSPExtensionManager> extToExtManager = new ConcurrentHashMap<>();
private static final Predicate<LanguageServerWrapper> RUNNING = (s) -> s.getStatus() != ServerStatus.STOPPED;
// @Override
// public void initComponent() {
// try {
// // Adds project listener.
// ApplicationManager.getApplication().getMessageBus().connect().subscribe(ProjectManager.TOPIC,
// new LSPProjectManagerListener());
// // Adds editor listener.
// EditorFactory.getInstance().addEditorFactoryListener(new LSPEditorListener(), this);
// // Adds VFS listener.
// VirtualFileManager.getInstance().addVirtualFileListener(new VFSListener());
// // Adds document event listener.
// ApplicationManager.getApplication().getMessageBus().connect().subscribe(AppTopics.FILE_DOCUMENT_SYNC,
// new LSPFileDocumentManagerListener());
//
// // in case if JVM forcefully exit.
// Runtime.getRuntime().addShutdownHook(new Thread(() -> projectToLanguageWrappers.values().stream()
// .flatMap(Collection::stream).filter(RUNNING).forEach(s -> s.stop(true))));
//
// LOG.info("Intellij Language Client initialized successfully");
// } catch (Exception e) {
// LOG.warn("Fatal error occurred when initializing Intellij language client.", e);
// }
// }
/**
* Use it to initialize the server connection for the given project (useful if no editor is launched)
*/
public void initProjectConnections(@NotNull Project project) {
String projectStr = FileUtils.projectToUri(project);
// find serverdefinition keys for this project and try to start a wrapper
extToServerDefinition.entrySet().stream().filter(e -> e.getKey().getRight().equals(projectStr)).forEach(entry -> {
updateLanguageWrapperContainers(project, entry.getKey(), entry.getValue()).start();
});
}
/**
* Adds a new server definition, attached to the given file extension.
* This definition will be applicable for any project, since a specific project is not defined.
* Plugin developers can register their application-level language server definitions using this API.
*
* @param definition The server definition
*/
@SuppressWarnings("unused")
public static void addServerDefinition(@NotNull LanguageServerDefinition definition) {
addServerDefinition(definition, null);
}
/**
* Adds a new server definition, attached to the given file extension and the project.
* Plugin developers can register their project-level language server definitions using this API.
*
* @param definition The server definition
*/
@SuppressWarnings("unused")
public static void addServerDefinition(@NotNull LanguageServerDefinition definition, @Nullable Project project) {
if (project != null) {
processDefinition(definition, FileUtils.projectToUri(project));
FileUtils.reloadEditors(project);
} else {
processDefinition(definition, "");
FileUtils.reloadAllEditors();
}
LOG.info("Added definition for " + definition);
}
/**
* Adds a new LSP extension manager, attached to the given file extension.
* Plugin developers should register their custom language server extensions using this API.
*
* @param ext File extension type
* @param manager LSP extension manager (Should be implemented by the developer)
*/
@SuppressWarnings("unused")
public static void addExtensionManager(@NotNull String ext, @NotNull LSPExtensionManager manager) {
if (extToExtManager.get(ext) != null) {
LOG.warn("An extension manager is already registered for \"" + ext + "\" extension");
}
extToExtManager.put(ext, manager);
}
/**
* @return All instantiated ServerWrappers
*/
public static Set<LanguageServerWrapper> getAllServerWrappersFor(String projectUri) {
Set<LanguageServerWrapper> allWrappers = new HashSet<>();
extToLanguageWrapper.forEach((stringStringPair, languageServerWrapper) -> {
if (FileUtils.projectToUri(languageServerWrapper.getProject()).equals(projectUri)) {
allWrappers.add(languageServerWrapper);
}
});
return allWrappers;
}
/**
* @return All registered LSP protocol extension managers.
*/
public static LSPExtensionManager getExtensionManagerFor(String fileExt) {
if (extToExtManager.containsKey(fileExt)) {
return extToExtManager.get(fileExt);
}
return null;
}
/**
* @param virtualFile The virtual file instance to be validated
* @return True if there is a LanguageServer supporting this extension, false otherwise
*/
public static boolean isExtensionSupported(VirtualFile virtualFile) {
return extToServerDefinition.keySet().stream().anyMatch(keyMap ->
keyMap.getLeft().equals(virtualFile.getExtension()) || (virtualFile.getName().matches(keyMap.getLeft())));
}
/**
* Called when an editor is opened. Instantiates a LanguageServerWrapper if necessary, and adds the Editor to the Wrapper
*
* @param editor the editor
*/
public static void editorOpened(Editor editor) {
VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
if (!FileUtils.isFileSupported(file)) {
LOG.debug("Handling open on a editor which host a LightVirtual/Null file");
return;
}
Project project = editor.getProject();
if (project == null) {
LOG.debug("Opened an unsupported editor, which does not have an attached project.");
return;
}
String projectUri = FileUtils.projectToUri(project);
if (projectUri == null) {
LOG.warn("File for editor " + editor.getDocument().getText() + " is null");
return;
}
pool(() -> {
String ext = file.getExtension();
final String fileName = file.getName();
LOG.info("Opened " + fileName);
// The ext can either be a file extension or a file pattern(regex expression).
// First try for the extension since it is the most comment usage, if not try to
// match file name.
LanguageServerDefinition serverDefinition = extToServerDefinition.get(new ImmutablePair<>(ext, projectUri));
if (serverDefinition == null) {
// Fallback to file name pattern matching, where the map key is a regex.
Optional<Pair<String, String>> keyForFile = extToServerDefinition.keySet().stream().
filter(keyPair -> fileName.matches(keyPair.getLeft()) && keyPair.getRight().equals(projectUri))
.findFirst();
if (keyForFile.isPresent()) {
serverDefinition = extToServerDefinition.get(keyForFile.get());
// ext must be the key since we are in file name mode.
ext = keyForFile.get().getLeft();
}
}
// If cannot find a project-specific server definition for the given file and project, repeat the
// above process to find an application level server definition for the given file extension/regex.
if (serverDefinition == null) {
serverDefinition = extToServerDefinition.get(new ImmutablePair<>(ext, ""));
}
if (serverDefinition == null) {
// Fallback to file name pattern matching, where the map key is a regex.
Optional<Pair<String, String>> keyForFile = extToServerDefinition.keySet().stream().
filter(keyPair -> fileName.matches(keyPair.getLeft()) && keyPair.getRight().isEmpty())
.findFirst();
if (keyForFile.isPresent()) {
serverDefinition = extToServerDefinition.get(keyForFile.get());
// ext must be the key since we are in file name mode.
ext = keyForFile.get().getLeft();
}
}
if (serverDefinition == null) {
LOG.warn("Could not find a server definition for " + ext);
return;
}
// Update project mapping for language servers.
LanguageServerWrapper wrapper = updateLanguageWrapperContainers(project, new ImmutablePair<>(ext, projectUri), serverDefinition);
LOG.info("Adding file " + fileName);
wrapper.connect(editor);
});
}
private static synchronized LanguageServerWrapper updateLanguageWrapperContainers(Project project, final Pair<String, String> key, LanguageServerDefinition serverDefinition) {
String projectUri = FileUtils.projectToUri(project);
LanguageServerWrapper wrapper = extToLanguageWrapper.get(key);
String ext = key.getLeft();
if (wrapper == null) {
LOG.info("Instantiating wrapper for " + ext + " : " + projectUri);
if (extToExtManager.get(ext) != null) {
wrapper = new LanguageServerWrapper(serverDefinition, project, extToExtManager.get(ext));
} else {
wrapper = new LanguageServerWrapper(serverDefinition, project);
}
String[] exts = serverDefinition.ext.split(LanguageServerDefinition.SPLIT_CHAR);
for (String ex : exts) {
extToLanguageWrapper.put(new ImmutablePair<>(ex, projectUri), wrapper);
}
Set<LanguageServerWrapper> wrappers = projectToLanguageWrappers
.computeIfAbsent(projectUri, k -> new HashSet<>());
wrappers.add(wrapper);
} else {
LOG.info("Wrapper already existing for " + ext + " , " + projectUri);
}
return wrapper;
}
/**
* Called when an editor is closed. Notifies the LanguageServerWrapper if needed
*
* @param editor the editor.
*/
public static void editorClosed(Editor editor) {
VirtualFile file = FileUtils.virtualFileFromEditor(editor);
if (!FileUtils.isFileSupported(file)) {
LOG.debug("Handling close on a editor which host a LightVirtual/Null file");
return;
}
pool(() -> {
LanguageServerWrapper serverWrapper = LanguageServerWrapper.forEditor(editor);
if (serverWrapper != null) {
LOG.info("Disconnecting " + FileUtils.editorToURIString(editor));
serverWrapper.disconnect(editor);
}
});
}
/**
* Returns current timeout values.
*
* @return A map of Timeout types and corresponding values(in milliseconds).
*/
public static Map<Timeouts, Integer> getTimeouts() {
return Timeout.getTimeouts();
}
/**
* Returns current timeout value of a given timeout type.
*
* @return A map of Timeout types and corresponding values(in milliseconds).
*/
@SuppressWarnings("unused")
public static int getTimeout(Timeouts timeoutType) {
return getTimeouts().get(timeoutType);
}
/**
* Overrides default timeout values with a given set of timeouts.
*
* @param newTimeouts A map of Timeout types and corresponding values to be set.
*/
public static void setTimeouts(Map<Timeouts, Integer> newTimeouts) {
Timeout.setTimeouts(newTimeouts);
}
/**
* @param timeout Timeout type
* @param value new timeout value to be set (in milliseconds).
*/
@SuppressWarnings("unused")
public static void setTimeout(Timeouts timeout, int value) {
Map<Timeouts, Integer> newTimeout = new HashMap<>();
newTimeout.put(timeout, value);
setTimeouts(newTimeout);
}
public static void removeWrapper(LanguageServerWrapper wrapper) {
if (wrapper.getProject() != null) {
String[] extensions = wrapper.getServerDefinition().ext.split(LanguageServerDefinition.SPLIT_CHAR);
for (String ext : extensions) {
MutablePair<String, String> extProjectPair = new MutablePair<>(ext, FileUtils.pathToUri(
new File(wrapper.getProjectRootPath()).getAbsolutePath()));
extToLanguageWrapper.remove(extProjectPair);
extToServerDefinition.remove(extProjectPair);
}
} else {
LOG.error("No attached projects found for wrapper");
}
}
public static Map<String, Set<LanguageServerWrapper>> getProjectToLanguageWrappers() {
return projectToLanguageWrappers;
}
@SuppressWarnings("unused")
public static void didChangeConfiguration(@NotNull DidChangeConfigurationParams params, @NotNull Project project) {
final Set<LanguageServerWrapper> serverWrappers = IntellijLanguageClient.getProjectToLanguageWrappers()
.get(FileUtils.projectToUri(project));
serverWrappers.forEach(s -> s.getRequestManager().didChangeConfiguration(params));
}
/**
* Returns the registered extension manager for this language server.
*
* @param definition The LanguageServerDefinition
*/
public static Optional<LSPExtensionManager> getExtensionManagerForDefinition(@NotNull LanguageServerDefinition definition) {
return Optional.ofNullable(extToExtManager.get(definition.ext.split(",")[0]));
}
// @Override
// public void disposeComponent() {
// Disposer.dispose(this);
// }
// @Override
// public void dispose() {
// Disposer.dispose(this);
// }
private static void processDefinition(LanguageServerDefinition definition, String projectUri) {
String[] extensions = definition.ext.split(LanguageServerDefinition.SPLIT_CHAR);
for (String ext : extensions) {
Pair<String, String> keyPair = new ImmutablePair<>(ext, projectUri);
if (extToServerDefinition.get(keyPair) == null) {
extToServerDefinition.put(keyPair, definition);
LOG.info("Added server definition for " + ext);
} else {
extToServerDefinition.replace(keyPair, definition);
LOG.info("Updated server definition for " + ext);
}
}
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.actions;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.find.FindBundle;
import com.intellij.find.findUsages.FindUsagesOptions;
import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.ui.JBColor;
import com.intellij.ui.LightweightHint;
import com.intellij.usageView.UsageInfo;
import com.intellij.usageView.UsageViewUtil;
import com.intellij.usages.Usage;
import com.intellij.usages.UsageInfo2UsageAdapter;
import com.intellij.usages.UsageTarget;
import com.intellij.usages.UsageViewManager;
import com.intellij.usages.UsageViewPresentation;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
/**
* Action for references / see usages (SHIFT+ALT+F7)
*/
public class LSPReferencesAction extends DumbAwareAction {
@Override
public void actionPerformed(AnActionEvent e) {
Editor editor = e.getData(CommonDataKeys.EDITOR);
if (editor != null) {
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager == null) {
return;
}
List<PsiElement2UsageTargetAdapter> targets = new ArrayList<>();
Pair<List<PsiElement>, List<VirtualFile>> references = eventManager
.references(editor.getCaretModel().getCurrentCaret().getOffset());
if (references.first != null && references.second != null) {
references.first.forEach(element -> targets.add(new PsiElement2UsageTargetAdapter(element, true)));
}
showReferences(editor, targets, editor.getCaretModel().getCurrentCaret().getLogicalPosition());
}
}
public void forManagerAndOffset(EditorEventManager manager, int offset) {
List<PsiElement2UsageTargetAdapter> targets = new ArrayList<>();
Pair<List<PsiElement>, List<VirtualFile>> references = manager.references(offset);
if (references.first != null && references.second != null) {
references.first.forEach(element -> targets.add(new PsiElement2UsageTargetAdapter(element, true)));
}
Editor editor = manager.editor;
showReferences(editor, targets, editor.offsetToLogicalPosition(offset));
}
private void showReferences(Editor editor, List<PsiElement2UsageTargetAdapter> targets, LogicalPosition position) {
if (targets.isEmpty()) {
short constraint = HintManager.ABOVE;
int flags = HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING;
JLabel label = new JLabel("No references found");
label.setBackground(new JBColor(new Color(150, 0, 0), new Color(150, 0, 0)));
LightweightHint hint = new LightweightHint(label);
Point p = HintManagerImpl.getHintPosition(hint, editor, position, constraint);
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, p, flags, 0, false,
HintManagerImpl.createHintHint(editor, p, hint, constraint).setContentActive(false));
} else {
List<Usage> usages = new ArrayList<>();
targets.forEach(ut -> {
PsiElement elem = ut.getElement();
usages.add(new UsageInfo2UsageAdapter(new UsageInfo(elem, -1, -1, false)));
});
if (editor == null) {
return;
}
Project project = editor.getProject();
if (project == null) {
return;
}
UsageViewPresentation presentation = createPresentation(targets.get(0).getElement(),
new FindUsagesOptions(editor.getProject()), false);
UsageViewManager.getInstance(project)
.showUsages(new UsageTarget[] { targets.get(0) }, usages.toArray(new Usage[usages.size()]),
presentation);
}
}
private UsageViewPresentation createPresentation(PsiElement psiElement, FindUsagesOptions options,
boolean toOpenInNewTab) {
UsageViewPresentation presentation = new UsageViewPresentation();
String scopeString = options.searchScope.getDisplayName();
presentation.setScopeText(scopeString);
String usagesString = options.generateUsagesString();
presentation.setSearchString(usagesString);
String title = FindBundle.message("find.usages.of.element.in.scope.panel.title", usagesString,
UsageViewUtil.getLongName(psiElement), scopeString);
presentation.setTabText(title);
presentation.setTabName(FindBundle
.message("find.usages.of.element.tab.name", usagesString, UsageViewUtil.getShortName(psiElement)));
presentation.setTargetsNodeText(StringUtil.capitalize(UsageViewUtil.getType(psiElement)));
presentation.setOpenInNewTab(toOpenInNewTab);
presentation.setShowCancelButton(true);
return presentation;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.actions;
import com.intellij.codeInsight.actions.ReformatCodeAction;
import com.intellij.lang.LanguageFormatting;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.requests.ReformatHandler;
/**
* Action overriding the default reformat action
* Fallback to the default action if the language is already supported or not supported by any language server
*/
public class LSPReformatAction extends ReformatCodeAction implements DumbAware {
private Logger LOG = Logger.getInstance(LSPReformatAction.class);
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getData(CommonDataKeys.PROJECT);
Editor editor = e.getData(CommonDataKeys.EDITOR);
if (editor == null || project == null) {
return;
}
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (LanguageFormatting.INSTANCE.allForLanguage(file.getLanguage()).isEmpty() && IntellijLanguageClient
.isExtensionSupported(file.getVirtualFile())) {
// if editor hasSelection, only reformat selection, not reformat the whole file
if (editor.getSelectionModel().hasSelection()) {
ReformatHandler.reformatSelection(editor);
} else {
ReformatHandler.reformatFile(editor);
}
} else {
super.actionPerformed(e);
}
}
@Override
public void update(AnActionEvent event) {
super.update(event);
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.actions;
import com.intellij.codeInsight.actions.LayoutCodeDialog;
import com.intellij.codeInsight.actions.LayoutCodeOptions;
import com.intellij.codeInsight.actions.ShowReformatFileDialog;
import com.intellij.codeInsight.actions.TextRangeType;
import com.intellij.lang.LanguageFormatting;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
/**
* Class overriding the default action handling the Reformat dialog event (CTRL+ALT+SHIFT+L by default)
* Fallback to the default action if the language is already supported or not supported by any language server
*/
public class LSPShowReformatDialogAction extends ShowReformatFileDialog implements DumbAware {
private String HELP_ID = "editing.codeReformatting";
private Logger LOG = Logger.getInstance(LSPShowReformatDialogAction.class);
@Override
public void actionPerformed(AnActionEvent e) {
Editor editor = e.getData(CommonDataKeys.EDITOR);
Project project = e.getData(CommonDataKeys.PROJECT);
if (editor != null && project != null) {
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
VirtualFile virFile = FileDocumentManager.getInstance().getFile(editor.getDocument());
boolean alreadySupported = !LanguageFormatting.INSTANCE.allForLanguage(psiFile.getLanguage()).isEmpty();
if (!alreadySupported && IntellijLanguageClient.isExtensionSupported(virFile)) {
boolean hasSelection = editor.getSelectionModel().hasSelection();
LayoutCodeDialog dialog = new LayoutCodeDialog(project, psiFile, hasSelection, HELP_ID);
dialog.show();
if (dialog.isOK()) {
LayoutCodeOptions options = dialog.getRunOptions();
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager != null) {
if (options.getTextRangeType() == TextRangeType.SELECTED_TEXT) {
eventManager.reformatSelection();
} else {
eventManager.reformat();
}
}
} else {
// if user chose cancel , the dialog in super.actionPerformed(e) will show again
// super.actionPerformed(e);
}
} else {
super.actionPerformed(e);
}
} else {
super.actionPerformed(e);
}
}
@Override
public void update(AnActionEvent event) {
super.update(event);
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
/**
* The client context which is received by {@link DefaultLanguageClient}. The context contain
* information about the runtime and its components.
*
* @author gayanper
*/
public interface ClientContext {
/**
* Returns the {@link EditorEventManager} for the given document URI.
*/
@Nullable
EditorEventManager getEditorEventManagerFor(@NotNull String documentUri);
/**
* Returns the {@link Project} associated with the LanuageClient.
*/
@Nullable
Project getProject();
/**
* Returns the {@link RequestManager} associated with the Language Server Connection.
*/
@Nullable
RequestManager getRequestManager();
}

View file

@ -0,0 +1,267 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client;
import com.falsepattern.zigbrains.lsp.requests.WorkspaceEditHandler;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationAction;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ui.Messages;
import com.intellij.util.ui.UIUtil;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.services.LanguageClient;
import org.jetbrains.annotations.NotNull;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import javax.swing.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class DefaultLanguageClient implements LanguageClient {
@NotNull
final private Logger LOG = Logger.getInstance(DefaultLanguageClient.class);
@NotNull
private final NotificationGroup STICKY_NOTIFICATION_GROUP = NotificationGroupManager.getInstance().getNotificationGroup("lsp");
@NotNull
final private Map<String, DynamicRegistrationMethods> registrations = new ConcurrentHashMap<>();
@NotNull
private final ClientContext context;
protected boolean isModal = false;
public DefaultLanguageClient(@NotNull ClientContext context) {
this.context = context;
}
@Override
public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) {
boolean response = WorkspaceEditHandler.applyEdit(params.getEdit(), "LSP edits");
return CompletableFuture.supplyAsync(() -> new ApplyWorkspaceEditResponse(response));
}
@Override
public CompletableFuture<List<Object>> configuration(ConfigurationParams configurationParams) {
return LanguageClient.super.configuration(configurationParams);
}
@Override
public CompletableFuture<List<WorkspaceFolder>> workspaceFolders() {
return LanguageClient.super.workspaceFolders();
}
@Override
public CompletableFuture<Void> registerCapability(RegistrationParams params) {
return CompletableFuture.runAsync(() -> params.getRegistrations().forEach(r -> {
String id = r.getId();
Optional<DynamicRegistrationMethods> method = DynamicRegistrationMethods.forName(r.getMethod());
method.ifPresent(dynamicRegistrationMethods -> registrations.put(id, dynamicRegistrationMethods));
}));
}
@Override
public CompletableFuture<Void> unregisterCapability(UnregistrationParams params) {
return CompletableFuture.runAsync(() -> params.getUnregisterations().forEach((Unregistration r) -> {
String id = r.getId();
Optional<DynamicRegistrationMethods> method = DynamicRegistrationMethods.forName(r.getMethod());
if (registrations.containsKey(id)) {
registrations.remove(id);
} else {
Map<DynamicRegistrationMethods, String> inverted = new HashMap<>();
for (Map.Entry<String, DynamicRegistrationMethods> entry : registrations.entrySet()) {
inverted.put(entry.getValue(), entry.getKey());
}
if (method.isPresent() && inverted.containsKey(method.get())) {
registrations.remove(inverted.get(method.get()));
}
}
}));
}
@Override
public void telemetryEvent(Object o) {
LOG.info(o.toString());
}
@Override
public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) {
String uri = FileUtils.sanitizeURI(publishDiagnosticsParams.getUri());
List<Diagnostic> diagnostics = publishDiagnosticsParams.getDiagnostics();
EditorEventManagerBase.diagnostics(uri, diagnostics);
}
@Override
public void showMessage(MessageParams messageParams) {
String title = "Language Server message";
String message = messageParams.getMessage();
if (isModal) {
ApplicationUtils.invokeLater(() -> {
MessageType msgType = messageParams.getType();
switch (msgType) {
case Error:
Messages.showErrorDialog(message, title);
break;
case Warning:
Messages.showWarningDialog(message, title);
break;
case Info:
case Log:
Messages.showInfoMessage(message, title);
break;
default:
LOG.warn("No message type for " + message);
break;
}
});
} else {
NotificationType type = getNotificationType(messageParams.getType());
final Notification notification = new Notification(
"lsp", messageParams.getType().toString(), messageParams.getMessage(), type);
notification.notify(context.getProject());
}
}
@Override
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams) {
List<MessageActionItem> actions = showMessageRequestParams.getActions();
String title = "Language Server " + showMessageRequestParams.getType().toString();
String message = showMessageRequestParams.getMessage();
MessageType msgType = showMessageRequestParams.getType();
String[] options = new String[actions == null ? 0 : actions.size()];
for (int i = 0, size = options.length; i < size; i++) {
options[i] = actions.get(i).getTitle();
}
int exitCode;
FutureTask<Integer> task;
if (isModal) {
Icon icon;
switch (msgType) {
case Error:
icon = UIUtil.getErrorIcon();
break;
case Warning:
icon = UIUtil.getWarningIcon();
break;
case Info:
case Log:
icon = UIUtil.getInformationIcon();
break;
default:
icon = null;
LOG.warn("No message type for " + message);
break;
}
task = new FutureTask<>(
() -> Messages.showDialog(message, title, options, 0, icon));
ApplicationManager.getApplication().invokeAndWait(task);
try {
exitCode = task.get();
} catch (InterruptedException | ExecutionException e) {
LOG.warn(e.getMessage());
exitCode = -1;
}
} else {
final Notification notification = STICKY_NOTIFICATION_GROUP.createNotification(title, message, getNotificationType(msgType));
final CompletableFuture<Integer> integerCompletableFuture = new CompletableFuture<>();
for (int i = 0, optionsSize = options.length; i < optionsSize; i++) {
int finalI = i;
notification.addAction(new NotificationAction(options[i]) {
@Override
public boolean isDumbAware() {
return true;
}
@Override
public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) {
integerCompletableFuture.complete(finalI);
notification.expire();
}
});
}
notification.whenExpired(() -> {
if (!integerCompletableFuture.isDone()) {
integerCompletableFuture.complete(-1);
}
});
notification.notify(context.getProject());
try {
exitCode = integerCompletableFuture.get();
} catch (InterruptedException | ExecutionException e) {
LOG.warn(e.getMessage());
exitCode = -1;
}
}
return CompletableFuture.completedFuture(actions == null || exitCode < 0 ? null : actions.get(exitCode));
}
protected NotificationType getNotificationType(@NotNull MessageType messageType) {
switch (messageType) {
case Error:
return NotificationType.ERROR;
case Warning:
return NotificationType.WARNING;
case Info:
case Log:
default:
return NotificationType.INFORMATION;
}
}
@Override
public void logMessage(MessageParams messageParams) {
String message = messageParams.getMessage();
MessageType msgType = messageParams.getType();
switch (msgType) {
case Error:
LOG.error(message);
break;
case Warning:
LOG.warn(message);
break;
case Info:
case Log:
LOG.info(message);
break;
default:
LOG.warn("Unknown message type '" + msgType + "' for " + message);
break;
}
}
@NotNull
protected final ClientContext getContext() {
return context;
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client;
import java.util.Arrays;
import java.util.Optional;
/**
* Enum for methods which may support DynamicRegistration
*/
public enum DynamicRegistrationMethods {
DID_CHANGE_CONFIGURATION("workspace/didChangeConfiguration"),
DID_CHANGE_WATCHED_FILES("workspace/didChangeWatchedFiles"),
SYMBOL("workspace/symbol"),
EXECUTE_COMMAND("workspace/executeCommand"),
SYNCHRONIZATION("textDocument/synchronization"),
COMPLETION("textDocument/completion"),
HOVER("textDocument/hover"),
SIGNATURE_HELP("textDocument/signatureHelp"),
REFERENCES("textDocument/references"),
DOCUMENT_HIGHLIGHT("textDocument/documentHighlight"),
DOCUMENT_SYMBOL("textDocument/documentSymbol"),
FORMATTING("textDocument/formatting"),
RANGE_FORMATTING("textDocument/rangeFormatting"),
ONTYPE_FORMATTING("textDocument/onTypeFormatting"),
DEFINITION("textDocument/definition"),
CODE_ACTION("textDocument/codeAction"),
CODE_LENS("textDocument/codeLens"),
DOCUMENT_LINK("textDocument/documentLink"),
RENAME("textDocument/rename");
private final String name;
DynamicRegistrationMethods(final String name) {
this.name = name;
}
public static Optional<DynamicRegistrationMethods> forName(final String name) {
return Arrays.stream(DynamicRegistrationMethods.values()).filter(n -> n.name.equals(name)).findAny();
}
public String getName() {
return name;
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
public class ServerWrapperBaseClientContext implements ClientContext {
private final LanguageServerWrapper wrapper;
public ServerWrapperBaseClientContext(@NotNull LanguageServerWrapper wrapper) {
this.wrapper = wrapper;
}
@Override
public EditorEventManager getEditorEventManagerFor(@NotNull String documentUri) {
return wrapper.getEditorManagerFor(documentUri);
}
@Nullable
@Override
public Project getProject() {
return wrapper.getProject();
}
@Nullable
@Override
public RequestManager getRequestManager() {
return wrapper.getRequestManager();
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.connection;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
/**
* A class symbolizing a stream to a process
* <p>
* commands - The commands to start the process
* workingDir - The working directory of the process
*/
public class ProcessStreamConnectionProvider implements StreamConnectionProvider {
private Logger LOG = Logger.getInstance(ProcessStreamConnectionProvider.class);
@Nullable
private ProcessBuilder builder;
@Nullable
private Process process = null;
private List<String> commands;
private String workingDir;
public ProcessStreamConnectionProvider(List<String> commands, String workingDir) {
this.commands = commands;
this.workingDir = workingDir;
this.builder = null;
}
public ProcessStreamConnectionProvider(@NotNull ProcessBuilder processBuilder) {
this.builder = processBuilder;
}
public void start() throws IOException {
if ((workingDir == null || commands == null || commands.isEmpty() || commands.contains(null)) && builder == null) {
throw new IOException("Unable to start language server: " + this.toString());
}
ProcessBuilder builder = createProcessBuilder();
LOG.info("Starting server process with commands " + commands + " and workingDir " + workingDir);
process = builder.start();
if (!process.isAlive()) {
throw new IOException("Unable to start language server: " + this.toString());
} else {
LOG.info("Server process started " + process);
}
}
private ProcessBuilder createProcessBuilder() {
if (builder != null) {
return builder;
} else {
commands.forEach(c -> c = c.replace("\'", ""));
ProcessBuilder builder = new ProcessBuilder(commands);
builder.directory(new File(workingDir));
builder.redirectError(ProcessBuilder.Redirect.INHERIT);
return builder;
}
}
@Nullable
@Override
public InputStream getInputStream() {
return process != null ? process.getInputStream() : null;
}
@Nullable
@Override
public OutputStream getOutputStream() {
return process != null ? process.getOutputStream() : null;
}
public void stop() {
if (process != null) {
process.destroy();
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ProcessStreamConnectionProvider) {
ProcessStreamConnectionProvider other = (ProcessStreamConnectionProvider) obj;
return commands.size() == other.commands.size() && new HashSet<>(commands).equals(new HashSet<>(other.commands))
&& workingDir.equals(other.workingDir);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(commands) ^ Objects.hashCode(workingDir);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.connection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface StreamConnectionProvider {
void start() throws IOException;
InputStream getInputStream();
OutputStream getOutputStream();
void stop();
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver;
import org.eclipse.lsp4j.CodeLensOptions;
import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.DocumentLinkOptions;
import org.eclipse.lsp4j.DocumentOnTypeFormattingOptions;
import org.eclipse.lsp4j.ExecuteCommandOptions;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.SignatureHelpOptions;
import org.eclipse.lsp4j.TextDocumentSyncKind;
/**
* Class containing the options of the language server.
*/
public class ServerOptions {
//Todo - Revisit and implement with accessors
public TextDocumentSyncKind syncKind;
public ServerCapabilities capabilities;
public CompletionOptions completionOptions;
public SignatureHelpOptions signatureHelpOptions;
public CodeLensOptions codeLensOptions;
public DocumentOnTypeFormattingOptions documentOnTypeFormattingOptions;
public DocumentLinkOptions documentLinkOptions;
public ExecuteCommandOptions executeCommandOptions;
public ServerOptions(ServerCapabilities serverCapabilities) {
this.capabilities = serverCapabilities;
if (capabilities.getTextDocumentSync().isRight()) {
this.syncKind = capabilities.getTextDocumentSync().getRight().getChange();
} else if (capabilities.getTextDocumentSync().isLeft()) {
this.syncKind = capabilities.getTextDocumentSync().getLeft();
}
this.completionOptions = capabilities.getCompletionProvider();
this.signatureHelpOptions = capabilities.getSignatureHelpProvider();
this.codeLensOptions = capabilities.getCodeLensProvider();
this.documentOnTypeFormattingOptions = capabilities.getDocumentOnTypeFormattingProvider();
this.documentLinkOptions = capabilities.getDocumentLinkProvider();
this.executeCommandOptions = capabilities.getExecuteCommandProvider();
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver;
/**
* An enum representing a server status
*/
public enum ServerStatus {
STOPPED, STARTING, STARTED, INITIALIZED, STOPPING
}

View file

@ -0,0 +1,706 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager;
import com.intellij.openapi.diagnostic.Logger;
import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
import org.eclipse.lsp4j.ApplyWorkspaceEditResponse;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionOptions;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.ColorInformation;
import org.eclipse.lsp4j.ColorPresentation;
import org.eclipse.lsp4j.ColorPresentationParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.DefinitionParams;
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.DocumentColorParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentHighlight;
import org.eclipse.lsp4j.DocumentHighlightParams;
import org.eclipse.lsp4j.DocumentLink;
import org.eclipse.lsp4j.DocumentLinkParams;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.FoldingRangeRequestParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.ImplementationParams;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RegistrationParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextDocumentSyncOptions;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.TypeDefinitionParams;
import org.eclipse.lsp4j.UnregistrationParams;
import org.eclipse.lsp4j.WillSaveTextDocumentParams;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Default implementation for LSP requests/notifications handling.
*/
public class DefaultRequestManager implements RequestManager {
private final Logger LOG = Logger.getInstance(DefaultRequestManager.class);
private final LanguageServerWrapper wrapper;
private final LanguageServer server;
private final LanguageClient client;
private final ServerCapabilities serverCapabilities;
private final TextDocumentSyncOptions textDocumentOptions;
private final WorkspaceService workspaceService;
private final TextDocumentService textDocumentService;
public DefaultRequestManager(LanguageServerWrapper wrapper, LanguageServer server, LanguageClient client,
ServerCapabilities serverCapabilities) {
this.wrapper = wrapper;
this.server = server;
this.client = client;
this.serverCapabilities = serverCapabilities;
textDocumentOptions = serverCapabilities.getTextDocumentSync().isRight() ? serverCapabilities.getTextDocumentSync().getRight() : null;
workspaceService = server.getWorkspaceService();
textDocumentService = server.getTextDocumentService();
}
public LanguageServerWrapper getWrapper() {
return wrapper;
}
public LanguageClient getClient() {
return client;
}
public LanguageServer getServer() {
return server;
}
public ServerCapabilities getServerCapabilities() {
return serverCapabilities;
}
// Client
@Override
public void showMessage(MessageParams messageParams) {
client.showMessage(messageParams);
}
@Override
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams) {
return client.showMessageRequest(showMessageRequestParams);
}
@Override
public void logMessage(MessageParams messageParams) {
client.logMessage(messageParams);
}
@Override
public void telemetryEvent(Object o) {
client.telemetryEvent(o);
}
@Override
public CompletableFuture<Void> registerCapability(RegistrationParams params) {
return client.registerCapability(params);
}
@Override
public CompletableFuture<Void> unregisterCapability(UnregistrationParams params) {
return client.unregisterCapability(params);
}
@Override
public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) {
return client.applyEdit(params);
}
@Override
public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) {
client.publishDiagnostics(publishDiagnosticsParams);
}
@Override
public CompletableFuture<Void> refreshSemanticTokens() {
return client.refreshSemanticTokens();
}
// Server
// General
@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
if (checkStatus()) {
try {
return server.initialize(params);
} catch (Exception e) {
crashed(e);
return null;
}
} else {
return null;
}
}
@Override
public void initialized(InitializedParams params) {
if (wrapper.getStatus() == ServerStatus.STARTED) {
try {
server.initialized(params);
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public CompletableFuture<Object> shutdown() {
if (checkStatus()) {
try {
return server.shutdown();
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public void exit() {
if (checkStatus()) {
try {
server.exit();
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public TextDocumentService getTextDocumentService() {
if (checkStatus()) {
try {
return textDocumentService;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public WorkspaceService getWorkspaceService() {
if (checkStatus()) {
try {
return workspaceService;
} catch (Exception e) {
crashed(e);
return null;
}
} else {
return null;
}
}
// Workspace service
@Override
public void didChangeConfiguration(DidChangeConfigurationParams params) {
if (checkStatus()) {
try {
workspaceService.didChangeConfiguration(params);
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
if (checkStatus()) {
try {
workspaceService.didChangeWatchedFiles(params);
} catch (Exception e) {
crashed(e);
}
}
}
public CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> symbol(WorkspaceSymbolParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(serverCapabilities.getWorkspaceSymbolProvider())
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
workspaceService.symbol(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
} else
return null;
}
public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
if (checkStatus()) {
try {
return serverCapabilities.getExecuteCommandProvider() != null ? workspaceService.executeCommand(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
// Text document service
@Override
public void didOpen(DidOpenTextDocumentParams params) {
if (checkStatus()) {
try {
if (Optional.ofNullable(textDocumentOptions).map(TextDocumentSyncOptions::getOpenClose).orElse(false)) {
textDocumentService.didOpen(params);
}
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public void didChange(DidChangeTextDocumentParams params) {
if (checkStatus()) {
try {
if (textDocumentOptions == null || textDocumentOptions.getChange() != null) {
textDocumentService.didChange(params);
}
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public void willSave(WillSaveTextDocumentParams params) {
if (checkStatus()) {
try {
if (Optional.ofNullable(textDocumentOptions).map(x -> x.getWillSave()).orElse(false)) {
textDocumentService.willSave(params);
}
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public CompletableFuture<List<TextEdit>> willSaveWaitUntil(WillSaveTextDocumentParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(textDocumentOptions).map(x -> x.getWillSaveWaitUntil()).orElse(false) ?
textDocumentService.willSaveWaitUntil(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public void didSave(DidSaveTextDocumentParams params) {
if (checkStatus()) {
try {
if (Optional.ofNullable(textDocumentOptions).map(x -> x.getSave()).isPresent()) {
textDocumentService.didSave(params);
}
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
if (checkStatus()) {
try {
if (Optional.ofNullable(textDocumentOptions).map(TextDocumentSyncOptions::getOpenClose).orElse(false)) {
textDocumentService.didClose(params);
}
} catch (Exception e) {
crashed(e);
}
}
}
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params) {
if (checkStatus()) {
try {
return (serverCapabilities.getCompletionProvider() != null) ? textDocumentService.completion(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) {
if (checkStatus()) {
try {
return (Optional.ofNullable(serverCapabilities.getCompletionProvider()).map(CompletionOptions::getResolveProvider).orElse(false)) ?
textDocumentService.resolveCompletionItem(unresolved) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<Hover> hover(TextDocumentPositionParams params) {
return hover(new HoverParams(params.getTextDocument(), params.getPosition()));
}
@Override
public CompletableFuture<Hover> hover(HoverParams params) {
if (checkStatus()) {
try {
return
Optional.ofNullable(serverCapabilities.getHoverProvider())
.map(e -> e.getRight() != null || (e.getLeft() != null && e.getLeft())).orElse(false) ?
textDocumentService.hover(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams params) {
return signatureHelp(new SignatureHelpParams(params.getTextDocument(), params.getPosition()));
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams params) {
if (checkStatus()) {
try {
return (serverCapabilities.getSignatureHelpProvider() != null) ? textDocumentService.signatureHelp(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<? extends Location>> references(ReferenceParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(serverCapabilities.getReferencesProvider())
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
textDocumentService.references(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams params) {
return documentHighlight(new DocumentHighlightParams(params.getTextDocument(), params.getPosition()));
}
@Override
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(DocumentHighlightParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(serverCapabilities.getDocumentHighlightProvider())
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
textDocumentService.documentHighlight(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(DocumentSymbolParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(serverCapabilities.getDocumentSymbolProvider())
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
textDocumentService.documentSymbol(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(serverCapabilities.getDocumentFormattingProvider())
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
textDocumentService.formatting(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
} else {
return null;
}
}
@Override
public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) {
if (checkStatus()) {
try {
return (serverCapabilities.getDocumentRangeFormattingProvider() != null) ? textDocumentService.rangeFormatting(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) {
if (checkStatus()) {
try {
return (serverCapabilities.getDocumentOnTypeFormattingProvider() != null) ?
textDocumentService.onTypeFormatting(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(TextDocumentPositionParams params) {
return definition(new DefinitionParams(params.getTextDocument(), params.getPosition()));
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(DefinitionParams params) {
if (checkStatus()) {
try {
return Optional.ofNullable(serverCapabilities.getDefinitionProvider())
.map(e -> e.getLeft() || e.getRight() != null).orElse(false) ?
textDocumentService.definition(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
if (checkStatus()) {
try {
return checkCodeActionProvider(serverCapabilities.getCodeActionProvider()) ? textDocumentService.codeAction(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
if (checkStatus()) {
try {
return (serverCapabilities.getCodeLensProvider() != null) ? textDocumentService.codeLens(params) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved) {
if (checkStatus()) {
try {
return (serverCapabilities.getCodeLensProvider() != null && serverCapabilities.getCodeLensProvider()
.getResolveProvider()) ? textDocumentService.resolveCodeLens(unresolved) : null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<List<DocumentLink>> documentLink(DocumentLinkParams params) {
if (checkStatus()) {
try {
return (serverCapabilities.getDocumentLinkProvider() != null) ?
textDocumentService.documentLink(params) :
null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<DocumentLink> documentLinkResolve(DocumentLink unresolved) {
if (checkStatus()) {
try {
return serverCapabilities.getDocumentLinkProvider() != null && Optional.ofNullable(serverCapabilities
.getDocumentLinkProvider().getResolveProvider()).orElse(false) ?
textDocumentService.documentLinkResolve(unresolved) :
null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
@Override
public CompletableFuture<WorkspaceEdit> rename(RenameParams params) {
// if (checkStatus()) {
// try {
// return (checkProvider((Either<Boolean, StaticRegistrationOptions>)serverCapabilities.getRenameProvider())) ?
// textDocumentService.rename(params) :
// null;
// } catch (Exception e) {
// crashed(e);
// return null;
// }
// }
return null;
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> implementation(ImplementationParams params) {
return null;
}
@Override
public CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> typeDefinition(TypeDefinitionParams params) {
return null;
}
@Override
public CompletableFuture<List<ColorInformation>> documentColor(DocumentColorParams params) {
return null;
}
@Override
public CompletableFuture<List<ColorPresentation>> colorPresentation(ColorPresentationParams params) {
return null;
}
@Override
public CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams params) {
if (checkStatus()) {
try {
return serverCapabilities.getFoldingRangeProvider() != null ?
textDocumentService.foldingRange(params) :
null;
} catch (Exception e) {
crashed(e);
return null;
}
}
return null;
}
public boolean checkStatus() {
return wrapper.getStatus() == ServerStatus.INITIALIZED;
}
private void crashed(Exception e) {
LOG.warn(e);
wrapper.crashed(e);
}
private boolean checkCodeActionProvider(Either<Boolean, CodeActionOptions> provider) {
return provider != null && ((provider.isLeft() && provider.getLeft()) || (provider.isRight()
&& provider.getRight() != null));
}
}

View file

@ -0,0 +1,182 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.eclipse.lsp4j.services.WorkspaceService;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Base representation of currently supported LSP-based requests and notifications.
*/
public interface RequestManager extends LanguageClient, TextDocumentService, WorkspaceService, LanguageServer {
//------------------------------------- Server2Client ---------------------------------------------------------//
@Override
void showMessage(MessageParams messageParams);
@Override
CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams showMessageRequestParams);
@Override
void logMessage(MessageParams messageParams);
@Override
void telemetryEvent(Object o);
@Override
CompletableFuture<Void> registerCapability(RegistrationParams params);
@Override
CompletableFuture<Void> unregisterCapability(UnregistrationParams params);
@Override
CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params);
@Override
void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams);
//--------------------------------------Client2Server-------------------------------------------------------------//
// General
@Override
CompletableFuture<InitializeResult> initialize(InitializeParams params);
@Override
void initialized(InitializedParams params);
@Override
CompletableFuture<Object> shutdown();
@Override
void exit();
// Workspace Service
@Override
void didChangeConfiguration(DidChangeConfigurationParams params);
@Override
void didChangeWatchedFiles(DidChangeWatchedFilesParams params);
@Override
CompletableFuture<Object> executeCommand(ExecuteCommandParams params);
// Text Document Service
@Override
void didOpen(DidOpenTextDocumentParams params);
@Override
void didChange(DidChangeTextDocumentParams params);
@Override
void willSave(WillSaveTextDocumentParams params);
@Override
CompletableFuture<List<TextEdit>> willSaveWaitUntil(WillSaveTextDocumentParams params);
@Override
void didSave(DidSaveTextDocumentParams params);
@Override
void didClose(DidCloseTextDocumentParams params);
@Override
CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams params);
@Override
CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved);
@Override
CompletableFuture<Hover> hover(HoverParams params);
@Deprecated
CompletableFuture<Hover> hover(TextDocumentPositionParams params);
@Deprecated
CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams params);
@Override
CompletableFuture<SignatureHelp> signatureHelp(SignatureHelpParams params);
@Override
CompletableFuture<List<? extends Location>> references(ReferenceParams params);
@Deprecated
CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams params);
@Override
CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(DocumentHighlightParams params);
@Override
CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol(DocumentSymbolParams params);
@Override
CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params);
@Override
CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params);
@Override
CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params);
@Deprecated
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(TextDocumentPositionParams params);
@Override
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> definition(DefinitionParams params);
@Override
CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params);
@Override
CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params);
@Override
CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved);
@Override
CompletableFuture<List<DocumentLink>> documentLink(DocumentLinkParams params);
@Override
CompletableFuture<DocumentLink> documentLinkResolve(DocumentLink unresolved);
@Override
CompletableFuture<WorkspaceEdit> rename(RenameParams params);
@Override
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> implementation(ImplementationParams params);
@Override
CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> typeDefinition(TypeDefinitionParams params);
@Override
CompletableFuture<List<ColorInformation>> documentColor(DocumentColorParams params);
@Override
CompletableFuture<List<ColorPresentation>> colorPresentation(ColorPresentationParams params);
@Override
CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams params);
}

View file

@ -0,0 +1,125 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
import com.intellij.openapi.diagnostic.Logger;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.lsp4j.InitializeParams;
import com.falsepattern.zigbrains.lsp.client.connection.StreamConnectionProvider;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* A trait representing a ServerDefinition
*/
public class LanguageServerDefinition {
private static final Logger LOG = Logger.getInstance(LanguageServerDefinition.class);
public String ext;
protected Map<String, String> languageIds = Collections.emptyMap();
private Map<String, StreamConnectionProvider> streamConnectionProviders = new ConcurrentHashMap<>();
public static final String SPLIT_CHAR = ",";
/**
* Starts a Language server for the given directory and returns a tuple (InputStream, OutputStream)
*
* @param workingDir The root directory
* @return The input and output streams of the server
* @throws IOException if the stream connection provider is crashed
*/
public Pair<InputStream, OutputStream> start(String workingDir) throws IOException {
StreamConnectionProvider streamConnectionProvider = streamConnectionProviders.get(workingDir);
if (streamConnectionProvider != null) {
return new ImmutablePair<>(streamConnectionProvider.getInputStream(), streamConnectionProvider.getOutputStream());
} else {
streamConnectionProvider = createConnectionProvider(workingDir);
streamConnectionProvider.start();
streamConnectionProviders.put(workingDir, streamConnectionProvider);
return new ImmutablePair<>(streamConnectionProvider.getInputStream(), streamConnectionProvider.getOutputStream());
}
}
/**
* Stops the Language server corresponding to the given working directory
*
* @param workingDir The root directory
*/
public void stop(String workingDir) {
StreamConnectionProvider streamConnectionProvider = streamConnectionProviders.get(workingDir);
if (streamConnectionProvider != null) {
streamConnectionProvider.stop();
streamConnectionProviders.remove(workingDir);
} else {
LOG.warn("No connection for workingDir " + workingDir + " and ext " + ext);
}
}
/**
* Returns the initialization options for the given uri.
*
* @param uri file URI
* @return initialization options
* @deprecated use {@link #customizeInitializeParams(InitializeParams)} instead
*/
@Deprecated
public Object getInitializationOptions(URI uri) {
return null;
}
/**
* Use this method to modify the {@link InitializeParams} that was initialized by this library. The values
* assigned to the passed {@link InitializeParams} after this method ends will be the ones sent to the LSP server.
*
* @param params the parameters with some prefilled values.
*/
public void customizeInitializeParams(InitializeParams params) {
}
@Override
public String toString() {
return "ServerDefinition for " + ext;
}
/**
* Creates a StreamConnectionProvider given the working directory
*
* @param workingDir The root directory
* @return The stream connection provider
*/
public StreamConnectionProvider createConnectionProvider(String workingDir) {
throw new UnsupportedOperationException();
}
public ServerListener getServerListener() {
return ServerListener.DEFAULT;
}
/**
* Return language id for the given extension. if there is no langauge ids registered then the
* return value will be the value of <code>extension</code>.
*/
public String languageIdFor(String extension) {
return languageIds.getOrDefault(extension, extension);
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
import com.falsepattern.zigbrains.lsp.client.connection.ProcessStreamConnectionProvider;
import com.falsepattern.zigbrains.lsp.client.connection.StreamConnectionProvider;
import java.util.Collections;
import java.util.Map;
/**
* A class representing {@link java.lang.ProcessBuilder} based metadata to launch a language server.
*/
@SuppressWarnings("unused")
public class ProcessBuilderServerDefinition extends LanguageServerDefinition {
protected ProcessBuilder processBuilder;
/**
* Creates new instance with the given language id which is different from the file extension.
*
* @param ext The extension.
* @param languageIds The language server ids mapping to extension(s).
* @param process The process builder instance to be started.
*/
@SuppressWarnings("WeakerAccess")
public ProcessBuilderServerDefinition(String ext, Map<String, String> languageIds, ProcessBuilder process) {
this.ext = ext;
this.languageIds = languageIds;
this.processBuilder = process;
}
/**
* Creates new instance.
*
* @param ext The extension.
* @param process The process builder instance to be started.
*/
@SuppressWarnings("unused")
public ProcessBuilderServerDefinition(String ext, ProcessBuilder process) {
this(ext, Collections.emptyMap(), process);
}
public String toString() {
return "ProcessBuilderServerDefinition : " + String.join(" ", processBuilder.command());
}
@Override
public StreamConnectionProvider createConnectionProvider(String workingDir) {
return new ProcessStreamConnectionProvider(processBuilder);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ProcessBuilderServerDefinition) {
ProcessBuilderServerDefinition processBuilderDef = (ProcessBuilderServerDefinition) obj;
return ext.equals(processBuilderDef.ext) && processBuilder.equals(processBuilderDef.processBuilder);
}
return false;
}
@Override
public int hashCode() {
return ext.hashCode() + 3 * processBuilder.hashCode();
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
import com.falsepattern.zigbrains.lsp.client.connection.ProcessStreamConnectionProvider;
import com.falsepattern.zigbrains.lsp.client.connection.StreamConnectionProvider;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
/**
* A class representing raw command based metadata to launch a language server.
*/
@SuppressWarnings("unused")
public class RawCommandServerDefinition extends LanguageServerDefinition {
protected String[] command;
/**
* Creates new instance with the given languag id which is different from the file extension.
*
* @param ext The extension
* @param languageIds The language server ids mapping to extension(s).
* @param command The command to run
*/
@SuppressWarnings("WeakerAccess")
public RawCommandServerDefinition(String ext, Map<String, String> languageIds, String[] command) {
this.ext = ext;
this.languageIds = languageIds;
this.command = command;
}
/**
* Creates new instance.
*
* @param ext The extension
* @param command The command to run
*/
@SuppressWarnings("unused")
public RawCommandServerDefinition(String ext, String[] command) {
this(ext, Collections.emptyMap(), command);
}
public String toString() {
return "RawCommandServerDefinition : " + String.join(" ", command);
}
@Override
public StreamConnectionProvider createConnectionProvider(String workingDir) {
return new ProcessStreamConnectionProvider(Arrays.asList(command), workingDir);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof RawCommandServerDefinition) {
RawCommandServerDefinition commandsDef = (RawCommandServerDefinition) obj;
return ext.equals(commandsDef.ext) && Arrays.equals(command, commandsDef.command);
}
return false;
}
@Override
public int hashCode() {
return ext.hashCode() + 3 * Arrays.hashCode(command);
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
public interface ServerListener {
ServerListener DEFAULT = new ServerListener() {
};
default void initialize(@NotNull LanguageServer server, @NotNull InitializeResult result) {
}
}

View file

@ -0,0 +1,808 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.wrapper;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.client.DefaultLanguageClient;
import com.falsepattern.zigbrains.lsp.client.ServerWrapperBaseClientContext;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.DefaultRequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
import com.falsepattern.zigbrains.lsp.editor.DocumentEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.listeners.DocumentListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.EditorMouseListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.EditorMouseMotionListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.LSPCaretListenerImpl;
import com.falsepattern.zigbrains.lsp.requests.Timeout;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.falsepattern.zigbrains.lsp.utils.LSPException;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.remoteServer.util.CloudNotifier;
import com.intellij.util.PlatformIcons;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.lsp4j.ClientCapabilities;
import org.eclipse.lsp4j.ClientInfo;
import org.eclipse.lsp4j.CodeActionCapabilities;
import org.eclipse.lsp4j.CodeActionKindCapabilities;
import org.eclipse.lsp4j.CodeActionLiteralSupportCapabilities;
import org.eclipse.lsp4j.CompletionCapabilities;
import org.eclipse.lsp4j.CompletionItemCapabilities;
import org.eclipse.lsp4j.DefinitionCapabilities;
import org.eclipse.lsp4j.DidChangeWatchedFilesCapabilities;
import org.eclipse.lsp4j.DocumentHighlightCapabilities;
import org.eclipse.lsp4j.ExecuteCommandCapabilities;
import org.eclipse.lsp4j.FoldingRangeCapabilities;
import org.eclipse.lsp4j.FoldingRangeKindSupportCapabilities;
import org.eclipse.lsp4j.FoldingRangeSupportCapabilities;
import org.eclipse.lsp4j.FormattingCapabilities;
import org.eclipse.lsp4j.HoverCapabilities;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.InitializedParams;
import org.eclipse.lsp4j.OnTypeFormattingCapabilities;
import org.eclipse.lsp4j.RangeFormattingCapabilities;
import org.eclipse.lsp4j.ReferencesCapabilities;
import org.eclipse.lsp4j.RenameCapabilities;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.SignatureHelpCapabilities;
import org.eclipse.lsp4j.SymbolCapabilities;
import org.eclipse.lsp4j.SynchronizationCapabilities;
import org.eclipse.lsp4j.TextDocumentClientCapabilities;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextDocumentSyncOptions;
import org.eclipse.lsp4j.WorkspaceClientCapabilities;
import org.eclipse.lsp4j.WorkspaceEditCapabilities;
import org.eclipse.lsp4j.WorkspaceFolder;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Message;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerOptions;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
import com.falsepattern.zigbrains.lsp.statusbar.LSPServerStatusWidget;
import com.falsepattern.zigbrains.lsp.statusbar.LSPServerStatusWidgetFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.INITIALIZED;
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STARTED;
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STARTING;
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STOPPED;
import static com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus.STOPPING;
/**
* The implementation of a LanguageServerWrapper (specific to a serverDefinition and a project)
*/
public class LanguageServerWrapper {
public LanguageServerDefinition serverDefinition;
private final LSPExtensionManager extManager;
private final Project project;
private final HashSet<Editor> toConnect = new HashSet<>();
private final String projectRootPath;
private final HashSet<String> urisUnderLspControl = new HashSet<>();
private final HashSet<Editor> connectedEditors = new HashSet<>();
private final Map<String, Set<EditorEventManager>> uriToEditorManagers = new HashMap<>();
private LanguageServer languageServer;
private LanguageClient client;
private RequestManager requestManager;
private InitializeResult initializeResult;
private Future<?> launcherFuture;
private CompletableFuture<InitializeResult> initializeFuture;
private boolean capabilitiesAlreadyRequested = false;
private int crashCount = 0;
private volatile boolean alreadyShownTimeout = false;
private volatile boolean alreadyShownCrash = false;
private volatile ServerStatus status = STOPPED;
private static final Map<Pair<String, String>, LanguageServerWrapper> uriToLanguageServerWrapper =
new ConcurrentHashMap<>();
private static final Map<Project, LanguageServerWrapper> projectToLanguageServerWrapper = new ConcurrentHashMap<>();
private static final Logger LOG = Logger.getInstance(LanguageServerWrapper.class);
private static final CloudNotifier notifier = new CloudNotifier("Language Server Protocol client");
public LanguageServerWrapper(@NotNull LanguageServerDefinition serverDefinition, @NotNull Project project) {
this(serverDefinition, project, null);
}
public LanguageServerWrapper(@NotNull LanguageServerDefinition serverDefinition, @NotNull Project project,
@Nullable LSPExtensionManager extManager) {
this.serverDefinition = serverDefinition;
this.project = project;
// We need to keep the project rootPath in addition to the project instance, since we cannot get the project
// base path if the project is disposed.
this.projectRootPath = project.getBasePath();
this.extManager = extManager;
projectToLanguageServerWrapper.put(project, this);
}
/**
* @param uri A file uri
* @param project The related project
* @return The wrapper for the given uri, or None
*/
public static LanguageServerWrapper forUri(String uri, Project project) {
return uriToLanguageServerWrapper.get(new ImmutablePair<>(uri, FileUtils.projectToUri(project)));
}
public static LanguageServerWrapper forVirtualFile(VirtualFile file, Project project) {
return uriToLanguageServerWrapper.get(new ImmutablePair<>(FileUtils.VFSToURI(file), FileUtils.projectToUri(project)));
}
/**
* @param editor An editor
* @return The wrapper for the given editor, or None
*/
public static LanguageServerWrapper forEditor(Editor editor) {
return uriToLanguageServerWrapper.get(new ImmutablePair<>(FileUtils.editorToURIString(editor), FileUtils.editorToProjectFolderUri(editor)));
}
public static LanguageServerWrapper forProject(Project project) {
return projectToLanguageServerWrapper.get(project);
}
public LanguageServerDefinition getServerDefinition() {
return serverDefinition;
}
public String getProjectRootPath() {
return projectRootPath;
}
/**
* @return if the server supports willSaveWaitUntil
*/
public boolean isWillSaveWaitUntil() {
return Optional.ofNullable(getServerCapabilities())
.map(ServerCapabilities::getTextDocumentSync)
.map(Either::getRight)
.map(TextDocumentSyncOptions::getWillSaveWaitUntil)
.orElse(false);
}
/**
* Warning: this is a long running operation
*
* @return the languageServer capabilities, or null if initialization job didn't complete
*/
@Nullable
public ServerCapabilities getServerCapabilities() {
if (initializeResult != null)
return initializeResult.getCapabilities();
else {
try {
start();
if (initializeFuture != null) {
initializeFuture.get((capabilitiesAlreadyRequested ? 0 : Timeout.getTimeout(Timeouts.INIT)), TimeUnit.MILLISECONDS);
notifySuccess(Timeouts.INIT);
}
} catch (TimeoutException e) {
notifyFailure(Timeouts.INIT);
String msg = String.format("%s \n is not initialized after %d seconds",
serverDefinition.toString(), Timeout.getTimeout(Timeouts.INIT) / 1000);
LOG.warn(msg, e);
ApplicationUtils.invokeLater(() -> {
if (!alreadyShownTimeout) {
notifier.showMessage(msg, MessageType.WARNING);
alreadyShownTimeout = true;
}
});
stop(false);
} catch (Exception e) {
LOG.warn(e);
stop(false);
}
}
capabilitiesAlreadyRequested = true;
return initializeResult != null ? initializeResult.getCapabilities() : null;
}
public void notifyResult(Timeouts timeouts, boolean success) {
getWidget().ifPresent(widget -> widget.notifyResult(timeouts, success));
}
public void notifySuccess(Timeouts timeouts) {
notifyResult(timeouts, true);
}
public void notifyFailure(Timeouts timeouts) {
notifyResult(timeouts, false);
}
/**
* Returns the EditorEventManager for a given uri
* <p>
* WARNING: actually a file can be present in multiple editors, this function just gives you one editor. use {@link #getEditorManagersFor(String)} instead
* only use for document level events such as open, close, ...
*
* @param uri the URI as a string
* @return the EditorEventManager (or null)
*/
public EditorEventManager getEditorManagerFor(String uri) {
FileEditor selectedEditor = FileEditorManager.getInstance(project).getSelectedEditor();
if (selectedEditor == null) {
return null;
}
VirtualFile currentOpenFile = selectedEditor.getFile();
VirtualFile requestedFile = FileUtils.virtualFileFromURI(uri);
if (currentOpenFile == null || requestedFile == null) {
return null;
}
if (requestedFile.equals(currentOpenFile)) {
return EditorEventManagerBase.forEditor((Editor) FileEditorManager.getInstance(project).getSelectedEditor());
}
if (uriToEditorManagers.containsKey(uri) && !uriToEditorManagers.get(uri).isEmpty()) {
return (EditorEventManager) uriToEditorManagers.get(uri).toArray()[0];
}
return null;
}
public Set<EditorEventManager> getEditorManagersFor(String uri) {
return uriToEditorManagers.get(uri);
}
/**
* @return The request manager for this wrapper
*/
public RequestManager getRequestManager() {
return requestManager;
}
/**
* @return whether the underlying connection to language languageServer is still active
*/
public boolean isActive() {
return launcherFuture != null && !launcherFuture.isDone() && !launcherFuture.isCancelled()
&& !alreadyShownTimeout && !alreadyShownCrash;
}
/**
* Connects an editor to the languageServer
*
* @param editor the editor
*/
public void connect(Editor editor) {
if (editor == null) {
LOG.warn("editor is null for " + serverDefinition);
return;
}
if (!FileUtils.isEditorSupported(editor)) {
LOG.debug("Editor hosts a unsupported file type by the LS library.");
return;
}
String uri = FileUtils.editorToURIString(editor);
if (connectedEditors.contains(editor)) {
return;
}
ImmutablePair<String, String> key = new ImmutablePair<>(uri, FileUtils.editorToProjectFolderUri(editor));
uriToLanguageServerWrapper.put(key, this);
start();
if (initializeFuture != null) {
ServerCapabilities capabilities = getServerCapabilities();
if (capabilities == null) {
LOG.warn("Capabilities are null for " + serverDefinition);
return;
}
initializeFuture.thenRun(() -> {
if (connectedEditors.contains(editor)) {
return;
}
try {
Either<TextDocumentSyncKind, TextDocumentSyncOptions> syncOptions = capabilities.getTextDocumentSync();
if (syncOptions != null) {
//Todo - Implement
// SelectionListenerImpl selectionListener = new SelectionListenerImpl();
DocumentListenerImpl documentListener = new DocumentListenerImpl();
EditorMouseListenerImpl mouseListener = new EditorMouseListenerImpl();
EditorMouseMotionListenerImpl mouseMotionListener = new EditorMouseMotionListenerImpl();
LSPCaretListenerImpl caretListener = new LSPCaretListenerImpl();
ServerOptions serverOptions = new ServerOptions(capabilities);
EditorEventManager manager;
if (extManager != null) {
manager = extManager.getExtendedEditorEventManagerFor(editor, documentListener,
mouseListener, mouseMotionListener, caretListener, requestManager, serverOptions,
this);
if (manager == null) {
manager = new EditorEventManager(editor, documentListener, mouseListener,
mouseMotionListener, caretListener,
requestManager, serverOptions, this);
}
} else {
manager = new EditorEventManager(editor, documentListener, mouseListener,
mouseMotionListener, caretListener,
requestManager, serverOptions, this);
}
// selectionListener.setManager(manager);
documentListener.setManager(manager);
mouseListener.setManager(manager);
mouseMotionListener.setManager(manager);
caretListener.setManager(manager);
manager.registerListeners();
if (!urisUnderLspControl.contains(uri)) {
manager.documentEventManager.registerListeners();
}
urisUnderLspControl.add(uri);
connectedEditors.add(editor);
if (uriToEditorManagers.containsKey(uri)) {
uriToEditorManagers.get(uri).add(manager);
} else {
Set<EditorEventManager> set = new HashSet<>();
set.add(manager);
uriToEditorManagers.put(uri, set);
manager.documentOpened();
}
LOG.info("Created a manager for " + uri);
synchronized (toConnect) {
toConnect.remove(editor);
}
for (Editor ed : new HashSet<>(toConnect)) {
connect(ed);
}
// Triggers annotators since this is the first editor which starts the LS
// and annotators are executed before LS is bootstrap to provide diagnostics.
ApplicationUtils.computableReadAction(() -> {
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (psiFile != null) {
DaemonCodeAnalyzer.getInstance(project).restart(psiFile);
}
return null;
});
}
} catch (Exception e) {
LOG.error(e);
}
});
} else {
synchronized (toConnect) {
toConnect.add(editor);
}
}
}
/*
* The shutdown request is sent from the client to the server. It asks the server to shut down, but to not exit \
* (otherwise the response might not be delivered correctly to the client).
* Only if the exit flag is true, particular server instance will exit.
*/
public void stop(boolean exit) {
if (this.status == STOPPED || this.status == STOPPING) {
return;
}
setStatus(STOPPING);
if (initializeFuture != null) {
initializeFuture.cancel(true);
}
try {
if (languageServer != null) {
CompletableFuture<Object> shutdown = languageServer.shutdown();
shutdown.get(Timeout.getTimeout(Timeouts.SHUTDOWN), TimeUnit.MILLISECONDS);
notifySuccess(Timeouts.SHUTDOWN);
if (exit) {
languageServer.exit();
}
}
} catch (Exception e) {
// most likely closed externally.
notifyFailure(Timeouts.SHUTDOWN);
LOG.warn("exception occured while trying to shut down", e);
} finally {
if (launcherFuture != null) {
launcherFuture.cancel(true);
}
if (serverDefinition != null) {
serverDefinition.stop(projectRootPath);
}
for (Editor ed : new HashSet<>(connectedEditors)) {
disconnect(ed);
}
// sadly this whole editor closing stuff runs asynchronously, so we cannot be sure the state is really clean here...
// therefore clear the mapping from here as it should be empty by now.
DocumentEventManager.clearState();
uriToEditorManagers.clear();
urisUnderLspControl.clear();
launcherFuture = null;
capabilitiesAlreadyRequested = false;
initializeResult = null;
initializeFuture = null;
languageServer = null;
setStatus(STOPPED);
}
}
/**
* Checks if the wrapper is already connected to the document at the given path.
*
* @param location file location
* @return True if the given file is connected.
*/
public boolean isConnectedTo(String location) {
return urisUnderLspControl.contains(location);
}
/**
* @return the LanguageServer
*/
@Nullable
public LanguageServer getServer() {
start();
if (initializeFuture != null && !initializeFuture.isDone()) {
initializeFuture.join();
}
return languageServer;
}
/**
* Starts the LanguageServer
*/
public void start() {
if (status == STOPPED && !alreadyShownCrash && !alreadyShownTimeout) {
setStatus(STARTING);
try {
Pair<InputStream, OutputStream> streams = serverDefinition.start(projectRootPath);
InputStream inputStream = streams.getKey();
OutputStream outputStream = streams.getValue();
InitializeParams initParams = getInitParams();
ExecutorService executorService = Executors.newCachedThreadPool();
MessageHandler messageHandler = new MessageHandler(serverDefinition.getServerListener(), () -> getStatus() != STOPPED);
if (extManager != null && extManager.getExtendedServerInterface() != null) {
Class<? extends LanguageServer> remoteServerInterFace = extManager.getExtendedServerInterface();
client = extManager.getExtendedClientFor(new ServerWrapperBaseClientContext(this));
Launcher<? extends LanguageServer> launcher = Launcher
.createLauncher(client, remoteServerInterFace, inputStream, outputStream, executorService,
messageHandler);
languageServer = launcher.getRemoteProxy();
launcherFuture = launcher.startListening();
} else {
client = new DefaultLanguageClient(new ServerWrapperBaseClientContext(this));
Launcher<LanguageServer> launcher = Launcher
.createLauncher(client, LanguageServer.class, inputStream, outputStream, executorService,
messageHandler);
languageServer = launcher.getRemoteProxy();
launcherFuture = launcher.startListening();
}
messageHandler.setLanguageServer(languageServer);
initializeFuture = languageServer.initialize(initParams).thenApply(res -> {
initializeResult = res;
LOG.info("Got initializeResult for " + serverDefinition + " ; " + projectRootPath);
if (extManager != null) {
requestManager = extManager.getExtendedRequestManagerFor(this, languageServer, client, res.getCapabilities());
if (requestManager == null) {
requestManager = new DefaultRequestManager(this, languageServer, client, res.getCapabilities());
}
} else {
requestManager = new DefaultRequestManager(this, languageServer, client, res.getCapabilities());
}
setStatus(STARTED);
// send the initialized message since some language servers depends on this message
requestManager.initialized(new InitializedParams());
setStatus(INITIALIZED);
return res;
});
} catch (LSPException | IOException | URISyntaxException e) {
LOG.warn(e);
ApplicationUtils.invokeLater(() ->
notifier.showMessage(String.format("Can't start server due to %s", e.getMessage()),
MessageType.WARNING));
removeServerWrapper();
}
}
}
private InitializeParams getInitParams() throws URISyntaxException {
InitializeParams initParams = new InitializeParams();
String projectRootUri = FileUtils.pathToUri(projectRootPath);
WorkspaceFolder workspaceFolder = new WorkspaceFolder(projectRootUri, this.project.getName());
initParams.setWorkspaceFolders(Collections.singletonList(workspaceFolder));
// workspace capabilities
WorkspaceClientCapabilities workspaceClientCapabilities = new WorkspaceClientCapabilities();
workspaceClientCapabilities.setApplyEdit(true);
workspaceClientCapabilities.setDidChangeWatchedFiles(new DidChangeWatchedFilesCapabilities());
workspaceClientCapabilities.setExecuteCommand(new ExecuteCommandCapabilities());
workspaceClientCapabilities.setWorkspaceEdit(new WorkspaceEditCapabilities());
workspaceClientCapabilities.setSymbol(new SymbolCapabilities());
workspaceClientCapabilities.setWorkspaceFolders(true);
workspaceClientCapabilities.setConfiguration(false);
// text document capabilities
TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities();
textDocumentClientCapabilities.setCodeAction(new CodeActionCapabilities());
textDocumentClientCapabilities.getCodeAction().setCodeActionLiteralSupport(new CodeActionLiteralSupportCapabilities(new CodeActionKindCapabilities()));
textDocumentClientCapabilities.setCompletion(new CompletionCapabilities(new CompletionItemCapabilities(true)));
textDocumentClientCapabilities.setDefinition(new DefinitionCapabilities());
textDocumentClientCapabilities.setDocumentHighlight(new DocumentHighlightCapabilities());
textDocumentClientCapabilities.setFormatting(new FormattingCapabilities());
textDocumentClientCapabilities.setHover(new HoverCapabilities());
textDocumentClientCapabilities.setOnTypeFormatting(new OnTypeFormattingCapabilities());
textDocumentClientCapabilities.setRangeFormatting(new RangeFormattingCapabilities());
textDocumentClientCapabilities.setReferences(new ReferencesCapabilities());
textDocumentClientCapabilities.setRename(new RenameCapabilities());
textDocumentClientCapabilities.setSignatureHelp(new SignatureHelpCapabilities());
textDocumentClientCapabilities.setSynchronization(new SynchronizationCapabilities(true, true, true));
FoldingRangeCapabilities foldingRangeCapabilities = new FoldingRangeCapabilities();
foldingRangeCapabilities.setFoldingRangeKind(new FoldingRangeKindSupportCapabilities(List.of("comment", "region", "imports")));
foldingRangeCapabilities.setFoldingRange(new FoldingRangeSupportCapabilities(true));
textDocumentClientCapabilities.setFoldingRange(foldingRangeCapabilities);
initParams.setCapabilities(
new ClientCapabilities(workspaceClientCapabilities, textDocumentClientCapabilities, null));
initParams.setClientInfo(new ClientInfo(ApplicationInfo.getInstance().getVersionName(), ApplicationInfo.getInstance().getFullVersion()));
// custom initialization options and initialize params provided by users
initParams.setInitializationOptions(serverDefinition.getInitializationOptions(URI.create(initParams.getWorkspaceFolders().get(0).getUri())));
serverDefinition.customizeInitializeParams(initParams);
return initParams;
}
public void logMessage(Message message) {
if (message instanceof ResponseMessage) {
ResponseMessage responseMessage = (ResponseMessage) message;
if (responseMessage.getError() != null && (responseMessage.getId()
.equals(Integer.toString(ResponseErrorCode.RequestCancelled.getValue())))) {
LOG.error(new ResponseErrorException(responseMessage.getError()));
}
}
}
public Project getProject() {
return project;
}
public ServerStatus getStatus() {
return status;
}
private void setStatus(ServerStatus status) {
this.status = status;
getWidget().ifPresent(widget -> widget.setStatus(status));
}
public void crashed(Exception e) {
crashCount += 1;
if (crashCount <= 3) {
reconnect();
} else {
ApplicationUtils.invokeLater(() -> {
if (alreadyShownCrash) {
reconnect();
} else {
int response = Messages.showYesNoDialog(String.format(
"LanguageServer for definition %s, project %s keeps crashing due to \n%s\n"
, serverDefinition.toString(), project.getName(), e.getMessage()),
"Language Server Client Warning", "Keep Connected", "Disconnect", PlatformIcons.CHECK_ICON);
if (response == Messages.NO) {
int confirm = Messages.showYesNoDialog("All the language server based plugin features will be disabled.\n" +
"Do you wish to continue?", "", PlatformIcons.WARNING_INTRODUCTION_ICON);
if (confirm == Messages.YES) {
// Disconnects from the language server.
stop(true);
} else {
reconnect();
}
} else {
reconnect();
}
}
alreadyShownCrash = true;
crashCount = 0;
});
}
}
private void reconnect() {
// Need to copy by value since connected editors gets cleared during 'stop()' invocation.
final Set<String> connected = new HashSet<>(urisUnderLspControl);
stop(true);
for (String uri : connected) {
connect(uri);
}
}
public List<String> getConnectedFiles() {
List<String> connected = new ArrayList<>();
urisUnderLspControl.forEach(s -> {
try {
connected.add(new URI(FileUtils.sanitizeURI(s)).toString());
} catch (URISyntaxException e) {
LOG.warn(e);
}
});
return connected;
}
public void removeWidget() {
getWidget().ifPresent(LSPServerStatusWidget::dispose);
}
/**
* Disconnects an editor from the LanguageServer
*
* @param editor The editor
*/
public void disconnect(Editor editor) {
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
connectedEditors.remove(editor);
if (manager != null) {
manager.removeListeners();
String uri = FileUtils.editorToURIString(editor);
Set<EditorEventManager> set = uriToEditorManagers.get(uri);
if (set != null) {
set.remove(manager);
if (set.isEmpty()) {
manager.documentClosed();
manager.documentEventManager.removeListeners();
uriToEditorManagers.remove(uri);
urisUnderLspControl.remove(uri);
uriToLanguageServerWrapper.remove(new ImmutablePair<>(uri, FileUtils.editorToProjectFolderUri(editor)));
}
}
}
if (connectedEditors.isEmpty()) {
stop(true);
}
}
/**
* Disconnects an editor from the LanguageServer
* <p>
* WARNING: only use this method if you have no editor instance and you restart all connections to the language server for all open editors
* prefer using disconnect(editor)
*
* @param uri The file uri
* @param projectUri The project root uri
*/
public void disconnect(String uri, String projectUri) {
uriToLanguageServerWrapper.remove(new ImmutablePair<>(FileUtils.sanitizeURI(uri), FileUtils.sanitizeURI(projectUri)));
Set<EditorEventManager> managers = uriToEditorManagers.get(uri);
if (managers == null) {
return;
}
for (EditorEventManager manager : managers) {
manager.removeListeners();
manager.documentEventManager.removeListeners();
connectedEditors.remove(manager.editor);
Set<EditorEventManager> editorEventManagers = uriToEditorManagers.get(uri);
if (editorEventManagers != null) {
editorEventManagers.remove(manager);
if (editorEventManagers.isEmpty()) {
uriToEditorManagers.remove(uri);
manager.documentClosed();
}
}
urisUnderLspControl.remove(uri);
uriToLanguageServerWrapper.remove(new ImmutablePair<>(FileUtils.sanitizeURI(uri), FileUtils.sanitizeURI(projectUri)));
}
if (connectedEditors.isEmpty()) {
stop(true);
}
}
public void removeServerWrapper() {
stop(true);
removeWidget();
IntellijLanguageClient.removeWrapper(this);
}
private void connect(String uri) {
List<Editor> editors = FileUtils.getAllOpenedEditorsForUri(project, uri);
for (Editor editor : editors) {
connect(editor);
}
}
/**
* Is the language server in a state where it can be restartable. Normally language server is
* restartable if it has timeout or has a startup error.
*/
public boolean isRestartable() {
return status == STOPPED && (alreadyShownTimeout || alreadyShownCrash);
}
/**
* Reset language server wrapper state so it can be started again if it was failed earlier.
*/
public void restart() {
ApplicationUtils.pool(() -> {
if (isRestartable()) {
alreadyShownCrash = false;
alreadyShownTimeout = false;
} else {
stop(true);
}
FileUtils.reloadEditors(project);
});
}
private Optional<LSPServerStatusWidget> getWidget() {
LSPServerStatusWidgetFactory factory = ((LSPServerStatusWidgetFactory) project.getService(StatusBarWidgetsManager.class).findWidgetFactory("LSP"));
if (factory != null) {
return Optional.of(factory.getWidget(project));
} else {
return Optional.empty();
}
}
/**
* Returns the extension manager associated with this language server wrapper.
*
* @return The result can be null if there is not extension manager defined.
*/
@Nullable
public final LSPExtensionManager getExtensionManager() {
return extManager;
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.client.languageserver.wrapper;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.jsonrpc.messages.Message;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.ServerListener;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
class MessageHandler implements Function<MessageConsumer, MessageConsumer> {
private ServerListener listener;
private BooleanSupplier isRunning;
private LanguageServer languageServer;
MessageHandler(@NotNull ServerListener listener, @NotNull BooleanSupplier isRunning) {
this.listener = listener;
this.isRunning = isRunning;
}
@Override
public MessageConsumer apply(MessageConsumer messageConsumer) {
return message -> {
if(isRunning.getAsBoolean()) {
handleMessage(message);
messageConsumer.consume(message);
}
};
}
private void handleMessage(Message message) {
if (message instanceof ResponseMessage) {
ResponseMessage responseMessage = (ResponseMessage) message;
if (responseMessage.getResult() instanceof InitializeResult) {
listener.initialize(languageServer, (InitializeResult) responseMessage.getResult());
}
}
}
void setLanguageServer(@NotNull LanguageServer languageServer) {
this.languageServer = languageServer;
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.completion.PlainPrefixMatcher;
import com.intellij.openapi.application.ex.ApplicationUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.util.ProcessingContext;
import org.eclipse.lsp4j.Position;
import org.jetbrains.annotations.NotNull;
/**
* The completion contributor for the LSP
*/
class LSPCompletionContributor extends CompletionContributor {
private static final Logger LOG = Logger.getInstance(LSPCompletionContributor.class);
@Override
public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
CompletionProvider<CompletionParameters> provider = new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
try {
ApplicationUtil.runWithCheckCanceled(() -> {
Editor editor = parameters.getEditor();
int offset = parameters.getOffset();
Position serverPos = DocumentUtils.offsetToLSPPos(editor, offset);
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
result.addAllElements(manager.completion(serverPos));
}
return null;
}, ProgressIndicatorProvider.getGlobalProgressIndicator());
} catch (ProcessCanceledException ignored) {
// ProcessCanceledException can be ignored.
} catch (Exception e) {
LOG.warn("LSP Completions ended with an error", e);
}
}
};
Editor editor = parameters.getEditor();
int offset = parameters.getOffset();
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
String prefix = manager.getCompletionPrefix(editor, offset);
provider.addCompletionVariants(parameters, new ProcessingContext(), result.withPrefixMatcher(new PlainPrefixMatcher(prefix)));
if (result.isStopped()) {
return;
}
super.fillCompletionVariants(parameters, result);
}
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.requests.HoverHandler;
import com.falsepattern.zigbrains.lsp.requests.Timeout;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.model.Pointer;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.TextRange;
import com.intellij.platform.backend.documentation.DocumentationResult;
import com.intellij.platform.backend.documentation.DocumentationTarget;
import com.intellij.platform.backend.documentation.DocumentationTargetProvider;
import com.intellij.platform.backend.presentation.TargetPresentation;
import com.intellij.psi.PsiFile;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiFileRange;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
public class LSPDocumentationTargetProvider implements DocumentationTargetProvider {
@Override
public @NotNull List<? extends @NotNull DocumentationTarget> documentationTargets(@NotNull PsiFile file, int offset) {
return Collections.singletonList(new LSPDocumentationTarget(file, offset));
}
public static class LSPDocumentationTarget implements DocumentationTarget {
private final Pointer<LSPDocumentationTarget> pointer;
private final PsiFile file;
private final int offset;
public LSPDocumentationTarget(PsiFile file, int offset) {
this.file = file;
this.offset = offset;
var range = TextRange.from(offset, 0);
SmartPsiFileRange base = SmartPointerManager.getInstance(file.getProject()).createSmartPsiFileRangePointer(file, range);
pointer = new FileRangePointer(base);
}
protected Logger LOG = Logger.getInstance(LSPDocumentationTargetProvider.class);
@Nullable
@Override
public DocumentationResult computeDocumentation() {
var editor = FileUtils.editorFromPsiFile(file);
if (editor == null) {
return null;
}
var manager = EditorEventManagerBase.forEditor(editor);
if (manager == null) {
return null;
}
var wrapper = manager.wrapper;
if (wrapper == null) {
return null;
}
var caretPos = editor.offsetToLogicalPosition(offset);
var serverPos = ApplicationUtils.computableReadAction(() -> DocumentUtils.logicalToLSPPos(caretPos, editor));
return DocumentationResult.asyncDocumentation(() -> {
var identifier = manager.getIdentifier();
var request = wrapper.getRequestManager().hover(new HoverParams(identifier, serverPos));
if (request == null) {
return null;
}
try {
var hover = request.get(Timeout.getTimeout(Timeouts.HOVER), TimeUnit.MILLISECONDS);
wrapper.notifySuccess(Timeouts.HOVER);
if (hover == null) {
LOG.debug(String.format("Hover is null for file %s and pos (%d;%d)", identifier.getUri(),
serverPos.getLine(), serverPos.getCharacter()));
return null;
}
String string = HoverHandler.getHoverString(hover);
if (StringUtils.isEmpty(string)) {
LOG.warn(String.format("Hover string returned is empty for file %s and pos (%d;%d)",
identifier.getUri(), serverPos.getLine(), serverPos.getCharacter()));
return null;
}
return DocumentationResult.documentation(string.lines().collect(Collectors.joining("<br>\n")));
} catch (TimeoutException e) {
LOG.warn(e);
wrapper.notifyFailure(Timeouts.HOVER);
} catch (InterruptedException | JsonRpcException | ExecutionException e) {
LOG.warn(e);
wrapper.crashed(e);
}
return null;
});
}
@NotNull
@Override
public TargetPresentation computePresentation() {
return TargetPresentation.builder("Doc from language server").presentation();
}
@NotNull
@Override
public Pointer<? extends DocumentationTarget> createPointer() {
return pointer;
}
private static class FileRangePointer implements Pointer<LSPDocumentationTarget> {
private final SmartPsiFileRange base;
public FileRangePointer(SmartPsiFileRange base) {
this.base = base;
}
@Override
public @Nullable LSPDocumentationTarget dereference() {
if (base.getElement() == null) {
return null;
}
if (base.getRange() == null) {
return null;
}
return new LSPDocumentationTarget(base.getElement(), TextRange.create(base.getRange()).getStartOffset());
}
}
}
}

View file

@ -0,0 +1,219 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.requests.Timeout;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.CustomFoldingBuilder;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import org.eclipse.lsp4j.FoldingRange;
import org.eclipse.lsp4j.FoldingRangeRequestParams;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
public class LSPFoldingRangeProvider extends CustomFoldingBuilder {
private static final Key<Boolean> ASYNC_FOLDING_KEY = new Key<>("ASYNC_FOLDING");
protected Logger LOG = Logger.getInstance(LSPFoldingRangeProvider.class);
private interface FoldingRangeAcceptor {
void accept(int start, int end, @Nullable String replacement);
}
private static class AFoldingRange {
public final int start;
public final int end;
public final String collapsedText;
private AFoldingRange(int start, int end, String collapsedText) {
this.start = start;
this.end = end;
this.collapsedText = collapsedText;
}
}
@Override
protected void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors, @NotNull PsiElement root, @NotNull Document document, boolean quick) {
// if the quick flag is set, we do nothing here
if (quick) {
return;
}
var async = async();
if (!async) {
doBuildLanguageFoldRegions((start, end, collapsedText) -> {
if (collapsedText != null) {
descriptors.add(new FoldingDescriptor(root.getNode(), new TextRange(start, end), null, collapsedText));
} else {
descriptors.add(new FoldingDescriptor(root.getNode(), new TextRange(start, end)));
}
}, root, document, false);
return;
}
var app = ApplicationManager.getApplication();
app.executeOnPooledThread(() -> {
var ranges = new ArrayList<AFoldingRange>();
doBuildLanguageFoldRegions((start, end, collapsedText) -> ranges.add(
new AFoldingRange(start, end, collapsedText == null ? "..." : collapsedText)),
root, document, true);
var editor = FileUtils.editorFromPsiFile(root.getContainingFile());
app.invokeLater(() -> {
if (editor.isDisposed()) {
return;
}
var foldingModel = editor.getFoldingModel();
var oldRegions = Arrays.stream(foldingModel.getAllFoldRegions()).filter(region -> {
var data = region.getUserData(ASYNC_FOLDING_KEY);
return data != null && data;
}).collect(Collectors.toList());
foldingModel.runBatchFoldingOperation(() -> {
for (var oldRegion: oldRegions) {
foldingModel.removeFoldRegion(oldRegion);
}
for (var range: ranges) {
var region = foldingModel.addFoldRegion(range.start, range.end, range.collapsedText);
if (region != null) {
region.putUserData(ASYNC_FOLDING_KEY, true);
}
}
});
});
});
}
private void doBuildLanguageFoldRegions(@NotNull FoldingRangeAcceptor acceptor, @NotNull PsiElement root, @NotNull Document document, boolean async) {
PsiFile psiFile = root.getContainingFile();
var editor = FileUtils.editorFromPsiFile(psiFile);
var wrapper = LanguageServerWrapper.forVirtualFile(psiFile.getVirtualFile(), root.getProject());
if (editor == null || wrapper == null || !editor.getDocument().equals(document)) {
return;
}
var manager = wrapper.getRequestManager();
if (manager == null) {
//IDE startup race condition
if (!async)
return;
//We can block the async thread for a moment; wait 2 more seconds
for (int i = 0; i < 20 && manager == null; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
//We got interrupted, bail
return;
}
manager = wrapper.getRequestManager();
}
if (manager == null)
return; //LSP did not connect in time, bail
}
TextDocumentIdentifier textDocumentIdentifier = FileUtils.editorToLSPIdentifier(editor);
FoldingRangeRequestParams params = new FoldingRangeRequestParams(textDocumentIdentifier);
CompletableFuture<List<FoldingRange>> future = manager.foldingRange(params);
if (future == null) {
return;
}
try {
List<FoldingRange> foldingRanges = future.get(Timeout.getTimeout(Timeouts.FOLDING), TimeUnit.MILLISECONDS);
wrapper.notifySuccess(Timeouts.FOLDING);
for (FoldingRange foldingRange : foldingRanges) {
int start = getStartOffset(editor, foldingRange, document);
int end = getEndOffset(editor, foldingRange, document);
int length = end - start;
if (length <= 0) {
continue;
}
if (end > root.getTextLength()) {
continue;
}
var collapsedText = getCollapsedText(foldingRange);
acceptor.accept(start, end, collapsedText);
}
} catch (TimeoutException | InterruptedException e) {
LOG.warn(e);
wrapper.notifyFailure(Timeouts.FOLDING);
} catch (JsonRpcException | ExecutionException e) {
LOG.warn(e);
wrapper.crashed(e);
}
}
protected boolean async() {
return true;
}
protected @Nullable String getCollapsedText(@NotNull FoldingRange foldingRange) {
return foldingRange.getCollapsedText();
}
private int getEndOffset(Editor editor, @NotNull FoldingRange foldingRange, @NotNull Document document) {
// EndCharacter is optional. When missing, it should be set to the length of the end line.
if (foldingRange.getEndCharacter() == null) {
return document.getLineEndOffset(foldingRange.getEndLine());
}
return DocumentUtils.LSPPosToOffset(editor, new Position(foldingRange.getEndLine(), foldingRange.getEndCharacter()));
}
private int getStartOffset(Editor editor, @NotNull FoldingRange foldingRange, @NotNull Document document) {
// StartCharacter is optional. When missing, it should be set to the length of the start line.
if (foldingRange.getStartCharacter() == null) {
return document.getLineEndOffset(foldingRange.getStartLine());
} else {
return DocumentUtils.LSPPosToOffset(editor, new Position(foldingRange.getStartLine(), foldingRange.getStartCharacter()));
}
}
@Override
protected String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
return null;
}
@Override
protected boolean isRegionCollapsedByDefault(@NotNull ASTNode node) {
return false;
}
}

View file

@ -0,0 +1,191 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.annotator;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationBuilder;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.ExternalAnnotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.SmartList;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.DiagnosticTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
public class LSPAnnotator extends ExternalAnnotator<Object, Object> {
private static final Logger LOG = Logger.getInstance(LSPAnnotator.class);
private static final Object RESULT = new Object();
private static final HashMap<DiagnosticSeverity, HighlightSeverity> lspToIntellijAnnotationsMap = new HashMap<>();
static {
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Error, HighlightSeverity.ERROR);
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Warning, HighlightSeverity.WARNING);
// seem flipped, but just different semantics lsp<->intellij. Hint is rendered without any squiggle
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Information, HighlightSeverity.WEAK_WARNING);
lspToIntellijAnnotationsMap.put(DiagnosticSeverity.Hint, HighlightSeverity.INFORMATION);
}
@Nullable
@Override
public Object collectInformation(@NotNull PsiFile file, @NotNull Editor editor, boolean hasErrors) {
try {
VirtualFile virtualFile = file.getVirtualFile();
// If the file is not supported, we skips the annotation by returning null.
if (!FileUtils.isFileSupported(virtualFile) || !IntellijLanguageClient.isExtensionSupported(virtualFile)) {
return null;
}
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager == null) {
return null;
}
// If the diagnostics list is locked, we need to skip annotating the file.
if (!(eventManager.isDiagnosticSyncRequired() || eventManager.isCodeActionSyncRequired())) {
return null;
}
return RESULT;
} catch (Exception e) {
return null;
}
}
@Nullable
@Override
public Object doAnnotate(Object collectedInfo) {
return RESULT;
}
@Override
public void apply(@NotNull PsiFile file, Object annotationResult, @NotNull AnnotationHolder holder) {
LanguageServerWrapper languageServerWrapper = LanguageServerWrapper.forVirtualFile(file.getVirtualFile(), file.getProject());
if (languageServerWrapper == null || languageServerWrapper.getStatus() != ServerStatus.INITIALIZED) {
return;
}
VirtualFile virtualFile = file.getVirtualFile();
if (FileUtils.isFileSupported(virtualFile) && IntellijLanguageClient.isExtensionSupported(virtualFile)) {
String uri = FileUtils.VFSToURI(virtualFile);
// TODO annotations are applied to a file / document not to an editor. so store them by file and not by editor..
EditorEventManager eventManager = EditorEventManagerBase.forUri(uri);
if (eventManager == null) {
return;
}
if (eventManager.isCodeActionSyncRequired()) {
try {
updateAnnotations(holder, eventManager);
} catch (ConcurrentModificationException e) {
// Todo - Add proper fix to handle concurrent modifications gracefully.
LOG.warn("Error occurred when updating LSP diagnostics due to concurrent modifications.", e);
} catch (Throwable t) {
LOG.warn("Error occurred when updating LSP diagnostics.", t);
}
} else if (eventManager.isDiagnosticSyncRequired()) {
try {
createAnnotations(holder, eventManager);
} catch (ConcurrentModificationException e) {
// Todo - Add proper fix to handle concurrent modifications gracefully.
LOG.warn("Error occurred when updating LSP code actions due to concurrent modifications.", e);
} catch (Throwable t) {
LOG.warn("Error occurred when updating LSP code actions.", t);
}
}
}
}
private void updateAnnotations(AnnotationHolder holder, EditorEventManager eventManager) {
final List<Annotation> annotations = eventManager.getAnnotations();
if (annotations == null) {
return;
}
var requests = eventManager.fetchQuickFixes();
annotations.forEach(annotation -> {
if (annotation.getQuickFixes() != null && !annotation.getQuickFixes().isEmpty()) {
AnnotationBuilder builder = holder.newAnnotation(annotation.getSeverity(), annotation.getMessage());
for (Annotation.QuickFixInfo quickFixInfo : annotation.getQuickFixes()) {
builder = builder.withFix(quickFixInfo.quickFix);
}
builder.create();
} else if (requests.containsKey(annotation)) {
AnnotationBuilder builder = holder.newAnnotation(annotation.getSeverity(), annotation.getMessage());
var request = requests.remove(annotation);
for (var quickFixInfo: request) {
builder = builder.withFix(quickFixInfo.action());
}
builder.create();
}
});
}
@Nullable
protected AnnotationBuilder createAnnotation(Editor editor, AnnotationHolder holder, Diagnostic diagnostic) {
final int start = DocumentUtils.LSPPosToOffset(editor, diagnostic.getRange().getStart());
final int end = DocumentUtils.LSPPosToOffset(editor, diagnostic.getRange().getEnd());
if (start >= end) {
return null;
}
final TextRange range = new TextRange(start, end);
return holder.newAnnotation(lspToIntellijAnnotationsMap.get(diagnostic.getSeverity()), diagnostic.getMessage())
.range(range);
}
private void createAnnotations(AnnotationHolder holder, EditorEventManager eventManager) {
final List<Diagnostic> diagnostics = eventManager.getDiagnostics();
final Editor editor = eventManager.editor;
List<Annotation> annotations = new ArrayList<>();
diagnostics.forEach(d -> {
var annotation = createAnnotation(editor, holder, d);
if (annotation != null) {
if (d.getTags() != null && d.getTags().contains(DiagnosticTag.Deprecated)) {
annotation = annotation.highlightType(ProblemHighlightType.LIKE_DEPRECATED);
}
annotation.create();
var theList = (SmartList<Annotation>) holder;
annotations.add(theList.get(theList.size() - 1));
}
});
eventManager.setAnnotations(annotations);
eventManager.setAnonHolder(holder);
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.fixes;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.requests.WorkspaceEditHandler;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import org.eclipse.lsp4j.CodeAction;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
public class LSPCodeActionFix implements IntentionAction {
private final String uri;
private final CodeAction codeAction;
public LSPCodeActionFix(String uri, @NotNull CodeAction codeAction) {
this.uri = uri;
this.codeAction = codeAction;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getText() {
return codeAction.getTitle();
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return "LSP Fixes";
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) {
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) {
if (codeAction.getEdit() != null) {
WorkspaceEditHandler.applyEdit(codeAction.getEdit(), codeAction.getTitle());
}
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
manager.executeCommands(Collections.singletonList(codeAction.getCommand()));
}
}
@Override
public boolean startInWriteAction() {
return true;
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.fixes;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import org.eclipse.lsp4j.Command;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import static java.util.Collections.singletonList;
public class LSPCommandFix implements IntentionAction {
private final String uri;
private final Command command;
public LSPCommandFix(String uri, @NotNull Command command) {
this.uri = uri;
this.command = command;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getText() {
return command.getTitle();
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return "LSP Fixes";
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) {
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) {
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
manager.executeCommands(singletonList(command));
}
}
@Override
public boolean startInWriteAction() {
return true;
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.icon;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
import com.intellij.icons.AllIcons;
import com.intellij.icons.AllIcons.Nodes;
import com.intellij.openapi.util.IconLoader;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.SymbolKind;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import java.util.HashMap;
import java.util.Map;
import javax.swing.Icon;
public class LSPDefaultIconProvider extends LSPIconProvider {
private final static Icon GREEN = IconLoader.getIcon("/images/started.png", LSPDefaultIconProvider.class);
private final static Icon YELLOW = IconLoader.getIcon("/images/starting.png", LSPDefaultIconProvider.class);
private final static Icon RED = IconLoader.getIcon("/images/stopped.png", LSPDefaultIconProvider.class);
public Icon getCompletionIcon(CompletionItemKind kind) {
if (kind == null) {
return null;
}
switch (kind) {
case Class:
return Nodes.Class;
case Enum:
return Nodes.Enum;
case Field:
return Nodes.Field;
case File:
return AllIcons.FileTypes.Any_type;
case Function:
return Nodes.Function;
case Interface:
return Nodes.Interface;
case Keyword:
return Nodes.UpLevel;
case Method:
return Nodes.Method;
case Module:
return Nodes.Module;
case Property:
return Nodes.Property;
case Reference:
return Nodes.MethodReference;
case Snippet:
return Nodes.Static;
case Text:
return AllIcons.FileTypes.Text;
case Unit:
return Nodes.Artifact;
case Variable:
return Nodes.Variable;
default:
return null;
}
}
public Icon getSymbolIcon(SymbolKind kind) {
if (kind == null) {
return null;
}
switch (kind) {
case Field:
case EnumMember:
return Nodes.Field;
case Method:
return Nodes.Method;
case Variable:
return Nodes.Variable;
case Class:
return Nodes.Class;
case Constructor:
return Nodes.ClassInitializer;
case Enum:
return Nodes.Enum;
default:
return Nodes.Tag;
}
}
public Map<ServerStatus, Icon> getStatusIcons() {
Map<ServerStatus, Icon> statusIconMap = new HashMap<>();
statusIconMap.put(ServerStatus.STOPPED, RED);
statusIconMap.put(ServerStatus.STARTING, YELLOW);
statusIconMap.put(ServerStatus.STARTED, YELLOW);
statusIconMap.put(ServerStatus.INITIALIZED, GREEN);
statusIconMap.put(ServerStatus.STOPPING, YELLOW);
return statusIconMap;
}
@Override
public boolean isSpecificFor(LanguageServerDefinition serverDefinition) {
return false;
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.icon;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.SymbolKind;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import java.util.Map;
import javax.swing.*;
public abstract class LSPIconProvider {
public abstract Icon getCompletionIcon(CompletionItemKind kind);
public abstract Map<ServerStatus, Icon> getStatusIcons();
public abstract Icon getSymbolIcon(SymbolKind kind);
public abstract boolean isSpecificFor(LanguageServerDefinition serverDefinition);
/**
* Return icon for symbol based on the symbol information. This method must only be used if you need more than the
* symbol kind to decide the icon which needs to be used. Otherwise always prefer implementing
* {@link #getSymbolIcon(SymbolKind)}.
*
* @return default implementation delegates to {@link #getSymbolIcon(SymbolKind)}.
*/
public Icon getSymbolIcon(SymbolInformation information) {
return getSymbolIcon(information.getKind());
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.label;
import com.intellij.openapi.project.Project;
import org.eclipse.lsp4j.SymbolInformation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class LSPDefaultLabelProvider implements LSPLabelProvider {
@NotNull
@Override
public String symbolNameFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project) {
return symbolInformation.getName();
}
@Nullable
@Override
public String symbolLocationFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project) {
return symbolInformation.getContainerName();
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.label;
import com.intellij.openapi.project.Project;
import org.eclipse.lsp4j.SymbolInformation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Extension to override the default Labels for Language Server elements such as symbols and completions.
*
* @author gayanper
*/
public interface LSPLabelProvider {
/**
* Generate the symbol name for the given {@link SymbolInformation}.
*/
@NotNull
String symbolNameFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project);
/**
* Generate the symbol location for the given {@link SymbolInformation}.
*/
@Nullable
String symbolLocationFor(@NotNull SymbolInformation symbolInformation, @NotNull Project project);
}

View file

@ -0,0 +1,780 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.psi;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.navigation.ItemPresentation;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.KeyWithDefaultValue;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.psi.ContributedReferenceHost;
import com.intellij.psi.FileViewProvider;
import com.intellij.psi.NavigatablePsiElement;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiInvalidElementAccessException;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiPolyVariantReference;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceService;
import com.intellij.psi.ResolveState;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.SearchScope;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.concurrency.AtomicFieldUpdater;
import com.intellij.util.keyFMap.KeyFMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
/**
* A simple PsiElement for LSP
*/
public class LSPPsiElement implements PsiNameIdentifierOwner, NavigatablePsiElement {
private final Key<KeyFMap> COPYABLE_USER_MAP_KEY = Key.create("COPYABLE_USER_MAP_KEY");
private final AtomicFieldUpdater<LSPPsiElement, KeyFMap> updater = AtomicFieldUpdater.forFieldOfType(LSPPsiElement.class, KeyFMap.class);
private final PsiManager manager;
private final LSPPsiReference reference;
private final Project project;
private String name;
private final PsiFile file;
public final int start;
public final int end;
/**
* @param name The name (text) of the element
* @param project The project it belongs to
* @param start The offset in the editor where the element starts
* @param end The offset where it ends
*/
public LSPPsiElement(String name, @NotNull Project project, int start, int end, PsiFile file) {
this.project = project;
this.name = name;
this.start = start;
this.end = end;
this.file = file;
manager = PsiManager.getInstance(project);
reference = new LSPPsiReference(this);
}
/**
* Concurrent writes to this field are via CASes only, using the {@link #updater}
*/
private volatile KeyFMap myUserMap = KeyFMap.EMPTY_MAP;
/**
* Returns the language of the PSI element.
*
* @return the language instance.
*/
@NotNull
public Language getLanguage() {
return PlainTextLanguage.INSTANCE;
}
/**
* Returns the PSI manager for the project to which the PSI element belongs.
*
* @return the PSI manager instance.
*/
public PsiManager getManager() {
return manager;
}
/**
* Returns the array of children for the PSI element. Important: In some implementations children are only composite
* elements, i.e. not a leaf elements
*
* @return the array of child elements.
*/
@NotNull
public PsiElement[] getChildren() {
return new PsiElement[0];
}
/**
* Returns the parent of the PSI element.
*
* @return the parent of the element, or null if the element has no parent.
*/
public PsiElement getParent() {
return getContainingFile();
}
/**
* Returns the first child of the PSI element.
*
* @return the first child, or null if the element has no children.
*/
public PsiElement getFirstChild() {
return null;
}
/**
* Returns the last child of the PSI element.
*
* @return the last child, or null if the element has no children.
*/
public PsiElement getLastChild() {
return null;
}
/**
* Returns the next sibling of the PSI element.
*
* @return the next sibling, or null if the node is the last in the list of siblings.
*/
public PsiElement getNextSibling() {
return null;
}
/**
* Returns the previous sibling of the PSI element.
*
* @return the previous sibling, or null if the node is the first in the list of siblings.
*/
public PsiElement getPrevSibling() {
return null;
}
/**
* Returns the text range in the document occupied by the PSI element.
*
* @return the text range.
*/
public TextRange getTextRange() {
return new TextRange(start, end);
}
/**
* Returns the text offset of the PSI element relative to its parent.
*
* @return the relative offset.
*/
public int getStartOffsetInParent() {
return start;
}
/**
* Returns the length of text of the PSI element.
*
* @return the text length.
*/
public int getTextLength() {
return end - start;
}
/**
* Finds a leaf PSI element at the specified offset from the start of the text range of this node.
*
* @param offset the relative offset for which the PSI element is requested.
* @return the element at the offset, or null if none is found.
*/
public PsiElement findElementAt(int offset) {
return null;
}
/**
* Finds a reference at the specified offset from the start of the text range of this node.
*
* @param offset the relative offset for which the reference is requested.
* @return the reference at the offset, or null if none is found.
*/
public PsiReference findReferenceAt(int offset) {
return null;
}
/**
* Returns the text of the PSI element as a character array.
*
* @return the element text as a character array.
*/
@NotNull
public char[] textToCharArray() {
return name.toCharArray();
}
/**
* Returns the PSI element which should be used as a navigation target when navigation to this PSI element is
* requested. The method can either return {@code this} or substitute a different element if this element does not
* have an associated file and offset. (For example, if the source code of a library is attached to a project, the
* navigation element for a compiled library class is its source class.)
*
* @return the navigation target element.
*/
public PsiElement getNavigationElement() {
return this;
}
/**
* Returns the PSI element which corresponds to this element and belongs to either the project source path or class
* path. The method can either return {@code this} or substitute a different element if this element does not belong
* to the source path or class path. (For example, the original element for a library source file is the
* corresponding compiled class file.)
*
* @return the original element.
*/
public PsiElement getOriginalElement() {
return null;
}
/**
* Checks if the text of this PSI element is equal to the specified character sequence.
*
* @param text the character sequence to compare with.
* @return true if the text is equal, false otherwise.
*/
public boolean textMatches(@NotNull CharSequence text) {
return getText() == text;
}
//Q: get rid of these methods?
/**
* Checks if the text of this PSI element is equal to the text of the specified PSI element.
*
* @param element the element to compare the text with.
* @return true if the text is equal, false otherwise.
*/
public boolean textMatches(PsiElement element) {
return getText().equals(element.getText());
}
/**
* Checks if the text of this element contains the specified character.
*
* @param c the character to search for.
* @return true if the character is found, false otherwise.
*/
public boolean textContains(char c) {
return getText().indexOf(c) >= 0;
}
/**
* Returns the text of the PSI element.
*
* @return the element text.
*/
public String getText() {
return name;
}
/**
* Passes the element to the specified visitor.
*
* @param visitor the visitor to pass the element to.
*/
public void accept(PsiElementVisitor visitor) {
visitor.visitElement(this);
}
/**
* Passes the children of the element to the specified visitor.
*
* @param visitor the visitor to pass the children to.
*/
public void acceptChildren(@NotNull PsiElementVisitor visitor) {
}
/**
* Creates a copy of the file containing the PSI element and returns the corresponding element in the created copy.
* Resolve operations performed on elements in the copy of the file will resolve to elements in the copy, not in the
* original file.
*
* @return the element in the file copy corresponding to this element.
*/
public PsiElement copy() {
return null;
}
/**
* Adds a child to this PSI element.
*
* @param element the child element to add.
* @return the element which was actually added (either { @code element} or its copy).
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement add(@NotNull PsiElement element) {
throw new IncorrectOperationException();
}
/**
* Adds a child to this PSI element, before the specified anchor element.
*
* @param element the child element to add.
* @param anchor the anchor before which the child element is inserted (must be a child of this PSI element)
* @return the element which was actually added (either { @code element} or its copy).
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement addBefore(@NotNull PsiElement element, PsiElement anchor) {
throw new IncorrectOperationException();
}
/**
* Adds a child to this PSI element, after the specified anchor element.
*
* @param element the child element to add.
* @param anchor the anchor after which the child element is inserted (must be a child of this PSI element)
* @return the element which was actually added (either { @code element} or its copy).
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement addAfter(@NotNull PsiElement element, PsiElement anchor) {
throw new IncorrectOperationException();
}
/**
* Checks if it is possible to add the specified element as a child to this element, and throws an exception if the
* add is not possible. Does not actually modify anything.
*
* @param element the child element to check the add possibility.
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
* @deprecated not all PSI implementations implement this method correctly.
*/
@Deprecated
public void checkAdd(@NotNull PsiElement element) {
throw new IncorrectOperationException();
}
/**
* Adds a range of elements as children to this PSI element.
*
* @param first the first child element to add.
* @param last the last child element to add (must have the same parent as { @code first})
* @return the first child element which was actually added (either { @code first} or its copy).
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement addRange(PsiElement first, PsiElement last) {
throw new IncorrectOperationException();
}
/**
* Adds a range of elements as children to this PSI element, before the specified anchor element.
*
* @param first the first child element to add.
* @param last the last child element to add (must have the same parent as { @code first})
* @param anchor the anchor before which the child element is inserted (must be a child of this PSI element)
* @return the first child element which was actually added (either { @code first} or its copy).
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement addRangeBefore(@NotNull PsiElement first, @NotNull PsiElement last, PsiElement anchor) {
throw new IncorrectOperationException();
}
/**
* Adds a range of elements as children to this PSI element, after the specified anchor element.
*
* @param first the first child element to add.
* @param last the last child element to add (must have the same parent as { @code first})
* @param anchor the anchor after which the child element is inserted (must be a child of this PSI element)
* @return the first child element which was actually added (either { @code first} or its copy).
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement addRangeAfter(PsiElement first, PsiElement last, PsiElement anchor) {
throw new IncorrectOperationException();
}
/**
* Deletes this PSI element from the tree.
*
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason (for
* example, the file containing the element is read-only).
*/
public void delete() {
throw new IncorrectOperationException();
}
/**
* Checks if it is possible to delete the specified element from the tree, and throws an exception if the add is not
* possible. Does not actually modify anything.
*
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
* @deprecated not all PSI implementations implement this method correctly.
*/
@Deprecated
public void checkDelete() {
throw new IncorrectOperationException();
}
/**
* Deletes a range of children of this PSI element from the tree.
*
* @param first the first child to delete (must be a child of this PSI element)
* @param last the last child to delete (must be a child of this PSI element)
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public void deleteChildRange(PsiElement first, PsiElement last) {
throw new IncorrectOperationException();
}
/**
* Replaces this PSI element (along with all its children) with another element (along with the children).
*
* @param newElement the element to replace this element with.
* @return the element which was actually inserted in the tree (either { @code newElement} or its copy)
* @throws IncorrectOperationException if the modification is not supported or not possible for some reason.
*/
public PsiElement replace(@NotNull PsiElement newElement) {
throw new IncorrectOperationException();
}
/**
* Checks if this PSI element is valid. Valid elements and their hierarchy members can be accessed for reading and
* writing. Valid elements can still correspond to underlying documents whose text is different, when those
* documents have been changed and not yet committed
* ({@link PsiDocumentManager#commitDocument(com.intellij.openapi.editor.Document)}).
* (In this case an attempt to change PSI will result in an exception).
* <p>
* Any access to invalid elements results in {@link PsiInvalidElementAccessException}.
* <p>
* Once invalid, elements can't become valid again.
* <p>
* Elements become invalid in following cases:
* <ul>
* <li>They have been deleted via PSI operation ({@link #delete()})</li>
* <li>They have been deleted as a result of an incremental reparse (document commit)</li>
* <li>Their containing file has been changed externally, or renamed so that its PSI had to be rebuilt from
* scratch</li>
* </ul>
*
* @return true if the element is valid, false otherwise.
* @see com.intellij.psi.util.PsiUtilCore#ensureValid(PsiElement)
*/
public boolean isValid() {
return true;
}
/**
* Checks if the contents of the element can be modified (if it belongs to a non-read-only source file.)
*
* @return true if the element can be modified, false otherwise.
*/
public boolean isWritable() {
return true;
}
/**
* Returns the reference from this PSI element to another PSI element (or elements), if one exists. If the element
* has multiple associated references (see {@link #getReferences()} for an example), returns the first associated
* reference.
*
* @return the reference instance, or null if the PSI element does not have any associated references.
* @see com.intellij.psi.search.searches.ReferencesSearch
*/
public PsiReference getReference() {
return reference;
}
/**
* Returns all references from this PSI element to other PSI elements. An element can have multiple references when,
* for example, the element is a string literal containing multiple sub-strings which are valid full-qualified class
* names. If an element contains only one text fragment which acts as a reference but the reference has multiple
* possible targets, {@link PsiPolyVariantReference} should be used instead of returning multiple references.
* <p>
* Actually, it's preferable to call {@link PsiReferenceService#getReferences} instead as it allows adding
* references by plugins when the element implements {@link ContributedReferenceHost}.
*
* @return the array of references, or an empty array if the element has no associated references.
* @see PsiReferenceService#getReferences
* @see com.intellij.psi.search.searches.ReferencesSearch
*/
@NotNull
public PsiReference[] getReferences() {
return new PsiReference[]{reference};
}
/**
* Passes the declarations contained in this PSI element and its children for processing to the specified scope
* processor.
*
* @param processor the processor receiving the declarations.
* @param lastParent the child of this element has been processed during the previous step of the tree up walk
* (declarations under this element do not need to be processed again)
* @param place the original element from which the tree up walk was initiated.
* @return true if the declaration processing should continue or false if it should be stopped.
*/
public boolean processDeclarations(@NotNull PsiScopeProcessor processor, @NotNull ResolveState state,
PsiElement lastParent,
@NotNull PsiElement place) {
return false;
}
/**
* Returns the element which should be used as the parent of this element in a tree up walk during a resolve
* operation. For most elements, this returns {@code getParent()}, but the context can be overridden for some
* elements like code fragments.
*
* @return the resolve context element.
*/
public PsiElement getContext() {
return null;
}
/**
* Checks if an actual source or class file corresponds to the element. Non-physical elements include, for example,
* PSI elements created for the watch expressions in the debugger. Non-physical elements do not generate tree change
* events. Also, {@link PsiDocumentManager#getDocument(PsiFile)} returns null for non-physical elements. Not to be
* confused with {@link FileViewProvider#isPhysical()}.
*
* @return true if the element is physical, false otherwise.
*/
public boolean isPhysical() {
return true;
}
/**
* Returns the scope in which the declarations for the references in this PSI element are searched.
*
* @return the resolve scope instance.
*/
@NotNull
public GlobalSearchScope getResolveScope() {
return getContainingFile().getResolveScope();
}
/**
* Returns the scope in which references to this element are searched.
*
* @return the search scope instance.
* @see { @link com.intellij.psi.search.PsiSearchHelper#getUseScope(PsiElement)}
*/
@NotNull
public SearchScope getUseScope() {
return getContainingFile().getResolveScope();
}
/**
* Returns the AST node corresponding to the element.
*
* @return the AST node instance.
*/
public ASTNode getNode() {
return null;
}
/**
* toString() should never be presented to the user.
*/
public String toString() {
return "Name : " + name + " at offset " + start + " to " + end + " in " + project;
}
/**
* This method shouldn't be called by clients directly, because there are no guarantees of it being symmetric. It's
* called by {@link PsiManager#areElementsEquivalent(PsiElement, PsiElement)} internally, which clients should
* invoke instead.<p/>
* <p>
* Implementations of this method should return {@code true} if the parameter is resolve-equivalent to {@code this},
* i.e. it represents the same entity from the language perspective. See also {@link
* PsiManager#areElementsEquivalent(PsiElement, PsiElement)} documentation.
*/
public boolean isEquivalentTo(PsiElement another) {
return this == another;
}
public Icon getIcon(int flags) {
return null;
}
public PsiElement getNameIdentifier() {
return this;
}
public PsiElement setName(@NotNull String name) {
this.name = name;
return this;
}
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
boolean control = true;
while (control) {
KeyFMap map = getUserMap();
KeyFMap newMap = (value == null) ? map.minus(key) : map.plus(key, value);
if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) {
control = false;
}
}
}
protected boolean changeUserMap(KeyFMap oldMap, KeyFMap newMap) {
return updater.compareAndSet(this, oldMap, newMap);
}
protected KeyFMap getUserMap() {
return myUserMap;
}
public <T> T getCopyableUserData(Key<T> key) {
KeyFMap map = getUserData(COPYABLE_USER_MAP_KEY);
return (map == null) ? null : map.get(key);
}
public <T> T getUserData(@NotNull Key<T> key) {
T t = getUserMap().get(key);
if (t == null && key instanceof KeyWithDefaultValue) {
KeyWithDefaultValue<T> key1 = (KeyWithDefaultValue<T>) key;
t = putUserDataIfAbsent(key, key1.getDefaultValue());
}
return t;
}
public <T> T putUserDataIfAbsent(Key<T> key, T value) {
while (true) {
KeyFMap map = getUserMap();
T oldValue = map.get(key);
if (oldValue != null) {
return oldValue;
}
KeyFMap newMap = map.plus(key, value);
if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap)) {
return value;
}
}
}
public <T> void putCopyableUserData(Key<T> key, T value) {
boolean control = true;
while (control) {
KeyFMap map = getUserMap();
KeyFMap copyableMap = map.get(COPYABLE_USER_MAP_KEY);
if (copyableMap == null)
copyableMap = KeyFMap.EMPTY_MAP;
KeyFMap newCopyableMap = (value == null) ? copyableMap.minus(key) : copyableMap.plus(key, value);
KeyFMap newMap = (newCopyableMap.isEmpty()) ?
map.minus(COPYABLE_USER_MAP_KEY) :
map.plus(COPYABLE_USER_MAP_KEY, newCopyableMap);
if ((newMap.equalsByReference(map)) || changeUserMap(map, newMap))
control = false;
}
}
public <T> boolean replace(Key<T> key, @Nullable T oldValue, @Nullable T newValue) {
while (true) {
KeyFMap map = getUserMap();
if (map.get(key) != oldValue) {
return false;
} else {
KeyFMap newMap = (newValue == null) ? map.minus(key) : map.plus(key, newValue);
if ((newMap == map) || changeUserMap(map, newMap)) {
return true;
}
}
}
}
public void copyCopyableDataTo(UserDataHolderBase clone) {
clone.putUserData(COPYABLE_USER_MAP_KEY, getUserData(COPYABLE_USER_MAP_KEY));
}
public boolean isUserDataEmpty() {
return getUserMap().isEmpty();
}
public ItemPresentation getPresentation() {
return new ItemPresentation() {
public String getPresentableText() {
return getName();
}
public String getLocationString() {
return getContainingFile().getName();
}
public Icon getIcon(boolean unused) {
return (unused) ? null : null; //iconProvider.getIcon(LSPPsiElement.this)
}
};
}
public String getName() {
return name;
}
public void navigate(boolean requestFocus) {
Editor editor = FileUtils.editorFromPsiFile(getContainingFile());
if (editor == null) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(getProject(), getContainingFile().getVirtualFile(),
getTextOffset());
ApplicationUtils.invokeLater(() -> ApplicationUtils
.writeAction(() -> FileEditorManager.getInstance(getProject()).openTextEditor(descriptor, false)));
}
}
/**
* Returns the file containing the PSI element.
*
* @return the file instance, or null if the PSI element is not contained in a file (for example, the element
* represents a package or directory).
* @throws PsiInvalidElementAccessException if this element is invalid
*/
public PsiFile getContainingFile() {
return file;
}
/**
* Returns the project to which the PSI element belongs.
*
* @return the project instance.
* @throws PsiInvalidElementAccessException if this element is invalid
*/
@NotNull
public Project getProject() {
return project;
}
/**
* Returns the offset in the file to which the caret should be placed when performing the navigation to the element.
* (For classes implementing {@link PsiNamedElement}, this should return the offset in the file of the name
* identifier.)
*
* @return the offset of the PSI element.
*/
public int getTextOffset() {
return start;
}
public boolean canNavigateToSource() {
return true;
}
public boolean canNavigate() {
return true;
}
protected void clearUserData() {
setUserMap(KeyFMap.EMPTY_MAP);
}
protected void setUserMap(KeyFMap map) {
myUserMap = map;
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.psi;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiPolyVariantReference;
import com.intellij.psi.PsiReference;
/**
* A simple PsiReference for LSP
*/
public class LSPPsiReference implements PsiReference {
private PsiElement element;
/**
* A simple PsiReference for LSP.
*
* @param element The corresponding PsiElement
*/
public LSPPsiReference(PsiElement element) {
this.element = element;
}
/**
* Returns the underlying (referencing) element of the reference.
*
* @return the underlying element of the reference.
*/
public PsiElement getElement() {
return element;
}
/**
* Returns the part of the underlying element which serves as a reference, or the complete text range of the element
* if the entire element is a reference.
*
* @return Relative range in element
*/
public TextRange getRangeInElement() {
return new TextRange(0, element.getTextLength());
}
/**
* Returns the element which is the target of the reference.
*
* @return the target element, or null if it was not possible to resolve the reference to a valid target.
* @see PsiPolyVariantReference#multiResolve(boolean)
*/
public PsiElement resolve() {
return element;
}
/**
* Returns the name of the reference target element which does not depend on import statements and other context
* (for example, the full-qualified name of the class if the reference targets a Java class).
*
* @return the canonical text of the reference.
*/
public String getCanonicalText() {
return element.getText();
}
/**
* Called when the reference target element has been renamed, in order to change the reference text according to the
* new name.
*
* @param newElementName the new name of the target element.
* @return the new underlying element of the reference.
* @throws IncorrectOperationException if the rename cannot be handled for some reason.
*/
public PsiElement handleElementRename(String newElementName) {
return element;
}
/**
* Changes the reference so that it starts to point to the specified element. This is called, for example, by the
* "Create Class from New" quickfix, to bind the (invalid) reference on which the quickfix was called to the newly
* created class.
*
* @param element the element which should become the target of the reference.
* @return the new underlying element of the reference.
* @throws IncorrectOperationException if the rebind cannot be handled for some reason.
*/
public PsiElement bindToElement(PsiElement element) {
this.element = element;
return element;
}
/**
* Checks if the reference targets the specified element.
*
* @param element the element to check target for.
* @return true if the reference targets that element, false otherwise.
*/
public boolean isReferenceTo(PsiElement element) {
return this.element == element;
}
/**
* Returns the array of String, {@link PsiElement} and/or {@link LookupElement} instances representing all
* identifiers that are visible at the location of the reference. The contents of the returned array is used to
* build the lookup list for basic code completion. (The list of visible identifiers may not be filtered by the
* completion prefix string - the filtering is performed later by IDEA core.)
*
* @return the array of available identifiers.
*/
public Object[] getVariants() {
return new Object[] {};
}
/**
* Returns false if the underlying element is guaranteed to be a reference, or true if the underlying element is a
* possible reference which should not be reported as an error if it fails to resolve. For example, a text in an XML
* file which looks like a full-qualified Java class name is a soft reference.
*
* @return true if the reference is soft, false otherwise.
*/
public boolean isSoft() {
return false;
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.rename;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.SearchScope;
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* The LSP based in-place renaming implementation.
*/
public class LSPInplaceRenamer extends MemberInplaceRenamer {
private Editor editor;
LSPInplaceRenamer(@NotNull PsiNamedElement elementToRename, PsiElement substituted, Editor editor) {
super(elementToRename, substituted, editor, elementToRename.getName(), elementToRename.getName());
this.editor = editor;
}
@Override
public Collection<PsiReference> collectRefs(SearchScope referencesSearchScope) {
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager != null) {
Pair<List<PsiElement>, List<VirtualFile>> results = eventManager
.references(editor.getCaretModel().getCurrentCaret().getOffset(), true, false);
List<PsiElement> references = results.getFirst();
List<VirtualFile> toClose = results.getSecond();
LSPRenameProcessor.addEditors(toClose);
return references.stream().map(PsiElement::getReference).collect(Collectors.toList());
} else {
return new ArrayList<>();
}
}
}

View file

@ -0,0 +1,170 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.rename;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
import com.intellij.codeInsight.template.impl.TemplateState;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.command.impl.StartMarkAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.NonEmptyInputValidator;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Pass;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil;
import com.intellij.refactoring.rename.PsiElementRenameHandler;
import com.intellij.refactoring.rename.RenameHandler;
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
import com.intellij.refactoring.rename.inplace.InplaceRefactoring;
import com.intellij.refactoring.rename.inplace.MemberInplaceRenameHandler;
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer;
import org.jetbrains.annotations.NotNull;
import com.falsepattern.zigbrains.lsp.contributors.psi.LSPPsiElement;
import java.util.List;
import static com.intellij.openapi.command.impl.StartMarkAction.START_MARK_ACTION_KEY;
/**
* The LSP based rename handler implementation.
*/
public class LSPRenameHandler implements RenameHandler {
@Override
public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
if (elements.length == 1) {
new MemberInplaceRenameHandler()
.doRename(elements[0], dataContext.getData(CommonDataKeys.EDITOR), dataContext);
} else {
invoke(project, dataContext.getData(CommonDataKeys.EDITOR), dataContext.getData(CommonDataKeys.PSI_FILE),
dataContext);
}
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) {
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
LSPPsiElement psiElement = getElementAtOffset(manager,
editor.getCaretModel().getCurrentCaret().getOffset());
if (psiElement != null) {
doRename(psiElement, editor);
}
}
}
private InplaceRefactoring doRename(PsiElement elementToRename, Editor editor) {
if (elementToRename instanceof PsiNameIdentifierOwner) {
RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(elementToRename);
if (processor.isInplaceRenameSupported()) {
StartMarkAction startMarkAction = editor.getUserData(START_MARK_ACTION_KEY);
if (startMarkAction == null || (processor.substituteElementToRename(elementToRename, editor)
== elementToRename)) {
processor.substituteElementToRename(elementToRename, editor, new Pass<PsiElement>() {
@Override
public void pass(PsiElement element) {
MemberInplaceRenamer renamer = createMemberRenamer(element,
(PsiNameIdentifierOwner) elementToRename, editor);
boolean startedRename = renamer.performInplaceRename();
if (!startedRename) {
performDialogRename(editor);
}
}
});
return null;
}
} else {
InplaceRefactoring inplaceRefactoring = editor.getUserData(InplaceRefactoring.INPLACE_RENAMER);
if ((inplaceRefactoring instanceof MemberInplaceRenamer)) {
TemplateState templateState = TemplateManagerImpl
.getTemplateState(InjectedLanguageEditorUtil.getTopLevelEditor(editor));
if (templateState != null) {
templateState.gotoEnd(true);
}
}
}
}
performDialogRename(editor);
return null;
}
@Override
public boolean isRenaming(DataContext dataContext) {
return isAvailableOnDataContext(dataContext);
}
@Override
public boolean isAvailableOnDataContext(DataContext dataContext) {
PsiElement element = PsiElementRenameHandler.getElement(dataContext);
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
PsiFile file = CommonDataKeys.PSI_FILE.getData(dataContext);
return editor != null && file != null && isAvailable(element, editor, file);
}
private boolean isAvailable(PsiElement psiElement, Editor editor, PsiFile psiFile) {
if (psiElement instanceof PsiFile || psiElement instanceof LSPPsiElement) {
return true;
} else {
return IntellijLanguageClient.isExtensionSupported(psiFile.getVirtualFile());
}
}
private MemberInplaceRenamer createMemberRenamer(PsiElement element, PsiNameIdentifierOwner elementToRename,
Editor editor) {
return new LSPInplaceRenamer((PsiNamedElement) element, elementToRename, editor);
}
private void performDialogRename(Editor editor) {
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
String renameTo = Messages.showInputDialog(
editor.getProject(), "Enter new name: ", "Rename", Messages.getQuestionIcon(), "",
new NonEmptyInputValidator());
if (renameTo != null && !renameTo.equals("")) {
manager.rename(renameTo);
}
}
}
private LSPPsiElement getElementAtOffset(EditorEventManager eventManager, int offset) {
Pair<List<PsiElement>, List<VirtualFile>> refResponse = eventManager.references(offset, true, false);
List<PsiElement> refs = refResponse.getFirst();
if (refs == null || refs.isEmpty()) {
return null;
}
PsiElement curElement = refs.stream()
.filter(e -> e.getTextRange().getStartOffset() <= offset && offset <= e.getTextRange().getEndOffset())
.findAny().orElse(null);
if (curElement != null) {
return new LSPPsiElement(curElement.getText(), curElement.getProject(),
curElement.getTextRange().getStartOffset(), curElement.getTextRange().getEndOffset(),
curElement.getContainingFile());
}
return null;
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.rename;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.requests.WorkspaceEditHandler;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.SearchScope;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import com.intellij.refactoring.rename.RenameDialog;
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
import com.intellij.usageView.UsageInfo;
import org.jetbrains.annotations.NotNull;
import com.falsepattern.zigbrains.lsp.contributors.psi.LSPPsiElement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* LSPRenameProcessor implementation.
*/
public class LSPRenameProcessor extends RenamePsiElementProcessor {
private PsiElement curElem;
private final Set<PsiElement> elements = new HashSet<>();
private static final Set<VirtualFile> openedEditors = new HashSet<>();
@Override
public boolean canProcessElement(@NotNull PsiElement element) {
return element instanceof LSPPsiElement;
}
@NotNull
@Override
public RenameDialog createRenameDialog(@NotNull Project project, @NotNull PsiElement element,
PsiElement nameSuggestionContext, Editor editor) {
return super.createRenameDialog(project, curElem, nameSuggestionContext, editor);
}
@NotNull
@SuppressWarnings("unused")
public Collection<PsiReference> findReferences(@NotNull PsiElement element, @NotNull SearchScope searchScope,
boolean searchInCommentsAndStrings) {
if (element instanceof LSPPsiElement) {
if (elements.contains(element)) {
return elements.stream().map(PsiElement::getReference).filter(Objects::nonNull).collect(Collectors.toList());
}
EditorEventManager
manager = EditorEventManagerBase.forEditor(FileUtils.editorFromPsiFile(element.getContainingFile()));
if (manager != null) {
Pair<List<PsiElement>, List<VirtualFile>> refs = manager.references(element.getTextOffset(), true, false);
if (refs.getFirst() != null && refs.getSecond() != null) {
addEditors(refs.getSecond());
return refs.getFirst().stream().map(PsiElement::getReference).filter(Objects::nonNull).collect(Collectors.toList());
}
}
}
return new ArrayList<>();
}
//TODO may rename invalid elements
@Override
public void renameElement(@NotNull PsiElement element, @NotNull String newName, @NotNull UsageInfo[] usages,
RefactoringElementListener listener) {
WorkspaceEditHandler.applyEdit(element, newName, usages, listener, new ArrayList<>(openedEditors));
openedEditors.clear();
elements.clear();
curElem = null;
}
@Override
public boolean isInplaceRenameSupported() {
return true;
}
public static void clearEditors() {
openedEditors.clear();
}
public static Set<VirtualFile> getEditors() {
return openedEditors;
}
static void addEditors(List<VirtualFile> toAdd) {
openedEditors.addAll(toAdd);
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.symbol;
import com.intellij.navigation.ItemPresentation;
import com.intellij.navigation.NavigationItem;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.Objects;
/**
* LSP implementation of NavigationItem for intellij
*
* @author gayanper
*/
public class LSPNavigationItem extends OpenFileDescriptor implements NavigationItem {
private ItemPresentation presentation;
LSPNavigationItem(String name, String location, Icon icon, @NotNull Project project, @NotNull VirtualFile file,
int logicalLine, int logicalColumn) {
super(project, file, logicalLine, logicalColumn);
presentation = new LSPItemPresentation(location, name, icon);
}
@Nullable
@Override
public String getName() {
return presentation.getPresentableText();
}
@Nullable
@Override
public ItemPresentation getPresentation() {
return presentation;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof LSPNavigationItem) {
LSPNavigationItem other = (LSPNavigationItem) obj;
return this.getLine() == other.getLine() && this.getColumn() == other.getColumn() &&
Objects.equals(this.getName(), other.getName());
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(this.getLine(), this.getColumn(), this.getName());
}
private class LSPItemPresentation implements ItemPresentation {
private String location;
private String presentableText;
private Icon icon;
LSPItemPresentation(String location, String presentableText, Icon icon) {
this.location = location;
this.presentableText = presentableText;
this.icon = icon;
}
@Nullable
@Override
public String getPresentableText() {
return presentableText;
}
@Nullable
@Override
public String getLocationString() {
return location;
}
@Nullable
@Override
public Icon getIcon(boolean unused) {
return icon;
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.symbol;
import com.intellij.ide.util.gotoByName.ChooseByNamePopup;
import com.intellij.navigation.ChooseByNameContributorEx;
import com.intellij.navigation.NavigationItem;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.Processor;
import com.intellij.util.indexing.FindSymbolParameters;
import com.intellij.util.indexing.IdFilter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
/**
* The symbol provider implementation for LSP client.
*
* @author gayanper
*/
public class LSPSymbolContributor implements ChooseByNameContributorEx {
private final WorkspaceSymbolProvider workspaceSymbolProvider = new WorkspaceSymbolProvider();
@Override
public void processNames(@NotNull Processor<? super String> processor, @NotNull GlobalSearchScope globalSearchScope, @Nullable IdFilter idFilter) {
String queryString = Optional.ofNullable(globalSearchScope.getProject())
.map(p -> p.getUserData(ChooseByNamePopup.CURRENT_SEARCH_PATTERN)).orElse("");
workspaceSymbolProvider.workspaceSymbols(queryString, globalSearchScope.getProject()).stream()
.filter(ni -> globalSearchScope.accept(ni.getFile()))
.map(NavigationItem::getName)
.forEach(processor::process);
}
@Override
public void processElementsWithName(@NotNull String s, @NotNull Processor<? super NavigationItem> processor, @NotNull FindSymbolParameters findSymbolParameters) {
workspaceSymbolProvider.workspaceSymbols(s, findSymbolParameters.getProject()).stream()
.filter(ni -> findSymbolParameters.getSearchScope().accept(ni.getFile()))
.forEach(processor::process);
}
}

View file

@ -0,0 +1,162 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.contributors.symbol;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.falsepattern.zigbrains.lsp.utils.GUIUtils;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.intellij.openapi.vfs.VirtualFile;
import org.eclipse.lsp4j.*;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPIconProvider;
import com.falsepattern.zigbrains.lsp.contributors.label.LSPLabelProvider;
/**
* The workspace symbole provider implementation based on LSP
*
* @author gayanper
*/
public class WorkspaceSymbolProvider {
private static final Logger LOG = Logger.getInstance(WorkspaceSymbolProvider.class);
public List<LSPNavigationItem> workspaceSymbols(String name, Project project) {
final Set<LanguageServerWrapper> serverWrappers = IntellijLanguageClient
.getProjectToLanguageWrappers()
.getOrDefault(FileUtils.projectToUri(project), Collections.emptySet());
final WorkspaceSymbolParams symbolParams = new WorkspaceSymbolParams(name);
return serverWrappers.stream().filter(s -> s.getStatus() == ServerStatus.INITIALIZED)
.flatMap(server -> collectSymbol(server, server.getRequestManager(), symbolParams))
.map(s -> createNavigationItem(s, project)).filter(Objects::nonNull).collect(Collectors.toList());
}
private LSPNavigationItem createNavigationItem(LSPSymbolResult result, Project project) {
final SymbolInformation information = (result.getSymbolInformation() != null) ?
result.getSymbolInformation() : from(result.getWorkspaceSymbol());
final Location location = information.getLocation();
final VirtualFile file = FileUtils.URIToVFS(location.getUri());
if (file != null) {
final LSPIconProvider iconProviderFor = GUIUtils.getIconProviderFor(result.getDefinition());
final LSPLabelProvider labelProvider = GUIUtils.getLabelProviderFor(result.getDefinition());
return new LSPNavigationItem(labelProvider.symbolNameFor(information, project),
labelProvider.symbolLocationFor(information, project), iconProviderFor.getSymbolIcon(information),
project, file,
location.getRange().getStart().getLine(),
location.getRange().getStart().getCharacter());
} else {
return null;
}
}
private SymbolInformation from(WorkspaceSymbol symbol) {
SymbolInformation information = new SymbolInformation();
information.setContainerName(symbol.getContainerName());
information.setKind(symbol.getKind());
information.setName(symbol.getName());
if(symbol.getLocation().isLeft()) {
information.setLocation(symbol.getLocation().getLeft());
} else {
information.setLocation(new Location());
information.getLocation().setUri(symbol.getLocation().getRight().getUri());
}
information.setTags(symbol.getTags());
if(symbol.getTags() != null) {
information.setDeprecated(symbol.getTags().contains(SymbolTag.Deprecated));
}
return information;
}
@SuppressWarnings("squid:S2142")
private Stream<LSPSymbolResult> collectSymbol(LanguageServerWrapper wrapper,
RequestManager requestManager,
WorkspaceSymbolParams symbolParams) {
final CompletableFuture<Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>>> request = requestManager
.symbol(symbolParams);
if (request == null) {
return Stream.empty();
}
try {
Either<List<? extends SymbolInformation>, List<? extends WorkspaceSymbol>> symbolInformations = request
.get(20000, TimeUnit.MILLISECONDS);
wrapper.notifySuccess(Timeouts.SYMBOLS);
if(symbolInformations.isLeft()) {
return symbolInformations.getLeft().stream().map(si -> new LSPSymbolResult(si, wrapper.getServerDefinition()));
} else if (symbolInformations.isRight()) {
return symbolInformations.getRight().stream().map(si -> new LSPSymbolResult(si, wrapper.getServerDefinition()));
}
} catch (TimeoutException e) {
LOG.warn(e);
wrapper.notifyFailure(Timeouts.SYMBOLS);
} catch (ExecutionException | InterruptedException e) {
LOG.warn(e);
wrapper.crashed(e);
}
return Stream.empty();
}
private static class LSPSymbolResult {
private SymbolInformation symbolInformation;
private WorkspaceSymbol workspaceSymbol;
private LanguageServerDefinition definition;
public LSPSymbolResult(SymbolInformation symbolInformation,
LanguageServerDefinition definition) {
this.symbolInformation = symbolInformation;
this.definition = definition;
}
public LSPSymbolResult(WorkspaceSymbol workspaceSymbol,
LanguageServerDefinition definition) {
this.workspaceSymbol = workspaceSymbol;
this.definition = definition;
}
public SymbolInformation getSymbolInformation() {
return symbolInformation;
}
public LanguageServerDefinition getDefinition() {
return definition;
}
public WorkspaceSymbol getWorkspaceSymbol() {
return workspaceSymbol;
}
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.editor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import org.eclipse.lsp4j.Location;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import java.awt.*;
public class CtrlRangeMarker {
Location location;
private Editor editor;
private RangeHighlighter range;
CtrlRangeMarker(Location location, Editor editor, RangeHighlighter range) {
this.location = location;
this.editor = editor;
this.range = range;
}
boolean highlightContainsOffset(int offset) {
if (!isDefinition()) {
return range.getStartOffset() <= offset && range.getEndOffset() >= offset;
} else {
return definitionContainsOffset(offset);
}
}
boolean definitionContainsOffset(int offset) {
return DocumentUtils.LSPPosToOffset(editor, location.getRange().getStart()) <= offset && offset <= DocumentUtils
.LSPPosToOffset(editor, location.getRange().getEnd());
}
/**
* Removes the highlighter and restores the default cursor
*/
void dispose() {
if (!isDefinition()) {
editor.getMarkupModel().removeHighlighter(range);
editor.getContentComponent().setCursor(Cursor.getDefaultCursor());
}
}
/**
* If the marker points to the definition itself
*/
boolean isDefinition() {
return range == null;
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.editor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
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.VersionedTextDocumentIdentifier;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class DocumentEventManager {
private final Document document;
private final DocumentListener documentListener;
private final TextDocumentSyncKind syncKind;
private final LanguageServerWrapper wrapper;
private final TextDocumentIdentifier identifier;
private int version = -1;
protected Logger LOG = Logger.getInstance(EditorEventManager.class);
private static final Map<String, DocumentEventManager> uriToDocumentEventManager = new HashMap<>();
private final Set<Document> openDocuments = new HashSet<>();
DocumentEventManager(Document document, DocumentListener documentListener, TextDocumentSyncKind syncKind, LanguageServerWrapper wrapper) {
this.document = document;
this.documentListener = documentListener;
this.syncKind = syncKind;
this.wrapper = wrapper;
this.identifier = new TextDocumentIdentifier(FileUtils.documentToUri(document));
}
public static void clearState() {
uriToDocumentEventManager.clear();
}
public static DocumentEventManager getOrCreateDocumentManager(Document document, DocumentListener listener, TextDocumentSyncKind syncKind, LanguageServerWrapper wrapper) {
DocumentEventManager manager = uriToDocumentEventManager.get(FileUtils.documentToUri(document));
if (manager != null) {
return manager;
}
manager = new DocumentEventManager(document, listener, syncKind, wrapper);
uriToDocumentEventManager.put(FileUtils.documentToUri(document), manager);
return manager;
}
public void removeListeners() {
document.removeDocumentListener(documentListener);
}
public void registerListeners() {
document.addDocumentListener(documentListener);
}
public int getDocumentVersion() {
return this.version;
}
public void documentChanged(DocumentEvent event) {
DidChangeTextDocumentParams changesParams = new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(),
Collections.singletonList(new TextDocumentContentChangeEvent()));
changesParams.getTextDocument().setUri(identifier.getUri());
changesParams.getTextDocument().setVersion(++version);
if (syncKind == TextDocumentSyncKind.Incremental) {
TextDocumentContentChangeEvent changeEvent = changesParams.getContentChanges().get(0);
CharSequence newText = event.getNewFragment();
int offset = event.getOffset();
int newTextLength = event.getNewLength();
EditorEventManager editorEventManager = EditorEventManagerBase.forUri(FileUtils.documentToUri(document));
if (editorEventManager == null) {
LOG.warn("no editor associated with document");
return;
}
Editor editor = editorEventManager.editor;
Position lspPosition = DocumentUtils.offsetToLSPPos(editor, offset);
if (lspPosition == null) {
return;
}
int startLine = lspPosition.getLine();
int startColumn = lspPosition.getCharacter();
CharSequence oldText = event.getOldFragment();
//if text was deleted/replaced, calculate the end position of inserted/deleted text
int endLine, endColumn;
if (oldText.length() > 0) {
endLine = startLine + StringUtil.countNewLines(oldText);
String content = oldText.toString();
String[] oldLines = content.split("\n");
int oldTextLength = oldLines.length == 0 ? 0 : oldLines[oldLines.length - 1].length();
endColumn = content.endsWith("\n") ? 0 : oldLines.length == 1 ? startColumn + oldTextLength : oldTextLength;
} else { //if insert or no text change, the end position is the same
endLine = startLine;
endColumn = startColumn;
}
Range range = new Range(new Position(startLine, startColumn), new Position(endLine, endColumn));
changeEvent.setRange(range);
changeEvent.setText(newText.toString());
} else if (syncKind == TextDocumentSyncKind.Full) {
changesParams.getContentChanges().get(0).setText(document.getText());
}
ApplicationUtils.pool(() -> wrapper.getRequestManager().didChange(changesParams));
}
public void documentOpened() {
if (openDocuments.contains(document)) {
LOG.warn("trying to send open notification for document which was already opened!");
} else {
openDocuments.add(document);
final String extension = FileUtilRt.getExtension(FileDocumentManager.getInstance().getFile(document).getName());
wrapper.getRequestManager().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(identifier.getUri(),
wrapper.serverDefinition.languageIdFor(extension),
++version,
document.getText())));
}
}
public void documentClosed() {
if (!openDocuments.contains(document)) {
LOG.warn("trying to close document which is not open");
} else if (EditorEventManagerBase.managersForUri(FileUtils.documentToUri(document)).size() > 1) {
LOG.warn("trying to close document which is still open in another editor!");
} else {
openDocuments.remove(document);
wrapper.getRequestManager().didClose(new DidCloseTextDocumentParams(identifier));
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,205 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.editor;
import com.intellij.openapi.editor.Editor;
import org.eclipse.lsp4j.Diagnostic;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.falsepattern.zigbrains.lsp.utils.OSUtils;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
public class EditorEventManagerBase {
private static final Map<String, Set<EditorEventManager>> uriToManagers = new ConcurrentHashMap<>();
private static final Map<Editor, EditorEventManager> editorToManager = new ConcurrentHashMap<>();
private static final int CTRL_KEY_CODE = OSUtils.isMac() ? KeyEvent.VK_META : KeyEvent.VK_CONTROL;
private volatile static boolean isKeyPressed = false;
private volatile static boolean isCtrlDown = false;
private volatile static CtrlRangeMarker ctrlRange;
static {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher((KeyEvent e) -> {
int eventId = e.getID();
if (eventId == KeyEvent.KEY_PRESSED) {
setIsKeyPressed(true);
if (e.getKeyCode() == CTRL_KEY_CODE) {
setIsCtrlDown(true);
}
} else if (eventId == KeyEvent.KEY_RELEASED) {
setIsKeyPressed(false);
if (e.getKeyCode() == CTRL_KEY_CODE) {
setIsCtrlDown(false);
if (getCtrlRange() != null) {
getCtrlRange().dispose();
setCtrlRange(null);
}
}
}
return false;
});
}
static synchronized CtrlRangeMarker getCtrlRange() {
return ctrlRange;
}
static synchronized void setCtrlRange(CtrlRangeMarker ctrlRange) {
EditorEventManagerBase.ctrlRange = ctrlRange;
}
static synchronized boolean getIsCtrlDown() {
return isCtrlDown;
}
static synchronized void setIsCtrlDown(boolean isCtrlDown) {
EditorEventManagerBase.isCtrlDown = isCtrlDown;
}
static synchronized boolean getIsKeyPressed() {
return isKeyPressed;
}
static synchronized void setIsKeyPressed(boolean isKeyPressed) {
EditorEventManagerBase.isKeyPressed = isKeyPressed;
}
private static void prune() {
pruneEditorManagerMap();
pruneUriManagerMap();
}
private static void pruneUriManagerMap() {
synchronized (uriToManagers) {
new HashMap<>(uriToManagers).forEach((key, value) -> {
new HashSet<>(value).forEach((manager) -> {
if (!manager.wrapper.isActive()) {
uriToManagers.get(key).remove(manager);
}
});
if (value.isEmpty()) {
uriToManagers.remove(key);
}
});
}
}
private static void pruneEditorManagerMap() {
new HashMap<>(editorToManager).forEach((key, value) -> {
if (!value.wrapper.isActive()) {
editorToManager.remove(key);
}
});
}
public static void registerManager(EditorEventManager manager) {
String uri = FileUtils.editorToURIString(manager.editor);
synchronized (uriToManagers) {
if (uriToManagers.containsKey(uri)) {
uriToManagers.get(uri).add(manager);
} else {
HashSet<EditorEventManager> set = new HashSet<>();
set.add(manager);
uriToManagers.put(uri, set);
}
}
editorToManager.put(manager.editor, manager);
}
public static void unregisterManager(EditorEventManager manager) {
editorToManager.remove(manager.editor);
String uri = FileUtils.editorToURIString(manager.editor);
synchronized (uriToManagers) {
Set<EditorEventManager> set = getEditorEventManagerCopy(uri);
if (set.isEmpty()) {
uriToManagers.remove(uri);
}
}
}
/**
* @param editor An editor
* @return The manager for the given editor, or None
*/
public static EditorEventManager forEditor(Editor editor) {
prune();
return editorToManager.get(editor);
}
/**
* Tells the server that all the documents will be saved
*/
public static void willSaveAll() {
prune();
editorToManager.forEach((key, value) -> value.willSave());
}
public static void diagnostics(String uri, List<Diagnostic> diagnostics) {
getEditorEventManagerCopy(uri).forEach(manager -> {
manager.diagnostics(diagnostics);
});
}
private static Set<EditorEventManager> getEditorEventManagerCopy(String uri) {
HashSet<EditorEventManager> result = new HashSet<>();
synchronized (uriToManagers) {
Set<EditorEventManager> managers = uriToManagers.get(uri);
if(managers != null) {
result.addAll(managers);
}
}
return result;
}
public static void willSave(String uri) {
EditorEventManager editorManager = forUri(uri);
if(editorManager != null) {
editorManager.willSave();
}
}
public static Set<EditorEventManager> managersForUri(String uri) {
return getEditorEventManagerCopy(uri);
}
/**
* WARNING: avoid using this function! It only gives you one editorEventManager, not all and not the one of the current editor.
* Only use for operations which are file-level (save, open, close,...) otherwise use {@link #managersForUri(String)} or {@link #forEditor(Editor)}
*/
public static EditorEventManager forUri(String uri) {
Set<EditorEventManager> managers = managersForUri(uri);
if(managers.size() >= 1) {
return managers.iterator().next();
}
return null;
}
public static void documentSaved(String uri) {
EditorEventManager editorManager = forUri(uri);
if(editorManager != null){
editorManager.documentSaved();
}
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.editor;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.util.TextRange;
public record QuickFixRequest(IntentionAction action, TextRange range) {
}

View file

@ -0,0 +1,127 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.extensions;
import com.falsepattern.zigbrains.lsp.client.ClientContext;
import com.falsepattern.zigbrains.lsp.client.DefaultLanguageClient;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerOptions;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.DefaultRequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPDefaultIconProvider;
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPIconProvider;
import com.falsepattern.zigbrains.lsp.contributors.label.LSPDefaultLabelProvider;
import com.falsepattern.zigbrains.lsp.contributors.label.LSPLabelProvider;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.listeners.EditorMouseListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.EditorMouseMotionListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.LSPCaretListenerImpl;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.psi.PsiFile;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
public interface LSPExtensionManager {
/**
* LSP allows you to provide custom(language-specific) {@link RequestManager} implementations.
* Request manager implementation is required to be modified in such situations where,
*
* <ul>
* <li> Adding support for custom LSP requests/notifications which are not part of the standard LS protocol.</li>
* <li> Default handling process of LSP requests/notifications is required to be customized.
* </ul>
* <p>
* As a starting point you can extend
* {@link DefaultRequestManager}.
*/
<T extends DefaultRequestManager> T getExtendedRequestManagerFor(LanguageServerWrapper wrapper,
LanguageServer server, LanguageClient client,
ServerCapabilities serverCapabilities);
/**
* LSP allows you to provide custom {@link EditorEventManager} implementations.
* Editor event manager implementation is required to be modified in such situations where,
*
* <ul>
* <li> Modifying/optimizing lsp features for custom requirements.
* </ul>
* <p>
* As a starting point you can extend
* {@link EditorEventManager}.
*/
<T extends EditorEventManager> T getExtendedEditorEventManagerFor(Editor editor, DocumentListener documentListener,
EditorMouseListenerImpl mouseListener,
EditorMouseMotionListenerImpl mouseMotionListener,
LSPCaretListenerImpl caretListener,
RequestManager requestManager,
ServerOptions serverOptions,
LanguageServerWrapper wrapper);
/**
* LSP allows you to provide extended/custom {@link LanguageServer} interfaces, if required.
*/
Class<? extends LanguageServer> getExtendedServerInterface();
/**
* LSP allows you to provide custom(language-specific) {@link LanguageClient} implementations.
* Language client is required to be modified in such situations where,
* <ul>
* <li> Adding support for custom client notifications which are not part of the standard LS protocol.</li>
* <li> Default handling process of LSP client requests/notifications is required to be customized.
* </ul>
* <p>
* As a starting point you can extend
* {@link DefaultLanguageClient}.
*/
LanguageClient getExtendedClientFor(ClientContext context);
/**
* The icon provider for the Language Server. Override and implement your own or extend the
* {@link LSPDefaultIconProvider} to customize the default icons.
*/
@NotNull
default LSPIconProvider getIconProvider() {
return new LSPDefaultIconProvider();
}
/**
* Some language servers might only need to start for files which has a specific content. This method can be used
* in such situation to control whether the file must be connected to a language server which is registered for the
* extension of this file.
*
* <b>Note:</b> By default this method returns <code>true</code>
*
* @param file PsiFile which is about to connect to a language server.
* @return <code>true</code> if the file is supported.
*/
default boolean isFileContentSupported(@NotNull PsiFile file) {
return true;
}
/**
* The label provider for the Language Server. Implement and override default behavior
* if it needs to be customize.
*/
@NotNull
default LSPLabelProvider getLabelProvider() {
return new LSPDefaultLabelProvider();
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
public class DocumentListenerImpl extends LSPListener implements DocumentListener {
/**
* Called before the text of the document is changed.
*
* @param event the event containing the information about the change.
*/
@Override
public void beforeDocumentChange(DocumentEvent event) {
}
/**
* Called after the text of the document has been changed.
*
* @param event the event containing the information about the change.
*/
@Override
public void documentChanged(DocumentEvent event) {
if (checkEnabled()) {
manager.documentChanged(event);
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.event.EditorMouseListener;
/**
* An EditorMouseListener implementation which listens to mouseExited, mouseEntered and mouseClicked events.
*/
public class EditorMouseListenerImpl extends LSPListener implements EditorMouseListener {
@Override
public void mousePressed(EditorMouseEvent editorMouseEvent) {
}
@Override
public void mouseClicked(EditorMouseEvent editorMouseEvent) {
if (checkEnabled()) {
manager.mouseClicked(editorMouseEvent);
}
}
@Override
public void mouseReleased(EditorMouseEvent editorMouseEvent) {
}
@Override
public void mouseEntered(EditorMouseEvent editorMouseEvent) {
if (checkEnabled()) {
manager.mouseEntered();
}
}
@Override
public void mouseExited(EditorMouseEvent editorMouseEvent) {
if (checkEnabled()) {
manager.mouseExited();
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.event.EditorMouseMotionListener;
/**
* Class listening for mouse movement in an editor (used for hover)
*/
public class EditorMouseMotionListenerImpl extends LSPListener implements EditorMouseMotionListener {
@Override
public void mouseMoved(EditorMouseEvent e) {
if (checkEnabled()) {
manager.mouseMoved(e);
}
}
@Override
public void mouseDragged(EditorMouseEvent editorMouseEvent) {
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class LSPCaretListenerImpl extends LSPListener implements CaretListener {
private Logger LOG = Logger.getInstance(LSPCaretListenerImpl.class);
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> scheduledFuture;
private static final long DEBOUNCE_INTERVAL_MS = 500;
public LSPCaretListenerImpl() {
scheduler = Executors.newScheduledThreadPool(1);
scheduledFuture = null;
}
@Override
public void caretPositionChanged(CaretEvent e) {
try {
if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
scheduledFuture.cancel(false);
}
scheduledFuture = scheduler.schedule(this::debouncedCaretPositionChanged, DEBOUNCE_INTERVAL_MS, TimeUnit.MILLISECONDS);
} catch (Exception err) {
LOG.warn("Error occurred when trying to update code actions", err);
}
}
private void debouncedCaretPositionChanged() {
if (checkEnabled()) {
manager.requestAndShowCodeActions();
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.editor.EditorKind;
import com.intellij.openapi.editor.event.EditorFactoryEvent;
import com.intellij.openapi.editor.event.EditorFactoryListener;
import org.jetbrains.annotations.NotNull;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
public class LSPEditorListener implements EditorFactoryListener {
public void editorReleased(@NotNull EditorFactoryEvent editorFactoryEvent) {
if(editorFactoryEvent.getEditor().getEditorKind() == EditorKind.MAIN_EDITOR){
IntellijLanguageClient.editorClosed(editorFactoryEvent.getEditor());
}
}
public void editorCreated(@NotNull EditorFactoryEvent editorFactoryEvent) {
if(editorFactoryEvent.getEditor().getEditorKind() == EditorKind.MAIN_EDITOR) {
IntellijLanguageClient.editorOpened(editorFactoryEvent.getEditor());
}
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManagerListener;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
public class LSPFileDocumentManagerListener implements FileDocumentManagerListener {
@Override
public void beforeDocumentSaving(@NotNull Document document) {
LSPFileEventManager.willSave(document);
}
@Override
public void unsavedDocumentsDropped() {
}
@Override
public void beforeAllDocumentsSaving() {
LSPFileEventManager.willSaveAllDocuments();
}
@Override
public void beforeFileContentReload(VirtualFile virtualFile, @NotNull Document document) {
}
@Override
public void fileWithNoDocumentChanged(@NotNull VirtualFile virtualFile) {
}
@Override
public void fileContentReloaded(@NotNull VirtualFile virtualFile, @NotNull Document document) {
}
@Override
public void fileContentLoaded(@NotNull VirtualFile virtualFile, @NotNull Document document) {
}
}

View file

@ -0,0 +1,249 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileMoveEvent;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.FileChangeType;
import org.eclipse.lsp4j.FileEvent;
import org.jetbrains.annotations.NotNull;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.falsepattern.zigbrains.lsp.utils.FileUtils.searchFiles;
class LSPFileEventManager {
private static final Logger LOG = Logger.getInstance(LSPFileEventManager.class);
/**
* Indicates that a document will be saved
*
* @param doc The document
*/
static void willSave(Document doc) {
String uri = FileUtils.VFSToURI(FileDocumentManager.getInstance().getFile(doc));
EditorEventManagerBase.willSave(uri);
}
/**
* Indicates that all documents will be saved
*/
static void willSaveAllDocuments() {
EditorEventManagerBase.willSaveAll();
}
/**
* Called when a file is changed. Notifies the server if this file was watched.
*
* @param file The file
*/
static void fileChanged(VirtualFile file) {
if (!FileUtils.isFileSupported(file)) {
return;
}
String uri = FileUtils.VFSToURI(file);
if (uri == null) {
return;
}
ApplicationUtils.invokeAfterPsiEvents(() -> {
EditorEventManagerBase.documentSaved(uri);
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(uri,
FileUtils.projectToUri(p), FileChangeType.Changed));
});
}
/**
* Called when a file is moved. Notifies the server if this file was watched.
*
* @param event The file move event
*/
static void fileMoved(VirtualFileMoveEvent event) {
try {
VirtualFile file = event.getFile();
if (!FileUtils.isFileSupported(file)) {
return;
}
String newFileUri = FileUtils.VFSToURI(file);
String oldParentUri = FileUtils.VFSToURI(event.getOldParent());
if (newFileUri == null || oldParentUri == null) {
return;
}
String oldFileUri = String.format("%s/%s", oldParentUri, event.getFileName());
ApplicationUtils.invokeAfterPsiEvents(() -> {
// Notifies the language server.
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(oldFileUri,
FileUtils.projectToUri(p), FileChangeType.Deleted));
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(newFileUri,
FileUtils.projectToUri(p), FileChangeType.Created));
FileUtils.findProjectsFor(file).forEach(p -> {
// Detaches old file from the wrappers.
Set<LanguageServerWrapper> wrappers = IntellijLanguageClient.getAllServerWrappersFor(FileUtils.projectToUri(p));
if (wrappers != null) {
wrappers.forEach(wrapper -> wrapper.disconnect(oldFileUri, FileUtils.projectToUri(p)));
}
// Re-open file to so that the new editor will be connected to the language server.
FileEditorManager fileEditorManager = FileEditorManager.getInstance(p);
ApplicationUtils.invokeLater(() -> {
fileEditorManager.closeFile(file);
fileEditorManager.openFile(file, true);
});
});
});
} catch (Exception e) {
LOG.warn("LSP file move event failed due to :", e);
}
}
/**
* Called when a file is deleted. Notifies the server if this file was watched.
*
* @param file The file
*/
static void fileDeleted(VirtualFile file) {
if (!FileUtils.isFileSupported(file)) {
return;
}
String uri = FileUtils.VFSToURI(file);
if (uri == null) {
return;
}
ApplicationUtils.invokeAfterPsiEvents(() -> {
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(uri,
FileUtils.projectToUri(p), FileChangeType.Deleted));
});
}
/**
* Called when a file is renamed. Notifies the server if this file was watched.
*
* @param oldFileName The old file name
* @param newFileName the new file name
*/
static void fileRenamed(String oldFileName, String newFileName) {
ApplicationUtils.invokeAfterPsiEvents(() -> {
try {
// Getting the right file is not trivial here since we only have the file name. Since we have to iterate over
// all opened projects and filter based on the file name.
Set<VirtualFile> files = Arrays.stream(ProjectManager.getInstance().getOpenProjects())
.flatMap(p -> searchFiles(newFileName, p).stream())
.collect(Collectors.toSet());
for (VirtualFile file : files) {
if (!FileUtils.isFileSupported(file)) {
continue;
}
String newFileUri = FileUtils.VFSToURI(file);
String oldFileUri = newFileUri.replace(file.getName(), oldFileName);
// Notifies the language server.
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(oldFileUri,
FileUtils.projectToUri(p), FileChangeType.Deleted));
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(newFileUri,
FileUtils.projectToUri(p), FileChangeType.Created));
FileUtils.findProjectsFor(file).forEach(p -> {
// Detaches old file from the wrappers.
Set<LanguageServerWrapper> wrappers = IntellijLanguageClient.getAllServerWrappersFor(FileUtils.projectToUri(p));
if (wrappers != null) {
wrappers.forEach(wrapper -> {
// make these calls first since the disconnect might stop the LS client if its last file.
wrapper.getRequestManager().didChangeWatchedFiles(
getDidChangeWatchedFilesParams(oldFileUri, FileChangeType.Deleted));
wrapper.getRequestManager().didChangeWatchedFiles(
getDidChangeWatchedFilesParams(newFileUri, FileChangeType.Created));
wrapper.disconnect(oldFileUri, FileUtils.projectToUri(p));
});
}
if (!newFileUri.equals(oldFileUri)) {
// Re-open file to so that the new editor will be connected to the language server.
FileEditorManager fileEditorManager = FileEditorManager.getInstance(p);
ApplicationUtils.invokeLater(() -> {
fileEditorManager.closeFile(file);
fileEditorManager.openFile(file, true);
});
}
});
}
} catch (Exception e) {
LOG.warn("LSP file rename event failed due to : ", e);
}
});
}
/**
* Called when a file is created. Notifies the server if needed.
*
* @param file The file
*/
static void fileCreated(VirtualFile file) {
if (!FileUtils.isFileSupported(file)) {
return;
}
String uri = FileUtils.VFSToURI(file);
if (uri != null) {
ApplicationUtils.invokeAfterPsiEvents(() -> {
FileUtils.findProjectsFor(file).forEach(p -> changedConfiguration(uri,
FileUtils.projectToUri(p), FileChangeType.Created));
});
}
}
private static void changedConfiguration(String uri, String projectUri, FileChangeType typ) {
ApplicationUtils.pool(() -> {
DidChangeWatchedFilesParams params = getDidChangeWatchedFilesParams(uri, typ);
Set<LanguageServerWrapper> wrappers = IntellijLanguageClient.getAllServerWrappersFor(projectUri);
if (wrappers == null) {
return;
}
for (LanguageServerWrapper wrapper : wrappers) {
if (wrapper.getRequestManager() != null
&& wrapper.getStatus() == ServerStatus.INITIALIZED) {
wrapper.getRequestManager().didChangeWatchedFiles(params);
}
}
});
}
@NotNull
private static DidChangeWatchedFilesParams getDidChangeWatchedFilesParams(String fileUri, FileChangeType typ) {
List<FileEvent> event = new ArrayList<>();
event.add(new FileEvent(fileUri, typ));
return new DidChangeWatchedFilesParams(event);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.intellij.openapi.diagnostic.Logger;
public class LSPListener {
private Logger LOG = Logger.getInstance(LSPListener.class);
protected EditorEventManager manager = null;
protected boolean enabled = true;
/**
* Sets the manager for this listener
*
* @param manager The manager
*/
public void setManager(EditorEventManager manager) {
this.manager = manager;
}
/**
* Checks if a manager is set, and logs and error if not the case
*
* @return true or false depending on if the manager is set
*/
protected boolean checkEnabled() {
if (manager == null) {
LOG.error("Manager is null");
return false;
}
return enabled;
}
public void disable() {
enabled = false;
}
public void enable() {
enabled = true;
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManagerListener;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
public class LSPProjectManagerListener implements ProjectManagerListener {
private static final Logger LOG = Logger.getInstance(LSPProjectManagerListener.class);
@Override
public void projectClosing(@NotNull Project project) {
// Removes all the attached LSP status widgets before closing a project. Otherwise the old status widget will
// not be removed when opening a new project in the same project window.
Set<LanguageServerWrapper> languageServerWrappers = IntellijLanguageClient.getProjectToLanguageWrappers().get(
FileUtils.projectToUri(project));
if (languageServerWrappers == null) {
// nothing to do
return;
}
languageServerWrappers.forEach(wrapper -> {
wrapper.removeWidget();
IntellijLanguageClient.removeWrapper(wrapper);
});
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import com.intellij.codeInsight.AutoPopupController;
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;
/**
* This class notifies an EditorEventManager that a character has been typed in the editor
*/
public class LSPTypedHandler extends TypedHandlerDelegate {
@Override
public Result charTyped(char c, Project project, @NotNull Editor editor, @NotNull PsiFile file) {
if (!FileUtils.isFileSupported(file.getVirtualFile())) {
return Result.CONTINUE;
}
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager != null) {
eventManager.characterTyped(c);
}
return Result.CONTINUE;
}
@Override
public Result checkAutoPopup(char charTyped, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
if (!FileUtils.isFileSupported(file.getVirtualFile())) {
return Result.CONTINUE;
}
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager == null) {
return Result.CONTINUE;
}
for (String triggerChar : manager.completionTriggers) {
if (triggerChar != null && triggerChar.length() == 1 && triggerChar.charAt(0) == charTyped) {
AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
return Result.STOP;
}
}
return Result.CONTINUE;
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.listeners;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileCopyEvent;
import com.intellij.openapi.vfs.VirtualFileEvent;
import com.intellij.openapi.vfs.VirtualFileListener;
import com.intellij.openapi.vfs.VirtualFileMoveEvent;
import com.intellij.openapi.vfs.VirtualFilePropertyEvent;
import org.jetbrains.annotations.NotNull;
public class VFSListener implements VirtualFileListener {
/**
* Fired when a virtual file is renamed from within IDEA, or its writable status is changed.
* For files renamed externally, {@link #fileCreated} and {@link #fileDeleted} events will be fired.
*
* @param event the event object containing information about the change.
*/
@Override
public void propertyChanged(@NotNull VirtualFilePropertyEvent event) {
if (event.getPropertyName().equals(VirtualFile.PROP_NAME)) {
LSPFileEventManager.fileRenamed((String) event.getOldValue(), (String) event.getNewValue());
}
}
/**
* Fired when the contents of a virtual file is changed.
*
* @param event the event object containing information about the change.
*/
@Override
public void contentsChanged(@NotNull VirtualFileEvent event) {
LSPFileEventManager.fileChanged(event.getFile());
}
/**
* Fired when a virtual file is deleted.
*
* @param event the event object containing information about the change.
*/
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
LSPFileEventManager.fileDeleted(event.getFile());
}
/**
* Fired when a virtual file is moved from within IDEA.
*
* @param event the event object containing information about the change.
*/
@Override
public void fileMoved(@NotNull VirtualFileMoveEvent event) {
LSPFileEventManager.fileMoved(event);
}
/**
* Fired when a virtual file is copied from within IDEA.
*
* @param event the event object containing information about the change.
*/
@Override
public void fileCopied(@NotNull VirtualFileCopyEvent event) {
fileCreated(event);
}
/**
* Fired when a virtual file is created. This event is not fired for files discovered during initial VFS initialization.
*
* @param event the event object containing information about the change.
*/
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
LSPFileEventManager.fileCreated(event.getFile());
}
/**
* Fired before the change of a name or writable status of a file is processed.
*
* @param event the event object containing information about the change.
*/
@Override
public void beforePropertyChange(@NotNull VirtualFilePropertyEvent event) {
}
/**
* Fired before the change of contents of a file is processed.
*
* @param event the event object containing information about the change.
*/
@Override
public void beforeContentsChange(@NotNull VirtualFileEvent event) {
}
/**
* Fired before the deletion of a file is processed.
*
* @param event the event object containing information about the change.
*/
@Override
public void beforeFileDeletion(@NotNull VirtualFileEvent event) {
}
/**
* Fired before the movement of a file is processed.
*
* @param event the event object containing information about the change.
*/
@Override
public void beforeFileMovement(@NotNull VirtualFileMoveEvent event) {
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.requests;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.ui.UIUtil;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.options.MutableDataSet;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.MarkedString;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.validation.NonNull;
import java.util.ArrayList;
import java.util.List;
/**
* Object used to process Hover responses
*/
public class HoverHandler {
private Logger LOG = Logger.getInstance(HoverHandler.class);
/**
* Returns the hover string corresponding to a Hover response
*
* @param hover The Hover
* @return The string response
*/
public static String getHoverString(@NonNull Hover hover) {
if (hover == null || hover.getContents() == null) {
return "";
}
Either<List<Either<String, MarkedString>>, MarkupContent> hoverContents = hover.getContents();
if (hoverContents.isLeft()) {
List<Either<String, MarkedString>> contents = hoverContents.getLeft();
if (contents != null && !contents.isEmpty()) {
List<String> result = new ArrayList<>();
for (Either<String, MarkedString> c : contents) {
String string = "";
if (c.isLeft() && !c.getLeft().isEmpty()) {
string = c.getLeft();
} else if (c.isRight()) {
MarkedString markedString = c.getRight();
string = (markedString.getLanguage() != null && !markedString.getLanguage().isEmpty()) ?
"```" + markedString.getLanguage() + " " + markedString.getValue() + "```" :
"";
}
MutableDataSet options = new MutableDataSet();
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
if (!string.isEmpty()) {
result.add(renderer.render(parser.parse(string)));
}
}
return "<html><style>p {margin: 0; color: " + (UIUtil.isUnderDarcula() ? "rgb(187,187,187)" : "black") + ";</style>" + String.join("\n\n", result) + "</html>";
} else {
return "";
}
} else if (hoverContents.isRight()) {
String markedContent = hoverContents.getRight().getValue();
if (markedContent.isEmpty()) {
return "";
}
MutableDataSet options = new MutableDataSet();
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
return "<html>" + renderer.render(parser.parse(markedContent)) + "</html>";
} else {
return "";
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.requests;
import com.intellij.openapi.editor.Editor;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
public class ReformatHandler {
/**
* Reformat a file given its editor
*
* @param editor The editor
*/
public static void reformatFile(Editor editor) {
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager != null) {
eventManager.reformat();
}
}
/**
* Reformat a selection in a file given its editor
*
* @param editor The editor
*/
public static void reformatSelection(Editor editor) {
EditorEventManager eventManager = EditorEventManagerBase.forEditor(editor);
if (eventManager != null) {
eventManager.reformatSelection();
}
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.requests;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* An object containing the Timeout for the various requests
*/
public class Timeout {
private static Map<Timeouts, Integer> timeouts = new ConcurrentHashMap<>();
static {
Arrays.stream(Timeouts.values()).forEach(t -> timeouts.put(t, t.getDefaultTimeout()));
}
public static int getTimeout(Timeouts type) {
return timeouts.get(type);
}
public static Map<Timeouts, Integer> getTimeouts() {
return timeouts;
}
public static void setTimeouts(Map<Timeouts, Integer> loaded) {
loaded.forEach((t, v) -> timeouts.replace(t, v));
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.requests;
/**
* Enumeration for the timeouts
*/
public enum Timeouts {
CODEACTION(2000), CODELENS(2000), COMPLETION(1000), DEFINITION(2000), DOC_HIGHLIGHT(1000), EXECUTE_COMMAND(
2000), FORMATTING(2000), HOVER(2000), INIT(10000), REFERENCES(2000), SIGNATURE(1000), SHUTDOWN(
5000), SYMBOLS(2000), WILLSAVE(2000), FOLDING(1000);
private final int defaultTimeout;
Timeouts(final int defaultTimeout) {
this.defaultTimeout = defaultTimeout;
}
public int getDefaultTimeout() {
return defaultTimeout;
}
}

View file

@ -0,0 +1,223 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.requests;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import com.intellij.usageView.UsageInfo;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ResourceOperation;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import com.falsepattern.zigbrains.lsp.contributors.psi.LSPPsiElement;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManagerBase;
import com.falsepattern.zigbrains.lsp.utils.ApplicationUtils;
import com.falsepattern.zigbrains.lsp.utils.DocumentUtils;
import com.falsepattern.zigbrains.lsp.utils.FileUtils;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static com.falsepattern.zigbrains.lsp.utils.ApplicationUtils.invokeLater;
import static com.falsepattern.zigbrains.lsp.utils.ApplicationUtils.writeAction;
import static com.falsepattern.zigbrains.lsp.utils.DocumentUtils.toEither;
/**
* An Object handling WorkspaceEdits
*/
public class WorkspaceEditHandler {
private static Logger LOG = Logger.getInstance(WorkspaceEditHandler.class);
public static void applyEdit(PsiElement elem, String newName, UsageInfo[] infos,
RefactoringElementListener listener, List<VirtualFile> openedEditors) {
Map<String, List<TextEdit>> edits = new HashMap<>();
if (elem instanceof LSPPsiElement) {
LSPPsiElement lspElem = (LSPPsiElement) elem;
if (Stream.of(infos).allMatch(info -> info.getElement() instanceof LSPPsiElement)) {
Stream.of(infos).forEach(ui -> {
Editor editor = FileUtils.editorFromVirtualFile(ui.getVirtualFile(), ui.getProject());
TextRange range = ui.getElement().getTextRange();
Range lspRange = new Range(DocumentUtils.offsetToLSPPos(editor, range.getStartOffset()),
DocumentUtils.offsetToLSPPos(editor, range.getEndOffset()));
TextEdit edit = new TextEdit(lspRange, newName);
String uri = null;
try {
uri = FileUtils.sanitizeURI(
new URL(ui.getVirtualFile().getUrl().replace(" ", FileUtils.SPACE_ENCODED)).toURI()
.toString());
} catch (MalformedURLException | URISyntaxException e) {
LOG.warn(e);
}
if (edits.keySet().contains(uri)) {
edits.get(uri).add(edit);
} else {
List<TextEdit> textEdits = new ArrayList<>();
textEdits.add(edit);
edits.put(uri, textEdits);
}
});
WorkspaceEdit workspaceEdit = new WorkspaceEdit(edits);
applyEdit(workspaceEdit, "Rename " + lspElem.getName() + " to " + newName, openedEditors);
}
}
}
public static boolean applyEdit(WorkspaceEdit edit, String name) {
return applyEdit(edit, name, new ArrayList<>());
}
/**
* Applies a WorkspaceEdit
*
* @param edit The edit
* @param name edit name
* @param toClose files to be closed
* @return True if everything was applied, false otherwise
*/
public static boolean applyEdit(WorkspaceEdit edit, String name, List<VirtualFile> toClose) {
final String newName = (name != null) ? name : "LSP edits";
if (edit != null) {
Map<String, List<TextEdit>> changes = edit.getChanges();
List<Either<TextDocumentEdit, ResourceOperation>> dChanges = edit.getDocumentChanges();
boolean[] didApply = new boolean[]{true};
Project[] curProject = new Project[]{null};
List<VirtualFile> openedEditors = new ArrayList<>();
//Get the runnable of edits for each editor to apply them all in one command
List<Runnable> toApply = new ArrayList<>();
if (dChanges != null) {
dChanges.forEach(tEdit -> {
if (tEdit.isLeft()) {
TextDocumentEdit textEdit = tEdit.getLeft();
VersionedTextDocumentIdentifier doc = textEdit.getTextDocument();
int version = doc.getVersion() != null ? doc.getVersion() : Integer.MAX_VALUE;
String uri = FileUtils.sanitizeURI(doc.getUri());
EditorEventManager manager = EditorEventManagerBase.forUri(uri);
if (manager != null) {
curProject[0] = manager.editor.getProject();
toApply.add(manager.getEditsRunnable(version, toEither(textEdit.getEdits()), newName, true));
} else {
toApply.add(
manageUnopenedEditor(textEdit.getEdits(), uri, version, openedEditors, curProject,
newName));
}
} else if (tEdit.isRight()) {
ResourceOperation resourceOp = tEdit.getRight();
//TODO
} else {
LOG.warn("Null edit");
}
});
} else if (changes != null) {
changes.forEach((key, lChanges) -> {
String uri = FileUtils.sanitizeURI(key);
EditorEventManager manager = EditorEventManagerBase.forUri(uri);
if (manager != null) {
curProject[0] = manager.editor.getProject();
toApply.add(manager.getEditsRunnable(Integer.MAX_VALUE, toEither(lChanges), newName, true));
} else {
toApply.add(manageUnopenedEditor(lChanges, uri, Integer.MAX_VALUE, openedEditors, curProject,
newName));
}
});
}
if (toApply.contains(null)) {
LOG.warn("Didn't apply, null runnable");
didApply[0] = false;
} else {
Runnable runnable = () -> toApply.forEach(Runnable::run);
invokeLater(() -> writeAction(() -> {
CommandProcessor.getInstance()
.executeCommand(curProject[0], runnable, name, "LSPPlugin", UndoConfirmationPolicy.DEFAULT,
false);
openedEditors.forEach(f -> FileEditorManager.getInstance(curProject[0]).closeFile(f));
toClose.forEach(f -> FileEditorManager.getInstance(curProject[0]).closeFile(f));
}));
}
return didApply[0];
} else {
return false;
}
}
/**
* Opens an editor when needed and gets the Runnable
*
* @param edits The text edits
* @param uri The uri of the file
* @param version The version of the file
* @param openedEditors
* @param curProject
* @param name
* @return The runnable containing the edits
*/
private static Runnable manageUnopenedEditor(List<TextEdit> edits, String uri, int version,
List<VirtualFile> openedEditors, Project[] curProject, String name) {
Project[] projects = ProjectManager.getInstance().getOpenProjects();
//Infer the project from the uri
Project project = Stream.of(projects)
.map(p -> new ImmutablePair<>(FileUtils.VFSToURI(ProjectUtil.guessProjectDir(p)), p))
.filter(p -> uri.startsWith(p.getLeft())).sorted(Collections.reverseOrder())
.map(ImmutablePair::getRight).findFirst().orElse(projects[0]);
VirtualFile file = null;
try {
file = LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(FileUtils.sanitizeURI(uri))));
} catch (URISyntaxException e) {
LOG.warn(e);
}
FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file);
Editor editor = ApplicationUtils
.computableWriteAction(() -> fileEditorManager.openTextEditor(descriptor, false));
openedEditors.add(file);
curProject[0] = editor.getProject();
Runnable runnable = null;
EditorEventManager manager = EditorEventManagerBase.forEditor(editor);
if (manager != null) {
runnable = manager.getEditsRunnable(version, toEither(edits), name, true);
}
return runnable;
}
}

View file

@ -0,0 +1,240 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.statusbar;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerStatus;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPDefaultIconProvider;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.utils.GUIUtils;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.StatusBarWidget;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Consumer;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.*;
public class LSPServerStatusWidget implements StatusBarWidget {
private final Map<Timeouts, Pair<Integer, Integer>> timeouts = new HashMap<>();
private final Project project;
private final String projectName;
private ServerStatus status = ServerStatus.STOPPED;
LSPServerStatusWidget(Project project) {
this.project = project;
this.projectName = project.getName();
for (Timeouts t : Timeouts.values()) {
timeouts.put(t, new MutablePair<>(0, 0));
}
}
public void notifyResult(Timeouts timeout, Boolean success) {
Pair<Integer, Integer> oldValue = timeouts.get(timeout);
if (success) {
timeouts.replace(timeout, new MutablePair<>(oldValue.getKey() + 1, oldValue.getValue()));
} else {
timeouts.replace(timeout, new MutablePair<>(oldValue.getKey(), oldValue.getValue() + 1));
}
}
public IconPresentation getPresentation() {
return new IconPresentation();
}
@Override
public void install(@NotNull StatusBar statusBar) {
}
/**
* Sets the status of the server
*
* @param status The new status
*/
public void setStatus(ServerStatus status) {
this.status = status;
updateWidget();
}
private void updateWidget() {
WindowManager manager = WindowManager.getInstance();
if (manager != null && project != null && !project.isDisposed()) {
StatusBar statusBar = manager.getStatusBar(project);
if (statusBar != null) {
statusBar.updateWidget(ID());
}
}
}
@Override
public void dispose() {}
@NotNull
@Override
public String ID() {
return "LSP";
}
public Project getProject() {
return project;
}
private class IconPresentation implements StatusBarWidget.IconPresentation {
@Nullable
@Override
public Icon getIcon() {
LanguageServerWrapper wrapper = LanguageServerWrapper.forProject(project);
Map<ServerStatus, Icon> icons = new LSPDefaultIconProvider().getStatusIcons();
if (wrapper != null) {
icons = GUIUtils.getIconProviderFor(wrapper.getServerDefinition())
.getStatusIcons();
}
return icons.get(status);
}
@SuppressWarnings("UsagesOfObsoleteApi")
@Nullable
@Override
public Consumer<MouseEvent> getClickConsumer() {
return (MouseEvent t) -> {
JBPopupFactory.ActionSelectionAid mnemonics = JBPopupFactory.ActionSelectionAid.MNEMONICS;
Component component = t.getComponent();
var wrapper = LanguageServerWrapper.forProject(project);
if (wrapper == null) {
var popup = JBPopupFactory.getInstance().createMessage("No language server active.");
Dimension dimension = popup.getContent().getPreferredSize();
Point at = new Point(0, -dimension.height);
popup.show(new RelativePoint(t.getComponent(), at));
return;
}
List<AnAction> actions = new ArrayList<>();
if (wrapper.getStatus() == ServerStatus.INITIALIZED) {
actions.add(new ShowConnectedFiles());
}
actions.add(new ShowTimeouts());
actions.add(new Restart());
String title = "Server Actions";
DataContext context = DataManager.getInstance().getDataContext(component);
DefaultActionGroup group = new DefaultActionGroup(actions);
ListPopup popup = JBPopupFactory.getInstance()
.createActionGroupPopup(title, group, context, mnemonics, true);
Dimension dimension = popup.getContent().getPreferredSize();
Point at = new Point(0, -dimension.height);
popup.show(new RelativePoint(t.getComponent(), at));
};
}
class ShowConnectedFiles extends AnAction implements DumbAware {
ShowConnectedFiles() {
super("&Show Connected Files", "Show the files connected to the server", null);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
var wrapper = LanguageServerWrapper.forProject(project);
if (wrapper == null) {
return;
}
StringBuilder connectedFiles = new StringBuilder("Connected files :");
wrapper.getConnectedFiles().forEach(f -> connectedFiles.append(System.lineSeparator()).append(f));
Messages.showInfoMessage(connectedFiles.toString(), "Connected Files");
}
}
class ShowTimeouts extends AnAction implements DumbAware {
ShowTimeouts() {
super("&Show Timeouts", "Show the timeouts proportions of the server", null);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
StringBuilder message = new StringBuilder();
message.append("<html>");
message.append("Timeouts (failed requests) :<br>");
timeouts.forEach((t, v) -> {
int timeouts = v.getRight();
message.append(t.name(), 0, 1).append(t.name().substring(1).toLowerCase()).append(" => ");
int total = v.getLeft() + timeouts;
if (total != 0) {
if (timeouts > 0) {
message.append("<font color=\"red\">");
}
message.append(timeouts).append("/").append(total).append(" (")
.append(100 * (double) timeouts / total).append("%)<br>");
if (timeouts > 0) {
message.append("</font>");
}
} else {
message.append("0/0 (0%)<br>");
}
});
message.append("</html>");
Messages.showInfoMessage(message.toString(), "Timeouts");
}
}
class Restart extends AnAction implements DumbAware {
Restart() {
super("&Restart", "Restarts the language server.", null);
}
@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
var wrapper = LanguageServerWrapper.forProject(project);
if (wrapper != null) {
wrapper.restart();
}
}
}
@Override
public String getTooltipText() {
LanguageServerWrapper wrapper = LanguageServerWrapper.forProject(project);
if (wrapper == null) {
return "Language server, project " + projectName;
} else {
return "Language server for extension " + wrapper.getServerDefinition().ext + ", project " + projectName;
}
}
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.statusbar;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.StatusBarWidget;
import com.intellij.openapi.wm.StatusBarWidgetFactory;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
public class LSPServerStatusWidgetFactory implements StatusBarWidgetFactory {
private final Map<Project, LSPServerStatusWidget> widgetForProject = new HashMap<>();
@Override
public @NonNls
@NotNull
String getId() {
return "LSP";
}
@Override
public @Nls
@NotNull
String getDisplayName() {
return "Language Server";
}
@Override
public boolean isAvailable(@NotNull Project project) {
return true;
}
@Override
public
StatusBarWidget createWidget(@NotNull Project project) {
return widgetForProject.computeIfAbsent(project, (k) -> new LSPServerStatusWidget(project));
}
@Override
public void disposeWidget(@NotNull StatusBarWidget statusBarWidget) {
if (statusBarWidget instanceof LSPServerStatusWidget) {
widgetForProject.remove(((LSPServerStatusWidget) statusBarWidget).getProject());
}
}
@Override
public boolean canBeEnabledOn(@NotNull StatusBar statusBar) {
return true;
}
public LSPServerStatusWidget getWidget(Project project) {
return widgetForProject.get(project);
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.NoAccessDuringPsiEvents;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Condition;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ApplicationUtils {
private final static ExecutorService EXECUTOR_SERVICE;
static {
// Single threaded executor is used to simulate a behavior of async sequencial execution.
// All runnables are executed asyncly but they are executed in the order of their submission.
EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
EXECUTOR_SERVICE.shutdownNow();
}
});
}
static public void invokeLater(Runnable runnable) {
ApplicationManager.getApplication().invokeLater(runnable);
}
static public void pool(Runnable runnable) {
EXECUTOR_SERVICE.submit(runnable);
}
static public <T> T computableReadAction(Computable<T> computable) {
return ApplicationManager.getApplication().runReadAction(computable);
}
static public void writeAction(Runnable runnable) {
ApplicationManager.getApplication().runWriteAction(runnable);
}
static public <T> T computableWriteAction(Computable<T> computable) {
return ApplicationManager.getApplication().runWriteAction(computable);
}
static public void invokeAfterPsiEvents(Runnable runnable) {
Runnable wrapper = () -> {
if (NoAccessDuringPsiEvents.isInsideEventProcessing()) {
invokeAfterPsiEvents(runnable);
} else {
runnable.run();
}
};
ApplicationManager.getApplication().invokeLater(wrapper, (Condition<Void>) value -> false);
}
}

View file

@ -0,0 +1,165 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.DocumentUtil;
import org.eclipse.lsp4j.InsertReplaceEdit;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static com.falsepattern.zigbrains.lsp.utils.ApplicationUtils.computableReadAction;
/**
* Various methods to convert offsets / logical position / server position
*/
public class DocumentUtils {
private static final Logger LOG = Logger.getInstance(DocumentUtils.class);
public static final String WIN_SEPARATOR = "\r\n";
public static final String LINUX_SEPARATOR = "\n";
/**
* Transforms a LogicalPosition (IntelliJ) to an LSP Position
*
* @param position the LogicalPosition
* @param editor The editor
* @return the Position
*/
@Nullable
public static Position logicalToLSPPos(LogicalPosition position, Editor editor) {
return offsetToLSPPos(editor, editor.logicalPositionToOffset(position));
}
/**
* Transforms a LogicalPosition (IntelliJ) to an LSP Position
*
* @param position the LogicalPosition
* @param editor The editor
* @return the Position
*/
@Nullable
public static Position offsetToLSPPos(LogicalPosition position, Editor editor) {
return offsetToLSPPos(editor, editor.logicalPositionToOffset(position));
}
/**
* Calculates a Position given an editor and an offset
*
* @param editor The editor
* @param offset The offset
* @return an LSP position
*/
@Nullable
public static Position offsetToLSPPos(Editor editor, int offset) {
return computableReadAction(() -> {
if (editor.isDisposed()) {
return null;
}
Document doc = editor.getDocument();
int line = doc.getLineNumber(offset);
int lineStart = doc.getLineStartOffset(line);
String lineTextBeforeOffset = doc.getText(TextRange.create(lineStart, offset));
int column = lineTextBeforeOffset.length();
return new Position(line, column);
});
}
/**
* Transforms an LSP position to an editor offset
*
* @param editor The editor
* @param pos The LSPPos
* @return The offset
*/
public static int LSPPosToOffset(Editor editor, Position pos) {
return computableReadAction(() -> {
if (editor == null) {
return -1;
}
if (editor.isDisposed()) {
return -2;
}
// lsp and intellij start lines/columns zero-based
Document doc = editor.getDocument();
int line = max(0, Math.min(pos.getLine(), doc.getLineCount()));
if (line >= doc.getLineCount()) {
return doc.getTextLength();
}
String lineText = doc.getText(DocumentUtil.getLineTextRange(doc, line));
final int positionInLine = max(0, min(lineText.length(), pos.getCharacter()));
int tabs = StringUtil.countChars(lineText, '\t', 0, positionInLine, false);
int tabSize = getTabSize(editor);
int column = positionInLine + tabs * (tabSize - 1);
int offset = editor.logicalPositionToOffset(new LogicalPosition(line, column));
if (pos.getCharacter() >= lineText.length()) {
LOG.debug(String.format("LSPPOS outofbounds: %s, line : %s, column : %d, offset : %d", pos,
lineText, column, offset));
}
int docLength = doc.getTextLength();
if (offset > docLength) {
LOG.debug(String.format("Offset greater than text length : %d > %d", offset, docLength));
}
return Math.min(max(offset, 0), docLength);
});
}
@Nullable
public static LogicalPosition getTabsAwarePosition(Editor editor, Position pos) {
return computableReadAction(() -> {
if (editor.isDisposed()) {
return null;
}
Document doc = editor.getDocument();
int line = max(0, Math.min(pos.getLine(), doc.getLineCount() - 1));
String lineText = doc.getText(DocumentUtil.getLineTextRange(doc, line));
final int positionInLine = max(0, min(lineText.length(), pos.getCharacter()));
int tabs = StringUtil.countChars(lineText, '\t', 0, positionInLine, false);
int tabSize = getTabSize(editor);
int column = positionInLine + tabs * (tabSize - 1);
return new LogicalPosition(line, column);
});
}
/**
* Retrieves the amount of whitespaces a tab represents.
*/
public static int getTabSize(Editor editor) {
return computableReadAction(() -> editor.getSettings().getTabSize(editor.getProject()));
}
public static boolean shouldUseSpaces(Editor editor) {
return computableReadAction(() -> !editor.getSettings().isUseTabCharacter(editor.getProject()));
}
public static List<Either<TextEdit, InsertReplaceEdit>> toEither(List<TextEdit> edits) {
return edits.stream().map(Either::<TextEdit, InsertReplaceEdit>forLeft).collect(Collectors.toList());
}
}

View file

@ -0,0 +1,368 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.LightVirtualFileBase;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.falsepattern.zigbrains.lsp.utils.ApplicationUtils.computableReadAction;
/**
* Various file / uri related methods
*/
public class FileUtils {
private final static OS os = (System.getProperty("os.name").toLowerCase().contains("win")) ? OS.WINDOWS : OS.UNIX;
private final static String COLON_ENCODED = "%3A";
public final static String SPACE_ENCODED = "%20";
private final static String URI_FILE_BEGIN = "file:";
private final static String URI_VALID_FILE_BEGIN = "file:///";
private final static char URI_PATH_SEP = '/';
private static final Logger LOG = Logger.getInstance(FileUtils.class);
public static List<Editor> getAllOpenedEditors(Project project) {
return computableReadAction(() -> {
List<Editor> editors = new ArrayList<>();
FileEditor[] allEditors = FileEditorManager.getInstance(project).getAllEditors();
for (FileEditor fEditor : allEditors) {
if (fEditor instanceof TextEditor) {
Editor editor = ((TextEditor) fEditor).getEditor();
if (editor.isDisposed() || !isEditorSupported(editor)) {
continue;
}
editors.add(editor);
}
}
return editors;
});
}
public static List<Editor> getAllOpenedEditorsForUri(Project project, String uri) {
VirtualFile file = virtualFileFromURI(uri);
return getAllOpenedEditorsForVirtualFile(project, file);
}
public static List<Editor> getAllOpenedEditorsForVirtualFile(Project project, VirtualFile file) {
return computableReadAction(() -> {
List<Editor> editors = new ArrayList<>();
FileEditor[] allEditors = FileEditorManager.getInstance(project).getAllEditors(file);
for (FileEditor fEditor : allEditors) {
if (fEditor instanceof TextEditor) {
Editor editor = ((TextEditor) fEditor).getEditor();
if (editor.isDisposed() || !isEditorSupported(editor)) {
continue;
}
editors.add(editor);
}
}
return editors;
});
}
/**
* This can be used to instantly apply a language server definition without restarting the IDE.
*/
public static void reloadAllEditors() {
Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
for (Project project : openProjects) {
reloadEditors(project);
}
}
/**
* This can be used to instantly apply a project-specific language server definition without restarting the
* project/IDE.
*
* @param project The project instance which need to be restarted
*/
public static void reloadEditors(@NotNull Project project) {
try {
List<Editor> allOpenedEditors = FileUtils.getAllOpenedEditors(project);
allOpenedEditors.forEach(IntellijLanguageClient::editorClosed);
allOpenedEditors.forEach(IntellijLanguageClient::editorOpened);
} catch (Exception e) {
LOG.warn(String.format("Refreshing project: %s is failed due to: ", project.getName()), e);
}
}
public static Editor editorFromPsiFile(PsiFile psiFile) {
return editorFromVirtualFile(psiFile.getVirtualFile(), psiFile.getProject());
}
public static Editor editorFromUri(String uri, Project project) {
return editorFromVirtualFile(virtualFileFromURI(uri), project);
}
@Nullable
public static Editor editorFromVirtualFile(VirtualFile file, Project project) {
FileEditor[] allEditors = FileEditorManager.getInstance(project).getAllEditors(file);
if (allEditors.length > 0 && allEditors[0] instanceof TextEditor) {
return ((TextEditor) allEditors[0]).getEditor();
}
return null;
}
public static VirtualFile virtualFileFromURI(String uri) {
try {
return LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(sanitizeURI(uri))));
} catch (URISyntaxException e) {
LOG.warn(e);
return null;
}
}
/**
* Returns a file type given an editor
*
* @param editor The editor
* @return The FileType
*/
public static FileType fileTypeFromEditor(Editor editor) {
return FileDocumentManager.getInstance().getFile(editor.getDocument()).getFileType();
}
/**
* Transforms an editor (Document) identifier to an LSP identifier
*
* @param editor The editor
* @return The TextDocumentIdentifier
*/
public static TextDocumentIdentifier editorToLSPIdentifier(Editor editor) {
return new TextDocumentIdentifier(editorToURIString(editor));
}
/**
* Returns the URI string corresponding to an Editor (Document)
*
* @param editor The Editor
* @return The URI
*/
public static String editorToURIString(Editor editor) {
return sanitizeURI(VFSToURI(FileDocumentManager.getInstance().getFile(editor.getDocument())));
}
public static VirtualFile virtualFileFromEditor(Editor editor) {
return FileDocumentManager.getInstance().getFile(editor.getDocument());
}
/**
* Returns the URI string corresponding to a VirtualFileSystem file
*
* @param file The file
* @return the URI
*/
public static String VFSToURI(VirtualFile file) {
return file == null? null : pathToUri(file.getPath());
}
/**
* Fixes common problems in uri, mainly related to Windows
*
* @param uri The uri to sanitize
* @return The sanitized uri
*/
public static String sanitizeURI(String uri) {
if (uri != null) {
StringBuilder reconstructed = new StringBuilder();
String uriCp = uri.replaceAll(" ", SPACE_ENCODED); //Don't trust servers
if (!uri.startsWith(URI_FILE_BEGIN)) {
LOG.warn("Malformed uri : " + uri);
return uri; //Probably not an uri
} else {
uriCp = uriCp.substring(URI_FILE_BEGIN.length());
while (uriCp.startsWith(Character.toString(URI_PATH_SEP))) {
uriCp = uriCp.substring(1);
}
reconstructed.append(URI_VALID_FILE_BEGIN);
if (os == OS.UNIX) {
return reconstructed.append(uriCp).toString();
} else {
reconstructed.append(uriCp.substring(0, uriCp.indexOf(URI_PATH_SEP)));
char driveLetter = reconstructed.charAt(URI_VALID_FILE_BEGIN.length());
if (Character.isLowerCase(driveLetter)) {
reconstructed.setCharAt(URI_VALID_FILE_BEGIN.length(), Character.toUpperCase(driveLetter));
}
if (reconstructed.toString().endsWith(COLON_ENCODED)) {
reconstructed.delete(reconstructed.length() - 3, reconstructed.length());
}
if (!reconstructed.toString().endsWith(":")) {
reconstructed.append(":");
}
return reconstructed.append(uriCp.substring(uriCp.indexOf(URI_PATH_SEP))).toString();
}
}
} else {
return null;
}
}
/**
* Transforms an URI string into a VFS file
*
* @param uri The uri
* @return The virtual file
*/
public static VirtualFile URIToVFS(String uri) {
try {
return LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(sanitizeURI(uri))));
} catch (URISyntaxException e) {
LOG.warn(e);
return null;
}
}
/**
* Returns the project base dir uri given an editor
*
* @param editor The editor
* @return The project whose the editor belongs
*/
public static String editorToProjectFolderUri(Editor editor) {
return pathToUri(editorToProjectFolderPath(editor));
}
public static String editorToProjectFolderPath(Editor editor) {
if (editor != null && editor.getProject() != null && editor.getProject().getBasePath() != null) {
return new File(editor.getProject().getBasePath()).getAbsolutePath();
}
return null;
}
/**
* Transforms a path into an URI string
*
* @param path The path
* @return The uri
*/
public static String pathToUri(@Nullable String path) {
return path != null ? sanitizeURI(new File(path).toURI().toString()) : null;
}
public static String projectToUri(Project project) {
if (project != null && project.getBasePath() != null) {
return pathToUri(new File(project.getBasePath()).getAbsolutePath());
}
return null;
}
public static String documentToUri(Document document) {
return sanitizeURI(VFSToURI(FileDocumentManager.getInstance().getFile(document)));
}
/**
* Object representing the OS type (Windows or Unix)
*/
public enum OS {
WINDOWS, UNIX
}
/**
* Checks if the given virtual file instance is supported by this LS client library.
*/
public static boolean isFileSupported(@Nullable VirtualFile file) {
if (file == null) {
return false;
}
if (file instanceof LightVirtualFileBase) {
return false;
}
if (file.getUrl().isEmpty() || file.getUrl().startsWith("jar:")) {
return false;
}
return IntellijLanguageClient.isExtensionSupported(file);
}
/**
* Find projects which contains the given file. This search runs among all open projects.
*/
@NotNull
public static Set<Project> findProjectsFor(@NotNull VirtualFile file) {
return Arrays.stream(ProjectManager.getInstance().getOpenProjects())
.filter(p -> searchFiles(file.getName(), p).stream().anyMatch(f -> f.getPath().equals(file.getPath())))
.collect(Collectors.toSet());
}
public static Collection<VirtualFile> searchFiles(String fileName, Project p) {
try {
return computableReadAction(() -> FilenameIndex.getVirtualFilesByName(fileName, GlobalSearchScope.projectScope(p)));
} catch (Throwable t) {
// Todo - Find a proper way to handle when IDEA file indexing is in-progress.
return Collections.emptyList();
}
}
/**
* Checks if the file in editor is supported by this LS client library.
*/
public static boolean isEditorSupported(@NotNull Editor editor) {
return isFileSupported(virtualFileFromEditor(editor)) &&
isFileContentSupported(editor);
}
// Always returns true unless the user has registered filtering to validate file content via LS protocol extension
// manager implementation.
private static boolean isFileContentSupported(Editor editor) {
return computableReadAction(() -> {
if (editor.getProject() == null) {
return true;
}
PsiFile file = PsiDocumentManager.getInstance(editor.getProject()).getPsiFile(editor.getDocument());
if (file == null) {
return true;
}
LSPExtensionManager lspExtManager = IntellijLanguageClient.getExtensionManagerFor(FileUtilRt.getExtension(file.getName()));
if (lspExtManager == null) {
return true;
}
return lspExtManager.isFileContentSupported(file);
});
}
}

View file

@ -0,0 +1,142 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.ide.browsers.BrowserLauncher;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.Hint;
import com.intellij.ui.LightweightHint;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.LanguageServerDefinition;
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPDefaultIconProvider;
import com.falsepattern.zigbrains.lsp.contributors.icon.LSPIconProvider;
import com.falsepattern.zigbrains.lsp.contributors.label.LSPDefaultLabelProvider;
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
import com.falsepattern.zigbrains.lsp.contributors.label.LSPLabelProvider;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.html.HTMLEditorKit;
import java.awt.*;
import java.net.URISyntaxException;
import java.util.Objects;
import java.util.Optional;
import static com.falsepattern.zigbrains.lsp.utils.ApplicationUtils.writeAction;
public final class GUIUtils {
private static final LSPDefaultIconProvider DEFAULT_ICON_PROVIDER = new LSPDefaultIconProvider();
private static final LSPLabelProvider DEFAULT_LABEL_PROVIDER = new LSPDefaultLabelProvider();
private static final Logger LOGGER = Logger.getInstance(GUIUtils.class);
private GUIUtils() {
}
public static Hint createAndShowEditorHint(Editor editor, String string, Point point) {
return createAndShowEditorHint(editor, string, point, HintManager.ABOVE,
HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING);
}
public static Hint createAndShowEditorHint(Editor editor, String string, Point point, int flags) {
return createAndShowEditorHint(editor, string, point, HintManager.ABOVE, flags);
}
/**
* Shows a hint in the editor
*
* @param editor The editor
* @param string The message / text of the hint
* @param point The position of the hint
* @param constraint The constraint (under/above)
* @param flags The flags (when the hint will disappear)
* @return The hint
*/
public static Hint createAndShowEditorHint(Editor editor, String string, Point point, short constraint, int flags) {
JTextPane textPane = new JTextPane();
textPane.setEditorKit(new HTMLEditorKit());
textPane.setText(string);
int width = textPane.getPreferredSize().width;
if (width > 600) {
// max-width does not seem to be supported, so use this rather ugly hack...
textPane.setText(string.replace("<style>", "<style>p {width: 600px}\n"));
}
textPane.setEditable(false);
textPane.addHyperlinkListener(e -> {
if ((e.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
&& Objects.nonNull(e.getURL())) {
try {
if ("http".equals(e.getURL().getProtocol())) {
BrowserLauncher.getInstance().browse(e.getURL().toURI());
} else {
final Project project = editor.getProject();
Optional<? extends Pair<Project, VirtualFile>> fileToOpen = Optional.ofNullable(project).map(
p -> Optional.ofNullable(VfsUtil.findFileByURL(e.getURL()))
.map(f -> new ImmutablePair<>(p, f))).orElse(Optional.empty());
fileToOpen.ifPresent(f -> {
final OpenFileDescriptor descriptor = new OpenFileDescriptor(f.getLeft(), f.getRight());
writeAction(() -> FileEditorManager.getInstance(f.getLeft()).openTextEditor(descriptor, true));
});
}
} catch (URISyntaxException ex) {
Messages.showErrorDialog("Invalid syntax in URL", "Open URL Error");
LOGGER.debug("Invalid URL was found.", ex);
}
}
});
LightweightHint hint = new LightweightHint(textPane);
Point p = HintManagerImpl.getHintPosition(hint, editor, editor.xyToLogicalPosition(point), constraint);
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, p, flags, 0, false,
HintManagerImpl.createHintHint(editor, p, hint, constraint).setContentActive(false));
return hint;
}
/**
* Returns a suitable LSPIconProvider given a ServerDefinition
*
* @param serverDefinition The serverDefinition
* @return The LSPIconProvider, or LSPDefaultIconProvider if none are found
*/
public static LSPIconProvider getIconProviderFor(LanguageServerDefinition serverDefinition) {
return IntellijLanguageClient.getExtensionManagerForDefinition(serverDefinition)
.map(LSPExtensionManager::getIconProvider).orElse(DEFAULT_ICON_PROVIDER);
}
/**
* Returns a suitable LSPLabelProvider given a ServerDefinition
*
* @param serverDefinition The serverDefinition
* @return The LSPLabelProvider, or the default if none are found
*/
public static LSPLabelProvider getLabelProviderFor(LanguageServerDefinition serverDefinition) {
return IntellijLanguageClient.getExtensionManagerForDefinition(serverDefinition)
.map(LSPExtensionManager::getLabelProvider).orElse(DEFAULT_LABEL_PROVIDER);
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
public abstract class LSPException extends RuntimeException {
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
import java.util.Locale;
public class OSUtils {
private static final String WINDOWS = "windows";
private static final String UNIX = "unix";
private static final String MAC = "mac";
private static final String OS = System.getProperty("os.name").toLowerCase(Locale.getDefault());
/**
* Returns name of the operating system running. null if not a unsupported operating system.
*
* @return operating system
*/
public static String getOperatingSystem() {
if (OSUtils.isWindows()) {
return WINDOWS;
} else if (OSUtils.isUnix()) {
return UNIX;
} else if (OSUtils.isMac()) {
return MAC;
}
return null;
}
public static boolean isWindows() {
return (OS.contains("win"));
}
public static boolean isMac() {
return (OS.contains("mac"));
}
public static boolean isUnix() {
return (OS.contains("nix") || OS.contains("nux") || OS.contains("aix"));
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright 2023 FalsePattern
*
* Licensed 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 com.falsepattern.zigbrains.lsp.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Object containing some useful methods for the plugin
*/
public class Utils {
/**
* Transforms an array into a string (using mkString, useful for Java)
*
* @param arr The array
* @param sep A separator
* @return The result of mkString
*/
public String arrayToString(Object[] arr, String sep) {
sep = (sep != null) ? sep : "";
return String.join(sep, Arrays.toString(arr));
}
/**
* Concatenate multiple arrays
*
* @param arr The arrays
* @return The concatenated arrays
*/
public Object[] concatenateArrays(Object[]... arr) {
List<Object> result = new ArrayList<>(Arrays.asList(arr));
return result.toArray();
}
public List<String> stringToList(String str, String sep) {
sep = (sep != null) ? sep : System.lineSeparator();
return new ArrayList<>(Arrays.asList(str.split(sep)));
}
public static String[] parseArgs(String[] strArr) {
List<String> buffer = new ArrayList<>();
boolean isSingleQuote = false;
boolean isDoubleQuote = false;
boolean wasEscaped = false;
StringBuilder curStr = new StringBuilder();
for (String str : strArr) {
for (int i = 0; i < str.length(); i++) {
switch (str.charAt(i)) {
case '\'':
if (!wasEscaped) {
isSingleQuote = !isSingleQuote;
}
wasEscaped = false;
curStr.append('\'');
break;
case '\"':
if (!wasEscaped) {
isDoubleQuote = !isDoubleQuote;
}
wasEscaped = false;
curStr.append('\"');
break;
case ' ':
if (isSingleQuote || isDoubleQuote) {
curStr.append(" ");
} else {
buffer.add(curStr.toString());
curStr.setLength(0);
}
wasEscaped = false;
break;
case '\\':
if (wasEscaped) {
wasEscaped = false;
} else {
wasEscaped = true;
}
curStr.append('\\');
break;
case 'c':
curStr.append('c');
wasEscaped = false;
break;
}
}
if (curStr.length() != 0) {
buffer.add(curStr.toString());
curStr.setLength(0);
}
}
String[] result = new String[buffer.size()];
buffer.toArray(result);
return result;
}
}

View file

@ -22,7 +22,7 @@ import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.util.Key;
import org.wso2.lsp4intellij.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import java.util.ArrayList;
import java.util.Comparator;

View file

@ -20,7 +20,7 @@ import com.falsepattern.zigbrains.zig.settings.AppSettingsState;
import org.eclipse.lsp4j.FoldingRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.wso2.lsp4intellij.contributors.LSPFoldingRangeProvider;
import com.falsepattern.zigbrains.lsp.contributors.LSPFoldingRangeProvider;
public class ZigFoldingRangeProvider extends LSPFoldingRangeProvider {
@Override

View file

@ -30,12 +30,12 @@ import org.eclipse.lsp4j.SemanticTokensEdit;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.wso2.lsp4intellij.client.languageserver.ServerOptions;
import org.wso2.lsp4intellij.client.languageserver.requestmanager.RequestManager;
import org.wso2.lsp4intellij.client.languageserver.wrapper.LanguageServerWrapper;
import org.wso2.lsp4intellij.editor.EditorEventManager;
import org.wso2.lsp4intellij.listeners.LSPCaretListenerImpl;
import org.wso2.lsp4intellij.requests.Timeouts;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerOptions;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.listeners.LSPCaretListenerImpl;
import com.falsepattern.zigbrains.lsp.requests.Timeouts;
import java.util.ArrayList;
import java.util.List;
@ -44,7 +44,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.wso2.lsp4intellij.requests.Timeout.getTimeout;
import static com.falsepattern.zigbrains.lsp.requests.Timeout.getTimeout;
public class ZLSEditorEventManager extends EditorEventManager {
private static String previousResultID = null;

View file

@ -21,16 +21,16 @@ import com.intellij.openapi.editor.event.DocumentListener;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.wso2.lsp4intellij.client.ClientContext;
import org.wso2.lsp4intellij.client.languageserver.ServerOptions;
import org.wso2.lsp4intellij.client.languageserver.requestmanager.DefaultRequestManager;
import org.wso2.lsp4intellij.client.languageserver.requestmanager.RequestManager;
import org.wso2.lsp4intellij.client.languageserver.wrapper.LanguageServerWrapper;
import org.wso2.lsp4intellij.editor.EditorEventManager;
import org.wso2.lsp4intellij.extensions.LSPExtensionManager;
import org.wso2.lsp4intellij.listeners.EditorMouseListenerImpl;
import org.wso2.lsp4intellij.listeners.EditorMouseMotionListenerImpl;
import org.wso2.lsp4intellij.listeners.LSPCaretListenerImpl;
import com.falsepattern.zigbrains.lsp.client.ClientContext;
import com.falsepattern.zigbrains.lsp.client.languageserver.ServerOptions;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.DefaultRequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.RequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.editor.EditorEventManager;
import com.falsepattern.zigbrains.lsp.extensions.LSPExtensionManager;
import com.falsepattern.zigbrains.lsp.listeners.EditorMouseListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.EditorMouseMotionListenerImpl;
import com.falsepattern.zigbrains.lsp.listeners.LSPCaretListenerImpl;
// There's a couple unchecked casts here, because LSPExtensionManager has generics where it shouldn't,
// but we have to live with it for now, I guess...

View file

@ -32,8 +32,8 @@ import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageServer;
import org.wso2.lsp4intellij.client.languageserver.requestmanager.DefaultRequestManager;
import org.wso2.lsp4intellij.client.languageserver.wrapper.LanguageServerWrapper;
import com.falsepattern.zigbrains.lsp.client.languageserver.requestmanager.DefaultRequestManager;
import com.falsepattern.zigbrains.lsp.client.languageserver.wrapper.LanguageServerWrapper;
import java.util.Collections;
import java.util.List;

View file

@ -18,7 +18,7 @@ package com.falsepattern.zigbrains.zig.lsp;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.PublishDiagnosticsCapabilities;
import org.wso2.lsp4intellij.client.languageserver.serverdefinition.RawCommandServerDefinition;
import com.falsepattern.zigbrains.lsp.client.languageserver.serverdefinition.RawCommandServerDefinition;
public class ZLSServerDefinition extends RawCommandServerDefinition {
public ZLSServerDefinition(String[] command) {

View file

@ -23,16 +23,12 @@ import com.intellij.notification.Notifications;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.ProjectActivity;
import com.intellij.openapi.startup.StartupActivity;
import kotlin.Result;
import kotlin.Unit;
import kotlin.coroutines.Continuation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.wso2.lsp4intellij.IntellijLanguageClient;
import com.falsepattern.zigbrains.lsp.IntellijLanguageClient;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;

View file

@ -12,40 +12,40 @@
<!-- region LSP4IntelliJ -->
<!-- register a listener on editor events, required for lsp file sync -->
<editorFactoryListener implementation="org.wso2.lsp4intellij.listeners.LSPEditorListener"/>
<fileDocumentManagerListener implementation="org.wso2.lsp4intellij.listeners.LSPFileDocumentManagerListener"/>
<editorFactoryListener implementation="com.falsepattern.zigbrains.lsp.listeners.LSPEditorListener"/>
<fileDocumentManagerListener implementation="com.falsepattern.zigbrains.lsp.listeners.LSPFileDocumentManagerListener"/>
<!-- for displaying notifications by lsp -->
<notificationGroup id="lsp"
displayType="STICKY_BALLOON"/>
<!-- for displaying the statusbar icon -->
<statusBarWidgetFactory implementation="org.wso2.lsp4intellij.statusbar.LSPServerStatusWidgetFactory"
id="org.wso2.lsp4intellij.statusbar.LSPServerStatusWidgetFactory"
<statusBarWidgetFactory implementation="com.falsepattern.zigbrains.lsp.statusbar.LSPServerStatusWidgetFactory"
id="com.falsepattern.zigbrains.lsp.statusbar.LSPServerStatusWidgetFactory"
order="first"/>
<!-- needed for completion -->
<completion.contributor implementationClass="org.wso2.lsp4intellij.contributors.LSPCompletionContributor"
id="org.wso2.lsp4intellij.contributors.LSPCompletionContributor"
<completion.contributor implementationClass="contributors.com.falsepattern.zigbrains.lsp.LSPCompletionContributor"
id="contributors.com.falsepattern.zigbrains.lsp.LSPCompletionContributor"
language="any"/>
<!-- needed for completion as well as signature help -->
<typedHandler implementation="org.wso2.lsp4intellij.listeners.LSPTypedHandler"
<typedHandler implementation="com.falsepattern.zigbrains.lsp.listeners.LSPTypedHandler"
id="LSPTypedHandler"/>
<!-- needed for code diagnostics -->
<externalAnnotator id="LSPAnnotator"
language="Zig"
implementationClass="org.wso2.lsp4intellij.contributors.annotator.LSPAnnotator"/>
implementationClass="com.falsepattern.zigbrains.lsp.contributors.annotator.LSPAnnotator"/>
<!-- needed for Workspace Symbols -->
<gotoSymbolContributor implementation="org.wso2.lsp4intellij.contributors.symbol.LSPSymbolContributor"
<gotoSymbolContributor implementation="com.falsepattern.zigbrains.lsp.contributors.symbol.LSPSymbolContributor"
id="LSPSymbolContributor"/>
<!-- needed for renaming -->
<renameHandler implementation="org.wso2.lsp4intellij.contributors.rename.LSPRenameHandler"
<renameHandler implementation="com.falsepattern.zigbrains.lsp.contributors.rename.LSPRenameHandler"
id="LSPRenameHandler"
order="first"/>
<renamePsiElementProcessor implementation="org.wso2.lsp4intellij.contributors.rename.LSPRenameProcessor"
<renamePsiElementProcessor implementation="com.falsepattern.zigbrains.lsp.contributors.rename.LSPRenameProcessor"
id="LSPRenameProcessor"
order="first"/>
@ -56,7 +56,7 @@
order="first"/>
<!-- needed for documentation -->
<platform.backend.documentation.targetProvider implementation="org.wso2.lsp4intellij.contributors.LSPDocumentationTargetProvider"/>
<platform.backend.documentation.targetProvider implementation="com.falsepattern.zigbrains.lsp.contributors.LSPDocumentationTargetProvider"/>
<!-- endregion LSP4IntelliJ -->
@ -122,7 +122,7 @@
<!-- region LSP4IntelliJ -->
<!-- needed for find references -->
<action class="org.wso2.lsp4intellij.actions.LSPReferencesAction"
<action class="com.falsepattern.zigbrains.lsp.actions.LSPReferencesAction"
id="LSPFindUsages">
<keyboard-shortcut first-keystroke="shift alt F7"
keymap="$default"/>
@ -137,9 +137,9 @@
<!-- region LSP4IntelliJ -->
<!-- required for lsp file sync -->
<listener class="org.wso2.lsp4intellij.listeners.VFSListener"
<listener class="com.falsepattern.zigbrains.lsp.listeners.VFSListener"
topic="com.intellij.openapi.vfs.VirtualFileListener"/>
<listener class="org.wso2.lsp4intellij.listeners.LSPProjectManagerListener"
<listener class="com.falsepattern.zigbrains.lsp.listeners.LSPProjectManagerListener"
topic="com.intellij.openapi.project.ProjectManagerListener"/>
<!-- endregion LSP4IntelliJ -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B