Bug 578413 - improve ConvertLineDelemiter performance

Avoid UI events for every line replaced. Instead construct a replacement
for the whole document content.

Because this is instantaneous there is no progress report within each
file anymore.

+ improve error msg if encoding is wrong to see the affected file upon
multi file convert.

Change-Id: I889e94687815658da46db1592bf2a5b73840b85f
Signed-off-by: Joerg Kubitz <jkubitz-eclipse@gmx.de>
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.text/+/190155
Tested-by: Platform Bot <platform-bot@eclipse.org>
diff --git a/org.eclipse.core.filebuffers.tests/META-INF/MANIFEST.MF b/org.eclipse.core.filebuffers.tests/META-INF/MANIFEST.MF
index b7f97e6..e519001 100644
--- a/org.eclipse.core.filebuffers.tests/META-INF/MANIFEST.MF
+++ b/org.eclipse.core.filebuffers.tests/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Plugin.name
 Bundle-SymbolicName: org.eclipse.core.filebuffers.tests;singleton:=true
-Bundle-Version: 3.12.100.qualifier
+Bundle-Version: 3.12.200.qualifier
 Bundle-Activator: org.eclipse.core.filebuffers.tests.FileBuffersTestPlugin
 Bundle-ActivationPolicy: lazy
 Bundle-Vendor: %Plugin.providerName
diff --git a/org.eclipse.core.filebuffers.tests/pom.xml b/org.eclipse.core.filebuffers.tests/pom.xml
index 35ffef0..6c552e2 100644
--- a/org.eclipse.core.filebuffers.tests/pom.xml
+++ b/org.eclipse.core.filebuffers.tests/pom.xml
@@ -18,7 +18,7 @@
     <relativePath>../tests-pom/</relativePath>
   </parent>
   <artifactId>org.eclipse.core.filebuffers.tests</artifactId>
-  <version>3.12.100-SNAPSHOT</version>
+  <version>3.12.200-SNAPSHOT</version>
   <packaging>eclipse-test-plugin</packaging>
   <properties>
   	<testSuite>${project.artifactId}</testSuite>
diff --git a/org.eclipse.core.filebuffers.tests/src/org/eclipse/core/filebuffers/tests/ConvertLineDelemiterTest.java b/org.eclipse.core.filebuffers.tests/src/org/eclipse/core/filebuffers/tests/ConvertLineDelemiterTest.java
new file mode 100644
index 0000000..c3cad09
--- /dev/null
+++ b/org.eclipse.core.filebuffers.tests/src/org/eclipse/core/filebuffers/tests/ConvertLineDelemiterTest.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Joerg Kubitz and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Joerg Kubitz - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.filebuffers.tests;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.function.Function;
+
+import org.junit.Test;
+
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.core.filebuffers.FileBuffers;
+import org.eclipse.core.filebuffers.manipulation.ConvertLineDelimitersOperation;
+import org.eclipse.core.filebuffers.manipulation.FileBufferOperationRunner;
+
+public class ConvertLineDelemiterTest {
+
+	private static final String[] DELIMS= new String[] {
+			"\r",
+			"\n",
+			"\r\n",
+	};
+
+	@Test
+	public void testWithDelimnAtEnd() throws Exception {
+		test(delim -> delim + "line1" + delim + "line2" + delim + delim + "line3" + delim);
+	}
+
+	@Test
+	public void testWithoutDelimnAtEnd() throws Exception {
+		test(delim -> "line1" + delim + "line2" + delim + delim + "line3");
+	}
+
+	void test(Function<String, String> testFile) throws Exception {
+		IProject p= ResourcesPlugin.getWorkspace().getRoot().getProject("ConvertLineDelemiterTest");
+		p.create(null);
+		p.open(null);
+		try {
+			for (String outputDelim : DELIMS) {
+				IFile[] files= new IFile[DELIMS.length];
+				int i= 0;
+				for (String inputDelim : DELIMS) {
+					String input= testFile.apply(inputDelim);
+					IFile file= ResourcesPlugin.getWorkspace().getRoot().getFile(new Path("/ConvertLineDelemiterTest/test" + i + ".txt"));
+					InputStream s= new ByteArrayInputStream(input.getBytes());
+					file.create(s, true, null);
+					files[i++]= file;
+				}
+				FileBufferOperationRunner runner= new FileBufferOperationRunner(FileBuffers.getTextFileBufferManager(), null);
+				ConvertLineDelimitersOperation op= new ConvertLineDelimitersOperation(outputDelim);
+				runner.execute(Arrays.stream(files).map(f -> f.getFullPath()).toArray(IPath[]::new), op, null);
+				for (IFile file : files) {
+					String actual= Files.readString(file.getLocation().toFile().toPath());
+					String expected= testFile.apply(outputDelim);
+					assertEquals(readable(expected), readable(actual));
+				}
+				for (IFile file : files) {
+					file.delete(true, null);
+				}
+			}
+		} finally {
+			p.delete(true, null);
+		}
+	}
+
+	private String readable(String s) {
+		s= s.replace("\r", "\\r");
+		s= s.replace("\n", "\\n");
+		return s;
+	}
+}
diff --git a/org.eclipse.core.filebuffers/META-INF/MANIFEST.MF b/org.eclipse.core.filebuffers/META-INF/MANIFEST.MF
index c2f2287..a288957 100644
--- a/org.eclipse.core.filebuffers/META-INF/MANIFEST.MF
+++ b/org.eclipse.core.filebuffers/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.core.filebuffers; singleton:=true
-Bundle-Version: 3.7.100.qualifier
+Bundle-Version: 3.7.200.qualifier
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Export-Package: 
diff --git a/org.eclipse.core.filebuffers/src/org/eclipse/core/filebuffers/manipulation/ConvertLineDelimitersOperation.java b/org.eclipse.core.filebuffers/src/org/eclipse/core/filebuffers/manipulation/ConvertLineDelimitersOperation.java
index 32779c3..54ba2d9 100644
--- a/org.eclipse.core.filebuffers/src/org/eclipse/core/filebuffers/manipulation/ConvertLineDelimitersOperation.java
+++ b/org.eclipse.core.filebuffers/src/org/eclipse/core/filebuffers/manipulation/ConvertLineDelimitersOperation.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2008 IBM Corporation and others.
+ * Copyright (c) 2000, 2022 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -10,16 +10,18 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Joerg Kubitz    - rewrite implementation
  *******************************************************************************/
 package org.eclipse.core.filebuffers.manipulation;
 
+import java.util.Objects;
+
 import org.eclipse.core.internal.filebuffers.FileBuffersPlugin;
 
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
-import org.eclipse.core.runtime.SubMonitor;
 
 import org.eclipse.core.filebuffers.IFileBufferStatusCodes;
 import org.eclipse.core.filebuffers.ITextFileBuffer;
@@ -29,11 +31,9 @@
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.DocumentRewriteSessionType;
 import org.eclipse.jface.text.IDocument;
-import org.eclipse.jface.text.IRegion;
 
 /**
- * A text file buffer operation that changes the line delimiters to a specified
- * line delimiter.
+ * A text file buffer operation that changes the line delimiters to a specified line delimiter.
  *
  * @since 3.1
  */
@@ -42,8 +42,7 @@
 	private String fLineDelimiter;
 
 	/**
-	 * Creates a new line delimiter conversion operation for the given target
-	 * delimiter.
+	 * Creates a new line delimiter conversion operation for the given target delimiter.
 	 *
 	 * @param lineDelimiter the target line delimiter
 	 */
@@ -56,24 +55,24 @@
 	protected MultiTextEditWithProgress computeTextEdit(ITextFileBuffer fileBuffer, IProgressMonitor progressMonitor) throws CoreException {
 		IDocument document= fileBuffer.getDocument();
 		int lineCount= document.getNumberOfLines();
-
-		SubMonitor subMonitor= SubMonitor.convert(progressMonitor, FileBuffersMessages.ConvertLineDelimitersOperation_task_generatingChanges, lineCount);
 		try {
-
-			MultiTextEditWithProgress multiEdit= new MultiTextEditWithProgress(FileBuffersMessages.ConvertLineDelimitersOperation_task_applyingChanges);
-
+			String original= document.get();
+			int newLengthGuess= original.length() + (fLineDelimiter.length() - 1) * lineCount;
+			StringBuilder sb= new StringBuilder(newLengthGuess);
 			for (int i= 0; i < lineCount; i++) {
-				final String delimiter= document.getLineDelimiter(i);
-				if (delimiter != null && !delimiter.isEmpty() && !delimiter.equals(fLineDelimiter)) {
-					IRegion region= document.getLineInformation(i);
-					multiEdit.addChild(new ReplaceEdit(region.getOffset() + region.getLength(), delimiter.length(), fLineDelimiter));
+				int offset= document.getLineOffset(i);
+				int length= document.getLineLength(i);
+				String delim= document.getLineDelimiter(i);
+				sb.append(original, offset, offset + length - (delim == null ? 0 : delim.length()));
+				if (delim != null) {
+					sb.append(fLineDelimiter);
 				}
-				subMonitor.split(1);
 			}
-
-			return multiEdit.getChildrenSize() <= 0 ? null : multiEdit;
-
-		} catch (BadLocationException x) {
+			String replaced= sb.toString();
+			MultiTextEditWithProgress multiEdit= new MultiTextEditWithProgress(FileBuffersMessages.ConvertLineDelimitersOperation_task_applyingChanges);
+			multiEdit.addChild(new ReplaceEdit(0, document.getLength(), replaced));
+			return Objects.equals(replaced, original) ? null : multiEdit;
+		} catch (BadLocationException | IndexOutOfBoundsException x) {
 			throw new CoreException(new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IFileBufferStatusCodes.CONTENT_CHANGE_FAILED, "", x)); //$NON-NLS-1$
 		}
 	}
diff --git a/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/FileBuffersMessages.properties b/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/FileBuffersMessages.properties
index 8e17fda..8acfe38 100644
--- a/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/FileBuffersMessages.properties
+++ b/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/FileBuffersMessages.properties
@@ -28,7 +28,7 @@
 
 ResourceTextFileBuffer_error_illegal_encoding_message_arg= Character encoding "{0}" is not a legal character encoding.
 ResourceTextFileBuffer_error_unsupported_encoding_message_arg= Character encoding "{0}" is not supported by this platform.
-ResourceTextFileBuffer_error_charset_mapping_failed_message_arg=Some characters cannot be mapped using "{0}" character encoding.\nEither change the encoding or remove the characters which are not supported by the "{0}" character encoding.
+ResourceTextFileBuffer_error_charset_mapping_failed_message_arg=Some characters cannot be mapped using "{0}" character encoding for file "{1}".\nEither change the encoding or remove the characters which are not supported by the "{0}" character encoding.
 ResourceTextFileBuffer_task_saving= Saving
 ResourceTextFileBuffer_oom_on_file_read=OutOfMemoryError occurred while reading file "{0}".
 
diff --git a/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/ResourceTextFileBuffer.java b/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/ResourceTextFileBuffer.java
index 8b903e4..fc287de 100644
--- a/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/ResourceTextFileBuffer.java
+++ b/org.eclipse.core.filebuffers/src/org/eclipse/core/internal/filebuffers/ResourceTextFileBuffer.java
@@ -334,7 +334,7 @@
 			stream= new ByteArrayInputStream(bytes, 0, byteBuffer.limit());
 		} catch (CharacterCodingException ex) {
 			Assert.isTrue(ex instanceof UnmappableCharacterException);
-			String message= NLSUtility.format(FileBuffersMessages.ResourceTextFileBuffer_error_charset_mapping_failed_message_arg, encoding);
+			String message= NLSUtility.format(FileBuffersMessages.ResourceTextFileBuffer_error_charset_mapping_failed_message_arg, new Object[] {encoding,getLocation().toString()});
 			IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IFileBufferStatusCodes.CHARSET_MAPPING_FAILED, message, ex);
 			throw new CoreException(s);
 		}