Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
scohen committed Feb 2, 2023
0 parents commit a38bd7c
Show file tree
Hide file tree
Showing 157 changed files with 8,976 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{md, markdown, eex}]
trim_trailing_whitespace = false
5 changes: 5 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Used by "mix format"
[
inputs: ["mix.exs", "config/*.exs"],
subdirectories: ["apps/*"]
]
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/
5 changes: 5 additions & 0 deletions .iex.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Node.start(:"[email protected]")
# Node.set_cookie(:lexical)
# Node.connect(:"[email protected]")

project = Lexical.Project.new("file://#{File.cwd!()}/../ex_ls/")
4 changes: 4 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
elixir 1.14.3-otp-25
erlang 25.2.1
nodejs 12.16.3
yarn 1.22.4
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Lexical

**TODO: Add description**

4 changes: 4 additions & 0 deletions apps/common/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions apps/common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
common-*.tar

# Temporary files, for example, from tests.
/tmp/
21 changes: 21 additions & 0 deletions apps/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Common

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `common` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:common, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/common>.

180 changes: 180 additions & 0 deletions apps/common/lib/code_unit.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
defmodule Lexical.CodeUnit do
@moduledoc """
Code unit and offset conversions
The LSP protocol speaks in positions, which defines where something happens in a document.
Positions have a start and an end, which are defined as code unit _offsets_ from the beginning
of a line. this module helps to convert between utf8, which most of the world speaks
natively, and utf16, which has been forced upon us by microsoft.
Converting between offsets and code units is 0(n), and allocations only happen if a
multi-byte character is detected, at which point, only that character is allocated.
This exploits the fact that most source code consists of ascii characters, with at best,
sporadic multi-byte characters in it. Thus, the vast majority of documents will not require
any allocations at all.
"""
@type utf8_code_unit :: non_neg_integer()
@type utf16_code_unit :: non_neg_integer()
@type utf8_offset :: non_neg_integer()
@type utf16_offset :: non_neg_integer()

@type error :: {:error, :misaligned} | {:error, :out_of_bounds}

# public

@doc """
Converts a utf8 character offset into a utf16 character offset. This implementation
clamps the maximum size of an offset so that any initial character position can be
passed in and the offset returned will reflect the end of the line.
"""
@spec utf16_offset(String.t(), utf8_offset()) :: utf16_offset()
def utf16_offset(binary, character_position) do
do_utf16_offset(binary, character_position, 0)
end

@doc """
Converts a utf16 character offset into a utf8 character offset. This implementation
clamps the maximum size of an offset so that any initial character position can be
passed in and the offset returned will reflect the end of the line.
"""
@spec utf8_offset(String.t(), utf16_offset()) :: utf8_offset()
def utf8_offset(binary, character_position) do
do_utf8_offset(binary, character_position, 0)
end

@spec to_utf8(String.t(), utf16_code_unit()) :: {:ok, utf8_code_unit()} | error
def to_utf8(binary, utf16_unit) do
do_to_utf8(binary, utf16_unit, 0)
end

@spec to_utf16(String.t(), utf8_code_unit()) :: {:ok, utf16_code_unit()} | error
def to_utf16(binary, utf16_unit) do
do_to_utf16(binary, utf16_unit, 0)
end

def count(:utf16, binary) do
do_count_utf16(binary, 0)
end

# Private

# UTF-16

def do_count_utf16(<<>>, count) do
count
end

def do_count_utf16(<<c, rest::binary>>, count) when c < 128 do
do_count_utf16(rest, count + 1)
end

def do_count_utf16(<<c::utf8, rest::binary>>, count) do
increment =
<<c::utf16>>
|> byte_size()
|> div(2)

do_count_utf16(rest, count + increment)
end

defp do_utf16_offset(_, 0, offset) do
offset
end

defp do_utf16_offset(<<>>, _, offset) do
# this clause pegs the offset at the end of the string
# no matter the character index
offset
end

defp do_utf16_offset(<<c, rest::binary>>, remaining, offset) when c < 128 do
do_utf16_offset(rest, remaining - 1, offset + 1)
end

defp do_utf16_offset(<<c::utf8, rest::binary>>, remaining, offset) do
s = <<c::utf8>>
increment = utf16_size(s)
do_utf16_offset(rest, remaining - 1, offset + increment)
end

defp do_to_utf16(_, 0, utf16_unit) do
{:ok, utf16_unit}
end

defp do_to_utf16(_, utf8_unit, _) when utf8_unit < 0 do
{:error, :misaligned}
end

defp do_to_utf16(<<>>, _remaining, _utf16_unit) do
{:error, :out_of_bounds}
end

defp do_to_utf16(<<c, rest::binary>>, utf8_unit, utf16_unit) when c < 128 do
do_to_utf16(rest, utf8_unit - 1, utf16_unit + 1)
end

defp do_to_utf16(<<c::utf8, rest::binary>>, utf8_unit, utf16_unit) do
utf8_string = <<c::utf8>>
increment = utf16_size(utf8_string)
decrement = byte_size(utf8_string)

do_to_utf16(rest, utf8_unit - decrement, utf16_unit + increment)
end

defp utf16_size(binary) when is_binary(binary) do
binary
|> :unicode.characters_to_binary(:utf8, :utf16)
|> byte_size()
|> div(2)
end

# UTF-8

defp do_utf8_offset(_, 0, offset) do
offset
end

defp do_utf8_offset(<<>>, _, offset) do
# this clause pegs the offset at the end of the string
# no matter the character index
offset
end

defp do_utf8_offset(<<c, rest::binary>>, remaining, offset) when c < 128 do
do_utf8_offset(rest, remaining - 1, offset + 1)
end

defp do_utf8_offset(<<c::utf8, rest::binary>>, remaining, offset) do
s = <<c::utf8>>
increment = utf8_size(s)
decrement = utf16_size(s)
do_utf8_offset(rest, remaining - decrement, offset + increment)
end

defp do_to_utf8(_, 0, utf8_unit) do
{:ok, utf8_unit}
end

defp do_to_utf8(_, utf_16_units, _) when utf_16_units < 0 do
{:error, :misaligned}
end

defp do_to_utf8(<<>>, _remaining, _utf8_unit) do
{:error, :out_of_bounds}
end

defp do_to_utf8(<<c, rest::binary>>, utf16_unit, utf8_unit) when c < 128 do
do_to_utf8(rest, utf16_unit - 1, utf8_unit + 1)
end

defp do_to_utf8(<<c::utf8, rest::binary>>, utf16_unit, utf8_unit) do
utf8_code_units = byte_size(<<c::utf8>>)
utf16_code_units = utf16_size(<<c::utf8>>)

do_to_utf8(rest, utf16_unit - utf16_code_units, utf8_unit + utf8_code_units)
end

defp utf8_size(binary) when is_binary(binary) do
byte_size(binary)
end
end
18 changes: 18 additions & 0 deletions apps/common/lib/common.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule Common do
@moduledoc """
Documentation for `Common`.
"""

@doc """
Hello world.
## Examples
iex> Common.hello()
:world
"""
def hello do
:world
end
end
3 changes: 3 additions & 0 deletions apps/common/lib/lexical.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Lexical do
@type uri :: String.t()
end
82 changes: 82 additions & 0 deletions apps/common/lib/process_cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
defmodule Lexical.ProcessCache do
@moduledoc """
A simple cache with a timeout that lives in the process dictionary
"""

defmodule Entry do
defstruct [:value, :expiry]

def new(value, timeout_ms) do
expiry_ts = now_ts() + timeout_ms
%__MODULE__{value: value, expiry: expiry_ts}
end

def valid?(%__MODULE__{} = entry) do
now_ts() < entry.expiry
end

defp now_ts do
System.os_time(:millisecond)
end
end

@type key :: term()
@type fetch_result :: {:ok, term()} | :error

@doc """
Retrieves a value from the cache
If the value is not found, the default is returned
"""
@spec get(key()) :: term() | nil
@spec get(key(), term()) :: term() | nil
def get(key, default \\ nil) do
case fetch(key) do
{:ok, val} -> val
:error -> default
end
end

@doc """
Retrieves a value from the cache
If the value is not found, the default is returned
"""
@spec fetch(key()) :: fetch_result()
def fetch(key) do
case Process.get(key, :unset) do
%Entry{} = entry ->
if Entry.valid?(entry) do
{:ok, entry.value}
else
Process.delete(key)
:error
end

:unset ->
:error
end
end

@doc """
Retrieves and optionally sets a value in the cache.
Trans looks up a value in the cache under key. If that value isn't
found, the compute_fn is then executed, and its return value is set
in the cache. The cached value will live in the cache for `timeout`
milliseconds
"""
def trans(key, timeout_ms \\ 5000, compute_fn) do
case fetch(key) do
:error ->
set(key, timeout_ms, compute_fn)

{:ok, result} ->
result
end
end

defp set(key, timeout_ms, compute_fn) do
value = compute_fn.()
Process.put(key, Entry.new(value, timeout_ms))
value
end
end
Loading

0 comments on commit a38bd7c

Please sign in to comment.