Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*******************************************************************************
* Copyright (c) 2025 Vegard IT GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
*******************************************************************************/
package org.eclipse.lsp4e.test.format;

import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
import org.eclipse.lsp4e.test.utils.TestUtils;
import org.eclipse.lsp4e.tests.mock.MockLanguageServer;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.texteditor.ITextEditor;
import org.junit.Test;

public class FormatHandlerReadOnlyTest extends AbstractTestWithProject {

@Test
public void testFormatOnReadOnlyFileAndMakeWritable() throws Exception {
// Mock formatting to prepend "//" at the start of each line
var edits = List.of( //
new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), "//"),
new TextEdit(new Range(new Position(1, 0), new Position(1, 0)), "//"));
MockLanguageServer.INSTANCE.setFormattingTextEdits(edits);

String content = "line1\nline2\n";
IFile file = TestUtils.createUniqueTestFile(project, content);

// Make file read-only
ResourceAttributes attrs = file.getResourceAttributes();
attrs.setReadOnly(true);
file.setResourceAttributes(attrs);
assertTrue(file.getResourceAttributes().isReadOnly());

// Open editor and select the whole document
var editor = TestUtils.openEditor(file);
var textEditor = (ITextEditor) editor;
IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
textEditor.getSelectionProvider().setSelection(new TextSelection(0, doc.getLength()));

// Before executing the format command, set up a poller that clicks "Yes"
// and records that the dialog was shown
var display = Display.getDefault();
var beforeShells = new HashSet<>(Arrays.asList(display.getShells()));
final var dialogShown = new AtomicBoolean(false);
display.asyncExec(new Runnable() {
Button findYesButton(Composite parent) {
for (Control child : parent.getChildren()) {
if (child instanceof Button button && button.getText().toLowerCase().contains("yes")) {
return button;
}
if (child instanceof Composite composite) {
Button result = findYesButton(composite);
if (result != null) {
return result;
}
}
}
return null;
}

@Override
public void run() {
Shell newShell = TestUtils.findNewShell(beforeShells, display);
if (newShell == null || !Messages.LSPFormatHandler_ReadOnlyEditor_title.equals(newShell.getText())) {
display.timerExec(50, this);
return;
}
dialogShown.set(true);
Button yes = findYesButton(newShell);
if (yes != null) {
yes.notifyListeners(SWT.Selection, new Event());
}
}
});

// Run format command which should prompt and then make the file writable
IHandlerService handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class);
handlerService.executeCommand("org.eclipse.lsp4e.format", null);

// File was made writable and edits were applied
TestUtils.waitForAndAssertCondition(5_000, dialogShown::get);
TestUtils.waitForAndAssertCondition(5_000, () -> !file.getResourceAttributes().isReadOnly());
TestUtils.waitForAndAssertCondition(5_000, () -> "//line1\n//line2\n".equals(doc.get()));
}
}
31 changes: 31 additions & 0 deletions org.eclipse.lsp4e/src/org/eclipse/lsp4e/internal/ResourceUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2025 Vegard IT GmbH and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
*******************************************************************************/
package org.eclipse.lsp4e.internal;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.core.runtime.CoreException;

public class ResourceUtil {

public static void setWritable(final IFile file) throws CoreException {
ResourceAttributes attrs = file.getResourceAttributes();
if (attrs != null) {
attrs.setReadOnly(false);
file.setResourceAttributes(attrs);
}
}

private ResourceUtil() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
import java.util.ConcurrentModificationException;

import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
Expand All @@ -25,19 +29,59 @@
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.ServerMessageHandler;
import org.eclipse.lsp4e.internal.LSPDocumentAbstractHandler;
import org.eclipse.lsp4e.internal.ResourceUtil;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4e.ui.UI;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorExtension;

public class LSPFormatHandler extends LSPDocumentAbstractHandler {

private final LSPFormatter formatter = new LSPFormatter();

static boolean setWritable(IFile file) {
final var shell = UI.getActiveShell();

if (!MessageDialog.openQuestion(shell, Messages.LSPFormatHandler_ReadOnlyEditor_title,
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_fileReadonly, file.getLocation().toFile()))) {
return false; // user aborted
}

try {
ResourceUtil.setWritable(file);
return true;
} catch (final CoreException ex) {
MessageDialog.openError(UI.getActiveShell(), Messages.LSPFormatHandler_ReadOnlyEditor_title,
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_makingWritableFailed, file.getLocation().toFile(),
ex.getStatus().getMessage()));
}
return false;
}

@Override
protected void execute(ExecutionEvent event, ITextEditor textEditor) {
// If the editor input is read-only, prompt to make writable
final var editorExt = Adapters.adapt(textEditor, ITextEditorExtension.class);
if (editorExt != null && editorExt.isEditorInputReadOnly()) {
final IEditorInput input = textEditor.getEditorInput();
final IFile file = input instanceof FileEditorInput fileInput ? fileInput.getFile() : null;
if (file == null) {
MessageDialog.openInformation(HandlerUtil.getActiveShell(event),
Messages.LSPFormatHandler_ReadOnlyEditor_title,
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_inputReadonly, input.getToolTipText()));
return;
}

if (!setWritable(file))
return;
}

final ISelection selection = HandlerUtil.getCurrentSelection(event);
if (selection instanceof final ITextSelection textSelection && !textSelection.isEmpty()) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
Expand All @@ -43,10 +45,13 @@
import org.eclipse.lsp4e.VersionedEdits;
import org.eclipse.lsp4e.internal.DocumentUtil;
import org.eclipse.lsp4e.ui.FormatterPreferencePage;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4e.ui.UI;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.FormattingOptions;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.progress.UIJob;
Expand All @@ -57,7 +62,7 @@ public class LSPonTypeFormattingReconcilingStrategy implements IReconcilingStrat
private final IPropertyChangeListener formattingPrefsListener = (final PropertyChangeEvent event) -> {
final var newValue = event.getNewValue();
if (newValue != null) {
if (event.getProperty() == FormatterPreferencePage.PREF_ON_TYPE_FORMATTING_ENABLED) {
if (FormatterPreferencePage.PREF_ON_TYPE_FORMATTING_ENABLED.equals(event.getProperty())) {
isOnTypeFormattingEnabled = Boolean.parseBoolean(newValue.toString());
}
}
Expand Down Expand Up @@ -158,6 +163,21 @@ void doFormatOnType(@Nullable ITextViewer viewer, @Nullable IDocument document,
if (document == null || event == null || event.fText.isEmpty() || viewer == null) {
return;
}

// If the backing resource is read-only, prompt to make it writable
if (LSPEclipseUtils.isReadOnly(document)) {
IFile file = LSPEclipseUtils.getFile(document);
if (file == null) {
Comment thread
sebthom marked this conversation as resolved.
final var label = LSPEclipseUtils.toUri(document);
MessageDialog.openInformation(UI.getActiveShell(), Messages.LSPFormatHandler_ReadOnlyEditor_title,
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_inputReadonly, String.valueOf(label)));
return;
}

if(!LSPFormatHandler.setWritable(file))
return;
}

final String[] triggerCharArr = new String[] { "" }; //$NON-NLS-1$
var executor = LanguageServers.forDocument(document)
.withCapability(capabilities -> {
Expand Down
4 changes: 4 additions & 0 deletions org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public final class Messages extends NLS {
public static String LSPFormatFilesHandler_FormattingSelectedFiles;
public static String LSPFormatHandler_DiscardedFormat;
public static String LSPFormatHandler_DiscardedFormatResponse;
public static String LSPFormatHandler_ReadOnlyEditor_title;
public static String LSPFormatHandler_ReadOnlyEditor_inputReadonly;
public static String LSPFormatHandler_ReadOnlyEditor_fileReadonly;
public static String LSPFormatHandler_ReadOnlyEditor_makingWritableFailed;
public static String LSPSymbolInWorkspaceDialog_DialogLabel;
public static String LSPSymbolInWorkspaceDialog_DialogTitle;
public static String updateCodelensMenu_job;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ LSPFormatFilesHandler_FormattingFile=Formatting {0}...
LSPFormatFilesHandler_FormattingSelectedFiles=Formatting selected files...
LSPFormatHandler_DiscardedFormat=Discarded Format
LSPFormatHandler_DiscardedFormatResponse=The format response has been discarded because the document has changed since the format request has been sent
LSPFormatHandler_ReadOnlyEditor_title=Read-only File Encountered
LSPFormatHandler_ReadOnlyEditor_inputReadonly=Editor input ''{0}'' is read-only.
LSPFormatHandler_ReadOnlyEditor_fileReadonly=File ''{0}'' is read-only.\n\nDo you wish to make it writable?
LSPFormatHandler_ReadOnlyEditor_makingWritableFailed=Could not make ''{0}'' writable:\n\n{1}
LSPSymbolInWorkspaceDialog_DialogLabel=Select an item to open.
LSPSymbolInWorkspaceDialog_DialogTitle=Open Symbol in Workspace
updateCodelensMenu_job=Update CodeLens menu
Expand Down