diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.txt | 4 | ||||
| -rw-r--r-- | build.bat | 8 | ||||
| -rw-r--r-- | src/analysis.odin | 1 | ||||
| -rw-r--r-- | src/config.odin | 8 | ||||
| -rw-r--r-- | src/documents.odin | 123 | ||||
| -rw-r--r-- | src/log.odin | 64 | ||||
| -rw-r--r-- | src/main.odin | 75 | ||||
| -rw-r--r-- | src/position.odin | 70 | ||||
| -rw-r--r-- | src/reader.odin | 64 | ||||
| -rw-r--r-- | src/requests.odin | 312 | ||||
| -rw-r--r-- | src/response.odin | 68 | ||||
| -rw-r--r-- | src/types.odin | 149 | ||||
| -rw-r--r-- | src/unmarshal.odin | 129 | ||||
| -rw-r--r-- | src/uri.odin | 94 | ||||
| -rw-r--r-- | src/writer.odin | 29 | ||||
| -rw-r--r-- | tests/test_project/src/main.odin | 10 | ||||
| -rw-r--r-- | tests/tests.odin | 432 |
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); + */ + +} + |