Skip to content

Commit 09e7a42

Browse files
committed
feat: prompt user when attempting to format a read-only file
1 parent 802ce5b commit 09e7a42

6 files changed

Lines changed: 201 additions & 1 deletion

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vegard IT GmbH and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
11+
*******************************************************************************/
12+
package org.eclipse.lsp4e.test.format;
13+
14+
import static org.junit.Assert.assertTrue;
15+
16+
import java.util.Arrays;
17+
import java.util.HashSet;
18+
import java.util.List;
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
21+
import org.eclipse.core.resources.IFile;
22+
import org.eclipse.core.resources.ResourceAttributes;
23+
import org.eclipse.jface.text.IDocument;
24+
import org.eclipse.jface.text.TextSelection;
25+
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
26+
import org.eclipse.lsp4e.test.utils.TestUtils;
27+
import org.eclipse.lsp4e.tests.mock.MockLanguageServer;
28+
import org.eclipse.lsp4e.ui.Messages;
29+
import org.eclipse.lsp4j.Position;
30+
import org.eclipse.lsp4j.Range;
31+
import org.eclipse.lsp4j.TextEdit;
32+
import org.eclipse.swt.SWT;
33+
import org.eclipse.swt.widgets.Button;
34+
import org.eclipse.swt.widgets.Composite;
35+
import org.eclipse.swt.widgets.Control;
36+
import org.eclipse.swt.widgets.Display;
37+
import org.eclipse.swt.widgets.Event;
38+
import org.eclipse.swt.widgets.Shell;
39+
import org.eclipse.ui.PlatformUI;
40+
import org.eclipse.ui.handlers.IHandlerService;
41+
import org.eclipse.ui.texteditor.ITextEditor;
42+
import org.junit.Test;
43+
44+
public class FormatHandlerReadOnlyTest extends AbstractTestWithProject {
45+
46+
@Test
47+
public void testFormatOnReadOnlyFileAndMakeWritable() throws Exception {
48+
// Mock formatting to prepend "//" at the start of each line
49+
var edits = List.of( //
50+
new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), "//"),
51+
new TextEdit(new Range(new Position(1, 0), new Position(1, 0)), "//"));
52+
MockLanguageServer.INSTANCE.setFormattingTextEdits(edits);
53+
54+
String content = "line1\nline2\n";
55+
IFile file = TestUtils.createUniqueTestFile(project, content);
56+
57+
// Make file read-only
58+
ResourceAttributes attrs = file.getResourceAttributes();
59+
attrs.setReadOnly(true);
60+
file.setResourceAttributes(attrs);
61+
assertTrue(file.getResourceAttributes().isReadOnly());
62+
63+
// Open editor and select the whole document
64+
var editor = TestUtils.openEditor(file);
65+
var textEditor = (ITextEditor) editor;
66+
IDocument doc = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
67+
textEditor.getSelectionProvider().setSelection(new TextSelection(0, doc.getLength()));
68+
69+
// Before executing the format command, set up a poller that clicks "Yes"
70+
// and records that the dialog was shown
71+
var display = Display.getDefault();
72+
var beforeShells = new HashSet<>(Arrays.asList(display.getShells()));
73+
final var dialogShown = new AtomicBoolean(false);
74+
display.asyncExec(new Runnable() {
75+
Button findYesButton(Composite parent) {
76+
for (Control child : parent.getChildren()) {
77+
if (child instanceof Button button && button.getText().toLowerCase().contains("yes")) {
78+
return button;
79+
}
80+
if (child instanceof Composite composite) {
81+
Button result = findYesButton(composite);
82+
if (result != null) {
83+
return result;
84+
}
85+
}
86+
}
87+
return null;
88+
}
89+
90+
@Override
91+
public void run() {
92+
Shell newShell = TestUtils.findNewShell(beforeShells, display);
93+
if (newShell == null || !Messages.LSPFormatHandler_ReadOnlyEditor_title.equals(newShell.getText())) {
94+
display.timerExec(50, this);
95+
return;
96+
}
97+
dialogShown.set(true);
98+
Button yes = findYesButton(newShell);
99+
if (yes != null) {
100+
yes.notifyListeners(SWT.Selection, new Event());
101+
}
102+
}
103+
});
104+
105+
// Run format command which should prompt and then make the file writable
106+
IHandlerService handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class);
107+
handlerService.executeCommand("org.eclipse.lsp4e.format", null);
108+
109+
// File was made writable and edits were applied
110+
TestUtils.waitForAndAssertCondition(5_000, dialogShown::get);
111+
TestUtils.waitForAndAssertCondition(5_000, () -> !file.getResourceAttributes().isReadOnly());
112+
TestUtils.waitForAndAssertCondition(5_000, () -> "//line1\n//line2\n".equals(doc.get()));
113+
}
114+
}

org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,4 +1625,12 @@ public static boolean isReadOnly(final IResource resource) {
16251625
ResourceAttributes attributes = resource.getResourceAttributes();
16261626
return attributes != null && attributes.isReadOnly();
16271627
}
1628+
1629+
public static void setWritable(final IFile file) throws CoreException {
1630+
ResourceAttributes attrs = file.getResourceAttributes();
1631+
if (attrs != null) {
1632+
attrs.setReadOnly(false);
1633+
file.setResourceAttributes(attrs);
1634+
}
1635+
}
16281636
}

org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/format/LSPFormatHandler.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
import java.util.ConcurrentModificationException;
1717

1818
import org.eclipse.core.commands.ExecutionEvent;
19+
import org.eclipse.core.resources.IFile;
20+
import org.eclipse.core.runtime.Adapters;
21+
import org.eclipse.core.runtime.CoreException;
1922
import org.eclipse.jdt.annotation.Nullable;
23+
import org.eclipse.jface.dialogs.MessageDialog;
2024
import org.eclipse.jface.text.BadLocationException;
2125
import org.eclipse.jface.text.IDocument;
2226
import org.eclipse.jface.text.ITextSelection;
@@ -29,15 +33,48 @@
2933
import org.eclipse.lsp4e.ui.UI;
3034
import org.eclipse.lsp4j.MessageParams;
3135
import org.eclipse.lsp4j.MessageType;
36+
import org.eclipse.osgi.util.NLS;
37+
import org.eclipse.ui.IEditorInput;
3238
import org.eclipse.ui.handlers.HandlerUtil;
39+
import org.eclipse.ui.part.FileEditorInput;
3340
import org.eclipse.ui.texteditor.ITextEditor;
41+
import org.eclipse.ui.texteditor.ITextEditorExtension;
3442

3543
public class LSPFormatHandler extends LSPDocumentAbstractHandler {
3644

3745
private final LSPFormatter formatter = new LSPFormatter();
3846

3947
@Override
4048
protected void execute(ExecutionEvent event, ITextEditor textEditor) {
49+
// If the editor input is read-only, prompt to make writable
50+
final var editorExt = Adapters.adapt(textEditor, ITextEditorExtension.class);
51+
if (editorExt != null && editorExt.isEditorInputReadOnly()) {
52+
final IEditorInput input = textEditor.getEditorInput();
53+
final IFile file = input instanceof FileEditorInput fileInput ? fileInput.getFile() : null;
54+
if (file == null) {
55+
MessageDialog.openInformation(HandlerUtil.getActiveShell(event),
56+
Messages.LSPFormatHandler_ReadOnlyEditor_title,
57+
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_inputReadonly, input.getToolTipText()));
58+
return;
59+
}
60+
61+
if (!MessageDialog.openQuestion(HandlerUtil.getActiveShell(event),
62+
Messages.LSPFormatHandler_ReadOnlyEditor_title,
63+
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_fileReadonly, file.getLocation().toFile()))) {
64+
return; // user aborted
65+
}
66+
67+
try {
68+
LSPEclipseUtils.setWritable(file);
69+
} catch (final CoreException ex) {
70+
MessageDialog.openError(HandlerUtil.getActiveShell(event),
71+
Messages.LSPFormatHandler_ReadOnlyEditor_title,
72+
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_makingWritableFailed,
73+
file.getLocation().toFile(), ex.getStatus().getMessage()));
74+
return;
75+
}
76+
}
77+
4178
final ISelection selection = HandlerUtil.getCurrentSelection(event);
4279
if (selection instanceof final ITextSelection textSelection && !textSelection.isEmpty()) {
4380

org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/format/LSPonTypeFormattingReconcilingStrategy.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
import org.eclipse.core.commands.operations.IOperationHistory;
2121
import org.eclipse.core.commands.operations.IOperationHistoryListener;
2222
import org.eclipse.core.commands.operations.OperationHistoryEvent;
23+
import org.eclipse.core.resources.IFile;
24+
import org.eclipse.core.runtime.CoreException;
2325
import org.eclipse.core.runtime.IProgressMonitor;
2426
import org.eclipse.core.runtime.IStatus;
2527
import org.eclipse.core.runtime.Status;
2628
import org.eclipse.jdt.annotation.Nullable;
29+
import org.eclipse.jface.dialogs.MessageDialog;
2730
import org.eclipse.jface.preference.IPreferenceStore;
2831
import org.eclipse.jface.text.BadLocationException;
2932
import org.eclipse.jface.text.DocumentEvent;
@@ -43,10 +46,12 @@
4346
import org.eclipse.lsp4e.VersionedEdits;
4447
import org.eclipse.lsp4e.internal.DocumentUtil;
4548
import org.eclipse.lsp4e.ui.FormatterPreferencePage;
49+
import org.eclipse.lsp4e.ui.Messages;
4650
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
4751
import org.eclipse.lsp4j.FormattingOptions;
4852
import org.eclipse.lsp4j.TextDocumentIdentifier;
4953
import org.eclipse.lsp4j.jsonrpc.messages.Either;
54+
import org.eclipse.osgi.util.NLS;
5055
import org.eclipse.swt.custom.StyledText;
5156
import org.eclipse.ui.PlatformUI;
5257
import org.eclipse.ui.progress.UIJob;
@@ -57,7 +62,7 @@ public class LSPonTypeFormattingReconcilingStrategy implements IReconcilingStrat
5762
private final IPropertyChangeListener formattingPrefsListener = (final PropertyChangeEvent event) -> {
5863
final var newValue = event.getNewValue();
5964
if (newValue != null) {
60-
if (event.getProperty() == FormatterPreferencePage.PREF_ON_TYPE_FORMATTING_ENABLED) {
65+
if (FormatterPreferencePage.PREF_ON_TYPE_FORMATTING_ENABLED.equals(event.getProperty())) {
6166
isOnTypeFormattingEnabled = Boolean.parseBoolean(newValue.toString());
6267
}
6368
}
@@ -158,6 +163,34 @@ void doFormatOnType(@Nullable ITextViewer viewer, @Nullable IDocument document,
158163
if (document == null || event == null || event.fText.isEmpty() || viewer == null) {
159164
return;
160165
}
166+
167+
// If the backing resource is read-only, prompt to make it writable
168+
if (LSPEclipseUtils.isReadOnly(document)) {
169+
final var shell = viewer.getTextWidget() != null ? viewer.getTextWidget().getShell() : null;
170+
IFile file = LSPEclipseUtils.getFile(document);
171+
if (file == null) {
172+
if (shell != null) {
173+
final var label = LSPEclipseUtils.toUri(document);
174+
MessageDialog.openInformation(shell, Messages.LSPFormatHandler_ReadOnlyEditor_title,
175+
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_inputReadonly, String.valueOf(label)));
176+
}
177+
return;
178+
}
179+
180+
if (shell != null && !MessageDialog.openQuestion(shell, Messages.LSPFormatHandler_ReadOnlyEditor_title,
181+
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_fileReadonly, file.getLocation().toFile()))) {
182+
return; // user aborted
183+
}
184+
185+
try {
186+
LSPEclipseUtils.setWritable(file);
187+
} catch (final CoreException ex) {
188+
MessageDialog.openError(shell, Messages.LSPFormatHandler_ReadOnlyEditor_title,
189+
NLS.bind(Messages.LSPFormatHandler_ReadOnlyEditor_makingWritableFailed,
190+
file.getLocation().toFile(), ex.getStatus().getMessage()));
191+
return;
192+
}
193+
}
161194
final String[] triggerCharArr = new String[] { "" }; //$NON-NLS-1$
162195
var executor = LanguageServers.forDocument(document)
163196
.withCapability(capabilities -> {

org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/Messages.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public final class Messages extends NLS {
6262
public static String LSPFormatFilesHandler_FormattingSelectedFiles;
6363
public static String LSPFormatHandler_DiscardedFormat;
6464
public static String LSPFormatHandler_DiscardedFormatResponse;
65+
public static String LSPFormatHandler_ReadOnlyEditor_title;
66+
public static String LSPFormatHandler_ReadOnlyEditor_inputReadonly;
67+
public static String LSPFormatHandler_ReadOnlyEditor_fileReadonly;
68+
public static String LSPFormatHandler_ReadOnlyEditor_makingWritableFailed;
6569
public static String LSPSymbolInWorkspaceDialog_DialogLabel;
6670
public static String LSPSymbolInWorkspaceDialog_DialogTitle;
6771
public static String updateCodelensMenu_job;

org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/messages.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ LSPFormatFilesHandler_FormattingFile=Formatting {0}...
5656
LSPFormatFilesHandler_FormattingSelectedFiles=Formatting selected files...
5757
LSPFormatHandler_DiscardedFormat=Discarded Format
5858
LSPFormatHandler_DiscardedFormatResponse=The format response has been discarded because the document has changed since the format request has been sent
59+
LSPFormatHandler_ReadOnlyEditor_title=Read-only File Encountered
60+
LSPFormatHandler_ReadOnlyEditor_inputReadonly=Editor input ''{0}'' is read-only.
61+
LSPFormatHandler_ReadOnlyEditor_fileReadonly=File ''{0}'' is read-only.\n\nDo you wish to make it writable?
62+
LSPFormatHandler_ReadOnlyEditor_makingWritableFailed=Could not make ''{0}'' writable:\n\n{1}
5963
LSPSymbolInWorkspaceDialog_DialogLabel=Select an item to open.
6064
LSPSymbolInWorkspaceDialog_DialogTitle=Open Symbol in Workspace
6165
updateCodelensMenu_job=Update CodeLens menu

0 commit comments

Comments
 (0)