From 6ec424ed8d34cf8a5f51e277a20429741b33ee96 Mon Sep 17 00:00:00 2001 From: DanielGavin Date: Sat, 7 Nov 2020 15:44:30 +0100 Subject: complete refractor of the project into packages --- src/analysis.odin | 155 ---------------- src/common/config.odin | 10 ++ src/common/position.odin | 257 +++++++++++++++++++++++++++ src/common/types.odin | 26 +++ src/common/uri.odin | 156 ++++++++++++++++ src/config.odin | 9 - src/documents.odin | 423 -------------------------------------------- src/index.odin | 95 ---------- src/index/build.odin | 86 +++++++++ src/index/file_index.odin | 7 + src/index/indexer.odin | 54 ++++++ src/index/memory_index.odin | 29 +++ src/index/symbol.odin | 66 +++++++ src/log.odin | 64 ------- src/main.odin | 27 +-- src/position.odin | 238 ------------------------- src/reader.odin | 64 ------- src/requests.odin | 362 ------------------------------------- src/response.odin | 68 ------- src/server/analysis.odin | 158 +++++++++++++++++ src/server/documents.odin | 402 +++++++++++++++++++++++++++++++++++++++++ src/server/log.odin | 64 +++++++ src/server/reader.odin | 64 +++++++ src/server/requests.odin | 364 ++++++++++++++++++++++++++++++++++++++ src/server/response.odin | 68 +++++++ src/server/types.odin | 154 ++++++++++++++++ src/server/unmarshal.odin | 129 ++++++++++++++ src/server/workspace.odin | 1 + src/server/writer.odin | 29 +++ src/types.odin | 191 -------------------- src/unmarshal.odin | 129 -------------- src/uri.odin | 156 ---------------- src/workspace.odin | 1 - src/writer.odin | 29 --- 34 files changed, 2141 insertions(+), 1994 deletions(-) delete mode 100644 src/analysis.odin create mode 100644 src/common/config.odin create mode 100644 src/common/position.odin create mode 100644 src/common/types.odin create mode 100644 src/common/uri.odin delete mode 100644 src/config.odin delete mode 100644 src/documents.odin delete mode 100644 src/index.odin create mode 100644 src/index/build.odin create mode 100644 src/index/file_index.odin create mode 100644 src/index/indexer.odin create mode 100644 src/index/memory_index.odin create mode 100644 src/index/symbol.odin delete mode 100644 src/log.odin delete mode 100644 src/position.odin delete mode 100644 src/reader.odin delete mode 100644 src/requests.odin delete mode 100644 src/response.odin create mode 100644 src/server/analysis.odin create mode 100644 src/server/documents.odin create mode 100644 src/server/log.odin create mode 100644 src/server/reader.odin create mode 100644 src/server/requests.odin create mode 100644 src/server/response.odin create mode 100644 src/server/types.odin create mode 100644 src/server/unmarshal.odin create mode 100644 src/server/workspace.odin create mode 100644 src/server/writer.odin delete mode 100644 src/types.odin delete mode 100644 src/unmarshal.odin delete mode 100644 src/uri.odin delete mode 100644 src/workspace.odin delete mode 100644 src/writer.odin (limited to 'src') diff --git a/src/analysis.odin b/src/analysis.odin deleted file mode 100644 index 23f18ae..0000000 --- a/src/analysis.odin +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import "core:odin/parser" -import "core:odin/ast" -import "core:odin/tokenizer" -import "core:fmt" -import "core:log" -import "core:strings" -import "core:path" - - -DocumentPositionContextDottedValue :: struct { - prefix: string, - postfix: string, -}; - -DocumentPositionContextGlobalValue :: struct { - -}; - -DocumentPositionContextUnknownValue :: struct { - -} - -DocumentPositionContextValue :: union { - DocumentPositionContextDottedValue, - DocumentPositionContextGlobalValue, - DocumentPositionContextUnknownValue -}; - -DocumentPositionContext :: struct { - value: DocumentPositionContextValue, -}; - - -tokenizer_error_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { - -} - -/* - Figure out what exactly is at the given position and whether it is in a function, struct, etc. -*/ -get_document_position_context :: proc(document: ^Document, position: Position) -> (DocumentPositionContext, bool) { - - position_context: DocumentPositionContext; - - absolute_position, ok := get_absolute_position(position, document.text); - - if !ok { - return position_context, false; - } - - - //Using the ast is not really viable since the code may be broken code - t: tokenizer.Tokenizer; - - tokenizer.init(&t, document.text, document.uri.path, tokenizer_error_handler); - - stack := make([dynamic] tokenizer.Token, context.temp_allocator); - - current_token: tokenizer.Token; - last_token: tokenizer.Token; - - struct_or_package_dotted: bool; - struct_or_package: tokenizer.Token; - - /* - Idea is to push and pop into braces, brackets, etc, and use the final stack to infer context - */ - - for true { - - current_token = tokenizer.scan(&t); - - #partial switch current_token.kind { - case .Period: - if last_token.kind == .Ident { - struct_or_package_dotted = true; - struct_or_package = last_token; - } - case .Ident: - case .EOF: - break; - case: - struct_or_package_dotted = false; - - } - - //fmt.println(current_token.text); - //fmt.println(); - - if current_token.pos.offset+len(current_token.text) >= absolute_position { - break; - } - - last_token = current_token; - } - - #partial switch current_token.kind { - case .Ident: - if struct_or_package_dotted { - position_context.value = DocumentPositionContextDottedValue { - prefix = struct_or_package.text, - postfix = current_token.text, - }; - } - else { - - } - case: - position_context.value = DocumentPositionContextUnknownValue { - - }; - } - - //fmt.println(position_context); - - return position_context, true; -} - - -get_definition_location :: proc(document: ^Document, position: Position) -> (Location, bool) { - - location: Location; - - position_context, ok := get_document_position_context(document, position); - - if !ok { - return location, false; - } - - symbol: Symbol; - - #partial switch v in position_context.value { - case DocumentPositionContextDottedValue: - symbol, ok = indexer_get_symbol(strings.concatenate({v.prefix, v.postfix}, context.temp_allocator)); - case: - return location, false; - } - - //fmt.println(indexer.symbol_table); - - if !ok { - return location, false; - } - - switch v in symbol { - case ProcedureSymbol: - location.range = v.range; - location.uri = v.uri.uri; - } - - return location, true; -} - diff --git a/src/common/config.odin b/src/common/config.odin new file mode 100644 index 0000000..cdbe4fd --- /dev/null +++ b/src/common/config.odin @@ -0,0 +1,10 @@ +package common + +Config :: struct { + workspace_folders: [dynamic] WorkspaceFolder, + completion_support_md: bool, + hover_support_md: bool, + collections: map [string] string, + running: bool, +}; + diff --git a/src/common/position.odin b/src/common/position.odin new file mode 100644 index 0000000..7eb4173 --- /dev/null +++ b/src/common/position.odin @@ -0,0 +1,257 @@ +package common + +import "core:strings" +import "core:unicode/utf8" +import "core:fmt" +import "core:odin/ast" + +/* + This file handles the conversion between utf-16 and utf-8 offsets in the text document + */ + +Position :: struct { + line: int, + character: int, +}; + +Range :: struct { + start: Position, + end: Position, +}; + +Location :: struct { + uri: string, + range: Range, +}; + + +AbsoluteRange :: struct { + start: int, + end: int, +}; + +AbsolutePosition :: int; + +get_absolute_position :: proc(position: Position, document_text: [] u8) -> (AbsolutePosition, bool) { + absolute: AbsolutePosition; + + if len(document_text) == 0 { + absolute = 0; + return absolute, true; + } + + line_count := 0; + index := 1; + last := document_text[0]; + + if !get_index_at_line(&index, &line_count, &last, document_text, position.line) { + return absolute, false; + } + + absolute = index + get_character_offset_u16_to_u8(position.character, document_text[index:]); + + return absolute, true; +} + +/* + Get the range of a token in utf16 space + */ +get_token_range :: proc(node: ast.Node, document_text: [] u8) -> Range { + range: Range; + + + go_backwards_to_endline :: proc(offset: int, document_text: [] u8) -> int { + + index := offset; + + for index > 0 && (document_text[index] != '\n' || document_text[index] != '\r') { + index -= 1; + } + + if index == 0 { + return 0; + } + + return index+1; + } + + pos_offset := min(len(document_text)-1, node.pos.offset); + end_offset := min(len(document_text)-1, node.end.offset); + + offset := go_backwards_to_endline(pos_offset, document_text); + + range.start.line = node.pos.line-1; + range.start.character = get_character_offset_u8_to_u16(node.pos.column-1, document_text[offset:]); + + offset = go_backwards_to_endline(end_offset, document_text); + + range.end.line = node.end.line-1; + range.end.character = get_character_offset_u8_to_u16(node.end.column-1, document_text[offset:]); + + return range; +} + +get_absolute_range :: proc(range: Range, document_text: [] u8) -> (AbsoluteRange, bool) { + + absolute: AbsoluteRange; + + if len(document_text) == 0 { + absolute.start = 0; + absolute.end = 0; + return absolute, true; + } + + line_count := 0; + index := 1; + last := document_text[0]; + + if !get_index_at_line(&index, &line_count, &last, document_text, range.start.line) { + return absolute, false; + } + + absolute.start = index + get_character_offset_u16_to_u8(range.start.character, document_text[index:]); + + //if the last line was indexed at zero we have to move it back to index 1. + //This happens when line = 0 + if index == 0 { + index = 1; + } + + if !get_index_at_line(&index, &line_count, &last, document_text, range.end.line) { + return absolute, false; + } + + absolute.end = index + get_character_offset_u16_to_u8(range.end.character, document_text[index:]); + + return absolute, true; +} + + +get_index_at_line :: proc(current_index: ^int, current_line: ^int, last: ^u8, document_text: []u8, end_line: int) -> bool { + + if end_line == 0 { + current_index^ = 0; + return true; + } + + if current_line^ == end_line { + return true; + } + + + 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; + +} + +get_character_offset_u16_to_u8 :: proc(character_offset: int, document_text: [] u8) -> int { + + utf8_idx := 0; + utf16_idx := 0; + + for utf16_idx < character_offset { + + r, w := utf8.decode_rune(document_text[utf8_idx:]); + + if r == '\n' { + return utf8_idx; + } + + else if r < 0x10000 { + utf16_idx += 1; + } + + else { + utf16_idx += 2; + } + + utf8_idx += w; + + } + + return utf8_idx; +} + +get_character_offset_u8_to_u16 :: proc(character_offset: int, document_text: [] u8) -> int { + + utf8_idx := 0; + utf16_idx := 0; + + for utf16_idx < character_offset { + + r, w := utf8.decode_rune(document_text[utf8_idx:]); + + if r == '\n' { + return utf16_idx; + } + + else if r < 0x10000 { + utf16_idx += 1; + } + + else { + utf16_idx += 2; + } + + utf8_idx += w; + + } + + return utf16_idx; + +} + +get_end_line_u16 :: proc(document_text: [] u8) -> int { + + utf8_idx := 0; + utf16_idx := 0; + + for utf8_idx < len(document_text) { + r, w := utf8.decode_rune(document_text[utf8_idx:]); + + if r == '\n' { + return utf16_idx; + } + + else if r < 0x10000 { + utf16_idx += 1; + } + + else { + utf16_idx += 2; + } + + utf8_idx += w; + + } + + return utf16_idx; +} \ No newline at end of file diff --git a/src/common/types.odin b/src/common/types.odin new file mode 100644 index 0000000..08bd8c2 --- /dev/null +++ b/src/common/types.odin @@ -0,0 +1,26 @@ +package common + + +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, +}; + +WorkspaceFolder :: struct { + name: string, + uri: string, +}; diff --git a/src/common/uri.odin b/src/common/uri.odin new file mode 100644 index 0000000..f8bee9e --- /dev/null +++ b/src/common/uri.odin @@ -0,0 +1,156 @@ +package common + +import "core:mem" +import "core:strings" +import "core:strconv" +import "core:fmt" +import "core:unicode/utf8" + +Uri :: struct { + uri: string, + decode_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.uri = strings.clone(value); + uri.decode_full = decoded; + uri.path = decoded[len(starts):]; + + return uri, true; +} + + +//Note(Daniel, Again some really incomplete and scuffed uri writer) +create_uri :: proc(path: string, allocator := context.allocator) -> Uri { + + builder := strings.make_builder(allocator); + + strings.write_string(&builder, "file:///"); + strings.write_string(&builder, encode_percent(path, context.temp_allocator)); + + uri: Uri; + + uri.uri = strings.to_string(builder); + uri.decode_full = strings.clone(path, allocator); + uri.path = uri.decode_full; + + return uri; +} + +delete_uri :: proc(uri: Uri) { + + if uri.uri != "" { + delete(uri.uri); + } + + if uri.decode_full != "" { + delete(uri.decode_full); + } +} + +encode_percent :: proc(value: string, allocator: mem.Allocator) -> string { + + builder := strings.make_builder(allocator); + + data := transmute([]u8)value; + index: int; + + for index < len(value) { + + r, w := utf8.decode_rune(data[index:]); + + if r > 127 || r == ':'{ + + for i := 0; i < w; i += 1 { + strings.write_string(&builder, strings.concatenate({"%", fmt.tprintf("%X", data[index+i])}, + context.temp_allocator)); + } + + } + + else { + strings.write_byte(&builder, data[index]); + } + + index += w; + } + + return strings.to_string(builder); +} + +@(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/config.odin b/src/config.odin deleted file mode 100644 index b4df050..0000000 --- a/src/config.odin +++ /dev/null @@ -1,9 +0,0 @@ -package main - -Config :: struct { - workspace_folders: [dynamic] WorkspaceFolder, - completion_support_md: bool, - hover_support_md: bool, - collections: map [string] string, -}; - diff --git a/src/documents.odin b/src/documents.odin deleted file mode 100644 index bda7c21..0000000 --- a/src/documents.odin +++ /dev/null @@ -1,423 +0,0 @@ -package main - -import "core:strings" -import "core:fmt" -import "core:log" -import "core:os" -import "core:odin/parser" -import "core:odin/ast" -import "core:odin/tokenizer" -import "core:path" - -ParserError :: struct { - message: string, - line: int, - column: int, - file: string, - offset: int, -}; - - -Package :: struct { - documents: [dynamic]^Document, -}; - -Document :: struct { - uri: Uri, - text: [] u8, - used_text: int, //allow for the text to be reallocated with more data than needed - client_owned: bool, - diagnosed_errors: bool, - indexed: bool, - ast: ast.File, - package_name: string, - imports: [] string, -}; - -DocumentStorage :: struct { - documents: map [string] Document, - packages: map [string] Package, -}; - -document_storage: DocumentStorage; - - -document_get :: proc(uri_string: string) -> ^Document { - - uri, parsed_ok := parse_uri(uri_string, context.temp_allocator); - - if !parsed_ok { - return nil; - } - - return &document_storage.documents[uri.path]; -} - -/* - Note(Daniel, Should there be reference counting of documents or just clear everything on workspace change? - You usually always need the documents that are loaded in core files, your own files, etc.) - */ - -/* - Server opens a new document with text from filesystem -*/ -document_new :: proc(path: string, config: ^Config) -> Error { - - text, ok := os.read_entire_file(path); - - uri := create_uri(path); - - if !ok { - log.error("Failed to parse uri"); - return .ParseError; - } - - document := Document { - uri = uri, - text = transmute([] u8)text, - client_owned = true, - used_text = len(text), - }; - - if err := document_refresh(&document, config, nil, false); err != .None { - log.error("Failed to refresh new document"); - return err; - } - - document_storage.documents[path] = document; - - if err := index_document(&document_storage.documents[path]); err != .None { - log.error("Failed to index new document"); - return err; - } - - - - return .None; -} - -/* - Client opens a document with transferred text -*/ - -document_open :: proc(uri_string: string, text: string, config: ^Config, writer: ^Writer) -> Error { - - uri, parsed_ok := parse_uri(uri_string); - - log.infof("document_open: %v", uri_string); - - if !parsed_ok { - log.error("Failed to parse uri"); - return .ParseError; - } - - if document := &document_storage.documents[uri.path]; document != nil { - - if document.client_owned { - log.errorf("Client called open on an already open document: %v ", document.uri.path); - return .InvalidRequest; - } - - if document.text != nil { - delete(document.text); - } - - if len(document.uri.uri) > 0 { - delete_uri(document.uri); - } - - document.uri = uri; - document.client_owned = true; - document.text = transmute([] u8)text; - document.used_text = len(document.text); - - if err := document_refresh(document, config, writer, true); err != .None { - return err; - } - - } - - else { - - document := Document { - uri = uri, - text = transmute([] u8)text, - client_owned = true, - used_text = len(text), - }; - - if err := document_refresh(&document, config, writer, true); err != .None { - return err; - } - - document_storage.documents[uri.path] = document; - } - - - - //hmm feels like odin needs some ownership semantic - delete(uri_string); - - return .None; -} - -/* - Function that applies changes to the given document through incremental syncronization - */ -document_apply_changes :: proc(uri_string: string, changes: [dynamic] TextDocumentContentChangeEvent, config: ^Config, writer: ^Writer) -> Error { - - uri, parsed_ok := parse_uri(uri_string, context.temp_allocator); - - if !parsed_ok { - return .ParseError; - } - - document := &document_storage.documents[uri.path]; - - if !document.client_owned { - log.errorf("Client called change on an document not opened: %v ", document.uri.path); - return .InvalidRequest; - } - - for change in changes { - - absolute_range, ok := get_absolute_range(change.range, document.text[:document.used_text]); - - if !ok { - return .ParseError; - } - - //lower bound is before the change - lower := document.text[:absolute_range.start]; - - //new change between lower and upper - middle := change.text; - - //upper bound is after the change - upper := document.text[absolute_range.end:document.used_text]; - - //total new size needed - document.used_text = len(lower) + len(change.text) + len(upper); - - //Reduce the amount of allocation by allocating more memory than needed - if document.used_text > len(document.text) { - new_text := make([]u8, document.used_text * 2); - - //join the 3 splices into the text - copy(new_text, lower); - copy(new_text[len(lower):], middle); - copy(new_text[len(lower)+len(middle):], upper); - - delete(document.text); - - document.text = new_text; - } - - else { - //order matters here, we need to make sure we swap the data already in the text before the middle - copy(document.text, lower); - copy(document.text[len(lower)+len(middle):], upper); - copy(document.text[len(lower):], middle); - } - - } - - return document_refresh(document, config, writer, true); -} - -document_close :: proc(uri_string: 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.uri.path); - return .InvalidRequest; - } - - document.client_owned = false; - - return .None; -} - - - -document_refresh :: proc(document: ^Document, config: ^Config, writer: ^Writer, parse_imports: bool) -> Error { - - errors, ok := parse_document(document, config); - - if !ok { - return .ParseError; - } - - //right now we don't allow to writer errors out from files read from the file directory, core files, etc. - if writer != nil && len(errors) > 0 { - document.diagnosed_errors = true; - - params := NotificationPublishDiagnosticsParams { - uri = document.uri.uri, - diagnostics = make([] Diagnostic, len(errors), context.temp_allocator), - }; - - for error, i in errors { - - params.diagnostics[i] = Diagnostic { - range = Range { - start = Position { - line = error.line - 1, - character = 0, - }, - end = Position { - line = error.line, - character = 0, - }, - }, - severity = DiagnosticSeverity.Error, - code = "test", - message = error.message, - }; - - } - - notifaction := Notification { - jsonrpc = "2.0", - method = "textDocument/publishDiagnostics", - params = params, - }; - - send_notification(notifaction, writer); - - } - - if writer != nil && len(errors) == 0 { - - //send empty diagnosis to remove the clients errors - if document.diagnosed_errors { - - notifaction := Notification { - jsonrpc = "2.0", - method = "textDocument/publishDiagnostics", - - params = NotificationPublishDiagnosticsParams { - uri = document.uri.uri, - diagnostics = make([] Diagnostic, len(errors), context.temp_allocator), - }, - }; - - document.diagnosed_errors = false; - - send_notification(notifaction, writer); - } - - } - - if parse_imports { - - /* - go through the imports from this document and see if we need to load them into memory(not owned by client), - and also refresh them if needed - */ - for imp in document.imports { - - if err := document_load_package(imp, config); err != .None { - return err; - } - - } - - } - - - return .None; -} - -document_load_package :: proc(package_directory: string, config: ^Config) -> Error { - - fd, err := os.open(package_directory); - - if err != 0 { - return .ParseError; - } - - files: []os.File_Info; - files, err = os.read_dir(fd, 100, context.temp_allocator); - - for file in files { - - //if we have never encountered the document - if _, ok := document_storage.documents[file.fullpath]; !ok { - - if doc_err := document_new(file.fullpath, config); doc_err != .None { - return doc_err; - } - - } - - } - - return .None; -} - - -current_errors: [dynamic] ParserError; - -parser_error_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { - error := ParserError { line = pos.line, column = pos.column, file = pos.file, - offset = pos.offset, message = fmt.tprintf(msg, ..args) }; - append(¤t_errors, error); -} - -parser_warning_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { - -} - -parse_document :: proc(document: ^Document, config: ^Config) -> ([] ParserError, bool) { - - p := parser.Parser { - err = parser_error_handler, - warn = parser_warning_handler, - }; - - current_errors = make([dynamic] ParserError, context.temp_allocator); - - document.ast = ast.File { - fullpath = document.uri.path, - src = document.text[:document.used_text], - }; - - parser.parse_file(&p, &document.ast); - - document.imports = make([]string, len(document.ast.imports)); - document.package_name = document.ast.pkg_name; - - for imp, index in document.ast.imports { - - //collection specified - if i := strings.index(imp.fullpath, ":"); i != -1 { - - collection := imp.fullpath[1:i]; - p := imp.fullpath[i+1:len(imp.fullpath)-1]; - - dir, ok := config.collections[collection]; - - if !ok { - continue; - } - - document.imports[index] = path.join(dir, p); - - } - - //relative - else { - - } - } - - return current_errors[:], true; -} \ No newline at end of file diff --git a/src/index.odin b/src/index.odin deleted file mode 100644 index 7d2774f..0000000 --- a/src/index.odin +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import "core:odin/ast" -import "core:fmt" -import "core:strings" - - -/* - Concept ideas: - - static indexing: - - is responsible for implementing the indexing of symbols for static files. - - This is to solve the scaling problem of large projects with many files and symbols, as most of these files will be static. - - Possible scopes for static files: - global scope (we don't have hiarachy of namespaces and therefore only need to look at the global scope) - - Scopes not part of the indexer: - function scope, file scope, package scope(these are only relevant for dynamic active files in your project, that use the ast instead of indexing) - - Potential features: - Allow for saving the indexer, instead of recreating it everytime the lsp starts(but you would have to account for stale data). - - - dynamic indexing: - - When the user modifies files we need some smaller index to handle everything the user is using right now. This will allow - us to rebuild parts of the index without too much of a performance hit. - - This index is first searched and if nothing is found look in the static index. - - interface ideas: - - index_search_fuzzy(symbol: string, scope: [] string) -> [] SymbolResult - - TODO(Daniel, Look into data structure for fuzzy searching) - - */ -BaseSymbol :: struct { - range: Range, - uri: ^Uri, -}; - -ProcedureSymbol :: struct { - using symbolbase: BaseSymbol, -}; - -Symbol :: union { - ProcedureSymbol, -}; - -Indexer :: struct { - symbol_table: map [string] Symbol, -}; - -indexer: Indexer; - -index_document :: proc(document: ^Document) -> Error { - - for decl in document.ast.decls { - - if value_decl, ok := decl.derived.(ast.Value_Decl); ok { - - name := string(document.text[value_decl.names[0].pos.offset:value_decl.names[0].end.offset]); - - if len(value_decl.values) == 1 { - - if proc_lit, ok := value_decl.values[0].derived.(ast.Proc_Lit); ok { - - symbol: ProcedureSymbol; - - symbol.range = get_token_range(proc_lit, document.text); - symbol.uri = &document.uri; - - indexer.symbol_table[strings.concatenate({document.package_name, name}, context.temp_allocator)] = symbol; - - //fmt.println(proc_lit.type); - - } - - } - - } - } - - //fmt.println(indexer.symbol_table); - - return .None; -} - -indexer_get_symbol :: proc(id: string) -> (Symbol, bool) { - return indexer.symbol_table[id]; -} diff --git a/src/index/build.odin b/src/index/build.odin new file mode 100644 index 0000000..d52cbca --- /dev/null +++ b/src/index/build.odin @@ -0,0 +1,86 @@ +package index + +import "core:path/filepath" +import "core:os" +import "core:fmt" +import "core:odin/parser" +import "core:odin/ast" +import "core:odin/tokenizer" + +import "shared:common" + +/* + Not fully sure how to handle rebuilding, but one thing is for sure, dynamic indexing has to have a background thread + rebuilding every minute or less to fight against stale information + */ + + +//test version for static indexing + +symbol_collection: SymbolCollection; + +build_static_index :: proc(allocator := context.allocator, config: ^common.Config) { + + //right now just collect the symbols from core + + core_path := config.collections["core"]; + + + symbol_collection = make_symbol_collection(allocator); + + walk_static_index_build := proc(info: os.File_Info, in_err: os.Errno) -> (err: os.Errno, skip_dir: bool) { + + if info.is_dir { + return 0, false; + } + + //fmt.println(info.fullpath); + + //bit worried about using temp allocator here since we might overwrite all our temp allocator budget + data, ok := os.read_entire_file(info.fullpath, context.allocator); + + if !ok { + fmt.println("failed to read"); + return 1, false; + } + + p := parser.Parser { + err = no_error_handler, + warn = no_warning_handler, + }; + + file := ast.File { + fullpath = info.fullpath, + src = data, + }; + + parser.parse_file(&p, &file); + + uri := common.create_uri(info.fullpath, context.temp_allocator); + + collect_symbols(&symbol_collection, file, uri.uri); + + delete(data); + + + return 0, false; + }; + + filepath.walk(core_path, walk_static_index_build); + + indexer.static_index = make_memory_index(symbol_collection); +} + + +no_error_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { + +} + +no_warning_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { + +} + + + + + diff --git a/src/index/file_index.odin b/src/index/file_index.odin new file mode 100644 index 0000000..fe787e5 --- /dev/null +++ b/src/index/file_index.odin @@ -0,0 +1,7 @@ +package index + +/* + This is indexer for static files operating on a file database to index symbols and files. + + NOTE(Daniel, Let's be honest probably will not be made any time soon) + */ \ No newline at end of file diff --git a/src/index/indexer.odin b/src/index/indexer.odin new file mode 100644 index 0000000..46a2192 --- /dev/null +++ b/src/index/indexer.odin @@ -0,0 +1,54 @@ +package index + +import "core:odin/ast" +import "core:fmt" +import "core:strings" + + +/* + Concept ideas: + + static indexing: + + is responsible for implementing the indexing of symbols for static files. + + This is to solve the scaling problem of large projects with many files and symbols, as most of these files will be static. + + Possible scopes for static files: + global scope (we don't have hiarachy of namespaces and therefore only need to look at the global scope) + + Scopes not part of the indexer: + function scope, file scope, package scope(these are only relevant for dynamic active files in your project, that use the ast instead of indexing) + + Potential features: + Allow for saving the indexer, instead of recreating it everytime the lsp starts(but you would have to account for stale data). + + + dynamic indexing: + + When the user modifies files we need some smaller index to handle everything the user is using right now. This will allow + us to rebuild parts of the index without too much of a performance hit. + + This index is first searched and if nothing is found look in the static index. + + interface ideas: + + index_search_fuzzy(symbol: string, scope: [] string) -> [] SymbolResult + + TODO(Daniel, Look into data structure for fuzzy searching) + + */ + + +Indexer :: struct { + static_index: MemoryIndex, +}; + +indexer: Indexer; + + +lookup :: proc(id: string) -> (Symbol, bool) { + return memory_index_lookup(&indexer.static_index, id); +} + +//indexer_fuzzy_search :: proc(name: string, scope: [] string, ) diff --git a/src/index/memory_index.odin b/src/index/memory_index.odin new file mode 100644 index 0000000..c5ce416 --- /dev/null +++ b/src/index/memory_index.odin @@ -0,0 +1,29 @@ +package index + +import "core:hash" + +/* + This is a in memory index designed for the dynamic indexing of symbols and files. + Designed for few files and should be fast at rebuilding. + + Right now the implementation is quite naive. + */ +MemoryIndex :: struct { + collection: SymbolCollection, +}; + + +make_memory_index :: proc(collection: SymbolCollection) -> MemoryIndex { + + return MemoryIndex { + collection = collection, + }; + +} + +memory_index_lookup :: proc(index: ^MemoryIndex, id: string) -> (Symbol, bool) { + + hashed := hash.murmur64(transmute([]u8)id); + + return index.collection.symbols[hashed]; +} \ No newline at end of file diff --git a/src/index/symbol.odin b/src/index/symbol.odin new file mode 100644 index 0000000..2cbd58c --- /dev/null +++ b/src/index/symbol.odin @@ -0,0 +1,66 @@ +package index + +import "core:odin/ast" +import "core:hash" +import "core:strings" +import "core:mem" + +import "shared:common" + +Symbol :: struct { + id: u64, + range: common.Range, + uri: string, +}; + +SymbolCollection :: struct { + allocator: mem.Allocator, + symbols: map[u64] Symbol, + unique_strings: map[u64] string, +}; + +make_symbol_collection :: proc(allocator := context.allocator) -> SymbolCollection { + return SymbolCollection { + allocator = allocator, + symbols = make(map[u64] Symbol, 16, allocator), + unique_strings = make(map[u64] string, 16, allocator), + }; +} + +collect_symbols :: proc(collection: ^SymbolCollection, file: ast.File, uri: string) -> common.Error { + + for decl in file.decls { + + if value_decl, ok := decl.derived.(ast.Value_Decl); ok { + + name := string(file.src[value_decl.names[0].pos.offset:value_decl.names[0].end.offset]); + + if len(value_decl.values) == 1 { + + if proc_lit, ok := value_decl.values[0].derived.(ast.Proc_Lit); ok { + + symbol: Symbol; + + symbol.range = common.get_token_range(proc_lit, file.src); + + uri_id := hash.murmur64(transmute([]u8)uri); + + if _, ok := collection.unique_strings[uri_id]; !ok { + collection.unique_strings[uri_id] = strings.clone(uri); + } + + symbol.uri = collection.unique_strings[uri_id]; + + id := hash.murmur64(transmute([]u8)strings.concatenate({file.pkg_name, name}, context.temp_allocator)); + + collection.symbols[id] = symbol; + + } + + } + + } + } + + return .None; +} diff --git a/src/log.odin b/src/log.odin deleted file mode 100644 index 82a52a4..0000000 --- a/src/log.odin +++ /dev/null @@ -1,64 +0,0 @@ -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", 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 index 382fd31..62c75f6 100644 --- a/src/main.odin +++ b/src/main.odin @@ -9,6 +9,10 @@ import "core:slice" import "core:strconv" import "core:encoding/json" +import "shared:index" +import "shared:server" +import "shared:common" + running: bool; os_read :: proc(handle: rawptr, data: [] byte) -> (int, int) @@ -23,9 +27,9 @@ os_write :: proc(handle: rawptr, data: [] byte) -> (int, int) //Note(Daniel, Should look into handling errors without crashing from parsing) -run :: proc(reader: ^Reader, writer: ^Writer) { +run :: proc(reader: ^server.Reader, writer: ^server.Writer) { - config: Config; + config: common.Config; //temporary collections being set manually, need to get client configuration set up. config.collections = make(map [string] string); @@ -34,11 +38,14 @@ run :: proc(reader: ^Reader, writer: ^Writer) { log.info("Starting Odin Language Server"); - running = true; + index.build_static_index(context.allocator, &config); + + + config.running = true; - for running { + for config.running { - header, success := read_and_parse_header(reader); + header, success := server.read_and_parse_header(reader); if(!success) { log.error("Failed to read and parse header"); @@ -47,14 +54,14 @@ run :: proc(reader: ^Reader, writer: ^Writer) { value: json.Value; - value, success = read_and_parse_body(reader, header); + value, success = server.read_and_parse_body(reader, header); if(!success) { log.error("Failed to read and parse body"); return; } - success = handle_request(value, &config, writer); + success = server.handle_request(value, &config, writer); if(!success) { log.error("Unrecoverable handle request"); @@ -74,10 +81,10 @@ end :: proc() { main :: proc() { - reader := make_reader(os_read, cast(rawptr)os.stdin); - writer := make_writer(os_write, cast(rawptr)os.stdout); + reader := server.make_reader(os_read, cast(rawptr)os.stdin); + writer := server.make_writer(os_write, cast(rawptr)os.stdout); - context.logger = create_lsp_logger(&writer); + context.logger = server.create_lsp_logger(&writer); run(&reader, &writer); } diff --git a/src/position.odin b/src/position.odin deleted file mode 100644 index 3278f10..0000000 --- a/src/position.odin +++ /dev/null @@ -1,238 +0,0 @@ -package main - -import "core:strings" -import "core:unicode/utf8" -import "core:fmt" -import "core:odin/ast" - -/* - This file handles the conversion between utf-16 and utf-8 offsets in the text document - */ - -AbsoluteRange :: struct { - start: int, - end: int, -}; - -AbsolutePosition :: int; - -get_absolute_position :: proc(position: Position, document_text: [] u8) -> (AbsolutePosition, bool) { - absolute: AbsolutePosition; - - if len(document_text) == 0 { - absolute = 0; - return absolute, true; - } - - line_count := 0; - index := 1; - last := document_text[0]; - - if !get_index_at_line(&index, &line_count, &last, document_text, position.line) { - return absolute, false; - } - - absolute = index + get_character_offset_u16_to_u8(position.character, document_text[index:]); - - return absolute, true; -} - -/* - Get the range of a token in utf16 space - */ -get_token_range :: proc(node: ast.Node, document_text: [] u8) -> Range { - range: Range; - - - go_backwards_to_endline :: proc(offset: int, document_text: [] u8) -> int { - - index := offset; - - for index > 0 && (document_text[index] != '\n' || document_text[index] != '\r') { - index -= 1; - } - - if index == 0 { - return 0; - } - - return index+1; - } - - offset := go_backwards_to_endline(node.pos.offset, document_text); - - range.start.line = node.pos.line-1; - range.start.character = get_character_offset_u8_to_u16(node.pos.column-1, document_text[offset:]); - - offset = go_backwards_to_endline(node.end.offset, document_text); - - range.end.line = node.end.line-1; - range.end.character = get_character_offset_u8_to_u16(node.end.column-1, document_text[offset:]); - - return range; -} - -get_absolute_range :: proc(range: Range, document_text: [] u8) -> (AbsoluteRange, bool) { - - absolute: AbsoluteRange; - - if len(document_text) == 0 { - absolute.start = 0; - absolute.end = 0; - return absolute, true; - } - - line_count := 0; - index := 1; - last := document_text[0]; - - if !get_index_at_line(&index, &line_count, &last, document_text, range.start.line) { - return absolute, false; - } - - absolute.start = index + get_character_offset_u16_to_u8(range.start.character, document_text[index:]); - - //if the last line was indexed at zero we have to move it back to index 1. - //This happens when line = 0 - if index == 0 { - index = 1; - } - - if !get_index_at_line(&index, &line_count, &last, document_text, range.end.line) { - return absolute, false; - } - - absolute.end = index + get_character_offset_u16_to_u8(range.end.character, document_text[index:]); - - return absolute, true; -} - - -get_index_at_line :: proc(current_index: ^int, current_line: ^int, last: ^u8, document_text: []u8, end_line: int) -> bool { - - if end_line == 0 { - current_index^ = 0; - return true; - } - - if current_line^ == end_line { - return true; - } - - - 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; - -} - -get_character_offset_u16_to_u8 :: proc(character_offset: int, document_text: [] u8) -> int { - - utf8_idx := 0; - utf16_idx := 0; - - for utf16_idx < character_offset { - - r, w := utf8.decode_rune(document_text[utf8_idx:]); - - if r == '\n' { - return utf8_idx; - } - - else if r < 0x10000 { - utf16_idx += 1; - } - - else { - utf16_idx += 2; - } - - utf8_idx += w; - - } - - return utf8_idx; -} - -get_character_offset_u8_to_u16 :: proc(character_offset: int, document_text: [] u8) -> int { - - utf8_idx := 0; - utf16_idx := 0; - - for utf16_idx < character_offset { - - r, w := utf8.decode_rune(document_text[utf8_idx:]); - - if r == '\n' { - return utf16_idx; - } - - else if r < 0x10000 { - utf16_idx += 1; - } - - else { - utf16_idx += 2; - } - - utf8_idx += w; - - } - - return utf16_idx; - -} - -get_end_line_u16 :: proc(document_text: [] u8) -> int { - - utf8_idx := 0; - utf16_idx := 0; - - for utf8_idx < len(document_text) { - r, w := utf8.decode_rune(document_text[utf8_idx:]); - - if r == '\n' { - return utf16_idx; - } - - else if r < 0x10000 { - utf16_idx += 1; - } - - else { - utf16_idx += 2; - } - - utf8_idx += w; - - } - - return utf16_idx; -} \ No newline at end of file diff --git a/src/reader.odin b/src/reader.odin deleted file mode 100644 index 31019a5..0000000 --- a/src/reader.odin +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 182772a..0000000 --- a/src/requests.odin +++ /dev/null @@ -1,362 +0,0 @@ -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 { - - 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, - "textDocument/didSave" = notification_did_save, - "textDocument/definition" = request_definition }; - - 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 - definitionProvider = true, - }, - }, - 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; -} - -request_definition :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error { - - params_object, ok := params.value.(json.Object); - - if !ok { - return .ParseError; - } - - definition_params: TextDocumentPositionParams; - - if unmarshal(params, definition_params, context.temp_allocator) != .None { - return .ParseError; - } - - - document := document_get(definition_params.textDocument.uri); - - if document == nil { - return .InternalError; - } - - location, ok2 := get_definition_location(document, definition_params.position); - - if !ok2 { - log.error("Failed to get definition location"); - return .InternalError; - } - - response := make_response_message( - params = location, - 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 { - log.error("Failed to parse open document notification"); - return .ParseError; - } - - open_params: DidOpenTextDocumentParams; - - if unmarshal(params, open_params, context.allocator) != .None { - log.error("Failed to parse open document notification"); - return .ParseError; - } - - return document_open(open_params.textDocument.uri, open_params.textDocument.text, config, writer); -} - -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; - } - - document_apply_changes(change_params.textDocument.uri, change_params.contentChanges, config, writer); - - 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); -} - -notification_did_save :: proc(params: json.Value, id: RequestId, config: ^Config, writer: ^Writer) -> Error { - - - - return .None; -} - diff --git a/src/response.odin b/src/response.odin deleted file mode 100644 index b9249f4..0000000 --- a/src/response.odin +++ /dev/null @@ -1,68 +0,0 @@ -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/server/analysis.odin b/src/server/analysis.odin new file mode 100644 index 0000000..e88532f --- /dev/null +++ b/src/server/analysis.odin @@ -0,0 +1,158 @@ +package server + +import "core:odin/parser" +import "core:odin/ast" +import "core:odin/tokenizer" +import "core:fmt" +import "core:log" +import "core:strings" +import "core:path" + +import "shared:common" +import "shared:index" + + + +DocumentPositionContextDottedValue :: struct { + prefix: string, + postfix: string, +}; + +DocumentPositionContextGlobalValue :: struct { + +}; + +DocumentPositionContextUnknownValue :: struct { + +} + +DocumentPositionContextValue :: union { + DocumentPositionContextDottedValue, + DocumentPositionContextGlobalValue, + DocumentPositionContextUnknownValue +}; + +DocumentPositionContext :: struct { + value: DocumentPositionContextValue, +}; + + +tokenizer_error_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { + +} + + +/* + Figure out what exactly is at the given position and whether it is in a function, struct, etc. +*/ +get_document_position_context :: proc(document: ^Document, position: common.Position) -> (DocumentPositionContext, bool) { + + position_context: DocumentPositionContext; + + absolute_position, ok := common.get_absolute_position(position, document.text); + + if !ok { + return position_context, false; + } + + + //Using the ast is not really viable since the code may be broken code + t: tokenizer.Tokenizer; + + tokenizer.init(&t, document.text, document.uri.path, tokenizer_error_handler); + + stack := make([dynamic] tokenizer.Token, context.temp_allocator); + + current_token: tokenizer.Token; + last_token: tokenizer.Token; + + struct_or_package_dotted: bool; + struct_or_package: tokenizer.Token; + + /* + Idea is to push and pop into braces, brackets, etc, and use the final stack to infer context + */ + + for true { + + current_token = tokenizer.scan(&t); + + #partial switch current_token.kind { + case .Period: + if last_token.kind == .Ident { + struct_or_package_dotted = true; + struct_or_package = last_token; + } + case .Ident: + case .EOF: + break; + case: + struct_or_package_dotted = false; + + } + + if current_token.pos.offset+len(current_token.text) >= absolute_position { + break; + } + + last_token = current_token; + } + + #partial switch current_token.kind { + case .Ident: + if struct_or_package_dotted { + position_context.value = DocumentPositionContextDottedValue { + prefix = struct_or_package.text, + postfix = current_token.text, + }; + } + else { + + } + case: + position_context.value = DocumentPositionContextUnknownValue { + + }; + } + + //fmt.println(position_context); + + return position_context, true; +} + + +get_definition_location :: proc(document: ^Document, position: common.Position) -> (common.Location, bool) { + + + location: common.Location; + + + + position_context, ok := get_document_position_context(document, position); + + if !ok { + return location, false; + } + + symbol: index.Symbol; + + #partial switch v in position_context.value { + case DocumentPositionContextDottedValue: + symbol, ok = index.lookup(strings.concatenate({v.prefix, v.postfix}, context.temp_allocator)); + case: + return location, false; + } + + //fmt.println(indexer.symbol_table); + + if !ok { + return location, false; + } + + location.range = symbol.range; + location.uri = symbol.uri; + + + return location, true; +} + diff --git a/src/server/documents.odin b/src/server/documents.odin new file mode 100644 index 0000000..59eeadd --- /dev/null +++ b/src/server/documents.odin @@ -0,0 +1,402 @@ +package server + +import "core:strings" +import "core:fmt" +import "core:log" +import "core:os" +import "core:odin/parser" +import "core:odin/ast" +import "core:odin/tokenizer" +import "core:path" + +import "shared:common" + +ParserError :: struct { + message: string, + line: int, + column: int, + file: string, + offset: int, +}; + + +Package :: struct { + documents: [dynamic]^Document, +}; + +Document :: struct { + uri: common.Uri, + text: [] u8, + used_text: int, //allow for the text to be reallocated with more data than needed + client_owned: bool, + diagnosed_errors: bool, + ast: ast.File, + package_name: string, + imports: [] string, +}; + +DocumentStorage :: struct { + documents: map [string] Document, + packages: map [string] Package, +}; + +document_storage: DocumentStorage; + + +document_get :: proc(uri_string: string) -> ^Document { + + uri, parsed_ok := common.parse_uri(uri_string, context.temp_allocator); + + if !parsed_ok { + return nil; + } + + return &document_storage.documents[uri.path]; +} + +/* + Note(Daniel, Should there be reference counting of documents or just clear everything on workspace change? + You usually always need the documents that are loaded in core files, your own files, etc.) + */ + +/* + Server opens a new document with text from filesystem +*/ +document_new :: proc(path: string, config: ^common.Config) -> common.Error { + + text, ok := os.read_entire_file(path); + + uri := common.create_uri(path); + + if !ok { + log.error("Failed to parse uri"); + return .ParseError; + } + + document := Document { + uri = uri, + text = transmute([] u8)text, + client_owned = false, + used_text = len(text), + }; + + + document_storage.documents[path] = document; + + return .None; +} + +/* + Client opens a document with transferred text +*/ + +document_open :: proc(uri_string: string, text: string, config: ^common.Config, writer: ^Writer) -> common.Error { + + uri, parsed_ok := common.parse_uri(uri_string); + + log.infof("document_open: %v", uri_string); + + if !parsed_ok { + log.error("Failed to parse uri"); + return .ParseError; + } + + if document := &document_storage.documents[uri.path]; document != nil { + + if document.client_owned { + log.errorf("Client called open on an already open document: %v ", document.uri.path); + return .InvalidRequest; + } + + if document.text != nil { + delete(document.text); + } + + if len(document.uri.uri) > 0 { + common.delete_uri(document.uri); + } + + document.uri = uri; + document.client_owned = true; + document.text = transmute([] u8)text; + document.used_text = len(document.text); + + if err := document_refresh(document, config, writer, true); err != .None { + return err; + } + + } + + else { + + document := Document { + uri = uri, + text = transmute([] u8)text, + client_owned = true, + used_text = len(text), + }; + + if err := document_refresh(&document, config, writer, true); err != .None { + return err; + } + + document_storage.documents[uri.path] = document; + } + + + + //hmm feels like odin needs some ownership semantic + delete(uri_string); + + return .None; +} + +/* + Function that applies changes to the given document through incremental syncronization + */ +document_apply_changes :: proc(uri_string: string, changes: [dynamic] TextDocumentContentChangeEvent, config: ^common.Config, writer: ^Writer) -> common.Error { + + uri, parsed_ok := common.parse_uri(uri_string, context.temp_allocator); + + if !parsed_ok { + return .ParseError; + } + + document := &document_storage.documents[uri.path]; + + if !document.client_owned { + log.errorf("Client called change on an document not opened: %v ", document.uri.path); + return .InvalidRequest; + } + + for change in changes { + + absolute_range, ok := common.get_absolute_range(change.range, document.text[:document.used_text]); + + if !ok { + return .ParseError; + } + + //lower bound is before the change + lower := document.text[:absolute_range.start]; + + //new change between lower and upper + middle := change.text; + + //upper bound is after the change + upper := document.text[absolute_range.end:document.used_text]; + + //total new size needed + document.used_text = len(lower) + len(change.text) + len(upper); + + //Reduce the amount of allocation by allocating more memory than needed + if document.used_text > len(document.text) { + new_text := make([]u8, document.used_text * 2); + + //join the 3 splices into the text + copy(new_text, lower); + copy(new_text[len(lower):], middle); + copy(new_text[len(lower)+len(middle):], upper); + + delete(document.text); + + document.text = new_text; + } + + else { + //order matters here, we need to make sure we swap the data already in the text before the middle + copy(document.text, lower); + copy(document.text[len(lower)+len(middle):], upper); + copy(document.text[len(lower):], middle); + } + + } + + return document_refresh(document, config, writer, true); +} + +document_close :: proc(uri_string: string) -> common.Error { + + uri, parsed_ok := common.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.uri.path); + return .InvalidRequest; + } + + document.client_owned = false; + + return .None; +} + + + +document_refresh :: proc(document: ^Document, config: ^common.Config, writer: ^Writer, parse_imports: bool) -> common.Error { + + errors, ok := parse_document(document, config); + + if !ok { + return .ParseError; + } + + //right now we don't allow to writer errors out from files read from the file directory, core files, etc. + if writer != nil && len(errors) > 0 { + document.diagnosed_errors = true; + + params := NotificationPublishDiagnosticsParams { + uri = document.uri.uri, + diagnostics = make([] Diagnostic, len(errors), context.temp_allocator), + }; + + for error, i in errors { + + params.diagnostics[i] = Diagnostic { + range = common.Range { + start = common.Position { + line = error.line - 1, + character = 0, + }, + end = common.Position { + line = error.line, + character = 0, + }, + }, + severity = DiagnosticSeverity.Error, + code = "test", + message = error.message, + }; + + } + + notifaction := Notification { + jsonrpc = "2.0", + method = "textDocument/publishDiagnostics", + params = params, + }; + + send_notification(notifaction, writer); + + } + + if writer != nil && len(errors) == 0 { + + //send empty diagnosis to remove the clients errors + if document.diagnosed_errors { + + notifaction := Notification { + jsonrpc = "2.0", + method = "textDocument/publishDiagnostics", + + params = NotificationPublishDiagnosticsParams { + uri = document.uri.uri, + diagnostics = make([] Diagnostic, len(errors), context.temp_allocator), + }, + }; + + document.diagnosed_errors = false; + + send_notification(notifaction, writer); + } + + } + + return .None; +} + +document_load_package :: proc(package_directory: string, config: ^common.Config) -> common.Error { + + fd, err := os.open(package_directory); + + if err != 0 { + return .ParseError; + } + + files: []os.File_Info; + files, err = os.read_dir(fd, 100, context.temp_allocator); + + for file in files { + + //if we have never encountered the document + if _, ok := document_storage.documents[file.fullpath]; !ok { + + if doc_err := document_new(file.fullpath, config); doc_err != .None { + return doc_err; + } + + } + + } + + return .None; +} + + +current_errors: [dynamic] ParserError; + +parser_error_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { + error := ParserError { line = pos.line, column = pos.column, file = pos.file, + offset = pos.offset, message = fmt.tprintf(msg, ..args) }; + append(¤t_errors, error); +} + +parser_warning_handler :: proc(pos: tokenizer.Pos, msg: string, args: ..any) { + +} + +parse_document :: proc(document: ^Document, config: ^common.Config) -> ([] ParserError, bool) { + + p := parser.Parser { + err = parser_error_handler, + warn = parser_warning_handler, + }; + + current_errors = make([dynamic] ParserError, context.temp_allocator); + + document.ast = ast.File { + fullpath = document.uri.path, + src = document.text[:document.used_text], + }; + + parser.parse_file(&p, &document.ast); + + /* + if document.imports != nil { + delete(document.imports); + delete(document.package_name); + } + document.imports = make([]string, len(document.ast.imports)); + document.package_name = document.ast.pkg_name; + + for imp, index in document.ast.imports { + + //collection specified + if i := strings.index(imp.fullpath, ":"); i != -1 { + + collection := imp.fullpath[1:i]; + p := imp.fullpath[i+1:len(imp.fullpath)-1]; + + dir, ok := config.collections[collection]; + + if !ok { + continue; + } + + document.imports[index] = path.join(dir, p); + + } + + //relative + else { + + } + } + */ + + return current_errors[:], true; +} \ No newline at end of file diff --git a/src/server/log.odin b/src/server/log.odin new file mode 100644 index 0000000..5ed007e --- /dev/null +++ b/src/server/log.odin @@ -0,0 +1,64 @@ +package server + +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", text); + + notification := Notification { + jsonrpc = "2.0", + method = "window/logMessage", + params = NotificationLoggingParams { + type = 1, + message = message, + } + }; + + send_notification(notification, data.writer); +} + diff --git a/src/server/reader.odin b/src/server/reader.odin new file mode 100644 index 0000000..f421d67 --- /dev/null +++ b/src/server/reader.odin @@ -0,0 +1,64 @@ +package server + +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/server/requests.odin b/src/server/requests.odin new file mode 100644 index 0000000..cd8e734 --- /dev/null +++ b/src/server/requests.odin @@ -0,0 +1,364 @@ +package server + +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" + +import "shared:common" + + +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: ^common.Config, writer: ^Writer) -> bool { + + 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, ^common.Config, ^Writer) -> common.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, + "textDocument/didSave" = notification_did_save, + "textDocument/definition" = request_definition }; + + fn: proc(json.Value, RequestId, ^common.Config, ^Writer) -> common.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: ^common.Config, writer: ^Writer) -> common.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]common.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 + definitionProvider = true, + }, + }, + id = id, + ); + + send_response(response, writer); + + return .None; +} + +request_initialized :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { + return .None; +} + +request_shutdown :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { + + response := make_response_message( + params = nil, + id = id, + ); + + send_response(response, writer); + + return .None; +} + +request_definition :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { + + params_object, ok := params.value.(json.Object); + + if !ok { + return .ParseError; + } + + definition_params: TextDocumentPositionParams; + + if unmarshal(params, definition_params, context.temp_allocator) != .None { + return .ParseError; + } + + + document := document_get(definition_params.textDocument.uri); + + if document == nil { + return .InternalError; + } + + location, ok2 := get_definition_location(document, definition_params.position); + + if !ok2 { + log.error("Failed to get definition location"); + return .InternalError; + } + + response := make_response_message( + params = location, + id = id, + ); + + send_response(response, writer); + + + return .None; +} + +notification_exit :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { + config.running = false; + return .None; +} + +notification_did_open :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { + + params_object, ok := params.value.(json.Object); + + if !ok { + log.error("Failed to parse open document notification"); + return .ParseError; + } + + open_params: DidOpenTextDocumentParams; + + if unmarshal(params, open_params, context.allocator) != .None { + log.error("Failed to parse open document notification"); + return .ParseError; + } + + return document_open(open_params.textDocument.uri, open_params.textDocument.text, config, writer); +} + +notification_did_change :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.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; + } + + document_apply_changes(change_params.textDocument.uri, change_params.contentChanges, config, writer); + + return .None; +} + +notification_did_close :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.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); +} + +notification_did_save :: proc(params: json.Value, id: RequestId, config: ^common.Config, writer: ^Writer) -> common.Error { + + + + return .None; +} + diff --git a/src/server/response.odin b/src/server/response.odin new file mode 100644 index 0000000..bd7a77e --- /dev/null +++ b/src/server/response.odin @@ -0,0 +1,68 @@ +package server + + +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/server/types.odin b/src/server/types.odin new file mode 100644 index 0000000..262f464 --- /dev/null +++ b/src/server/types.odin @@ -0,0 +1,154 @@ +package server + +import "core:encoding/json" + +import "shared:common" + +/* + General types +*/ + +//TODO(Daniel, move some of the more specific structs to their appropriate place) + +RequestId :: union { + string, + i64, +}; + +ResponseParams :: union { + ResponseInitializeParams, + rawptr, + common.Location, +}; + +ResponseMessage :: struct { + jsonrpc: string, + id: RequestId, + result: ResponseParams, +}; + +ResponseMessageError :: struct { + jsonrpc: string, + id: RequestId, + error: ResponseError, +}; + +ResponseError :: struct { + code: common.Error, + message: string, +}; + +NotificationLoggingParams :: struct { + type: int, + message: string, +}; + +NotificationPublishDiagnosticsParams :: struct { + uri: string, + diagnostics: [] Diagnostic, +}; + +NotificationParams :: union { + NotificationLoggingParams, + NotificationPublishDiagnosticsParams, +}; + +Notification :: struct { + jsonrpc: string, + method: string, + params: NotificationParams +}; + +ResponseInitializeParams :: struct { + capabilities: ServerCapabilities, +}; + +RequestInitializeParams :: struct { + trace: string, + workspaceFolders: [dynamic] common.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, + definitionProvider: bool, +}; + +CompletionClientCapabilities :: struct { + +}; + +HoverClientCapabilities :: struct { + dynamicRegistration: bool, + contentFormat: [dynamic] MarkupKind, +}; + +TextDocumentClientCapabilities :: struct { + completion: CompletionClientCapabilities, + hover: HoverClientCapabilities, +}; + +ClientCapabilities :: struct { + textDocument: TextDocumentClientCapabilities, +}; + +TextDocumentContentChangeEvent :: struct { + range: common.Range, + text: string, +}; + +Version :: union { + int, + json.Null, +}; + +VersionedTextDocumentIdentifier :: struct { + uri: string, +}; + +TextDocumentIdentifier :: struct { + uri: string, +}; + +TextDocumentItem :: struct { + uri: string, + text: string, +}; + +DiagnosticSeverity :: enum { + Error = 1, + Warning = 2, + Information = 3, + Hint = 4, +}; + +Diagnostic :: struct { + range: common.Range, + severity: DiagnosticSeverity, + code: string, + message: string, +}; + +DidOpenTextDocumentParams :: struct { + textDocument: TextDocumentItem, +}; + +DidChangeTextDocumentParams :: struct { + textDocument: VersionedTextDocumentIdentifier, + contentChanges: [dynamic] TextDocumentContentChangeEvent, +}; + +DidCloseTextDocumentParams :: struct { + textDocument: TextDocumentIdentifier, +}; + +TextDocumentPositionParams :: struct { + textDocument: TextDocumentIdentifier, + position: common.Position, +}; \ No newline at end of file diff --git a/src/server/unmarshal.odin b/src/server/unmarshal.odin new file mode 100644 index 0000000..e5ca619 --- /dev/null +++ b/src/server/unmarshal.odin @@ -0,0 +1,129 @@ +package server + +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.. (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/src/types.odin b/src/types.odin deleted file mode 100644 index f8964e5..0000000 --- a/src/types.odin +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import "core:encoding/json" - -/* - General types -*/ - -//TODO(Daniel, move some of the more specific structs to their appropriate place) - -RequestId :: union { - string, - i64, -}; - -ResponseParams :: union { - ResponseInitializeParams, - rawptr, - Location, -}; - -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, -}; - -NotificationPublishDiagnosticsParams :: struct { - uri: string, - diagnostics: [] Diagnostic, -}; - -NotificationParams :: union { - NotificationLoggingParams, - NotificationPublishDiagnosticsParams, -}; - -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, - definitionProvider: bool, -}; - -CompletionClientCapabilities :: struct { - -}; - -HoverClientCapabilities :: struct { - dynamicRegistration: bool, - contentFormat: [dynamic] MarkupKind, -}; - -TextDocumentClientCapabilities :: struct { - completion: CompletionClientCapabilities, - hover: HoverClientCapabilities, -}; - -ClientCapabilities :: struct { - textDocument: TextDocumentClientCapabilities, -}; - -Position :: struct { - line: int, - character: int, -}; - -Range :: struct { - start: Position, - end: Position, -}; - -Location :: struct { - uri: string, - range: Range, -}; - -TextDocumentContentChangeEvent :: struct { - range: Range, - text: string, -}; - -Version :: union { - int, - json.Null, -}; - -VersionedTextDocumentIdentifier :: struct { - uri: string, -}; - -TextDocumentIdentifier :: struct { - uri: string, -}; - -TextDocumentItem :: struct { - uri: string, - text: string, -}; - -DiagnosticSeverity :: enum { - Error = 1, - Warning = 2, - Information = 3, - Hint = 4, -}; - -Diagnostic :: struct { - range: Range, - severity: DiagnosticSeverity, - code: string, - message: string, -}; - -DidOpenTextDocumentParams :: struct { - textDocument: TextDocumentItem, -}; - -DidChangeTextDocumentParams :: struct { - textDocument: VersionedTextDocumentIdentifier, - contentChanges: [dynamic] TextDocumentContentChangeEvent, -}; - -DidCloseTextDocumentParams :: struct { - textDocument: TextDocumentIdentifier, -}; - -TextDocumentPositionParams :: struct { - textDocument: TextDocumentIdentifier, - position: Position, -}; \ No newline at end of file diff --git a/src/unmarshal.odin b/src/unmarshal.odin deleted file mode 100644 index 297bd89..0000000 --- a/src/unmarshal.odin +++ /dev/null @@ -1,129 +0,0 @@ -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.. (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.uri = strings.clone(value); - uri.decode_full = decoded; - uri.path = decoded[len(starts):]; - - return uri, true; -} - - -//Note(Daniel, Again some really incomplete and scuffed uri writer) -create_uri :: proc(path: string, allocator := context.allocator) -> Uri { - - builder := strings.make_builder(allocator); - - strings.write_string(&builder, "file:///"); - strings.write_string(&builder, encode_percent(path, context.temp_allocator)); - - uri: Uri; - - uri.uri = strings.to_string(builder); - uri.decode_full = strings.clone(path, allocator); - uri.path = uri.decode_full; - - return uri; -} - -delete_uri :: proc(uri: Uri) { - - if uri.uri != "" { - delete(uri.uri); - } - - if uri.decode_full != "" { - delete(uri.decode_full); - } -} - -encode_percent :: proc(value: string, allocator: mem.Allocator) -> string { - - builder := strings.make_builder(allocator); - - data := transmute([]u8)value; - index: int; - - for index < len(value) { - - r, w := utf8.decode_rune(data[index:]); - - if r > 127 || r == ':'{ - - for i := 0; i < w; i += 1 { - strings.write_string(&builder, strings.concatenate({"%", fmt.tprintf("%X", data[index+i])}, - context.temp_allocator)); - } - - } - - else { - strings.write_byte(&builder, data[index]); - } - - index += w; - } - - return strings.to_string(builder); -} - -@(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/workspace.odin b/src/workspace.odin deleted file mode 100644 index 8b13789..0000000 --- a/src/workspace.odin +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/writer.odin b/src/writer.odin deleted file mode 100644 index ccb1762..0000000 --- a/src/writer.odin +++ /dev/null @@ -1,29 +0,0 @@ -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; -} - - -- cgit v1.2.3