Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"require": {
"php": ">=8.1",
"ext-mbstring": "*",
"league/uri": "^7.6",
"league/uri-interfaces": "^7.6"
},
Expand Down
2 changes: 1 addition & 1 deletion src/SimpleSourceLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class SimpleSourceLocation extends SourceLocationMixin
/**
* Creates a new location indicating $offset within $sourceUrl.
*
* $line and $column default to assuming the source is a single line. This
* $line and $column default to assuming the source is a single ASCII line. This
* means that $line defaults to 0 and $column defaults to $offset.
*/
public function __construct(
Expand Down
17 changes: 15 additions & 2 deletions src/SourceFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,15 @@ private function binarySearch(int $offset): int

/**
* The 0-based column of that offset.
*
* Unlike offsets (which are byte-offsets), columns are computed based on Unicode
* codepoints to provide a better experience.
*/
public function getColumn(int $offset): int
{
$line = $this->getLine($offset);

return $offset - $this->lineStarts[$line];
return mb_strlen(substr($this->string, $this->lineStarts[$line], $offset - $this->lineStarts[$line]), 'UTF-8');
}

/**
Expand All @@ -237,7 +240,17 @@ public function getOffset(int $line, int $column = 0): int
throw new \OutOfRangeException('Column may not be negative.');
}

$result = $this->lineStarts[$line] + $column;
if ($column === 0) {
$result = $this->lineStarts[$line];
} else {
$lineContent = substr($this->string, $this->lineStarts[$line], $this->lineStarts[$line + 1] ?? null);

if ($column > mb_strlen($lineContent, 'UTF-8')) {
throw new \OutOfRangeException("Line $line doesn't have $column columns.");
}

$result = $this->lineStarts[$line] + \strlen(mb_substr($lineContent, 0, $column, 'UTF-8'));
}

if ($result > \strlen($this->string) || ($line + 1 < \count($this->lineStarts) && $result >= $this->lineStarts[$line + 1])) {
throw new \OutOfRangeException("Line $line doesn't have $column columns.");
Expand Down
20 changes: 20 additions & 0 deletions tests/SourceFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,24 @@ public function testGetTextEndDefaultsToTheEndOfTheFile(): void
{
self::assertEquals("g boom\nzip zap zop", $this->file->getText(20));
}

public function testGetColumnCountsUnicodeCharacters(): void
{
$file = SourceFile::fromString("foo\nbar éà\nbaz");

self::assertEquals(4, $file->getColumn(8));
self::assertEquals(5, $file->getColumn(10));
self::assertEquals(6, $file->getColumn(12));
self::assertEquals(0, $file->getColumn(13));
}

public function testGetOffsetCountsUnicodeCharactersForColumns(): void
{
$file = SourceFile::fromString("foo\nbar éà\nbaz");

self::assertEquals(8, $file->getOffset(1, 4));
self::assertEquals(10, $file->getOffset(1, 5));
self::assertEquals(12, $file->getOffset(1, 6));
self::assertEquals(13, $file->getOffset(2, 0));
}
}