Optimizing my syntax highlighter - android

I've decided I have to write my own syntax highlighter. So far it's working but it's realtime (you type, it highlights) and it's slow.
I'll try to explain how it works. Each time the user types something into the EditText it runs the highlighter (via TextWatcher). The highlighter searches through the text until it finds the beginning of a word and then searches until it finds the end of the same word. Once it finds a word it searches through an array of keywords, if it finds a match it sets a spannable at that location. It keeps looping until it reaches the end of the document.
Again, it works so far (just trying out this idea before I continue with this method), but it's so slow. Some times it can take over a second just to go through a few lines. It slows down how fast the text appears in the EditText. - I also set where the highlighter starts after text is entered at the last position where the user typed so it doesnt have to go through the whole doc each time, it helps a little but not much.
Here's the basic of my EditText:
public class CodeView extends EditText {
private int mTxtChangeStart;
String mStructures[] = this.getResources().getStringArray(R.array.structures);
public CodeView(Context context, AttributeSet attrs) {
super(context, attrs);
addTextChangedListener(inputTextWatcher);
...
}
TextWatcher inputTextWatcher = new TextWatcher() {
#Override
public void afterTextChanged(Editable s) {
syntaxHighlight();
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
//Set where we should start highlighting
mTxtChangeStart = start;
}
#Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
};
private void syntaxHighlight() {
//Time how long it takes for debugging
long syntime = System.currentTimeMillis();
Log.d("", "Start Syntax Highlight");
//Get the position where to start searching for words
int strt = mTxtChangeStart;
//Get the editable text
Editable txt = getText();
//Back up the starting position to the nearest space
try {
for(;;) {
if(strt <= 0) break;
char c = txt.charAt(strt);
if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
strt--;
} else {
break;
}
}
} catch (IndexOutOfBoundsException e) {
Log.e("", "Find start position failed: " + e.getMessage());
}
//Just seeing how long this part took
long findStartPosTime = System.currentTimeMillis();
Log.d("", "Find starting position took " + String.valueOf(System.currentTimeMillis() - findStartPosTime) + " milliseconds");
//the 'end of a word' position
int fin = strt;
//Get the total length of the search text
int totalLength = txt.length();
//Start finding words
//This loop is to find the first character of a word
//It loops until the current character isnt a space, tab, linebreak etc.
while(fin < totalLength && strt < totalLength) {
for(;;) {
//Not sure why I added these two lines - not needed here
//fin++;
//if(fin >= totalLength) { break; } //We're at the end of the document
//Check if there is a space at the first character.
try {
for(;;) { //Loop until we find a useable character
char c = txt.charAt(strt);
if (c == ' ' || c == '\t' || c == '\n' || c == '\r'){
strt++; //Go to the next character if there is a space
} else {
break; //Found a character (not a space, tab or linebreak) - break the loop
}
}
}catch(IndexOutOfBoundsException e) {
Log.e("", e.getMessage());
break;
}
//Make sure fin isnt less than strt
if(strt > fin) { fin = strt; }
//Now we search for the end of the word
//Loop until we find a space at the end of a word
try {
for(;;) {
char c = txt.charAt(fin);
if(c != ' ' && c != '\t' && c != '\n' && c != '\r') {
fin++; //Didn't find whitespace here, keep looking
} else {
break; //Now we found whitespace, end of a word
}
}
break;
} catch (IndexOutOfBoundsException e) {
//If this happens it should mean it just reached the end of the document.
Log.e("", "End of doc? : " + e.getMessage());
break;
}
}
Log.d("", "It took " + String.valueOf(System.currentTimeMillis() - findStartPosTime) + " milliseconds to find a word");
//Make sure fin isnt less that start, again
if(strt > fin) { fin = strt; }
//Debug time, how long it took to find a word
long matchTime = System.currentTimeMillis();
//Found a word, see if it matches a word in our string[]
try {
for(String mStruct : mStructures) {
if(String.valueOf(txt.subSequence(strt, fin)).equals(mStruct)) {
//highlight
Spannable s = (Spannable) txt;
s.setSpan(new ForegroundColorSpan(Color.RED), strt, fin, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
//Can someone explain why this is still setting the spannable to the main editable???
//It should be set to txt right???
break;
} else {
/*Spannable s = (Spannable) txt;
s.setSpan(new ForegroundColorSpan(Color.BLACK), strt, fin, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
txt.removeSpan(s);*/
}
}
}catch (IndexOutOfBoundsException e) {
e.printStackTrace();
Log.e("", "word match error: " + e.getMessage());
}
//Finally set strt to fin and start again!
strt = fin;
Log.d("", "match a word time " + String.valueOf(System.currentTimeMillis() - matchTime) + " milliseconds");
}//end main while loop
Log.d("", "Syntax Highlight Finished in " + (System.currentTimeMillis() - syntime) + " milliseconds");
mTextChanged = false;
}
}
"structures" resource (php.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="structures">
<item>if</item>
<item>else</item>
<item>else if</item>
<item>while</item>
<item>do-while</item>
<item>for</item>
<item>foreach</item>
<item>break</item>
<item>continue</item>
<item>switch</item>
<item>declare</item>
<item>return</item>
<item>require</item>
<item>include</item>
<item>require_once</item>
<item>include_once</item>
<item>goto</item>
</string-array>
</resources>
Anyone have any suggestions how to make this search faster? I know I have a lot of loops but I'm not sure how else to do it.
Thanks a lot!

Can you split the string on the delimiters you have there rather than looking at each character? That would speed it up some. (String.split())

Related

Prevent whitespace in a string after one is entered

I made a code where user can't enter first space in a string.
User is allowed to enter white space after min 2 characters.
I need to redefine my method so user enters white space once, and only once after the two or more characters. After that it should be prevented. How do I do that?
case UPDATE_NAME:
if (firstName.getText().toString().startsWith(" "))
firstName.setText(firstName.getText().toString().trim());
if (firstName.getText().toString().contains(" "))
firstName.setText(firstName.getText().toString().replace(" ", " "));
int indexOfSpace = firstName.getText().toString().lastIndexOf(" ");
if (indexOfSpace > 0) {
String beforeSpace = firstName.getText().toString().substring(0, indexOfSpace);
String[] splitted = beforeSpace.split(" ");
if (splitted != null && splitted.length > 0) {
if (splitted[splitted.length - 1].length() < 2)
firstName.setText(firstName.getText().toString().trim());
}
}
Use a regex pattern. I made one that should match your requirements.
\S{2}\S*\s\S*\n
Explanation:
\S{2} two non whitespace
\S* n non whitespace
\s a whitespace
\S* n non whitespace
\n newline (i only added that for regexr, you may not need it)
Alternate way:
Iterate over String.charAt(int), return false if there is a whitespace in the first two chars, count all whitespaces, return false if n > 1.
This method should meet your requirements:
private static boolean isValidFirstName(String firstName) {
if (firstName != null && !firstName.startsWith(" ")) {
int numberOfSpaces = firstName.length() - firstName.replace(" ", "").length();
if (firstName.length() < 2 || numberOfSpaces <= 1) {
return true;
}
}
return false;
}
What you need to do is use a TextWatcher
public class CustomWatcher implements TextWatcher {
private String myText;
private int count = 0;
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
myText= s;
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
#Override
public void afterTextChanged(Editable s) {
//check if there is a space in the first 2 characters, if so, sets the string to the previous before the space
if(s.length() < 3 && s.contains(" "))
s= myText;
//if the length is higher than 2, and the count is higher than 0 (1 space added already), puts the string back if a space is entered
else if(s.contains(" ") && count > 0)
s= myText;
//If none of the above is verified and you enter a space, increase count so the previous if statement can do its job
else if(s.contains(" "))
count++;
}
}
And then, set it to your EditText
mTargetEditText.addTextChangedListener(new CustomWatcher());
You can control your editText(I assume) with a TextWatcher, you would only need to check inside afterTextChanged() if length is <2 and else if the string contains the char " ".

how to check if "textfield1" < "textfield2"

I would like to check if a filled in textfield is greater than an other filled in textfield.
So like:
if (textfield1.getText().toString().equals(""))
Toast.makeText(getActivity(), "textfield one is empty, please fill in a number", Toast.LENGTH_SHORT).show();
i would like something like this:
if (textfield1.getText().toString().less than textfield2.getText().toString())
Toast.makeText(getActivity(), "textfield one is less than textfield two, this is not allowed", Toast.LENGTH_SHORT).show();
i can't find how
I assume there are numbers in your TextViews? Make an Integer from the String and compare those numbers:
Integer input1 = Integer.parseInt(textfield1.getText().toString());
Integer input2 = Integer.parseInt(textfield2.getText().toString());
if (input1 < input2) { }
If it is input length you are talking about use String.length() like so:
if (textfield1.getText().toString().length() < textfield2.getText().toString().length()) {
}
try this
if(textfield1.getText().toString().trim().length() > 0 && textfield2.getText().toString().trim().length() > 0) {
try {
int i1 = Integer.parseInt(textfield1.getText().toString().trim());
int i2 = Integer.parseInt(textfield2.getText().toString().trim());
if(i1 > i2) {
// do needfull here
}
} catch(Exception ex) {
Log.e("tag", ex.getMessage());
// user entered some character which is not number
}
}

runtime TextWatcher control

I have 4 edittext and i would like to implement a TextWatcher with a control value.
Et1Burro.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable value) {
// you can call or do what you want with your EditText here
Dvalue = GetEditValue(value);
double et4tot = 0, et2fibra = 0, et3zucc = 0;
// et1burro + et2fibra = et4tot
// et1burro + et2fibra + et3zucc = 100
try {
et4tot = Double.parseDouble(Et4Tot.getText().toString());
} catch (NumberFormatException e) { e.printStackTrace(); }
try {
et2fibra = Double.parseDouble(Et2Fibra.getText().toString());
} catch (NumberFormatException e) { e.printStackTrace(); }
try {
et3zucc = Double.parseDouble(Et3Zucc.getText().toString());
} catch (NumberFormatException e) { e.printStackTrace(); }
if ((Dvalue < 1) || (Dvalue > 100) || ((Dvalue + et2fibra) != et4tot ) || ((Dvalue + et2fibra + et3zucc) != 100 ))
{
//segnala errore
Et1Burro.setTextColor(getActivity().getBaseContext().getResources().getColor(R.color.Red));
}else
Et1Burro.setTextColor(getActivity().getBaseContext().getResources().getColor(R.color.Black));
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void onTextChanged(CharSequence s, int start, int before, int count) {}
});
i would like to have a red number if the range number is wrong and a black number if is correct.
I think is better to implement a AsynckTask for control the number or not?
for example the 4 edittext value are:
A,B,C,D
the relation for correct value are:
A+B = D
A+B+C = 100
C = 100 - D
correct example value are A=35, B=35, C=30, D=70
but if in teh first edit (A) the user insert the first caracter ex 35 the program respond with RED value because the other value are 0, and when the user compile all the edittext with the value 35,35,30,70 the anly value that are Black is the last.
I hope to be clear...
Because you are updating only one edit text's color
What i would suggest is,
Set default color to RED for all edit texts
Write a function for your "control value" logic. Call this function only when there are values in all edit texts.
Update all the editbox's color to black if the values entered are passing your control logic. Else you have to set it to RED

Android - Strip extra \r\n's in stream?

Updated question: I am trying to connect to a terminal emulator using a library in android, this will connect to a serial device and should show me sent/received data. I should be able to send data over the connection via a text box below the terminal or by typing in the terminal itself and hitting enter on the keyboard in both cases.
When I was sending data via textbox I had to append \n to the data to get to a new line when I pressed the enter key like so:
mEntry = (EditText) findViewById(R.id.term_entry);
mEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView v, int actionId,
KeyEvent event) {
/* Ignore enter-key-up events. */
if (event != null && event.getAction() == KeyEvent.ACTION_UP) {
return false;
}
Editable e = (Editable) v.getText();
String data = e.toString() + "\n";
sendOverSerial(data.getBytes());
TextKeyListener.clear(e);
return true;
}
});
And the write method:
public void write(byte[] bytes, int offset, int count) {
super.write(bytes, offset, count);
if (isRunning()) {
doLocalEcho(bytes);
}
return;
}
When I was hitting enter after typing in the terminal session itself no new line was occurring at all. So I had to test the data for \r and replace it with \r\n:
private void doLocalEcho(byte[] data) {
String str = new String(data);
appendToEmulator(data, 0, data.length);
notifyUpdate();
}
public void write(byte[] bytes, int offset, int count) {
// Count the number of CRs
String str = new String(bytes);
int numCRs = 0;
for (int i = offset; i < offset + count; ++i) {
if (bytes[i] == '\r') {
++numCRs;
}
}
if (numCRs == 0) {
// No CRs -- just send data as-is
super.write(bytes, offset, count);
if (isRunning()) {
doLocalEcho(bytes);
}
return;
}
// Convert CRs into CRLFs
byte[] translated = new byte[count + numCRs];
int j = 0;
for (int i = offset; i < offset + count; ++i) {
if (bytes[i] == '\r') {
translated[j++] = '\r';
translated[j++] = '\n';
} else {
translated[j++] = bytes[i];
}
}
super.write(translated, 0, translated.length);
// If server echo is off, echo the entered characters locally
if (isRunning()) {
doLocalEcho(translated);
}
}
So that worked fine, now when I typed in the terminal session itself and hit enter i got the newline I wanted. However now every time I send data from the text box with with \n there was an extra space between every newline as well as getting the extra newline.
http://i.imgur.com/gtdIH.png
So I thought that when counting the number of carriage returns peek ahead at the next byte, and if it is '\n' don't count it:
for (int i = offset; i < offset + count; ++i) {
if (bytes[i] == '\r' &&
(
(i+1 < offset + count) && // next byte isn't out of index
(bytes[i+1] != '\n')
) // next byte isn't a new line
)
{
++numCRs;
}
}
This fixed the problem of the spaces...but that was rather stupid as I am now back in a circle to the original problem, if I type directly in the terminal there is no new line, as it sees the \r\n and sees the next byte is invalid. What would be the best way to get both working together? I either have these weird extra spaces and all input is fine, or normal spacing and I can't enter text directly from the terminal, only the textbox. I assume it's really easy to fix, I just am scratching my head looking at it.
EDIT: Not fixed, thought I had but then when I enter directly from the terminal enter does not produce a new line. It must be because the \n is ignored after the \r
I have most of this fixed. When counting the number of carriage returns peek ahead at the next byte, and if it is '\n' don't count it:
for (int i = offset; i < offset + count; ++i) {
if (bytes[i] == '\r' &&
(
(i+1 < offset + count) && // next byte isn't out of index
(bytes[i+1] != '\n')
) // next byte isn't a new line
)
{
++numCRs;
}
}
The only problem left now is that I still get the prompt back twice like this:
switch#
switch#

How to use replaceAll() correct

I have a hangman-app which I fetch a random word from Db i created, then I save it to randomedWord and then i make another String for holding randomedWord but replaced with only "_". This hiddenWord is displayed so the user knows how many chars there is.
When a user hits Enter a onlicklistener fires guess() method:
I have following code which initates a local String which has the value of a TextView(userInput). Then if randomedWord contains the guess I want to put in guess into the same position as it is in the randomedWord, but now to hiddenWord and then update the TextView again.
Guess method:
public void guess()
{
String guess = userInput.getText().toString();
if(randomedWord.contains(guess))
{
hiddenWord = hiddenWord.replaceAll(guess, guess);
this.wordHolder.setText(hiddenWord);
} else
{
showImages();
}
}
The problem I think is this line:
hiddenWord = hiddenWord.replaceAll(guess, guess);
because hiddenWord just contains "_" and therefore I can't replace with (guess, guess) where the first is WHAT to be replaced and last is WITHWHAT.
How do I replace the same POSITION as it is in randomedWord with guess into hiddenWord?
I would change your approach slightly. When a user inputs and guess() is called, find all occurences of that guess in randomedWord and then set the characters in hiddenWord to guess. Using StringBuilder and String.indexOf(), it would look something like this. Also, guess will need to be a char:
StringBuilder builder = new StringBuilder(hiddenWord);
int index = word.indexOf(guess);
while (index >= 0) {
builder.setCharAt(index, guess);
index = word.indexOf(guess, index + 1);
}
hiddenWord = builder.toString();
My solution to the problem looks like this:
// INVWORD METHOD WHICH TURNS THE FETCHED WORD INTO A COPY BUT ONLY "_"
public void invWord()
{
hiddenWord = randomedWord;
hiddenWord = hiddenWord.replaceAll(".", "_");
}
// GUESS METHOD WHICH IS CALLED WHEN ENTER-BUTTON ISCLICKED
public void guess() throws IndexOutOfBoundsException
{
char guess = userInput.getText().charAt(0);
StringBuilder builder = new StringBuilder(hiddenWord);
String j = ""+guess;
int index = randomedWord.indexOf(guess);
if (randomedWord.contains(j))
{
while (index >= 0)
{
builder.setCharAt(index, guess);
index = randomedWord.indexOf(guess, index + 1);
hiddenWord = builder.toString().trim();
wordHolder.setText(hiddenWord);
if (hiddenWord.trim().contains(randomedWord))
{
winner();
}
}
}
else
{
showImages();
}
}
Problem is that I would need to add a +" " to the hiddenWord = hiddenWord.replaceAll(".", "_"); to make the word be like _ _ _ _ _ and makes the user see how many ketters the word is. In my solution it will only be long ____, but it works. If I set index*2 at builder.setCharAt(index, guess); the formatting will be good but then winner(); is never run, I guess because the " " makes the hiddenWord no longer = ranomedWord.
How can I solve this?
Got it all solved! The feeling when everything works smoothly.... :)
Heres the code:
// Set listener to enter-button and do guess when textfield is not empty and toast that input need if it is
enterLetterButton.setOnClickListener(new OnClickListener(){
public void onClick(View arg0) {
if (!(userInput.getText().toString().isEmpty()) )
{
guess();
} else if (userInput.getText().toString().isEmpty())
{
Toast toast = Toast.makeText(getApplicationContext(), "You need to insert a letter", Toast.LENGTH_SHORT);
toast.show();
}
}
});
// invWord() method. Converts the randomedWord to a string of "_".
public void invWord()
{
hiddenWord = randomedWord;
hiddenWord = hiddenWord.replaceAll(".", "_" +" ");
}
//guess() method. Puts the guess(char) at the index fetched and if the hiddenWord contains no more "_" winner() is called
public void guess() throws IndexOutOfBoundsException
{
char guess = userInput.getText().charAt(0);
StringBuilder builder = new StringBuilder(hiddenWord);
String j = ""+guess;
int index = randomedWord.indexOf(guess);
if (randomedWord.contains(j))
{
while (index >= 0)
{
builder.setCharAt(index*2, guess);
index = randomedWord.indexOf(guess, index + 1);
hiddenWord = builder.toString().trim();
wordHolder.setText(hiddenWord);
if (!(hiddenWord.toString().contains("_".toString())) )
{
winner();
}
}
}
else
{
showImages();
}
}

Categories

Resources