aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanielGavin <danielgavin5@hotmail.com>2020-11-03 13:54:13 +0100
committerDanielGavin <danielgavin5@hotmail.com>2020-11-03 13:54:13 +0100
commit61692cd57e39a89824cd27f1bf7e610df26f25d1 (patch)
tree9801905c2be1ef91295a43186b7b555cfbfdd626
hello there
-rw-r--r--.gitignore1
-rw-r--r--README.txt4
-rw-r--r--build.bat8
-rw-r--r--src/analysis.odin1
-rw-r--r--src/config.odin8
-rw-r--r--src/documents.odin123
-rw-r--r--src/log.odin64
-rw-r--r--src/main.odin75
-rw-r--r--src/position.odin70
-rw-r--r--src/reader.odin64
-rw-r--r--src/requests.odin312
-rw-r--r--src/response.odin68
-rw-r--r--src/types.odin149
-rw-r--r--src/unmarshal.odin129
-rw-r--r--src/uri.odin94
-rw-r--r--src/writer.odin29
-rw-r--r--tests/test_project/src/main.odin10
-rw-r--r--tests/tests.odin432
18 files changed, 1641 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..adb36c8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.exe \ No newline at end of file
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..09cc8bc
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,4 @@
+# ols
+Language server for Odin
+
+
diff --git a/build.bat b/build.bat
new file mode 100644
index 0000000..6dd3267
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,8 @@
+@echo off
+
+odin run tests\ -llvm-api -show-timings -microarch:native -collection:shared=src -out:ols
+
+
+odin build src\ -llvm-api -show-timings -microarch:native -collection:shared=src -out:ols
+
+
diff --git a/src/analysis.odin b/src/analysis.odin
new file mode 100644
index 0000000..85f0393
--- /dev/null
+++ b/src/analysis.odin
@@ -0,0 +1 @@
+package main \ No newline at end of file
diff --git a/src/config.odin b/src/config.odin
new file mode 100644
index 0000000..7557259
--- /dev/null
+++ b/src/config.odin
@@ -0,0 +1,8 @@
+package main
+
+Config :: struct {
+ workspace_folders: [dynamic] WorkspaceFolder,
+ completion_support_md: bool,
+ hover_support_md: bool,
+};
+
diff --git a/src/documents.odin b/src/documents.odin
new file mode 100644
index 0000000..0618f0f
--- /dev/null
+++ b/src/documents.odin
@@ -0,0 +1,123 @@
+package main
+
+import "core:strings"
+import "core:fmt"
+import "core:log"
+
+Package :: struct {
+ documents: [dynamic]^Document,
+};
+
+Document :: struct {
+ uri: string,
+ path: string,
+ text: string,
+ client_owned: bool,
+ lines: [dynamic] int,
+};
+
+DocumentStorage :: struct {
+ documents: map [string] Document,
+};
+
+document_storage: DocumentStorage;
+
+
+document_open :: proc(uri_string: string, text: string) -> Error {
+
+ uri, parsed_ok := parse_uri(uri_string, context.temp_allocator);
+
+ if !parsed_ok {
+ return .ParseError;
+ }
+
+ if document := &document_storage.documents[uri.path]; document != nil {
+
+ //According to the specification you can't call open more than once without closing it.
+ if document.client_owned {
+ log.errorf("Client called open on an already open document: %v ", document.path);
+ return .InvalidRequest;
+ }
+
+ if document.text != "" {
+ delete(document.text);
+ }
+
+ document.client_owned = true;
+ document.text = text;
+
+ if err := document_refresh(document); err != .None {
+ return err;
+ }
+
+ document_refresh(document);
+ }
+
+ else {
+
+ document := Document {
+ uri = uri.full,
+ path = uri.path,
+ text = text,
+ client_owned = true,
+ };
+
+ if err := document_refresh(&document); err != .None {
+ return err;
+ }
+
+ document_storage.documents[uri.path] = document;
+ }
+
+
+
+ //hmm feels like odin needs some ownership semantic
+ delete(uri_string);
+
+ return .None;
+}
+
+document_apply_changes :: proc(uri_string: string, changes: [dynamic] TextDocumentContentChangeEvent) -> Error {
+
+
+
+
+
+
+ return .None;
+}
+
+document_close :: proc(uri_string: string, text: string) -> Error {
+
+ uri, parsed_ok := parse_uri(uri_string, context.temp_allocator);
+
+ if !parsed_ok {
+ return .ParseError;
+ }
+
+ document := &document_storage.documents[uri.path];
+
+ if document == nil || !document.client_owned {
+ log.errorf("Client called close on a document that was never opened: %v ", document.path);
+ return .InvalidRequest;
+ }
+
+ document.client_owned = false;
+
+ if document.text != "" {
+ delete(document.text);
+ }
+
+ document.text = text;
+
+ if err := document_refresh(document); err != .None {
+ return err;
+ }
+
+ return .None;
+}
+
+document_refresh :: proc(document: ^Document) -> Error {
+ return .None;
+}
+
diff --git a/src/log.odin b/src/log.odin
new file mode 100644
index 0000000..fd370c9
--- /dev/null
+++ b/src/log.odin
@@ -0,0 +1,64 @@
+package main
+
+import "core:fmt";
+import "core:strings";
+import "core:os";
+import "core:time";
+import "core:log";
+
+
+Default_Console_Logger_Opts :: log.Options{
+ .Level,
+ .Terminal_Color,
+ .Short_File_Path,
+ .Line,
+ .Procedure,
+} | log.Full_Timestamp_Opts;
+
+
+Lsp_Logger_Data :: struct {
+ writer: ^Writer,
+}
+
+create_lsp_logger :: proc(writer: ^Writer, lowest := log.Level.Debug, opt := Default_Console_Logger_Opts) -> log.Logger {
+ data := new(Lsp_Logger_Data);
+ data.writer = writer;
+ return log.Logger{lsp_logger_proc, data, lowest, opt};
+}
+
+destroy_lsp_logger :: proc(log: ^log.Logger) {
+ free(log.data);
+}
+
+lsp_logger_proc :: proc(logger_data: rawptr, level: log.Level, text: string, options: log.Options, location := #caller_location) {
+ data := cast(^Lsp_Logger_Data)logger_data;
+
+ backing: [1024]byte; //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths.
+ buf := strings.builder_from_slice(backing[:]);
+
+ when time.IS_SUPPORTED {
+ if log.Full_Timestamp_Opts & options != nil {
+ fmt.sbprint(&buf, "[");
+ t := time.now();
+ y, m, d := time.date(t);
+ h, min, s := time.clock(t);
+ if .Date in options { fmt.sbprintf(&buf, "%d-%02d-%02d ", y, m, d); }
+ if .Time in options { fmt.sbprintf(&buf, "%02d:%02d:%02d", h, min, s); }
+ fmt.sbprint(&buf, "] ");
+ }
+ }
+
+ message := fmt.tprintf("%s %s", buf, text);
+
+ notification := Notification {
+ jsonrpc = "2.0",
+ method = "window/logMessage",
+ params = NotificationLoggingParams {
+ type = 1,
+ message = message,
+ }
+ };
+
+ send_notification(notification, data.writer);
+}
+
diff --git a/src/main.odin b/src/main.odin
new file mode 100644
index 0000000..896f275
--- /dev/null
+++ b/src/main.odin
@@ -0,0 +1,75 @@
+package main
+
+import "core:fmt"
+import "core:log"
+import "core:mem"
+import "core:os"
+import "core:strings"
+import "core:slice"
+import "core:strconv"
+import "core:encoding/json"
+
+running: bool;
+
+os_read :: proc(handle: rawptr, data: [] byte) -> (int, int)
+{
+ return os.read(cast(os.Handle)handle, data);
+}
+
+os_write :: proc(handle: rawptr, data: [] byte) -> (int, int)
+{
+ return os.write(cast(os.Handle)handle, data);
+}
+
+//Note(Daniel, Should look into handling errors without crashing from parsing)
+
+run :: proc(reader: ^Reader, writer: ^Writer) {
+
+ config: Config;
+
+ log.info("Starting Odin Language Server");
+
+ running = true;
+
+ for running {
+
+ header, success := read_and_parse_header(reader);
+
+ if(!success) {
+ log.error("Failed to read and parse header");
+ return;
+ }
+
+
+ value: json.Value;
+ value, success = read_and_parse_body(reader, header);
+
+ if(!success) {
+ log.error("Failed to read and parse body");
+ return;
+ }
+
+ success = handle_request(value, &config, writer);
+
+ if(!success) {
+ log.error("Unrecoverable handle request");
+ return;
+ }
+
+ }
+
+}
+
+end :: proc() {
+
+}
+
+
+main :: proc() {
+
+ reader := make_reader(os_read, cast(rawptr)os.stdin);
+ writer := make_writer(os_write, cast(rawptr)os.stdout);
+
+ run(&reader, &writer);
+}
+
diff --git a/src/position.odin b/src/position.odin
new file mode 100644
index 0000000..bcc095f
--- /dev/null
+++ b/src/position.odin
@@ -0,0 +1,70 @@
+package main
+
+import "core:strings"
+import "core:unicode/utf8"
+
+/*
+ This file handles the conversion from utf-16 to utf-8 offsets in the text document
+ */
+
+
+AbsoluteRange :: struct {
+ begin: int,
+ end: int,
+};
+
+get_absolute_range :: proc(range: Range, document_text: string) -> (AbsoluteRange, bool) {
+
+ absolute: AbsoluteRange;
+
+ if len(document_text) >= 2 {
+ return absolute, false;
+ }
+
+ line_count := 0;
+ index := 1;
+ last := document_text[0];
+
+ get_index_at_line(&index, &index, &last, document_text, range.start.line);
+
+
+
+
+ return absolute, true;
+}
+
+
+get_index_at_line :: proc(current_index: ^int, current_line: ^int, last: ^u8, document_text: string, end_line: int) -> bool {
+
+ for ; current_index^ < len(document_text); current_index^ += 1 {
+
+ current := document_text[current_index^];
+
+ if last^ == '\r' {
+ current_line^ += 1;
+
+ if current_line^ == end_line {
+ last^ = current;
+ current_index^ += 1;
+ return true;
+ }
+
+ }
+
+ else if current == '\n' {
+ current_line^ += 1;
+
+ if current_line^ == end_line {
+ last^ = current;
+ current_index^ += 1;
+ return true;
+ }
+
+ }
+
+ last^ = document_text[current_index^];
+ }
+
+ return false;
+
+} \ No newline at end of file
diff --git a/src/reader.odin b/src/reader.odin
new file mode 100644
index 0000000..31019a5
--- /dev/null
+++ b/src/reader.odin
@@ -0,0 +1,64 @@
+package main
+
+import "core:os"
+import "core:mem"
+import "core:strings"
+
+ReaderFn :: proc(rawptr, [] byte) -> (int, int);
+
+Reader :: struct {
+ reader_fn: ReaderFn,
+ reader_context: rawptr,
+};
+
+make_reader :: proc(reader_fn: ReaderFn, reader_context: rawptr) -> Reader {
+ return Reader { reader_context = reader_context, reader_fn = reader_fn };
+}
+
+
+read_u8 :: proc(reader: ^Reader) -> (u8, bool) {
+
+ value : [1] byte;
+
+ read, err := reader.reader_fn(reader.reader_context, value[:]);
+
+ if(err != 0 || read != 1) {
+ return 0, false;
+ }
+
+ return value[0], true;
+}
+
+read_until_delimiter :: proc(reader: ^Reader, delimiter: u8, builder: ^strings.Builder) -> bool {
+
+ for true {
+
+ value, success := read_u8(reader);
+
+ if(!success) {
+ return false;
+ }
+
+ strings.write_byte(builder, value);
+
+ if(value == delimiter) {
+ break;
+ }
+ }
+
+ return true;
+}
+
+read_sized :: proc(reader: ^Reader, data: []u8) -> bool {
+
+ read, err := reader.reader_fn(reader.reader_context, data);
+
+ if(err != 0 || read != len(data)) {
+ return false;
+ }
+
+ return true;
+}
+
+
+
diff --git a/src/requests.odin b/src/requests.odin
new file mode 100644
index 0000000..61f4f6a
--- /dev/null
+++ b/src/requests.odin
@@ -0,0 +1,312 @@
+package main
+
+import "core:fmt"
+import "core:log"
+import "core:mem"
+import "core:os"
+import "core:strings"
+import "core:slice"
+import "core:strconv"
+import "core:encoding/json"
+
+
+Header :: struct {
+ content_length: int,
+ content_type: string,
+};
+
+make_response_message :: proc(id: RequestId, params: ResponseParams) -> ResponseMessage {
+
+ return ResponseMessage {
+ jsonrpc = "2.0",
+ id = id,
+ result = params,
+ };
+
+}
+
+make_response_message_error :: proc(id: RequestId, error: ResponseError) -> ResponseMessageError {
+
+ return ResponseMessageError {
+ jsonrpc = "2.0",
+ id = id,
+ error = error,
+ };
+
+}
+
+read_and_parse_header :: proc(reader: ^Reader) -> (Header, bool) {
+
+ header: Header;
+
+ builder := strings.make_builder(context.temp_allocator);
+
+ found_content_length := false;
+
+ for true {
+
+ strings.reset_builder(&builder);
+
+ if !read_until_delimiter(reader, '\n', &builder) {
+ log.error("Failed to read with delimiter");
+ return header, false;
+ }
+
+ message := strings.to_string(builder);
+
+ if len(message) == 0 || message[len(message)-2] != '\r' {
+ log.error("No carriage return");
+ return header, false;
+ }
+
+ if len(message)==2 {
+ break;
+ }
+
+ index := strings.last_index_byte (message, ':');
+
+ if index == -1 {
+ log.error("Failed to find semicolon");
+ return header, false;
+ }
+
+ header_name := message[0 : index];
+ header_value := message[len(header_name) + 2 : len(message)-1];
+
+ if strings.compare(header_name, "Content-Length") == 0 {
+
+ if len(header_value) == 0 {
+ log.error("Header value has no length");
+ return header, false;
+ }
+
+ value, ok := strconv.parse_int(header_value);
+
+ if !ok {
+ log.error("Failed to parse content length value");
+ return header, false;
+ }
+
+ header.content_length = value;
+
+ found_content_length = true;
+
+ }
+
+ else if strings.compare(header_name, "Content-Type") == 0 {
+ if len(header_value) == 0 {
+ log.error("Header value has no length");
+ return header, false;
+ }
+ }
+
+ }
+
+ return header, found_content_length;
+}
+
+read_and_parse_body :: proc(reader: ^Reader, header: Header) -> (json.Value, bool) {
+
+ value: json.Value;
+
+ data := make([]u8, header.content_length, context.temp_allocator);
+
+ if !read_sized(reader, data) {
+ log.error("Failed to read body");
+ return value, false;
+ }
+
+ err: json.Error;
+
+ value, err = json.parse(data = data, allocator = context.temp_allocator, parse_integers = true);
+
+ if(err != json.Error.None) {
+ log.error("Failed to parse body");
+ return value, false;
+ }
+
+ return value, true;
+}
+
+
+handle_request :: proc(request: json.Value, config: ^Config, writer: ^Writer) -> bool {
+ log.info("Handling request");
+
+ root, ok := request.value.(json.Object);
+
+ if !ok {
+ log.error("No root object");
+ return false;
+ }
+
+ id: RequestId;
+ id_value: json.Value;
+ id_value, ok = root["id"];
+
+ if ok {
+ #partial
+ switch v in id_value.value {
+ case json.String:
+ id = v;
+ case json.Integer:
+ id = v;
+ case:
+ id = 0;
+ }
+ }
+
+ method := root["method"].value.(json.String);
+
+ call_map : map [string] proc(json.Value, RequestId, ^Config, ^Writer) -> Error =
+ {"initialize" = request_initialize,
+ "initialized" = request_initialized,
+ "shutdown" = request_shutdown,
+ "exit" = notification_exit,
+ "textDocument/didOpen" = notification_did_open,
+ "textDocument/didChange" = notification_did_change,
+ "textDocument/didClose" = notification_did_close};
+
+ fn: proc(json.Value, RequestId, ^Config, ^Writer) -> Error;
+ fn, ok = call_map[method];
+
+
+ if !ok {
+ response := make_response_message_error(
+ id = id,
+ error = ResponseError {code = .MethodNotFound, message = ""}
+ );
+
+ send_error(response, writer);
+ }
+
+ else {
+ err := fn(root["params"], id, config, writer);
+
+ if err != .None {
+
+ response := make_response_message_error(
+ id = id,
+ error = ResponseError {code = err, message = ""}
+ );
+
+ send_error(response, writer);
+ }
+ }
+
+ return true;
+}
+
+request_initialize :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+
+ params_object, ok := params.value.(json.Object);
+
+ if !ok {
+ return .ParseError;
+ }
+
+ initialize_params: RequestInitializeParams;
+
+ if unmarshal(params, initialize_params, context.temp_allocator) != .None {
+ return .ParseError;
+ }
+
+ config.workspace_folders = make([dynamic]WorkspaceFolder);
+
+ for s in initialize_params.workspaceFolders {
+ append_elem(&config.workspace_folders, s);
+ }
+
+ for format in initialize_params.capabilities.textDocument.hover.contentFormat {
+ if format == .Markdown {
+ config.hover_support_md = true;
+ }
+ }
+
+ response := make_response_message(
+ params = ResponseInitializeParams {
+ capabilities = ServerCapabilities {
+ textDocumentSync = 2, //incremental
+ },
+ },
+ id = id,
+ );
+
+ send_response(response, writer);
+
+ return .None;
+}
+
+request_initialized :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+ return .None;
+}
+
+request_shutdown :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+
+ response := make_response_message(
+ params = nil,
+ id = id,
+ );
+
+ send_response(response, writer);
+
+ return .None;
+}
+
+notification_exit :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+ running = false;
+ return .None;
+}
+
+notification_did_open :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+
+ params_object, ok := params.value.(json.Object);
+
+ if !ok {
+ return .ParseError;
+ }
+
+ open_params: DidOpenTextDocumentParams;
+
+ if unmarshal(params, open_params, context.allocator) != .None {
+ return .ParseError;
+ }
+
+ return document_open(open_params.textDocument.uri, open_params.textDocument.text);
+}
+
+notification_did_change :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+
+ params_object, ok := params.value.(json.Object);
+
+ if !ok {
+ return .ParseError;
+ }
+
+ change_params: DidChangeTextDocumentParams;
+
+ if unmarshal(params, change_params, context.temp_allocator) != .None {
+ return .ParseError;
+ }
+
+ fmt.println(change_params);
+
+ return .None;
+}
+
+notification_did_close :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error {
+
+ params_object, ok := params.value.(json.Object);
+
+ if !ok {
+ return .ParseError;
+ }
+
+ close_params: DidCloseTextDocumentParams;
+
+ if unmarshal(params, close_params, context.temp_allocator) != .None {
+ return .ParseError;
+ }
+
+ return document_close(close_params.textDocument.uri, close_params.textDocument.text);
+}
+
diff --git a/src/response.odin b/src/response.odin
new file mode 100644
index 0000000..b9249f4
--- /dev/null
+++ b/src/response.odin
@@ -0,0 +1,68 @@
+package main
+
+
+import "core:fmt"
+import "core:encoding/json"
+
+send_notification :: proc(notification: Notification, writer: ^Writer) -> bool {
+
+ data, error := json.marshal(notification, context.temp_allocator);
+
+ header := fmt.tprintf("Content-Length: {}\r\n\r\n", len(data));
+
+ if error != json.Marshal_Error.None {
+ return false;
+ }
+
+ if(!write_sized(writer, transmute([]u8)header)) {
+ return false;
+ }
+
+ if(!write_sized(writer, data)) {
+ return false;
+ }
+
+ return true;
+}
+
+send_response :: proc(response: ResponseMessage, writer: ^Writer) -> bool {
+
+ data, error := json.marshal(response, context.temp_allocator);
+
+ header := fmt.tprintf("Content-Length: {}\r\n\r\n", len(data));
+
+ if error != json.Marshal_Error.None {
+ return false;
+ }
+
+ if(!write_sized(writer, transmute([]u8)header)) {
+ return false;
+ }
+
+ if(!write_sized(writer, data)) {
+ return false;
+ }
+
+ return true;
+}
+
+send_error :: proc(response: ResponseMessageError, writer: ^Writer) -> bool {
+
+ data, error := json.marshal(response, context.temp_allocator);
+
+ header := fmt.tprintf("Content-Length: {}\r\n\r\n", len(data));
+
+ if error != json.Marshal_Error.None {
+ return false;
+ }
+
+ if(!write_sized(writer, transmute([]u8)header)) {
+ return false;
+ }
+
+ if(!write_sized(writer, data)) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/types.odin b/src/types.odin
new file mode 100644
index 0000000..8ef22be
--- /dev/null
+++ b/src/types.odin
@@ -0,0 +1,149 @@
+package main
+
+import "core:encoding/json"
+
+RequestId :: union {
+ string,
+ i64,
+};
+
+ResponseParams :: union {
+ ResponseInitializeParams,
+ rawptr,
+};
+
+ResponseMessage :: struct {
+ jsonrpc: string,
+ id: RequestId,
+ result: ResponseParams,
+};
+
+ResponseMessageError :: struct {
+ jsonrpc: string,
+ id: RequestId,
+ error: ResponseError,
+};
+
+Error :: enum {
+ None = 0,
+
+ // Defined by JSON RPC
+ ParseError = -32700,
+ InvalidRequest = -32600,
+ MethodNotFound = -32601,
+ InvalidParams = -32602,
+ InternalError = -32603,
+ serverErrorStart = -32099,
+ serverErrorEnd = -32000,
+ ServerNotInitialized = -32002,
+ UnknownErrorCode = -32001,
+
+ // Defined by the protocol.
+ RequestCancelled = -32800,
+ ContentModified = -32801,
+};
+
+ResponseError :: struct {
+ code: Error,
+ message: string,
+};
+
+NotificationLoggingParams :: struct {
+ type: int,
+ message: string,
+};
+
+NotificationParams :: union {
+ NotificationLoggingParams,
+};
+
+Notification :: struct {
+ jsonrpc: string,
+ method: string,
+ params: NotificationParams
+};
+
+WorkspaceFolder :: struct {
+ name: string,
+ uri: string,
+};
+
+ResponseInitializeParams :: struct {
+ capabilities: ServerCapabilities,
+};
+
+RequestInitializeParams :: struct {
+ trace: string,
+ workspaceFolders: [dynamic] WorkspaceFolder,
+ capabilities: ClientCapabilities,
+};
+
+//Can't really follow the uppercase style for enums when i need to represent it as text as well
+MarkupKind :: enum {
+ Plaintext,
+ Markdown,
+};
+
+ServerCapabilities :: struct {
+ textDocumentSync: int,
+};
+
+CompletionClientCapabilities :: struct {
+
+};
+
+HoverClientCapabilities :: struct {
+ dynamicRegistration: bool,
+ contentFormat: [dynamic] MarkupKind,
+};
+
+TextDocumentClientCapabilities :: struct {
+ completion: CompletionClientCapabilities,
+ hover: HoverClientCapabilities,
+};
+
+ClientCapabilities :: struct {
+ textDocument: TextDocumentClientCapabilities,
+};
+
+DidOpenTextDocumentParams :: struct {
+ textDocument: TextDocumentItem,
+};
+
+Position :: struct {
+ line: int,
+ character: int,
+};
+
+Range :: struct {
+ start: Position,
+ end: Position,
+};
+
+TextDocumentContentChangeEvent :: struct {
+ range: Range,
+ text: string,
+};
+
+Version :: union {
+ int,
+ json.Null,
+};
+
+VersionedTextDocumentIdentifier :: struct {
+ uri: string,
+};
+
+DidChangeTextDocumentParams :: struct {
+ textDocument: VersionedTextDocumentIdentifier,
+ contentChanges: [dynamic] TextDocumentContentChangeEvent,
+};
+
+DidCloseTextDocumentParams :: struct{
+ textDocument: TextDocumentItem,
+};
+
+TextDocumentItem :: struct {
+ uri: string,
+ text: string,
+}; \ No newline at end of file
diff --git a/src/unmarshal.odin b/src/unmarshal.odin
new file mode 100644
index 0000000..8869e7a
--- /dev/null
+++ b/src/unmarshal.odin
@@ -0,0 +1,129 @@
+package main
+
+import "core:encoding/json"
+import "core:strings"
+import "core:runtime"
+import "core:mem"
+import "core:fmt"
+
+//Note(Daniel, investigate if you can use some sort of attribute not to be forced to have the same variable name as the json name)
+
+unmarshal :: proc(json_value: json.Value, v: any, allocator := context.allocator) -> json.Marshal_Error {
+
+ using runtime;
+
+ if v == nil {
+ return .None;
+ }
+
+ type_info := type_info_base(type_info_of(v.id));
+
+ #partial
+ switch j in json_value.value {
+ case json.Object:
+ #partial switch variant in type_info.variant {
+ case Type_Info_Struct:
+ for field, i in variant.names {
+ a := any{rawptr(uintptr(v.data) + uintptr(variant.offsets[i])), variant.types[i].id};
+ if ret := unmarshal(j[field], a, allocator); ret != .None {
+ return ret;
+ }
+ }
+ }
+ case json.Array:
+ #partial switch variant in type_info.variant {
+ case Type_Info_Dynamic_Array:
+ array := (^mem.Raw_Dynamic_Array)(v.data);
+ if array.data == nil {
+ array.data = mem.alloc(len(j)*variant.elem_size, variant.elem.align, allocator);
+ array.len = len(j);
+ array.cap = len(j);
+ array.allocator = allocator;
+ }
+ else {
+ return .Invalid_Data;
+ }
+
+ for i in 0..<array.len {
+ a := any{rawptr(uintptr(array.data) + uintptr(variant.elem_size * i)), variant.elem.id};
+
+ if ret := unmarshal(j[i], a, allocator); ret != .None {
+ return ret;
+ }
+ }
+
+ case:
+ return .Unsupported_Type;
+ }
+ case json.String:
+ #partial switch variant in type_info.variant {
+ case Type_Info_String:
+ str := (^string)(v.data);
+ str^ = strings.clone(j, allocator);
+
+ case Type_Info_Enum:
+ for name, i in variant.names {
+
+ lower_name := strings.to_lower(name, allocator);
+ lower_j := strings.to_lower(string(j), allocator);
+
+ if lower_name == lower_j {
+ mem.copy(v.data, &variant.values[i], size_of(variant.base));
+ }
+
+ delete(lower_name, allocator);
+ delete(lower_j, allocator);
+ }
+ }
+ case json.Integer:
+ #partial switch variant in &type_info.variant {
+ case Type_Info_Integer:
+ switch type_info.size {
+ case 8:
+ tmp := i64(j);
+ mem.copy(v.data, &tmp, type_info.size);
+
+ case 4:
+ tmp := i32(j);
+ mem.copy(v.data, &tmp, type_info.size);
+
+ case 2:
+ tmp := i16(j);
+ mem.copy(v.data, &tmp, type_info.size);
+
+ case 1:
+ tmp := i8(j);
+ mem.copy(v.data, &tmp, type_info.size);
+ case:
+ return .Invalid_Data;
+ }
+ case Type_Info_Union:
+ tag_ptr := uintptr(v.data) + variant.tag_offset;
+ }
+ case json.Float:
+ if _, ok := type_info.variant.(Type_Info_Float); ok {
+ switch type_info.size {
+ case 8:
+ tmp := f64(j);
+ mem.copy(v.data, &tmp, type_info.size);
+ case 4:
+ tmp := f32(j);
+ mem.copy(v.data, &tmp, type_info.size);
+ case:
+ return .Invalid_Data;
+ }
+
+ }
+ case json.Null:
+ case json.Boolean :
+ if _, ok := type_info.variant.(Type_Info_Boolean); ok {
+ tmp := bool(j);
+ mem.copy(v.data, &tmp, type_info.size);
+ }
+ case:
+ return .Unsupported_Type;
+ }
+
+ return .None;
+}
+
diff --git a/src/uri.odin b/src/uri.odin
new file mode 100644
index 0000000..9660e22
--- /dev/null
+++ b/src/uri.odin
@@ -0,0 +1,94 @@
+package main
+
+import "core:mem"
+import "core:strings"
+import "core:strconv"
+import "core:fmt"
+
+Uri :: struct {
+ full: string,
+ path: string,
+};
+
+//Note(Daniel, This is an extremely incomplete uri parser and for now ignores fragment and query and only handles file schema)
+
+parse_uri :: proc(value: string, allocator := context.allocator) -> (Uri, bool) {
+
+ uri: Uri;
+
+ decoded, ok := decode_percent(value, allocator);
+
+ if !ok {
+ return uri, false;
+ }
+
+ starts := "file:///";
+
+ if !starts_with(decoded, starts) {
+ return uri, false;
+ }
+
+ uri.full = decoded;
+ uri.path = decoded[len(starts):];
+
+ return uri, true;
+}
+
+@(private)
+starts_with :: proc(value: string, starts_with: string) -> bool {
+
+ if len(value) < len(starts_with) {
+ return false;
+ }
+
+ for i := 0; i < len(starts_with); i += 1 {
+
+ if value[i] != starts_with[i] {
+ return false;
+ }
+
+ }
+
+ return true;
+}
+
+
+@(private)
+decode_percent :: proc(value: string, allocator: mem.Allocator) -> (string, bool) {
+
+ builder := strings.make_builder(allocator);
+
+ for i := 0; i < len(value); i += 1 {
+
+ if value[i] == '%' {
+
+ if i+2 < len(value) {
+
+ v, ok := strconv.parse_i64_of_base(value[i+1:i+3], 16);
+
+ if !ok {
+ strings.destroy_builder(&builder);
+ return "", false;
+ }
+
+ strings.write_byte(&builder, cast(byte)v);
+
+ i+= 2;
+ }
+
+ else {
+ strings.destroy_builder(&builder);
+ return "", false;
+ }
+
+ }
+
+ else {
+ strings.write_byte(&builder, value[i]);
+ }
+
+ }
+
+ return strings.to_string(builder), true;
+}
+
diff --git a/src/writer.odin b/src/writer.odin
new file mode 100644
index 0000000..ccb1762
--- /dev/null
+++ b/src/writer.odin
@@ -0,0 +1,29 @@
+package main
+
+import "core:os"
+import "core:mem"
+import "core:fmt"
+import "core:strings"
+
+WriterFn :: proc(rawptr, [] byte) -> (int, int);
+
+Writer :: struct {
+ writer_fn: WriterFn,
+ writer_context: rawptr,
+};
+
+make_writer :: proc(writer_fn: WriterFn, writer_context: rawptr) -> Writer {
+ return Writer { writer_context = writer_context, writer_fn = writer_fn };
+}
+
+write_sized :: proc(writer: ^Writer, data: []byte) -> bool {
+ written, err := writer.writer_fn(writer.writer_context, data);
+
+ if(err != 0 || written != len(data)) {
+ return false;
+ }
+
+ return true;
+}
+
+
diff --git a/tests/test_project/src/main.odin b/tests/test_project/src/main.odin
new file mode 100644
index 0000000..c88ac4e
--- /dev/null
+++ b/tests/test_project/src/main.odin
@@ -0,0 +1,10 @@
+package main
+
+import "core:fmt"
+import "core:log"
+
+
+
+main :: proc() {
+
+} \ No newline at end of file
diff --git a/tests/tests.odin b/tests/tests.odin
new file mode 100644
index 0000000..773eeff
--- /dev/null
+++ b/tests/tests.odin
@@ -0,0 +1,432 @@
+package test
+
+import "core:log"
+import "core:mem"
+import "core:fmt"
+import "core:os"
+import "core:strings"
+
+import src "../src"
+
+
+initialize_request := `
+{ "jsonrpc":"2.0",
+ "id":0,
+ "method":"initialize",
+ "params": {
+ "processId": 39964,
+ "clientInfo": {
+ "name": "vscode",
+ "version": "1.50.1"
+ },
+ "rootPath": "c:\\Users\\danie\\OneDrive\\Desktop\\Computer_Science\\test",
+ "rootUri": "file:///c%3A/Users/danie/OneDrive/Desktop/Computer_Science/test",
+ "capabilities": {
+ "workspace": {
+ "applyEdit": true,
+ "workspaceEdit": {
+ "documentChanges": true,
+ "resourceOperations": [
+ "create",
+ "rename",
+ "delete"
+ ],
+ "failureHandling": "textOnlyTransactional"
+ },
+ "didChangeConfiguration": {
+ "dynamicRegistration": true
+ },
+ "didChangeWatchedFiles": {
+ "dynamicRegistration": true
+ },
+ "symbol": {
+ "dynamicRegistration": true,
+ "symbolKind": {
+ "valueSet": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26
+ ]
+ }
+ },
+ "executeCommand": {
+ "dynamicRegistration": true
+ },
+ "configuration": true,
+ "workspaceFolders": true
+ },
+ "textDocument": {
+ "publishDiagnostics": {
+ "relatedInformation": true,
+ "versionSupport": false,
+ "tagSupport": {
+ "valueSet": [
+ 1,
+ 2
+ ]
+ }
+ },
+ "synchronization": {
+ "dynamicRegistration": true,
+ "willSave": true,
+ "willSaveWaitUntil": true,
+ "didSave": true
+ },
+ "completion": {
+ "dynamicRegistration": true,
+ "contextSupport": true,
+ "completionItem": {
+ "snippetSupport": true,
+ "commitCharactersSupport": true,
+ "documentationFormat": [
+ "markdown",
+ "plaintext"
+ ],
+ "deprecatedSupport": true,
+ "preselectSupport": true,
+ "tagSupport": {
+ "valueSet": [
+ 1
+ ]
+ }
+ },
+ "completionItemKind": {
+ "valueSet": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25
+ ]
+ }
+ },
+ "hover": {
+ "dynamicRegistration": true,
+ "contentFormat": [
+ "markdown",
+ "plaintext"
+ ]
+ },
+ "signatureHelp": {
+ "dynamicRegistration": true,
+ "signatureInformation": {
+ "documentationFormat": [
+ "markdown",
+ "plaintext"
+ ],
+ "parameterInformation": {
+ "labelOffsetSupport": true
+ }
+ },
+ "contextSupport": true
+ },
+ "definition": {
+ "dynamicRegistration": true,
+ "linkSupport": true
+ },
+ "references": {
+ "dynamicRegistration": true
+ },
+ "documentHighlight": {
+ "dynamicRegistration": true
+ },
+ "documentSymbol": {
+ "dynamicRegistration": true,
+ "symbolKind": {
+ "valueSet": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 24,
+ 25,
+ 26
+ ]
+ },
+ "hierarchicalDocumentSymbolSupport": true
+ },
+ "codeAction": {
+ "dynamicRegistration": true,
+ "isPreferredSupport": true,
+ "codeActionLiteralSupport": {
+ "codeActionKind": {
+ "valueSet": [
+ "",
+ "quickfix",
+ "refactor",
+ "refactor.extract",
+ "refactor.inline",
+ "refactor.rewrite",
+ "source",
+ "source.organizeImports"
+ ]
+ }
+ }
+ },
+ "codeLens": {
+ "dynamicRegistration": true
+ },
+ "formatting": {
+ "dynamicRegistration": true
+ },
+ "rangeFormatting": {
+ "dynamicRegistration": true
+ },
+ "onTypeFormatting": {
+ "dynamicRegistration": true
+ },
+ "rename": {
+ "dynamicRegistration": true,
+ "prepareSupport": true
+ },
+ "documentLink": {
+ "dynamicRegistration": true,
+ "tooltipSupport": true
+ },
+ "typeDefinition": {
+ "dynamicRegistration": true,
+ "linkSupport": true
+ },
+ "implementation": {
+ "dynamicRegistration": true,
+ "linkSupport": true
+ },
+ "colorProvider": {
+ "dynamicRegistration": true
+ },
+ "foldingRange": {
+ "dynamicRegistration": true,
+ "rangeLimit": 5000,
+ "lineFoldingOnly": true
+ },
+ "declaration": {
+ "dynamicRegistration": true,
+ "linkSupport": true
+ },
+ "selectionRange": {
+ "dynamicRegistration": true
+ }
+ },
+ "window": {
+ "workDoneProgress": true
+ }
+ },
+ "trace": "verbose",
+ "workspaceFolders": [
+ {
+ "uri": "file:///c%3A/Users/danie/OneDrive/Desktop/Computer_Science/test",
+ "name": "test"
+ }
+ ]
+ }
+}`;
+
+shutdown_request := `{
+"jsonrpc":"2.0",
+"id":0,
+"method":"shutdown"
+}`;
+
+exit_notification := `{
+"jsonrpc":"2.0",
+"id":0,
+"method":"exit"
+}`;
+
+
+TestReadBuffer :: struct {
+ index: int,
+ data: [] byte,
+};
+
+test_read :: proc(handle: rawptr, data: [] byte) -> (int, int)
+{
+ buffer := cast(^TestReadBuffer)handle;
+
+ if len(buffer.data) <= len(data) + buffer.index {
+ dst := data[:];
+ src := buffer.data[buffer.index:len(buffer.data)];
+
+ copy(dst, src);
+
+ buffer.index += len(src);
+ return len(src), 0;
+ }
+
+ else {
+ dst := data[:];
+ src := buffer.data[buffer.index:];
+
+ copy(dst, src);
+
+ buffer.index += len(dst);
+ return len(dst), 0;
+ }
+}
+
+make_request :: proc(request: string) -> string {
+ return fmt.tprintf("Content-Length: %v\r\n\r\n%v", len(request), request);
+}
+
+
+test_init_check_shutdown :: proc() -> bool {
+
+ buffer := TestReadBuffer {
+ data = transmute([]byte) strings.join({make_request(initialize_request), make_request(shutdown_request), make_request(exit_notification)}, "", context.temp_allocator),
+ };
+
+ reader := src.make_reader(test_read, &buffer);
+ writer := src.make_writer(src.os_write, cast(rawptr)os.stdout);
+
+ src.run(&reader, &writer);
+
+ delete(buffer.data);
+
+ return true;
+}
+
+
+test_open_and_change_notification :: proc() -> bool {
+
+ open_notification := `{
+ "jsonrpc":"2.0",
+ "id":0,
+ "method": "textDocument/didOpen",
+ "params": {
+ "textDocument": {
+ "uri": "file:///c%3A/Users/danie/OneDrive/Desktop/Computer_Science/ols/tests/test_project/src/main.odin",
+ "languageId": "odin",
+ "version": 1,
+ "text": "package main\r\n\r\nimport \"core:fmt\"\r\nimport \"core:log\"\r\n\r\n\r\n\r\nmain :: proc() {\r\n\r\n}"
+ }
+ }
+ }`;
+
+ change_notification := `{
+ "jsonrpc":"2.0",
+ "id":0,
+ "method": "textDocument/didChange",
+ "params": {
+ "textDocument": {
+ "uri": "file:///c%3A/Users/danie/OneDrive/Desktop/Computer_Science/ols/tests/test_project/src/main.odin",
+ "version": 2
+ },
+ "contentChanges": [
+ {
+ "range": {
+ "start": {
+ "line": 8,
+ "character": 1
+ },
+ "end": {
+ "line": 8,
+ "character": 1
+ }
+ },
+ "rangeLength": 0,
+ "text": "h"
+ }
+ ]
+ }
+ }`;
+
+ buffer := TestReadBuffer {
+ data = transmute([]byte) strings.join({make_request(initialize_request), make_request(open_notification),
+ make_request(change_notification), make_request(shutdown_request),
+ make_request(exit_notification)}, "", context.temp_allocator),
+ };
+
+ reader := src.make_reader(test_read, &buffer);
+ writer := src.make_writer(src.os_write, cast(rawptr)os.stdout);
+
+ src.run(&reader, &writer);
+
+ delete(buffer.data);
+
+ return true;
+}
+
+
+main :: proc() {
+
+ context.logger = log.create_console_logger();
+
+ test_init_check_shutdown();
+
+ test_open_and_change_notification();
+
+ /*
+
+
+
+ reader := src.make_reader(test_read, &TestReadBuffer, context.temp_allocator);
+
+ header, success := src.read_and_parse_header(&reader);
+
+ log.info(header);
+ */
+
+}
+