Skip to content

Commit 6b4ed0d

Browse files
committed
EmulatorView: fix linkification on non-basic lines
Linkification currently assumes that each char in the array representing the line maps one-to-one onto a screen column. However, this is demonstrably false in a Unicode environment, where one screen column can take an arbitrary number of chars (surrogate pairs and/or combining characters) and one char can take two screen columns (East Asian wide characters in the BMP). As a result, links end up misplaced on lines where these more advanced Unicode features are in use. To fix this, we need to properly determine the screen columns the link spans, taking into account the above; unfortunately, there appears to be no way short of iterating over the entire line up to that point to discover this. To lessen the performance hit, we add support to UnicodeTranscript and TranscriptScreen to allow EmulatorView to determine if scanning the entire line is necessary and only scan the line if we must.
1 parent b5782f6 commit 6b4ed0d

File tree

3 files changed

+106
-15
lines changed

3 files changed

+106
-15
lines changed

libraries/emulatorview/src/jackpal/androidterm/emulatorview/EmulatorView.java

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -266,31 +266,59 @@ private boolean startsWith(CharSequence s, int start, int end,
266266
private int createLinks(int row)
267267
{
268268
TranscriptScreen transcriptScreen = mEmulator.getScreen();
269-
char [] result = transcriptScreen.getScriptLine(row);
269+
char [] line = transcriptScreen.getScriptLine(row);
270270
int lineCount = 1;
271271

272272
//Nothing to do if there's no text.
273-
if(result == null)
273+
if(line == null)
274274
return lineCount;
275275

276-
SpannableStringBuilder textToLinkify = new SpannableStringBuilder(new String(result));
276+
/* If this is not a basic line, the array returned from getScriptLine()
277+
* could have arbitrary garbage at the end -- find the point at which
278+
* the line ends and only include that in the text to linkify.
279+
*
280+
* XXX: The fact that the array returned from getScriptLine() on a
281+
* basic line contains no garbage is an implementation detail -- the
282+
* documented behavior explicitly allows garbage at the end! */
283+
int lineLen;
284+
boolean textIsBasic = transcriptScreen.isBasicLine(row);
285+
if (textIsBasic) {
286+
lineLen = line.length;
287+
} else {
288+
// The end of the valid data is marked by a NUL character
289+
for (lineLen = 0; line[lineLen] != 0; ++lineLen);
290+
}
291+
292+
SpannableStringBuilder textToLinkify = new SpannableStringBuilder(new String(line, 0, lineLen));
277293

278294
boolean lineWrap = transcriptScreen.getScriptLineWrap(row);
279295

280296
//While the current line has a wrap
281297
while (lineWrap)
282298
{
283299
//Get next line
284-
result = transcriptScreen.getScriptLine(row + lineCount);
300+
int nextRow = row + lineCount;
301+
line = transcriptScreen.getScriptLine(nextRow);
285302

286303
//If next line is blank, don't try and append
287-
if(result == null)
304+
if(line == null)
288305
break;
289306

290-
textToLinkify.append(new String(result));
307+
boolean lineIsBasic = transcriptScreen.isBasicLine(nextRow);
308+
if (textIsBasic && !lineIsBasic) {
309+
textIsBasic = lineIsBasic;
310+
}
311+
if (lineIsBasic) {
312+
lineLen = line.length;
313+
} else {
314+
// The end of the valid data is marked by a NUL character
315+
for (lineLen = 0; line[lineLen] != 0; ++lineLen);
316+
}
317+
318+
textToLinkify.append(new String(line, 0, lineLen));
291319

292320
//Check if line after next is wrapped
293-
lineWrap = transcriptScreen.getScriptLineWrap(row + lineCount);
321+
lineWrap = transcriptScreen.getScriptLineWrap(nextRow);
294322
++lineCount;
295323
}
296324

@@ -299,14 +327,16 @@ private int createLinks(int row)
299327
URLSpan [] urls = textToLinkify.getSpans(0, textToLinkify.length(), URLSpan.class);
300328
if(urls.length > 0)
301329
{
330+
int columns = mColumns;
331+
302332
//re-index row to 0 if it is negative
303333
int screenRow = row - mTopRow;
304334

305335
//Create and initialize set of links
306336
URLSpan [][] linkRows = new URLSpan[lineCount][];
307337
for(int i=0; i<lineCount; ++i)
308338
{
309-
linkRows[i] = new URLSpan[mColumns];
339+
linkRows[i] = new URLSpan[columns];
310340
Arrays.fill(linkRows[i], null);
311341
}
312342

@@ -317,11 +347,52 @@ private int createLinks(int row)
317347
int spanStart = textToLinkify.getSpanStart(url);
318348
int spanEnd = textToLinkify.getSpanEnd(url);
319349

320-
//Build accurate indices for multi-line links
321-
int startRow = spanStart / mColumns;
322-
int startCol = spanStart % mColumns;
323-
int endRow = spanEnd / mColumns;
324-
int endCol = spanEnd % mColumns;
350+
// Build accurate indices for links
351+
int startRow;
352+
int startCol;
353+
int endRow;
354+
int endCol;
355+
if (textIsBasic) {
356+
// Basic line -- can assume one char per column
357+
startRow = spanStart / mColumns;
358+
startCol = spanStart % mColumns;
359+
endRow = spanEnd / mColumns;
360+
endCol = spanEnd % mColumns;
361+
} else {
362+
/* Iterate over the line to get starting and ending columns
363+
* for this span */
364+
startRow = 0;
365+
startCol = 0;
366+
for (int i = 0; i < spanStart; ++i) {
367+
char c = textToLinkify.charAt(i);
368+
if (Character.isHighSurrogate(c)) {
369+
++i;
370+
startCol += UnicodeTranscript.charWidth(c, textToLinkify.charAt(i));
371+
} else {
372+
startCol += UnicodeTranscript.charWidth(c);
373+
}
374+
if (startCol >= columns) {
375+
++startRow;
376+
startCol %= columns;
377+
}
378+
}
379+
380+
endRow = startRow;
381+
endCol = startCol;
382+
for (int i = spanStart; i < spanEnd; ++i) {
383+
char c = textToLinkify.charAt(i);
384+
if (Character.isHighSurrogate(c)) {
385+
++i;
386+
endCol += UnicodeTranscript.charWidth(c, textToLinkify.charAt(i));
387+
} else {
388+
endCol += UnicodeTranscript.charWidth(c);
389+
}
390+
if (endCol >= columns) {
391+
++endRow;
392+
endCol %= columns;
393+
}
394+
}
395+
}
325396

326397
//Fill linkRows with the URL where appropriate
327398
for(int i=startRow; i <= endRow; ++i)

libraries/emulatorview/src/jackpal/androidterm/emulatorview/TranscriptScreen.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ public void resize(int columns, int rows, int style) {
433433
* @param row The row index to be queried
434434
* @return The line of text at this row index
435435
*/
436-
public char[] getScriptLine(int row)
436+
char[] getScriptLine(int row)
437437
{
438438
try
439439
{
@@ -454,8 +454,20 @@ public char[] getScriptLine(int row)
454454
* @param row The row to check for line-wrap status
455455
* @return The line wrap status of the row provided
456456
*/
457-
public boolean getScriptLineWrap(int row)
457+
boolean getScriptLineWrap(int row)
458458
{
459459
return mData.getLineWrap(row);
460460
}
461+
462+
/**
463+
* Get whether the line at this index is "basic" (contains only BMP
464+
* characters of width 1).
465+
*/
466+
boolean isBasicLine(int row) {
467+
if (mData != null) {
468+
return mData.isBasicLine(row);
469+
} else {
470+
return true;
471+
}
472+
}
461473
}

libraries/emulatorview/src/jackpal/androidterm/emulatorview/UnicodeTranscript.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,14 @@ public StyleRow getLineColor(int row) {
650650
return getLineColor(row, 0, mColumns);
651651
}
652652

653+
boolean isBasicLine(int row) {
654+
if (row < -mActiveTranscriptRows || row > mScreenRows-1) {
655+
throw new IllegalArgumentException();
656+
}
657+
658+
return (mLines[externalToInternalRow(row)] instanceof char[]);
659+
}
660+
653661
public boolean getChar(int row, int column) {
654662
return getChar(row, column, 0);
655663
}

0 commit comments

Comments
 (0)