Bug 574195: [SourceEditor] Add support for context specific tab size to
assist proposals / browser information input

  - Add format type FORMAT_HTMLSOURCE_INPUT for browser information
    input
  - Add nullable annotations to DefaultBrowserInformationInput

Change-Id: Id5714c989c97300e80271762ab57d3e34944009d
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ecommons/text/ui/DefaultBrowserInformationInput.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ecommons/text/ui/DefaultBrowserInformationInput.java
index 5bf869c..8f32a1c 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ecommons/text/ui/DefaultBrowserInformationInput.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ecommons/text/ui/DefaultBrowserInformationInput.java
@@ -14,8 +14,8 @@
 
 package org.eclipse.statet.ecommons.text.ui;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
 
 import org.eclipse.jface.internal.text.html.BrowserInformationControlInput;
 import org.eclipse.jface.internal.text.html.HTMLPrinter;
@@ -25,6 +25,8 @@
 import org.eclipse.swt.graphics.FontData;
 
 import org.eclipse.statet.jcommons.lang.Disposable;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
 
 import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
 
@@ -32,16 +34,22 @@
 /**
  * 
  */
+@NonNullByDefault
 public class DefaultBrowserInformationInput extends BrowserInformationControlInput {
 	
 	
-	private static Formatter FORMATTER;
+	public static final int FORMAT_NONE= 0;
+	public static final int FORMAT_TEXT_INPUT= 1;
+	public static final int FORMAT_SOURCE_INPUT= 2;
+	public static final int FORMAT_HTMLBODY_INPUT= 3;
+	public static final int FORMAT_HTMLSOURCE_INPUT= 4;
+	
+	
+	private static @Nullable Formatter FORMATTER;
 	
 	private static class Formatter implements IPropertyChangeListener, Disposable {
 		
-		private static final Pattern TAB_PATTERN= Pattern.compile("\\\t"); //$NON-NLS-1$
-		
-		private String STYLE_SHEET;
+		private String stylesheet= nonNullLateInit();
 		
 		public Formatter() {
 			JFaceResources.getFontRegistry().addListener(this);
@@ -65,124 +73,151 @@
 		private void updateStyleSheet() {
 			String style =
 				// Font definitions
-				"html         { font-family: sans-serif; font-size: 9pt; font-style: normal; font-weight: normal; }\n"+
-				"body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt { font-size: 1em; }\n"+
-				"pre          { font-family: monospace; }\n"+
+				"html        { font-family: sans-serif; font-size: 9pt; font-style: normal; font-weight: normal; }\n" +
+				"body, h1, h2, h3, h4, h5, h6, p, table, td, caption, th, ul, ol, dl, li, dd, dt { font-size: 1em; }\n" +
+				"pre         { font-family: monospace; }\n" +
 				// Margins
-				"html         { margin: 0px; padding: 0px }"+
-				"body         { overflow: auto; margin-top: 0.25em; margin-bottom: 0.5em; margin-left: 0.25em; margin-right: 0.25em; }\n"+
-				"h1           { margin-top: 0.3em; margin-bottom: 0.04em; }\n"+
-				"h2           { margin-top: 2em; margin-bottom: 0.25em; }\n"+
-				"h3           { margin-top: 1.7em; margin-bottom: 0.25em; }\n"+
-				"h4           { margin-top: 2em; margin-bottom: 0.3em; }\n"+
-				"h5           { margin-top: 0px; margin-bottom: 0px; }\n"+
-				"p            { margin-top: 1em; margin-bottom: 1em; }\n"+
-//				"pre          { margin-left: 0.6em; }\n"+
-				"ul           { margin-top: 0px; margin-bottom: 1em; }\n"+
-				"li           { margin-top: 0px; margin-bottom: 0px; }\n"+
-				"li p         { margin-top: 0px; margin-bottom: 0px; }\n"+
-				"ol           { margin-top: 0px; margin-bottom: 1em; }\n"+
-				"dl           { margin-top: 0px; margin-bottom: 1em; }\n"+
-				"dt           { margin-top: 0px; margin-bottom: 0px; font-weight: bold; }\n"+
-				"dd           { margin-top: 0px; margin-bottom: 0px; }\n"+
+				"html        { margin: 0px; padding: 0px }" +
+				"body        { overflow: auto; margin-top: 0.25em; margin-bottom: 0.5em; margin-left: 0.25em; margin-right: 0.25em; }\n" +
+				"h1          { margin-top: 0.3em; margin-bottom: 0.04em; }\n" +
+				"h2          { margin-top: 2em; margin-bottom: 0.25em; }\n" +
+				"h3          { margin-top: 1.7em; margin-bottom: 0.25em; }\n" +
+				"h4          { margin-top: 2em; margin-bottom: 0.3em; }\n" +
+				"h5          { margin-top: 0px; margin-bottom: 0px; }\n" +
+				"p           { margin-top: 1em; margin-bottom: 1em; }\n" +
+//				"pre         { margin-left: 0.6em; }\n" +
+				"ul          { margin-top: 0px; margin-bottom: 1em; }\n" +
+				"li          { margin-top: 0px; margin-bottom: 0px; }\n" +
+				"li p        { margin-top: 0px; margin-bottom: 0px; }\n" +
+				"ol          { margin-top: 0px; margin-bottom: 1em; }\n" +
+				"dl          { margin-top: 0px; margin-bottom: 1em; }\n" +
+				"dt          { margin-top: 0px; margin-bottom: 0px; font-weight: bold; }\n" +
+				"dd          { margin-top: 0px; margin-bottom: 0px; }\n" +
 				// Styles and colors
-				"a:link       { color: #0000FF; }\n"+
-				"a:hover      { color: #000080; }\n"+
-				"a:visited    { text-decoration: underline; }\n"+
-				"h4           { font-style: italic; }\n"+
-				"strong       { font-weight: bold; }\n"+
-				"em           { font-style: italic; }\n"+
-				"var          { font-style: italic; }\n"+
-				"th           { font-weight: bold; }\n";
+				"a:link      { color: #0000FF; }\n" +
+				"a:hover     { color: #000080; }\n" +
+				"a:visited   { text-decoration: underline; }\n" +
+				"h4          { font-style: italic; }\n" +
+				"strong      { font-weight: bold; }\n" +
+				"em          { font-style: italic; }\n" +
+				"var         { font-style: italic; }\n" +
+				"th          { font-weight: bold; }\n";
 			try {
 				final FontData[] fontData= JFaceResources.getFontRegistry().getFontData(JFaceResources.DIALOG_FONT);
 				if (fontData != null && fontData.length > 0) {
-					style= style.replace("9pt", fontData[0].getHeight()+"pt");
-					style= style.replace("sans-serif", "sans-serif, '"+fontData[0].getName()+"'");
+					style= style.replace("9pt", fontData[0].getHeight() + "pt");
+					style= style.replace("sans-serif", "sans-serif, '" + fontData[0].getName() + "'");
 				}
 			}
 			catch (final Throwable e) {
 			}
-			this.STYLE_SHEET= style;
+			this.stylesheet= style;
 		}
 		
-		String format(String content, final int formatting) {
-			final StringBuilder s;
+		String format(String content, final int formatting,
+				final int tabSize) {
+			final String stylesheet= this.stylesheet;
+			
+			final StringBuilder sb;
 			switch (formatting) {
 			case FORMAT_NONE:
 				return content;
 			case FORMAT_TEXT_INPUT:
 				content= HTMLPrinter.convertToHTMLContent(content);
-				s= new StringBuilder(content.length() + 1000);
-				s.append(content);
+				sb= new StringBuilder(content.length() + stylesheet.length() + 500);
+				sb.append(content);
+				break;
+			case FORMAT_HTMLBODY_INPUT:
+				sb= new StringBuilder(content.length() + stylesheet.length() + 500);
+				sb.append(content);
 				break;
 			case FORMAT_SOURCE_INPUT:
 				content= HTMLPrinter.convertToHTMLContent(content);
-				final Matcher matcher= TAB_PATTERN.matcher(content);
-				if (matcher.find()) {
-					content= matcher.replaceAll("    "); //$NON-NLS-1$
+				//$FALL-THROUGH$
+			case FORMAT_HTMLSOURCE_INPUT:
+				sb= new StringBuilder((int)(content.length() * 1.066) + stylesheet.length() + 500);
+				sb.append("<pre>"); //$NON-NLS-1$
+				{	final String spaces= " ".repeat(tabSize); //$NON-NLS-1$
+					int fromIdx= 0;
+					int tabIdx;
+					while ((tabIdx= content.indexOf('\t', fromIdx)) >= 0) {
+						sb.append(content, fromIdx, tabIdx);
+						sb.append(spaces);
+						fromIdx= tabIdx + 1;
+					}
+					sb.append(content, fromIdx, content.length());
 				}
-				s= new StringBuilder(content.length() + 1000);
-				s.append("<pre>"); //$NON-NLS-1$
-				s.append(content);
-				s.append("</pre>"); //$NON-NLS-1$
-				break;
-			case FORMAT_HTMLBODY_INPUT:
-				s= new StringBuilder(content.length() + 1000);
-				s.append(content);
+				sb.append("</pre>"); //$NON-NLS-1$
 				break;
 			default:
 				throw new IllegalArgumentException("Unsupported format"); //$NON-NLS-1$
 			}
 			
-			HTMLPrinter.insertPageProlog(s, 0, this.STYLE_SHEET);
-			HTMLPrinter.addPageEpilog(s);
-			return s.toString();
+			HTMLPrinter.insertPageProlog(sb, 0, this.stylesheet);
+			HTMLPrinter.addPageEpilog(sb);
+			return sb.toString();
 		}
 		
 	}
 	
 	
-	public static final int FORMAT_NONE= 0;
-	public static final int FORMAT_HTMLBODY_INPUT= 1;
-	public static final int FORMAT_TEXT_INPUT= 2;
-	public static final int FORMAT_SOURCE_INPUT= 3;
+	private final String name;
+	private final String html;
 	
 	
-	private final String fName;
-	private final String fHtml;
-	
-	
-	public DefaultBrowserInformationInput(final BrowserInformationControlInput previous, final String name, final String content, final int formatting) {
+	public DefaultBrowserInformationInput(final String name,
+			final String content, final int formatting,
+			final int tabSize,
+			final @Nullable BrowserInformationControlInput previous) {
 		super(previous);
 		
-		this.fName= name;
-		this.fHtml= getFormatter().format(content, formatting);
+		this.name= nonNullAssert(name);
+		this.html= getFormatter().format(nonNullAssert(content), formatting, tabSize);
 	}
 	
+	public DefaultBrowserInformationInput(final String name,
+			final String content, final int formatting,
+			final @Nullable BrowserInformationControlInput previous) {
+		this(name, content, formatting, 4, previous);
+	}
+	
+	public DefaultBrowserInformationInput(final String name,
+			final String content, final int formatting,
+			final int tabSize) {
+		this(name, content, formatting, tabSize, null);
+	}
+	
+	public DefaultBrowserInformationInput(final String name,
+			final String content, final int formatting) {
+		this(name, content, formatting, 4, null);
+	}
+	
+	
 	protected Formatter getFormatter() {
 		synchronized (DefaultBrowserInformationInput.class) {
-			if (FORMATTER == null) {
-				FORMATTER= new Formatter();
+			var formatter= FORMATTER;
+			if (formatter == null) {
+				formatter= new Formatter();
+				FORMATTER= formatter;
 			}
-			return FORMATTER;
+			return formatter;
 		}
 	}
 	
 	
 	@Override
 	public String getInputName() {
-		return this.fName;
+		return this.name;
 	}
 	
 	@Override
 	public Object getInputElement() {
-		return this.fHtml;
+		return this.html;
 	}
 	
 	@Override
 	public String getHtml() {
-		return this.fHtml;
+		return this.html;
 	}
 	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/SourceEditorViewerConfiguration.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/SourceEditorViewerConfiguration.java
index 6aa572f..be973c1 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/SourceEditorViewerConfiguration.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/SourceEditorViewerConfiguration.java
@@ -114,19 +114,14 @@
 				return new BrowserInformationControl(parent, JFaceResources.DIALOG_FONT, false) {
 					
 					@Override
-					public void setInformation(String content) {
+					public void setInformation(final String content) {
 						if (content.startsWith("...<br")) { // spell correction change proposal //$NON-NLS-1$
-							content= content.replace("\\t", "    "); //$NON-NLS-1$ //$NON-NLS-2$
-							final StringBuffer s= new StringBuffer(content.length()+1000);
-							s.append("<pre>"); //$NON-NLS-1$
-							s.append(content);
-							s.append("</pre>"); //$NON-NLS-1$
-							setInput(new DefaultBrowserInformationInput(null, "", s.toString(),  //$NON-NLS-1$
-									DefaultBrowserInformationInput.FORMAT_HTMLBODY_INPUT));
+							setInput(new DefaultBrowserInformationInput("", //$NON-NLS-1$
+									content, DefaultBrowserInformationInput.FORMAT_HTMLSOURCE_INPUT ));
 						}
 						else {
-							setInput(new DefaultBrowserInformationInput(null, "", content, //$NON-NLS-1$
-									DefaultBrowserInformationInput.FORMAT_TEXT_INPUT));
+							setInput(new DefaultBrowserInformationInput("", //$NON-NLS-1$
+									content, DefaultBrowserInformationInput.FORMAT_TEXT_INPUT ));
 						}
 					}
 				};
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/AssistInvocationContext.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/AssistInvocationContext.java
index 3e41612..1436b99 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/AssistInvocationContext.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/AssistInvocationContext.java
@@ -296,4 +296,8 @@
 	}
 	
 	
+	public int getTabSize() {
+		return 4;
+	}
+	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java
index 7b8e37e..c8d4526 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java
@@ -144,16 +144,21 @@
 	}
 	
 	@Override
-	public String getAdditionalProposalInfo() {
+	public @Nullable String getAdditionalProposalInfo() {
 		return this.description;
 	}
 	
 	@Override
-	public Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
-		return new DefaultBrowserInformationInput(null, getDisplayString(), this.description, 
-				DefaultBrowserInformationInput.FORMAT_TEXT_INPUT);
+	public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
+		final var description= this.description;
+		if (description == null) {
+			return null;
+		}
+		return new DefaultBrowserInformationInput(getDisplayString(),
+				description, DefaultBrowserInformationInput.FORMAT_TEXT_INPUT );
 	}
 	
+	
 	@Override
 	public @Nullable IContextInformation getContextInformation() {
 		return null;
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java
index 03839d9..9b40228 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java
@@ -254,9 +254,13 @@
 	}
 	
 	@Override
-	public Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
-		return new DefaultBrowserInformationInput(null, getDisplayString(), this.description, 
-				DefaultBrowserInformationInput.FORMAT_TEXT_INPUT);
+	public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
+		final var description= this.description;
+		if (description == null) {
+			return null;
+		}
+		return new DefaultBrowserInformationInput(getDisplayString(),
+				description, DefaultBrowserInformationInput.FORMAT_TEXT_INPUT );
 	}
 	
 	
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/TemplateProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/TemplateProposal.java
index a12fe92..679cb18 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/TemplateProposal.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/TemplateProposal.java
@@ -214,17 +214,19 @@
 		try {
 			final TemplateContext context= getContext();
 			context.setReadOnly(true);
+			final String preview;
 			if (context instanceof IWorkbenchTemplateContext) {
-				return new DefaultBrowserInformationInput(
-						null, getDisplayString(), ((IWorkbenchTemplateContext) context).evaluateInfo(getTemplate()),
-						DefaultBrowserInformationInput.FORMAT_SOURCE_INPUT );
+				preview= ((IWorkbenchTemplateContext)context).evaluateInfo(getTemplate());
+			}
+			else {
+				final TemplateBuffer templateBuffer= context.evaluate(getTemplate());
+				preview= (templateBuffer != null) ? templateBuffer.toString() : null;
 			}
 			
-			final TemplateBuffer templateBuffer= context.evaluate(getTemplate());
-			if (templateBuffer != null) {
-				return new DefaultBrowserInformationInput(
-						null, getDisplayString(), templateBuffer.toString(),
-						DefaultBrowserInformationInput.FORMAT_SOURCE_INPUT );
+			if (preview != null) {
+				return new DefaultBrowserInformationInput(getDisplayString(),
+						preview, DefaultBrowserInformationInput.FORMAT_SOURCE_INPUT,
+						getInvocationContext().getTabSize() );
 			}
 		}
 		catch (final TemplateException | BadLocationException e) {}