From 2b1d406a6e65b4cf62c16e348f42c9ff3ee5d76b Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Tue, 18 Jul 2023 20:26:03 -0400 Subject: [PATCH 01/17] feat: Add Completion --- compiler/src/diagnostics/modules.re | 9 +- compiler/src/language_server/completion.re | 496 ++++++++++++++++++++ compiler/src/language_server/completion.rei | 39 ++ compiler/src/language_server/driver.re | 6 + compiler/src/language_server/hover.re | 3 +- compiler/src/language_server/initialize.re | 14 + compiler/src/language_server/message.re | 15 + compiler/src/language_server/message.rei | 5 + compiler/src/language_server/trace.re | 12 +- 9 files changed, 590 insertions(+), 9 deletions(-) create mode 100644 compiler/src/language_server/completion.re create mode 100644 compiler/src/language_server/completion.rei diff --git a/compiler/src/diagnostics/modules.re b/compiler/src/diagnostics/modules.re index 052f8a25e..344764a7b 100644 --- a/compiler/src/diagnostics/modules.re +++ b/compiler/src/diagnostics/modules.re @@ -1,16 +1,17 @@ open Grain_typed; -type export_kind = +type provide_kind = | Function | Value | Record | Enum | Abstract - | Exception; + | Exception + | Module; -type export = { +type provide = { name: string, - kind: export_kind, + kind: provide_kind, signature: string, }; diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re new file mode 100644 index 000000000..916514a74 --- /dev/null +++ b/compiler/src/language_server/completion.re @@ -0,0 +1,496 @@ +open Grain_utils; +open Grain_typed; +open Grain_diagnostics; +open Sourcetree; + +// This is the full enumeration of all CompletionItemKind as declared by the language server +// protocol (https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind), +// but not all will be used by Grain LSP +[@deriving (enum, yojson)] +type completion_item_kind = + // Since these are using ppx_deriving enum, order matters + | [@value 1] CompletionItemKindText + | CompletionItemKindMethod + | CompletionItemKindFunction + | CompletionItemKindConstructor + | CompletionItemKindField + | CompletionItemKindVariable + | CompletionItemKindClass + | CompletionItemKindInterface + | CompletionItemKindModule + | CompletionItemKindProperty + | CompletionItemKindUnit + | CompletionItemKindValue + | CompletionItemKindEnum + | CompletionItemKindKeyword + | CompletionItemKindSnippet + | CompletionItemKindColor + | CompletionItemKindFile + | CompletionItemKindReference + | CompletionItemKindFolder + | CompletionItemKindEnumMember + | CompletionItemKindConstant + | CompletionItemKindStruct + | CompletionItemKindEvent + | CompletionItemKindOperator + | CompletionItemKindTypeParameter; + +[@deriving (enum, yojson)] +type completion_trigger_kind = + // Since these are using ppx_deriving enum, order matters + | [@value 1] CompletionTriggerInvoke + | CompletionTriggerCharacter + | CompletionTriggerForIncompleteCompletions; + +let completion_item_kind_to_yojson = severity => + completion_item_kind_to_enum(severity) |> [%to_yojson: int]; +let completion_item_kind_of_yojson = json => + Result.bind(json |> [%of_yojson: int], value => { + switch (completion_item_kind_of_enum(value)) { + | Some(severity) => Ok(severity) + | None => Result.Error("Invalid enum value") + } + }); + +let completion_trigger_kind_to_yojson = kind => + completion_trigger_kind_to_enum(kind) |> [%to_yojson: int]; +let completion_trigger_kind_of_yojson = json => + Result.bind(json |> [%of_yojson: int], value => { + switch (completion_trigger_kind_of_enum(value)) { + | Some(kind) => Ok(kind) + | None => Result.Error("Invalid enum value") + } + }); + +[@deriving yojson] +type completion_item = { + label: string, + kind: completion_item_kind, + detail: string, + documentation: string, +}; + +[@deriving yojson({strict: false})] +type completion_context = { + [@key "triggerKind"] + trigger_kind: completion_trigger_kind, + [@key "triggerCharacter"] [@default None] + trigger_character: option(string), +}; + +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionParams +module RequestParams = { + [@deriving yojson({strict: false})] + type t = { + [@key "textDocument"] + text_document: Protocol.text_document_identifier, + position: Protocol.position, + [@default None] + context: option(completion_context), + }; +}; + +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionList +module ResponseResult = { + [@deriving yojson] + type t = { + isIncomplete: bool, + items: list(completion_item), + }; +}; + +// maps Grain types to LSP CompletionItemKind +let rec get_kind = (desc: Types.type_desc) => + switch (desc) { + | TTyVar(_) => CompletionItemKindVariable + | TTyArrow(_) => CompletionItemKindFunction + | TTyTuple(_) => CompletionItemKindStruct + | TTyRecord(_) => CompletionItemKindStruct + | TTyConstr(_) => CompletionItemKindConstructor + | TTySubst(s) => get_kind(s.desc) + | TTyLink(t) => get_kind(t.desc) + | _ => CompletionItemKindText + }; + +let send_completion = + (~id: Protocol.message_id, completions: list(completion_item)) => { + Protocol.response( + ~id, + ResponseResult.to_yojson({isIncomplete: false, items: completions}), + ); +}; + +module Resolution = { + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem + module RequestParams = { + // TODO: implement the rest of the fields + [@deriving yojson({strict: false})] + type t = {label: string}; + }; + + // As per https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion + // If computing full completion items is expensive, servers can additionally provide a handler for + // the completion item resolve request (‘completionItem/resolve’). This request is sent when a + // completion item is selected in the user interface. + let process = + ( + ~id: Protocol.message_id, + ~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), + ~documents: Hashtbl.t(Protocol.uri, string), + params: RequestParams.t, + ) => { + Trace.log("Completable: Resolution Request Recieved"); + // Right now we just resolve nothing to clear the client's request + // In future we may want to send more details back with Graindoc details for example + send_completion( + ~id, + [ + { + label: "testing", + kind: CompletionItemKindValue, + detail: "Where does this go deatil", + documentation: "test item", + }, + ], + ); + }; +}; + +type completionState = + | ComletableCode(string) + | CompletableExpr(string) + | CompletableType(string); + +let find_completable_state = (documents, uri, positon: Protocol.position) => { + // try and find the code we are completing in the original source + switch (Hashtbl.find_opt(documents, uri)) { + | None => None + | Some(source_code) => + // TODO: We need to handle crlf, and also mutability == bad + // Get Document + let lines = String.split_on_char('\n', source_code); + // TODO: Handle Multiple Lines, so we need to convert to an index + let index = positon.character; + // Calculate Current Position + Trace.log( + "Completable: Completable Line " ++ List.nth(lines, positon.line), + ); + // Search File To Grab Context + let rec searchForContext = (search_code, offset) => { + // If Last Element is = then we are in expr + // If Last Element is : then we are in type + // If Last Element was { + // If we detect a use before than we are in a from + // Otherwise we are in a ComletableCode + switch (String_utils.char_at(search_code, offset)) { + // TODO: Return Proper Inside + | Some('=') => + CompletableExpr( + String_utils.slice(~first=offset, ~last=index, search_code), + ) + | Some(':') => + CompletableType( + String_utils.slice(~first=offset, ~last=index, search_code), + ) + // TODO: Search for using statements + // | Some('{') => + // CompletableExpr( + // String_utils.slice(~first=offset, ~last=index, search_code), + // ) + | _ when offset <= 0 => ComletableCode(search_code) + | _ => searchForContext(search_code, offset - 1) + }; + }; + let context = + searchForContext(List.nth(lines, positon.line), positon.character); + Some(context); + }; +}; + +let build_keyword_completion = keyword => { + { + // TODO: Would be better if these were actual snippet completions + label: keyword, + kind: CompletionItemKindKeyword, + detail: "", + documentation: "", + }; +}; +let get_completions = + ( + program: Typedtree.typed_program, + compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), + root: list(string), + current_search: string, + include_keywords: bool, + include_modules: bool, + include_values: bool, + ) => { + // Find Keywords + let keyword_completions = + if (include_keywords) { + // TODO: Include all keywords, Maybe figure out if i can grab these from some place + let keywords = [ + "let", + "type", + "module", + "include", + "from", + "record", + "enum", + "provide", + "abstract", + "if", + "while", + "for", + ]; + List.map(build_keyword_completion, keywords); + } else { + []; + }; + // TODO: Get The Current Environment, using the path + let getEnvironment = (env, path: list(string)) => { + let rec getModuleEnv = (env, module_path, path: list(string)) => { + switch (path) { + | [pathItem] => + // Get The Module's Exports + Some(([], [])) + | [pathItem, ...path] => + // Get The Module's Exports + let provides = Env.find_modtype(module_path, env).mtd_type; + switch (provides) { + // Figure out why this can occur + | None => None + | Some(TModSignature(provides)) => + // Scan the List, try and find module exports + let needed_module = List.find(provide => true, provides); + Some(([], [])); + | Some(_) => + Trace.log("Completable: Not Found Direct Module Signature"); + None; + }; + // TODO: I think this is an impossible case + | [] => None + }; + }; + // If we are going down to modules we go one way, otherwise we go the other + Some( + switch (path) { + // Base + | [] => + let modules = + Env.fold_modules( + (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, + None, + env, + [], + ); + let values = + Env.fold_values( + (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, + None, + env, + [], + ); + (modules, values); + // We Need to go deeper + | [baseModule, ...rest] => + // TODO: There has to be a better way todo this + let modules = + Env.fold_modules( + (tag, path, decl, acc) => + if (String_utils.starts_with(tag, baseModule)) { + List.append(acc, [path]); + } else { + acc; + }, + None, + env, + [], + ); + switch (modules) { + // We did not find the module + | [] => ([], []) + // We found at least one matching module + | [mod_path, ..._] => + switch (getModuleEnv(env, mod_path, rest)) { + | None => ([], []) + | Some(x) => x + } + }; + }, + ); + }; + let environment_completions = + switch (getEnvironment(program.env, root)) { + | None => [] + | Some((modules, values)) => + // TODO: Find Modules + let module_completions = + if (include_modules) { + List.map( + ((tag, decl)) => + { + label: tag, + kind: CompletionItemKindModule, + detail: "", + documentation: "", + }, + modules, + ); + } else { + []; + }; + // TODO: Find Values + let value_completions = + if (include_values) { + List.map( + ((tag, decl: Types.value_description)) => + { + label: tag, + kind: get_kind(decl.val_type.desc), + detail: Printtyp.string_of_type_scheme(decl.val_type), + documentation: "", + }, + values, + ); + } else { + []; + }; + // Merge Our Results + List.concat([module_completions, value_completions]); + }; + // Merge Our Results + let completions: list(completion_item) = + List.concat([keyword_completions, environment_completions]); + // Filter Our Results + let filtered_completions = + List.filter( + ({label}) => + String_utils.starts_with( + StringLabels.lowercase_ascii(label), + StringLabels.lowercase_ascii(current_search), + ), + completions, + ); + // Return Our Results + filtered_completions; +}; + +let process = + ( + ~id: Protocol.message_id, + ~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), + ~documents: Hashtbl.t(Protocol.uri, string), + params: RequestParams.t, + ) => { + switch (Hashtbl.find_opt(compiled_code, params.text_document.uri)) { + | None => send_completion(~id, []) + | Some({program, sourcetree}) => + // Get Completable state and filter + let completableState = + find_completable_state( + documents, + params.text_document.uri, + params.position, + ); + // Collect Completables + let completions = + switch (completableState) { + | None => [] + | Some(completableState) => + switch (completableState) { + | ComletableCode(str) => + Trace.log("Completable: Code " ++ str); + let completionPath = String.split_on_char('.', str); + switch (List.rev(completionPath)) { + | [] + | ["", ""] => [] + | [search] => + get_completions( + program, + compiled_code, + [], + search, + true, + true, + true, + ) + | [search, ...root] => + get_completions( + program, + compiled_code, + List.rev(root), + search, + false, + true, + true, + ) + }; + // switch (pathRoot) { + // // Just A . + // | None => [] + // // A word no dot + // | Some(true) => + // // TODO: Suggest Keywords + // let module_completions = + // List.map( + // (i: string) => { + // let item: completion_item = { + // label: i, + // kind: CompletionItemKindModule, + // detail: "", + // documentation: "", + // }; + // item; + // }, + // modules, + // ); + // let values: list((string, Types.value_description)) = + // Env.fold_values( + // (tag, path, vd, acc) => {List.append(acc, [(tag, vd)])}, + // None, + // program.env, + // [], + // ); + // let valueCompletions = + // List.map( + // ((i: string, l: Types.value_description)) => { + // let item: completion_item = { + // label: i, + // kind: get_kind(l.val_type.desc), + // detail: Printtyp.string_of_type_scheme(l.val_type), + // documentation: "", + // }; + // item; + // }, + // values, + // ); + // let completions = + // List.concat([module_completions, valueCompletions]); + // List.filter( + // ({label}) => + // String.starts_with( + // ~prefix=StringLabels.lowercase_ascii(str), + // StringLabels.lowercase_ascii(label), + // ), + // completions, + // ); + // // A Module Path + // | Some(false) => [] + // }; + | CompletableExpr(str) => + Trace.log("Completable: Expr " ++ str); + // TODO: Build Path + // TODO: Get Module If Available + // TODO: Filter Out Operators + []; + | CompletableType(str) => + Trace.log("Completable: Type " ++ str); + // TODO: Suggest Type Info + []; + } + }; + send_completion(~id, completions); + }; +}; diff --git a/compiler/src/language_server/completion.rei b/compiler/src/language_server/completion.rei new file mode 100644 index 000000000..931f1913f --- /dev/null +++ b/compiler/src/language_server/completion.rei @@ -0,0 +1,39 @@ +open Grain_typed; + +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionParams +module RequestParams: { + [@deriving yojson({strict: false})] + type t; +}; + +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionList +module ResponseResult: { + [@deriving yojson] + type t; +}; + +let process: + ( + ~id: Protocol.message_id, + ~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), + ~documents: Hashtbl.t(Protocol.uri, string), + RequestParams.t + ) => + unit; + +module Resolution: { + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem + module RequestParams: { + [@deriving yojson({strict: false})] + type t; + }; + + let process: + ( + ~id: Protocol.message_id, + ~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), + ~documents: Hashtbl.t(Protocol.uri, string), + RequestParams.t + ) => + unit; +}; \ No newline at end of file diff --git a/compiler/src/language_server/driver.re b/compiler/src/language_server/driver.re index 7a30f64a2..d3ac0ce16 100644 --- a/compiler/src/language_server/driver.re +++ b/compiler/src/language_server/driver.re @@ -31,6 +31,12 @@ let process = msg => { | TextDocumentCodeLens(id, params) when is_initialized^ => Lenses.process(~id, ~compiled_code, ~documents, params); Reading; + | TextDocumentCompletion(id, params) when is_initialized^ => + Completion.process(~id, ~compiled_code, ~documents, params); + Reading; + | CompletionItemResolve(id, params) when is_initialized^ => + Completion.Resolution.process(~id, ~compiled_code, ~documents, params); + Reading; | Shutdown(id, params) when is_initialized^ => Shutdown.process(~id, ~compiled_code, ~documents, params); is_shutting_down := true; diff --git a/compiler/src/language_server/hover.re b/compiler/src/language_server/hover.re index a3009e6ac..88447a06e 100644 --- a/compiler/src/language_server/hover.re +++ b/compiler/src/language_server/hover.re @@ -91,7 +91,7 @@ let module_lens = (decl: Types.module_declaration) => { let vals = Modules.get_provides(decl); let signatures = List.map( - (v: Modules.export) => + (v: Modules.provide) => switch (v.kind) { | Function | Value => Format.sprintf("let %s", v.signature) @@ -99,6 +99,7 @@ let module_lens = (decl: Types.module_declaration) => { | Enum | Abstract | Exception => v.signature + | Module => Format.sprintf("module %s", v.name) }, vals, ); diff --git a/compiler/src/language_server/initialize.re b/compiler/src/language_server/initialize.re index 57a6c851d..5cb3e4d23 100644 --- a/compiler/src/language_server/initialize.re +++ b/compiler/src/language_server/initialize.re @@ -27,6 +27,14 @@ module RequestParams = { // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeResult module ResponseResult = { + [@deriving yojson] + type completion_values = { + [@key "resolveProvider"] + resolve_provider: bool, + [@key "triggerCharacters"] + trigger_characters: list(string), + }; + [@deriving yojson] type code_values = { [@key "resolveProvider"] @@ -41,6 +49,8 @@ module ResponseResult = { text_document_sync: Protocol.text_document_sync_kind, [@key "hoverProvider"] hover_provider: bool, + [@key "completionProvider"] + completion_provider: completion_values, [@key "definitionProvider"] definition_provider: Protocol.definition_client_capabilities, [@key "typeDefinitionProvider"] @@ -69,6 +79,10 @@ module ResponseResult = { document_formatting_provider: true, text_document_sync: Full, hover_provider: true, + completion_provider: { + resolve_provider: true, + trigger_characters: ["."], + }, definition_provider: { link_support: true, }, diff --git a/compiler/src/language_server/message.re b/compiler/src/language_server/message.re index cdb277f5e..280dbeefd 100644 --- a/compiler/src/language_server/message.re +++ b/compiler/src/language_server/message.re @@ -4,6 +4,11 @@ type t = | Initialize(Protocol.message_id, Initialize.RequestParams.t) | TextDocumentHover(Protocol.message_id, Hover.RequestParams.t) | TextDocumentCodeLens(Protocol.message_id, Lenses.RequestParams.t) + | TextDocumentCompletion(Protocol.message_id, Completion.RequestParams.t) + | CompletionItemResolve( + Protocol.message_id, + Completion.Resolution.RequestParams.t, + ) | Shutdown(Protocol.message_id, Shutdown.RequestParams.t) | Exit(Protocol.message_id, Exit.RequestParams.t) | TextDocumentDidOpen(Protocol.uri, Code_file.DidOpen.RequestParams.t) @@ -37,6 +42,16 @@ let of_request = (msg: Protocol.request_message): t => { | Ok(params) => TextDocumentCodeLens(id, params) | Error(msg) => Error(msg) } + | {method: "textDocument/completion", id: Some(id), params: Some(params)} => + switch (Completion.RequestParams.of_yojson(params)) { + | Ok(params) => TextDocumentCompletion(id, params) + | Error(msg) => Error(msg) + } + | {method: "completionItem/resolve", id: Some(id), params: Some(params)} => + switch (Completion.Resolution.RequestParams.of_yojson(params)) { + | Ok(params) => CompletionItemResolve(id, params) + | Error(msg) => Error(msg) + } | {method: "shutdown", id: Some(id), params: None} => switch (Shutdown.RequestParams.of_yojson(`Null)) { | Ok(params) => Shutdown(id, params) diff --git a/compiler/src/language_server/message.rei b/compiler/src/language_server/message.rei index 1a72f4841..a944075db 100644 --- a/compiler/src/language_server/message.rei +++ b/compiler/src/language_server/message.rei @@ -2,6 +2,11 @@ type t = | Initialize(Protocol.message_id, Initialize.RequestParams.t) | TextDocumentHover(Protocol.message_id, Hover.RequestParams.t) | TextDocumentCodeLens(Protocol.message_id, Lenses.RequestParams.t) + | TextDocumentCompletion(Protocol.message_id, Completion.RequestParams.t) + | CompletionItemResolve( + Protocol.message_id, + Completion.Resolution.RequestParams.t, + ) | Shutdown(Protocol.message_id, Shutdown.RequestParams.t) | Exit(Protocol.message_id, Exit.RequestParams.t) | TextDocumentDidOpen(Protocol.uri, Code_file.DidOpen.RequestParams.t) diff --git a/compiler/src/language_server/trace.re b/compiler/src/language_server/trace.re index 80a39ab5f..84ce08d79 100644 --- a/compiler/src/language_server/trace.re +++ b/compiler/src/language_server/trace.re @@ -36,10 +36,14 @@ let log = (~verbose=?, message: string) => switch (trace_level^) { | Off => () | Messages => - Protocol.notification( - ~method="$/logTrace", - NotificationParams.to_yojson({message, verbose: None}), - ) + if (String.starts_with(~prefix="Completable:", message)) { + Protocol.notification( + ~method="$/logTrace", + NotificationParams.to_yojson({message, verbose: None}), + ); + } else { + (); + } | Verbose => Protocol.notification( ~method="$/logTrace", From acc91e8066ff6af3568c90e16d4b4df3cdda4e4f Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Thu, 20 Jul 2023 18:52:56 -0400 Subject: [PATCH 02/17] feat: Actual Completion Working --- compiler/src/language_server/completion.re | 407 ++++++++++----------- compiler/src/language_server/doc.re | 68 ++++ compiler/src/language_server/doc.rei | 9 + compiler/src/language_server/hover.re | 69 +--- 4 files changed, 274 insertions(+), 279 deletions(-) create mode 100644 compiler/src/language_server/doc.re create mode 100644 compiler/src/language_server/doc.rei diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 916514a74..96b6fd55a 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -182,15 +182,19 @@ let find_completable_state = (documents, uri, positon: Protocol.position) => { // If Last Element was { // If we detect a use before than we are in a from // Otherwise we are in a ComletableCode + // TODO: Support other places, improve this half parser switch (String_utils.char_at(search_code, offset)) { - // TODO: Return Proper Inside | Some('=') => CompletableExpr( - String_utils.slice(~first=offset, ~last=index, search_code), + String.trim( + String_utils.slice(~first=offset + 2, ~last=index, search_code), + ), ) | Some(':') => CompletableType( - String_utils.slice(~first=offset, ~last=index, search_code), + String.trim( + String_utils.slice(~first=offset, ~last=index, search_code), + ), ) // TODO: Search for using statements // | Some('{') => @@ -216,20 +220,113 @@ let build_keyword_completion = keyword => { documentation: "", }; }; -let get_completions = +let build_value_completion = + (~env, (value_name: string, value_desc: Types.value_description)) => { + { + label: value_name, + // TODO: Consider Making This More Fine Grained, based off the type + kind: CompletionItemKindValue, + detail: Doc.print_type(env, value_desc.val_type), + // TODO: Maybe generate grain doc + documentation: "", + }; +}; +let build_module_completion = + (~env, (module_name: string, mod_desc: Types.module_declaration)) => { + { + label: module_name, + kind: CompletionItemKindModule, + detail: Doc.print_mod_type(mod_desc), + // TODO: Maybe generate grain doc + documentation: "", + }; +}; + +let build_type_completion = + (~env, (type_name: string, type_desc: Types.type_declaration)) => { + { + label: type_name, + kind: CompletionItemKindTypeParameter, + // TODO: Add a kind here + detail: "", + // TODO: Maybe generate grain doc + documentation: "", + }; +}; + +let rec get_completions = + ( + ~include_values, + ~include_types, + env, + root_decl: Types.module_declaration, + path: list(string), + ) => { + // Get The Modules Exports + let provides = + switch (root_decl.md_type) { + | TModSignature(provides) => + List.map( + (s: Types.signature_item) => { + switch (s) { + // Enabled + | TSigValue(ident, decl) when include_values => + Some(build_value_completion(~env, (ident.name, decl))) + | TSigType(ident, decl, _) when include_types => + Some(build_type_completion(~env, (ident.name, decl))) + | TSigModule(ident, decl, _) => + Some(build_module_completion(~env, (ident.name, decl))) + // Dissabled + | TSigValue(_, _) + | TSigType(_, _, _) + | TSigTypeExt(_, _, _) + | TSigModType(_, _) => None + } + }, + provides, + ) + | _ => [] + }; + // Filter + switch (path) { + | [_] + | [] => List.filter_map(x => x, provides) + | [pathItem, ...path] => + // Find the desired module + let subMod = + switch (root_decl.md_type) { + | TModSignature(provides) => + List.find_opt( + (s: Types.signature_item) => { + switch (s) { + | TSigModule(ident, decl, _) when ident.name == pathItem => true + | _ => false + } + }, + provides, + ) + | _ => None + }; + switch (subMod) { + | Some(TSigModule(_, decl, _)) => + get_completions(env, decl, path, ~include_values, ~include_types) + | _ => [] + }; + }; +}; +let get_top_level_completions = ( + ~include_keywords, + ~include_values, + ~include_types, program: Typedtree.typed_program, compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), - root: list(string), - current_search: string, - include_keywords: bool, - include_modules: bool, - include_values: bool, + search: list(string), ) => { - // Find Keywords + // Keyword Completions + // TODO: Include all keywords, Maybe figure out if i can grab these from some place let keyword_completions = if (include_keywords) { - // TODO: Include all keywords, Maybe figure out if i can grab these from some place let keywords = [ "let", "type", @@ -237,6 +334,7 @@ let get_completions = "include", "from", "record", + "rec", "enum", "provide", "abstract", @@ -248,133 +346,64 @@ let get_completions = } else { []; }; - // TODO: Get The Current Environment, using the path - let getEnvironment = (env, path: list(string)) => { - let rec getModuleEnv = (env, module_path, path: list(string)) => { - switch (path) { - | [pathItem] => - // Get The Module's Exports - Some(([], [])) - | [pathItem, ...path] => - // Get The Module's Exports - let provides = Env.find_modtype(module_path, env).mtd_type; - switch (provides) { - // Figure out why this can occur - | None => None - | Some(TModSignature(provides)) => - // Scan the List, try and find module exports - let needed_module = List.find(provide => true, provides); - Some(([], [])); - | Some(_) => - Trace.log("Completable: Not Found Direct Module Signature"); - None; - }; - // TODO: I think this is an impossible case - | [] => None - }; + // Value Completions + // TODO: add Compiler Built ins, maybe there is a list somewhere + let value_completions = + if (include_values) { + let values = + Env.fold_values( + (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, + None, + program.env, + [], + ); + List.map(build_value_completion(~env=program.env), values); + } else { + []; }; - // If we are going down to modules we go one way, otherwise we go the other - Some( - switch (path) { - // Base - | [] => - let modules = - Env.fold_modules( - (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, - None, - env, - [], - ); - let values = - Env.fold_values( - (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, - None, - env, - [], - ); - (modules, values); - // We Need to go deeper - | [baseModule, ...rest] => - // TODO: There has to be a better way todo this - let modules = - Env.fold_modules( - (tag, path, decl, acc) => - if (String_utils.starts_with(tag, baseModule)) { - List.append(acc, [path]); - } else { - acc; - }, - None, - env, - [], - ); - switch (modules) { - // We did not find the module - | [] => ([], []) - // We found at least one matching module - | [mod_path, ..._] => - switch (getModuleEnv(env, mod_path, rest)) { - | None => ([], []) - | Some(x) => x - } - }; - }, + // Module Completions + let modules = + Env.fold_modules( + (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, + None, + program.env, + [], ); - }; - let environment_completions = - switch (getEnvironment(program.env, root)) { - | None => [] - | Some((modules, values)) => - // TODO: Find Modules - let module_completions = - if (include_modules) { - List.map( - ((tag, decl)) => - { - label: tag, - kind: CompletionItemKindModule, - detail: "", - documentation: "", - }, - modules, - ); - } else { - []; - }; - // TODO: Find Values - let value_completions = - if (include_values) { - List.map( - ((tag, decl: Types.value_description)) => - { - label: tag, - kind: get_kind(decl.val_type.desc), - detail: Printtyp.string_of_type_scheme(decl.val_type), - documentation: "", - }, - values, - ); - } else { - []; - }; - // Merge Our Results - List.concat([module_completions, value_completions]); + let module_completions = + List.map(build_module_completion(~env=program.env), modules); + // Merge Completions + let completions = + List.concat([keyword_completions, value_completions, module_completions]); + // Find The Top Level Modules + let completions = + switch (search) { + // User Wrote Nothing + | [] + // User Wrote A Single Word, i.e Top Level Search + | [_] => completions + // User Wrote A Path + | [search, ...path] => + Trace.log(Printf.sprintf("Completable: Root %s", search)); + // Find Module Root + switch (List.find_opt(((mod_name, _)) => mod_name == search, modules)) { + | Some((_, root_decl)) => + get_completions( + program.env, + root_decl, + path, + ~include_values, + ~include_types, + ) + | None => [] + }; }; - // Merge Our Results - let completions: list(completion_item) = - List.concat([keyword_completions, environment_completions]); - // Filter Our Results - let filtered_completions = - List.filter( - ({label}) => - String_utils.starts_with( - StringLabels.lowercase_ascii(label), - StringLabels.lowercase_ascii(current_search), - ), - completions, - ); - // Return Our Results - filtered_completions; + Trace.log( + Printf.sprintf( + "Completable: Completions Count %d", + List.length(completions), + ), + ); + completions; }; let process = @@ -403,92 +432,36 @@ let process = | ComletableCode(str) => Trace.log("Completable: Code " ++ str); let completionPath = String.split_on_char('.', str); - switch (List.rev(completionPath)) { - | [] - | ["", ""] => [] - | [search] => - get_completions( - program, - compiled_code, - [], - search, - true, - true, - true, - ) - | [search, ...root] => - get_completions( - program, - compiled_code, - List.rev(root), - search, - false, - true, - true, - ) - }; - // switch (pathRoot) { - // // Just A . - // | None => [] - // // A word no dot - // | Some(true) => - // // TODO: Suggest Keywords - // let module_completions = - // List.map( - // (i: string) => { - // let item: completion_item = { - // label: i, - // kind: CompletionItemKindModule, - // detail: "", - // documentation: "", - // }; - // item; - // }, - // modules, - // ); - // let values: list((string, Types.value_description)) = - // Env.fold_values( - // (tag, path, vd, acc) => {List.append(acc, [(tag, vd)])}, - // None, - // program.env, - // [], - // ); - // let valueCompletions = - // List.map( - // ((i: string, l: Types.value_description)) => { - // let item: completion_item = { - // label: i, - // kind: get_kind(l.val_type.desc), - // detail: Printtyp.string_of_type_scheme(l.val_type), - // documentation: "", - // }; - // item; - // }, - // values, - // ); - // let completions = - // List.concat([module_completions, valueCompletions]); - // List.filter( - // ({label}) => - // String.starts_with( - // ~prefix=StringLabels.lowercase_ascii(str), - // StringLabels.lowercase_ascii(label), - // ), - // completions, - // ); - // // A Module Path - // | Some(false) => [] - // }; + get_top_level_completions( + ~include_keywords=true, + ~include_values=true, + ~include_types=false, + program, + compiled_code, + completionPath, + ); | CompletableExpr(str) => Trace.log("Completable: Expr " ++ str); - // TODO: Build Path - // TODO: Get Module If Available - // TODO: Filter Out Operators - []; + let completionPath = String.split_on_char('.', str); + get_top_level_completions( + ~include_keywords=false, + ~include_values=true, + ~include_types=true, + program, + compiled_code, + completionPath, + ); | CompletableType(str) => Trace.log("Completable: Type " ++ str); - // TODO: Suggest Type Info - []; + let completionPath = String.split_on_char('.', str); + get_top_level_completions( + ~include_keywords=false, + ~include_values=false, + ~include_types=true, + program, + compiled_code, + completionPath, + ); } }; send_completion(~id, completions); diff --git a/compiler/src/language_server/doc.re b/compiler/src/language_server/doc.re new file mode 100644 index 000000000..688819dac --- /dev/null +++ b/compiler/src/language_server/doc.re @@ -0,0 +1,68 @@ +open Grain; +open Compile; +open Grain_parsing; +open Grain_utils; +open Grain_typed; +open Grain_diagnostics; +open Sourcetree; + +// We need to use the "grain-type" markdown syntax to have correct coloring on hover items +let grain_type_code_block = Markdown.code_block(~syntax="grain-type"); +// Used for module hovers +let grain_code_block = Markdown.code_block(~syntax="grain"); + +let markdown_join = (a, b) => { + // Horizonal rules between code blocks render a little funky + // so we manually add linebreaks + Printf.sprintf( + "%s\n---\n

\n%s", + a, + b, + ); +}; + +let supressed_types = [Builtin_types.path_void, Builtin_types.path_bool]; + +let print_type = (env, ty) => { + let instance = grain_type_code_block(Printtyp.string_of_type_scheme(ty)); + try({ + let (path, _, decl) = Ctype.extract_concrete_typedecl(env, ty); + // Avoid showing the declaration for supressed types + if (List.exists( + supressed_type => Path.same(path, supressed_type), + supressed_types, + )) { + raise(Not_found); + }; + markdown_join( + grain_code_block( + Printtyp.string_of_type_declaration( + ~ident=Ident.create(Path.last(path)), + decl, + ), + ), + instance, + ); + }) { + | Not_found => instance + }; +}; + +let print_mod_type = (decl: Types.module_declaration) => { + let vals = Modules.get_provides(decl); + let signatures = + List.map( + (v: Modules.provide) => + switch (v.kind) { + | Function + | Value => Format.sprintf("let %s", v.signature) + | Record + | Enum + | Abstract + | Exception => v.signature + | Module => Format.sprintf("module %s", v.name) + }, + vals, + ); + grain_code_block(String.concat("\n", signatures)); +}; \ No newline at end of file diff --git a/compiler/src/language_server/doc.rei b/compiler/src/language_server/doc.rei new file mode 100644 index 000000000..0972bf15a --- /dev/null +++ b/compiler/src/language_server/doc.rei @@ -0,0 +1,9 @@ +open Grain_typed; + +let grain_type_code_block: string => string; + +let grain_code_block: string => string; + +let print_type: (Env.t, Types.type_expr) => string; + +let print_mod_type: Types.module_declaration => string; diff --git a/compiler/src/language_server/hover.re b/compiler/src/language_server/hover.re index 88447a06e..04db2bd19 100644 --- a/compiler/src/language_server/hover.re +++ b/compiler/src/language_server/hover.re @@ -28,11 +28,6 @@ module ResponseResult = { }; }; -// We need to use the "grain-type" markdown syntax to have correct coloring on hover items -let grain_type_code_block = Markdown.code_block(~syntax="grain-type"); -// Used for module hovers -let grain_code_block = Markdown.code_block(~syntax="grain"); - let send_hover = (~id: Protocol.message_id, ~range: Protocol.range, result) => { Protocol.response( ~id, @@ -46,80 +41,30 @@ let send_hover = (~id: Protocol.message_id, ~range: Protocol.range, result) => { ); }; -let markdown_join = (a, b) => { - // Horizonal rules between code blocks render a little funky - // so we manually add linebreaks - Printf.sprintf( - "%s\n---\n

\n%s", - a, - b, - ); -}; - let send_no_result = (~id: Protocol.message_id) => { Protocol.response(~id, `Null); }; -let supressed_types = [Builtin_types.path_void, Builtin_types.path_bool]; - -let print_type = (env, ty) => { - let instance = grain_type_code_block(Printtyp.string_of_type_scheme(ty)); - try({ - let (path, _, decl) = Ctype.extract_concrete_typedecl(env, ty); - // Avoid showing the declaration for supressed types - if (List.exists( - supressed_type => Path.same(path, supressed_type), - supressed_types, - )) { - raise(Not_found); - }; - markdown_join( - grain_code_block( - Printtyp.string_of_type_declaration( - ~ident=Ident.create(Path.last(path)), - decl, - ), - ), - instance, - ); - }) { - | Not_found => instance - }; -}; - let module_lens = (decl: Types.module_declaration) => { - let vals = Modules.get_provides(decl); - let signatures = - List.map( - (v: Modules.provide) => - switch (v.kind) { - | Function - | Value => Format.sprintf("let %s", v.signature) - | Record - | Enum - | Abstract - | Exception => v.signature - | Module => Format.sprintf("module %s", v.name) - }, - vals, - ); - grain_code_block(String.concat("\n", signatures)); + Doc.print_mod_type(decl); }; let value_lens = (env: Env.t, ty: Types.type_expr) => { - print_type(env, ty); + Doc.print_type(env, ty); }; let pattern_lens = (p: Typedtree.pattern) => { - print_type(p.pat_env, p.pat_type); + Doc.print_type(p.pat_env, p.pat_type); }; let type_lens = (ty: Typedtree.core_type) => { - grain_type_code_block(Printtyp.string_of_type_scheme(ty.ctyp_type)); + Doc.grain_type_code_block(Printtyp.string_of_type_scheme(ty.ctyp_type)); }; let declaration_lens = (ident: Ident.t, decl: Types.type_declaration) => { - grain_type_code_block(Printtyp.string_of_type_declaration(~ident, decl)); + Doc.grain_type_code_block( + Printtyp.string_of_type_declaration(~ident, decl), + ); }; let include_lens = (env: Env.t, path: Path.t) => { From 32cf4ff2807b123ff2e77cbe3b34413f6ec350e7 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Thu, 20 Jul 2023 19:46:21 -0400 Subject: [PATCH 03/17] feat: Improve Completion --- compiler/src/language_server/completion.re | 206 ++++++++++++-------- compiler/src/language_server/completion.rei | 19 +- compiler/src/language_server/doc.re | 2 +- compiler/src/language_server/driver.re | 3 - compiler/src/language_server/hover.re | 2 +- compiler/src/language_server/initialize.re | 4 +- compiler/src/language_server/message.re | 9 - compiler/src/language_server/message.rei | 4 - compiler/src/language_server/trace.re | 12 +- 9 files changed, 130 insertions(+), 131 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 96b6fd55a..86d00704a 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -120,42 +120,6 @@ let send_completion = ); }; -module Resolution = { - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem - module RequestParams = { - // TODO: implement the rest of the fields - [@deriving yojson({strict: false})] - type t = {label: string}; - }; - - // As per https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion - // If computing full completion items is expensive, servers can additionally provide a handler for - // the completion item resolve request (‘completionItem/resolve’). This request is sent when a - // completion item is selected in the user interface. - let process = - ( - ~id: Protocol.message_id, - ~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), - ~documents: Hashtbl.t(Protocol.uri, string), - params: RequestParams.t, - ) => { - Trace.log("Completable: Resolution Request Recieved"); - // Right now we just resolve nothing to clear the client's request - // In future we may want to send more details back with Graindoc details for example - send_completion( - ~id, - [ - { - label: "testing", - kind: CompletionItemKindValue, - detail: "Where does this go deatil", - documentation: "test item", - }, - ], - ); - }; -}; - type completionState = | ComletableCode(string) | CompletableExpr(string) @@ -166,24 +130,15 @@ let find_completable_state = (documents, uri, positon: Protocol.position) => { switch (Hashtbl.find_opt(documents, uri)) { | None => None | Some(source_code) => - // TODO: We need to handle crlf, and also mutability == bad // Get Document - let lines = String.split_on_char('\n', source_code); - // TODO: Handle Multiple Lines, so we need to convert to an index + let source = + Str.global_replace(Str.regexp({|\r\n|}), {|\n|}, source_code); + let lines = String.split_on_char('\n', source); let index = positon.character; - // Calculate Current Position - Trace.log( - "Completable: Completable Line " ++ List.nth(lines, positon.line), - ); // Search File To Grab Context - let rec searchForContext = (search_code, offset) => { - // If Last Element is = then we are in expr - // If Last Element is : then we are in type - // If Last Element was { - // If we detect a use before than we are in a from - // Otherwise we are in a ComletableCode - // TODO: Support other places, improve this half parser + let rec searchForContext = (search_code, offset, cut, hasHitSep) => { switch (String_utils.char_at(search_code, offset)) { + // Find Context | Some('=') => CompletableExpr( String.trim( @@ -196,24 +151,40 @@ let find_completable_state = (documents, uri, positon: Protocol.position) => { String_utils.slice(~first=offset, ~last=index, search_code), ), ) - // TODO: Search for using statements - // | Some('{') => - // CompletableExpr( - // String_utils.slice(~first=offset, ~last=index, search_code), - // ) + // Context Seperators + | Some('(') + | Some(',') + | Some('[') => + searchForContext( + search_code, + offset - 1, + hasHitSep ? cut : offset - 1, + true, + ) + // Must be something else | _ when offset <= 0 => ComletableCode(search_code) - | _ => searchForContext(search_code, offset - 1) + | _ => + searchForContext( + search_code, + offset - 1, + hasHitSep ? cut : offset - 1, + true, + ) }; }; let context = - searchForContext(List.nth(lines, positon.line), positon.character); + searchForContext( + List.nth(lines, positon.line), + positon.character, + positon.character, + false, + ); Some(context); }; }; let build_keyword_completion = keyword => { { - // TODO: Would be better if these were actual snippet completions label: keyword, kind: CompletionItemKindKeyword, detail: "", @@ -224,10 +195,8 @@ let build_value_completion = (~env, (value_name: string, value_desc: Types.value_description)) => { { label: value_name, - // TODO: Consider Making This More Fine Grained, based off the type kind: CompletionItemKindValue, detail: Doc.print_type(env, value_desc.val_type), - // TODO: Maybe generate grain doc documentation: "", }; }; @@ -237,7 +206,6 @@ let build_module_completion = label: module_name, kind: CompletionItemKindModule, detail: Doc.print_mod_type(mod_desc), - // TODO: Maybe generate grain doc documentation: "", }; }; @@ -247,9 +215,7 @@ let build_type_completion = { label: type_name, kind: CompletionItemKindTypeParameter, - // TODO: Add a kind here detail: "", - // TODO: Maybe generate grain doc documentation: "", }; }; @@ -324,30 +290,44 @@ let get_top_level_completions = search: list(string), ) => { // Keyword Completions - // TODO: Include all keywords, Maybe figure out if i can grab these from some place let keyword_completions = if (include_keywords) { let keywords = [ - "let", - "type", - "module", + "primitive", + "foreign", + "wasm", + "while", + "for", + "continue", + "break", + "return", + "if", + "when", + "else", "include", + "use", + "provide", + "abstract", + "except", "from", + "type", + "enum", "record", + "module", + "let", + "mut", "rec", - "enum", - "provide", - "abstract", - "if", - "while", - "for", + "match", + "assert", + "fail", + "exception", + "throw", ]; List.map(build_keyword_completion, keywords); } else { []; }; // Value Completions - // TODO: add Compiler Built ins, maybe there is a list somewhere let value_completions = if (include_values) { let values = @@ -357,7 +337,40 @@ let get_top_level_completions = program.env, [], ); - List.map(build_value_completion(~env=program.env), values); + let builtins = [ + ("None", CompletionItemKindEnumMember), + ("Some", CompletionItemKindEnumMember), + ("Ok", CompletionItemKindEnumMember), + ("Err", CompletionItemKindEnumMember), + ("true", CompletionItemKindConstant), + ("false", CompletionItemKindConstant), + ("void", CompletionItemKindConstant), + ]; + let builtins = + List.map( + ((label, kind)) => {label, kind, detail: "", documentation: ""}, + builtins, + ); + List.append( + List.map(build_value_completion(~env=program.env), values), + builtins, + ); + } else { + []; + }; + // Type Completions + let type_completions = + if (include_types) { + let types = + Env.fold_types( + (tag, path, (decl, descr), acc) => { + List.append(acc, [(tag, decl)]) + }, + None, + program.env, + [], + ); + List.map(build_type_completion(~env=program.env), types); } else { []; }; @@ -373,7 +386,12 @@ let get_top_level_completions = List.map(build_module_completion(~env=program.env), modules); // Merge Completions let completions = - List.concat([keyword_completions, value_completions, module_completions]); + List.concat([ + keyword_completions, + value_completions, + type_completions, + module_completions, + ]); // Find The Top Level Modules let completions = switch (search) { @@ -383,7 +401,6 @@ let get_top_level_completions = | [_] => completions // User Wrote A Path | [search, ...path] => - Trace.log(Printf.sprintf("Completable: Root %s", search)); // Find Module Root switch (List.find_opt(((mod_name, _)) => mod_name == search, modules)) { | Some((_, root_decl)) => @@ -395,14 +412,33 @@ let get_top_level_completions = ~include_types, ) | None => [] - }; + } }; - Trace.log( - Printf.sprintf( - "Completable: Completions Count %d", - List.length(completions), - ), - ); + // Remove Operators + let operators = [ + '$', + '&', + '*', + '/', + '+', + '-', + '=', + '>', + '<', + '^', + '|', + '!', + '?', + '%', + ':', + '.', + ]; + let completions = + List.filter( + ({label}: completion_item) => + !List.exists(o => String.contains(label, o), operators), + completions, + ); completions; }; @@ -446,7 +482,7 @@ let process = get_top_level_completions( ~include_keywords=false, ~include_values=true, - ~include_types=true, + ~include_types=false, program, compiled_code, completionPath, diff --git a/compiler/src/language_server/completion.rei b/compiler/src/language_server/completion.rei index 931f1913f..1634034bf 100644 --- a/compiler/src/language_server/completion.rei +++ b/compiler/src/language_server/completion.rei @@ -19,21 +19,4 @@ let process: ~documents: Hashtbl.t(Protocol.uri, string), RequestParams.t ) => - unit; - -module Resolution: { - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem - module RequestParams: { - [@deriving yojson({strict: false})] - type t; - }; - - let process: - ( - ~id: Protocol.message_id, - ~compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), - ~documents: Hashtbl.t(Protocol.uri, string), - RequestParams.t - ) => - unit; -}; \ No newline at end of file + unit; \ No newline at end of file diff --git a/compiler/src/language_server/doc.re b/compiler/src/language_server/doc.re index 688819dac..31060ae89 100644 --- a/compiler/src/language_server/doc.re +++ b/compiler/src/language_server/doc.re @@ -64,5 +64,5 @@ let print_mod_type = (decl: Types.module_declaration) => { }, vals, ); - grain_code_block(String.concat("\n", signatures)); + String.concat("\n", signatures); }; \ No newline at end of file diff --git a/compiler/src/language_server/driver.re b/compiler/src/language_server/driver.re index d3ac0ce16..8f0169a1c 100644 --- a/compiler/src/language_server/driver.re +++ b/compiler/src/language_server/driver.re @@ -34,9 +34,6 @@ let process = msg => { | TextDocumentCompletion(id, params) when is_initialized^ => Completion.process(~id, ~compiled_code, ~documents, params); Reading; - | CompletionItemResolve(id, params) when is_initialized^ => - Completion.Resolution.process(~id, ~compiled_code, ~documents, params); - Reading; | Shutdown(id, params) when is_initialized^ => Shutdown.process(~id, ~compiled_code, ~documents, params); is_shutting_down := true; diff --git a/compiler/src/language_server/hover.re b/compiler/src/language_server/hover.re index 04db2bd19..abf600986 100644 --- a/compiler/src/language_server/hover.re +++ b/compiler/src/language_server/hover.re @@ -46,7 +46,7 @@ let send_no_result = (~id: Protocol.message_id) => { }; let module_lens = (decl: Types.module_declaration) => { - Doc.print_mod_type(decl); + Doc.grain_code_block(Doc.print_mod_type(decl)); }; let value_lens = (env: Env.t, ty: Types.type_expr) => { diff --git a/compiler/src/language_server/initialize.re b/compiler/src/language_server/initialize.re index 5cb3e4d23..6807859ed 100644 --- a/compiler/src/language_server/initialize.re +++ b/compiler/src/language_server/initialize.re @@ -80,8 +80,8 @@ module ResponseResult = { text_document_sync: Full, hover_provider: true, completion_provider: { - resolve_provider: true, - trigger_characters: ["."], + resolve_provider: false, + trigger_characters: [".", ",", "(", ":", "["], }, definition_provider: { link_support: true, diff --git a/compiler/src/language_server/message.re b/compiler/src/language_server/message.re index 280dbeefd..4bf39e3c1 100644 --- a/compiler/src/language_server/message.re +++ b/compiler/src/language_server/message.re @@ -5,10 +5,6 @@ type t = | TextDocumentHover(Protocol.message_id, Hover.RequestParams.t) | TextDocumentCodeLens(Protocol.message_id, Lenses.RequestParams.t) | TextDocumentCompletion(Protocol.message_id, Completion.RequestParams.t) - | CompletionItemResolve( - Protocol.message_id, - Completion.Resolution.RequestParams.t, - ) | Shutdown(Protocol.message_id, Shutdown.RequestParams.t) | Exit(Protocol.message_id, Exit.RequestParams.t) | TextDocumentDidOpen(Protocol.uri, Code_file.DidOpen.RequestParams.t) @@ -47,11 +43,6 @@ let of_request = (msg: Protocol.request_message): t => { | Ok(params) => TextDocumentCompletion(id, params) | Error(msg) => Error(msg) } - | {method: "completionItem/resolve", id: Some(id), params: Some(params)} => - switch (Completion.Resolution.RequestParams.of_yojson(params)) { - | Ok(params) => CompletionItemResolve(id, params) - | Error(msg) => Error(msg) - } | {method: "shutdown", id: Some(id), params: None} => switch (Shutdown.RequestParams.of_yojson(`Null)) { | Ok(params) => Shutdown(id, params) diff --git a/compiler/src/language_server/message.rei b/compiler/src/language_server/message.rei index a944075db..d143b0563 100644 --- a/compiler/src/language_server/message.rei +++ b/compiler/src/language_server/message.rei @@ -3,10 +3,6 @@ type t = | TextDocumentHover(Protocol.message_id, Hover.RequestParams.t) | TextDocumentCodeLens(Protocol.message_id, Lenses.RequestParams.t) | TextDocumentCompletion(Protocol.message_id, Completion.RequestParams.t) - | CompletionItemResolve( - Protocol.message_id, - Completion.Resolution.RequestParams.t, - ) | Shutdown(Protocol.message_id, Shutdown.RequestParams.t) | Exit(Protocol.message_id, Exit.RequestParams.t) | TextDocumentDidOpen(Protocol.uri, Code_file.DidOpen.RequestParams.t) diff --git a/compiler/src/language_server/trace.re b/compiler/src/language_server/trace.re index 84ce08d79..80a39ab5f 100644 --- a/compiler/src/language_server/trace.re +++ b/compiler/src/language_server/trace.re @@ -36,14 +36,10 @@ let log = (~verbose=?, message: string) => switch (trace_level^) { | Off => () | Messages => - if (String.starts_with(~prefix="Completable:", message)) { - Protocol.notification( - ~method="$/logTrace", - NotificationParams.to_yojson({message, verbose: None}), - ); - } else { - (); - } + Protocol.notification( + ~method="$/logTrace", + NotificationParams.to_yojson({message, verbose: None}), + ) | Verbose => Protocol.notification( ~method="$/logTrace", From 8cecae16da127a2442b3d1ab244b5368c8ac2e1b Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Thu, 3 Aug 2023 15:15:14 -0400 Subject: [PATCH 04/17] chore: Run Formatter --- compiler/src/language_server/completion.rei | 2 +- compiler/src/language_server/doc.re | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/language_server/completion.rei b/compiler/src/language_server/completion.rei index 1634034bf..a81d42db6 100644 --- a/compiler/src/language_server/completion.rei +++ b/compiler/src/language_server/completion.rei @@ -19,4 +19,4 @@ let process: ~documents: Hashtbl.t(Protocol.uri, string), RequestParams.t ) => - unit; \ No newline at end of file + unit; diff --git a/compiler/src/language_server/doc.re b/compiler/src/language_server/doc.re index 31060ae89..72ce07374 100644 --- a/compiler/src/language_server/doc.re +++ b/compiler/src/language_server/doc.re @@ -65,4 +65,4 @@ let print_mod_type = (decl: Types.module_declaration) => { vals, ); String.concat("\n", signatures); -}; \ No newline at end of file +}; From 1d3259133a026288f04738b2e9a9432985ed826b Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Sun, 20 Aug 2023 13:28:08 -0400 Subject: [PATCH 05/17] chore: remove unused function --- compiler/src/language_server/completion.re | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 86d00704a..d3693e25f 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -99,19 +99,6 @@ module ResponseResult = { }; }; -// maps Grain types to LSP CompletionItemKind -let rec get_kind = (desc: Types.type_desc) => - switch (desc) { - | TTyVar(_) => CompletionItemKindVariable - | TTyArrow(_) => CompletionItemKindFunction - | TTyTuple(_) => CompletionItemKindStruct - | TTyRecord(_) => CompletionItemKindStruct - | TTyConstr(_) => CompletionItemKindConstructor - | TTySubst(s) => get_kind(s.desc) - | TTyLink(t) => get_kind(t.desc) - | _ => CompletionItemKindText - }; - let send_completion = (~id: Protocol.message_id, completions: list(completion_item)) => { Protocol.response( From d75fad1fb3ffd9d0ee9fe7e7cbb4382ef734f882 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Sun, 20 Aug 2023 14:19:16 -0400 Subject: [PATCH 06/17] chore: Improve context detection --- compiler/src/language_server/completion.re | 108 +++++++++++++-------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index d3693e25f..e275aa65e 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -112,7 +112,23 @@ type completionState = | CompletableExpr(string) | CompletableType(string); -let find_completable_state = (documents, uri, positon: Protocol.position) => { +let convert_position_to_offset = (source, position: Protocol.position) => { + let lines = String.split_on_char('\n', source); + List_utils.fold_lefti( + (offset, line_number, line_str) => + if (position.line == line_number) { + offset + position.character; + } else if (line_number < position.line) { + // Add 1 for the newline + offset + String.length(line_str) + 1; + } else { + offset; + }, + 0, + lines, + ); +}; +let find_completable_state = (documents, uri, position: Protocol.position) => { // try and find the code we are completing in the original source switch (Hashtbl.find_opt(documents, uri)) { | None => None @@ -120,53 +136,63 @@ let find_completable_state = (documents, uri, positon: Protocol.position) => { // Get Document let source = Str.global_replace(Str.regexp({|\r\n|}), {|\n|}, source_code); - let lines = String.split_on_char('\n', source); - let index = positon.character; - // Search File To Grab Context - let rec searchForContext = (search_code, offset, cut, hasHitSep) => { - switch (String_utils.char_at(search_code, offset)) { - // Find Context - | Some('=') => - CompletableExpr( - String.trim( - String_utils.slice(~first=offset + 2, ~last=index, search_code), + let offset = convert_position_to_offset(source, position); + let source_chars = + List.rev( + String_utils.explode(String_utils.slice(~last=offset, source)), + ); + // Search file to grab context + let rec searchForContext = + ( + search_code: list(char), + curr_offset, + slice_offset, + has_hit_info, + ) => { + switch (search_code) { + // Context Finders + // TODO: Make sure there is some sort of whitespace between + | ['t', 'e', 'l', ...rest] => + Some( + CompletableExpr( + String_utils.slice(~first=curr_offset, ~last=offset, source), ), ) - | Some(':') => - CompletableType( - String.trim( - String_utils.slice(~first=offset, ~last=index, search_code), + | ['e', 'p', 'y', 't', ...rest] => + Some( + CompletableType( + String_utils.slice(~first=curr_offset, ~last=offset, source), ), ) - // Context Seperators - | Some('(') - | Some(',') - | Some('[') => - searchForContext( - search_code, - offset - 1, - hasHitSep ? cut : offset - 1, - true, - ) - // Must be something else - | _ when offset <= 0 => ComletableCode(search_code) - | _ => - searchForContext( - search_code, - offset - 1, - hasHitSep ? cut : offset - 1, - true, + | ['\n', ...rest] when !has_hit_info => + Some( + ComletableCode( + String_utils.slice(~first=curr_offset, ~last=offset, source), + ), ) + // Context Seperators + | ['(', ...rest] + | [')', ...rest] + | ['{', ...rest] + | ['}', ...rest] + | ['[', ...rest] + | [']', ...rest] + | [',', ...rest] + | [':', ...rest] + | ['=', ...rest] => + if (slice_offset == 0) { + searchForContext(rest, curr_offset - 1, curr_offset, true); + } else { + searchForContext(rest, curr_offset - 1, slice_offset, true); + } + // Any old char + | [_, ...rest] => + searchForContext(rest, curr_offset - 1, slice_offset, has_hit_info) + // We Did not find a marker + | [] => None }; }; - let context = - searchForContext( - List.nth(lines, positon.line), - positon.character, - positon.character, - false, - ); - Some(context); + searchForContext(source_chars, offset, 0, false); }; }; From 301f218cee08a25cbab79c0fc8d7b844161bdc29 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Sun, 20 Aug 2023 14:24:23 -0400 Subject: [PATCH 07/17] chore: Better detect `let` and `type`, do not recommend stuff where we are writing an identifier. --- compiler/src/language_server/completion.re | 44 ++++++++++++++++------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index e275aa65e..0fef3fa60 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -144,6 +144,7 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { // Search file to grab context let rec searchForContext = ( + ~last_char_whitespace=false, search_code: list(char), curr_offset, slice_offset, @@ -152,24 +153,43 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { switch (search_code) { // Context Finders // TODO: Make sure there is some sort of whitespace between - | ['t', 'e', 'l', ...rest] => - Some( - CompletableExpr( - String_utils.slice(~first=curr_offset, ~last=offset, source), - ), - ) - | ['e', 'p', 'y', 't', ...rest] => - Some( - CompletableType( - String_utils.slice(~first=curr_offset, ~last=offset, source), - ), - ) + | ['t', 'e', 'l', ...rest] when last_char_whitespace => + if (has_hit_info) { + Some( + CompletableExpr( + String_utils.slice(~first=curr_offset, ~last=offset, source), + ), + ); + } else { + None; + } + | ['e', 'p', 'y', 't', ...rest] when last_char_whitespace => + if (has_hit_info) { + Some( + CompletableType( + String_utils.slice(~first=curr_offset, ~last=offset, source), + ), + ); + } else { + None; + } | ['\n', ...rest] when !has_hit_info => Some( ComletableCode( String_utils.slice(~first=curr_offset, ~last=offset, source), ), ) + // Whitespace + | [' ', ...rest] + | ['\n', ...rest] + | ['\t', ...rest] => + searchForContext( + rest, + curr_offset - 1, + slice_offset, + has_hit_info, + ~last_char_whitespace=true, + ) // Context Seperators | ['(', ...rest] | [')', ...rest] From d549a0c0bec720671d1311ffda0586222bf48238 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Sun, 20 Aug 2023 14:25:37 -0400 Subject: [PATCH 08/17] chore: Better type seperation. --- compiler/src/language_server/completion.re | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 0fef3fa60..ccca64c6b 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -199,7 +199,9 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { | [']', ...rest] | [',', ...rest] | [':', ...rest] - | ['=', ...rest] => + | ['=', ...rest] + | ['<', ...rest] + | ['>', ...rest] => if (slice_offset == 0) { searchForContext(rest, curr_offset - 1, curr_offset, true); } else { From b1d09dd582d40e0cd5b239013625910df29bfde0 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Sun, 20 Aug 2023 14:27:12 -0400 Subject: [PATCH 09/17] chore: remove random todo --- compiler/src/language_server/completion.re | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index ccca64c6b..a7ac8a9d2 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -152,7 +152,6 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { ) => { switch (search_code) { // Context Finders - // TODO: Make sure there is some sort of whitespace between | ['t', 'e', 'l', ...rest] when last_char_whitespace => if (has_hit_info) { Some( From e49bc98516a6ab2103e86d74d4c1c0ba038608fd Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 1 Jan 2024 17:40:28 -0500 Subject: [PATCH 10/17] chore: Rename `doc.re` to `document.re` --- compiler/src/language_server/completion.re | 4 ++-- compiler/src/language_server/{doc.re => document.re} | 0 .../src/language_server/{doc.rei => document.rei} | 0 compiler/src/language_server/hover.re | 12 +++++++----- 4 files changed, 9 insertions(+), 7 deletions(-) rename compiler/src/language_server/{doc.re => document.re} (100%) rename compiler/src/language_server/{doc.rei => document.rei} (100%) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index a7ac8a9d2..ba1962b4a 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -230,7 +230,7 @@ let build_value_completion = { label: value_name, kind: CompletionItemKindValue, - detail: Doc.print_type(env, value_desc.val_type), + detail: Document.print_type(env, value_desc.val_type), documentation: "", }; }; @@ -239,7 +239,7 @@ let build_module_completion = { label: module_name, kind: CompletionItemKindModule, - detail: Doc.print_mod_type(mod_desc), + detail: Document.print_mod_type(mod_desc), documentation: "", }; }; diff --git a/compiler/src/language_server/doc.re b/compiler/src/language_server/document.re similarity index 100% rename from compiler/src/language_server/doc.re rename to compiler/src/language_server/document.re diff --git a/compiler/src/language_server/doc.rei b/compiler/src/language_server/document.rei similarity index 100% rename from compiler/src/language_server/doc.rei rename to compiler/src/language_server/document.rei diff --git a/compiler/src/language_server/hover.re b/compiler/src/language_server/hover.re index abf600986..d72a7b325 100644 --- a/compiler/src/language_server/hover.re +++ b/compiler/src/language_server/hover.re @@ -46,23 +46,25 @@ let send_no_result = (~id: Protocol.message_id) => { }; let module_lens = (decl: Types.module_declaration) => { - Doc.grain_code_block(Doc.print_mod_type(decl)); + Document.grain_code_block(Document.print_mod_type(decl)); }; let value_lens = (env: Env.t, ty: Types.type_expr) => { - Doc.print_type(env, ty); + Document.print_type(env, ty); }; let pattern_lens = (p: Typedtree.pattern) => { - Doc.print_type(p.pat_env, p.pat_type); + Document.print_type(p.pat_env, p.pat_type); }; let type_lens = (ty: Typedtree.core_type) => { - Doc.grain_type_code_block(Printtyp.string_of_type_scheme(ty.ctyp_type)); + Document.grain_type_code_block( + Printtyp.string_of_type_scheme(ty.ctyp_type), + ); }; let declaration_lens = (ident: Ident.t, decl: Types.type_declaration) => { - Doc.grain_type_code_block( + Document.grain_type_code_block( Printtyp.string_of_type_declaration(~ident, decl), ); }; From 6b7a8ab9eaed2060a7abf32a569f7cacd53fe4c4 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 1 Jan 2024 21:39:33 -0500 Subject: [PATCH 11/17] chore: Make `Module.*` completion work properly. --- compiler/src/language_server/completion.re | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index ba1962b4a..0be623385 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -156,7 +156,7 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { if (has_hit_info) { Some( CompletableExpr( - String_utils.slice(~first=curr_offset, ~last=offset, source), + String_utils.slice(~first=slice_offset, ~last=offset, source), ), ); } else { @@ -166,7 +166,7 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { if (has_hit_info) { Some( CompletableType( - String_utils.slice(~first=curr_offset, ~last=offset, source), + String_utils.slice(~first=slice_offset, ~last=offset, source), ), ); } else { @@ -175,7 +175,7 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { | ['\n', ...rest] when !has_hit_info => Some( ComletableCode( - String_utils.slice(~first=curr_offset, ~last=offset, source), + String_utils.slice(~first=slice_offset, ~last=offset, source), ), ) // Whitespace @@ -500,7 +500,7 @@ let process = | Some(completableState) => switch (completableState) { | ComletableCode(str) => - Trace.log("Completable: Code " ++ str); + let str = String.trim(str); let completionPath = String.split_on_char('.', str); get_top_level_completions( ~include_keywords=true, @@ -511,7 +511,7 @@ let process = completionPath, ); | CompletableExpr(str) => - Trace.log("Completable: Expr " ++ str); + let str = String.trim(str); let completionPath = String.split_on_char('.', str); get_top_level_completions( ~include_keywords=false, @@ -522,7 +522,7 @@ let process = completionPath, ); | CompletableType(str) => - Trace.log("Completable: Type " ++ str); + let str = String.trim(str); let completionPath = String.split_on_char('.', str); get_top_level_completions( ~include_keywords=false, From a9859c27254876f4b13e177fcfce58575952269e Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 15 Jan 2024 17:47:38 -0500 Subject: [PATCH 12/17] Add more `trigger_characters` --- compiler/src/language_server/initialize.re | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/language_server/initialize.re b/compiler/src/language_server/initialize.re index 6807859ed..54981fe3b 100644 --- a/compiler/src/language_server/initialize.re +++ b/compiler/src/language_server/initialize.re @@ -81,7 +81,7 @@ module ResponseResult = { hover_provider: true, completion_provider: { resolve_provider: false, - trigger_characters: [".", ",", "(", ":", "["], + trigger_characters: [".", ",", "(", ":", "[", "\""], }, definition_provider: { link_support: true, From 722995205597e7be2e38c1a0cf61a14d075ad5c9 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Mon, 15 Jan 2024 17:48:35 -0500 Subject: [PATCH 13/17] chore: Future framework for include completions --- compiler/src/language_server/completion.re | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 0be623385..7914862e0 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -108,9 +108,10 @@ let send_completion = }; type completionState = - | ComletableCode(string) + | CompletableCode(string) | CompletableExpr(string) - | CompletableType(string); + | CompletableType(string) + | CompletableInclude(string); let convert_position_to_offset = (source, position: Protocol.position) => { let lines = String.split_on_char('\n', source); @@ -172,9 +173,15 @@ let find_completable_state = (documents, uri, position: Protocol.position) => { } else { None; } + | ['e', 'd', 'u', 'l', 'c', 'n', 'i', ...rest] when last_char_whitespace => + Some( + CompletableInclude( + String_utils.slice(~first=slice_offset, ~last=offset, source), + ), + ) | ['\n', ...rest] when !has_hit_info => Some( - ComletableCode( + CompletableCode( String_utils.slice(~first=slice_offset, ~last=offset, source), ), ) @@ -499,7 +506,7 @@ let process = | None => [] | Some(completableState) => switch (completableState) { - | ComletableCode(str) => + | CompletableCode(str) => let str = String.trim(str); let completionPath = String.split_on_char('.', str); get_top_level_completions( @@ -532,6 +539,7 @@ let process = compiled_code, completionPath, ); + | CompletableInclude(str) => [] } }; send_completion(~id, completions); From 828ec4eb5f9bae9688c291f079758574ace1cfe3 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Wed, 14 Feb 2024 22:46:24 -0500 Subject: [PATCH 14/17] chore: Fix issues after rebase --- compiler/src/language_server/document.rei | 2 ++ compiler/src/language_server/hover.re | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/language_server/document.rei b/compiler/src/language_server/document.rei index 0972bf15a..b5040bf9c 100644 --- a/compiler/src/language_server/document.rei +++ b/compiler/src/language_server/document.rei @@ -4,6 +4,8 @@ let grain_type_code_block: string => string; let grain_code_block: string => string; +let markdown_join: (string, string) => string; + let print_type: (Env.t, Types.type_expr) => string; let print_mod_type: Types.module_declaration => string; diff --git a/compiler/src/language_server/hover.re b/compiler/src/language_server/hover.re index d72a7b325..222acbbb3 100644 --- a/compiler/src/language_server/hover.re +++ b/compiler/src/language_server/hover.re @@ -85,7 +85,7 @@ let include_lens = (env: Env.t, path: Path.t) => { let exception_declaration_lens = (ident: Ident.t, ext: Types.extension_constructor) => { - grain_type_code_block( + Document.grain_type_code_block( Printtyp.string_of_extension_constructor(~ident, ext), ); }; From f8e4ab3099fdbca8e66db453e1a188358f21e0f9 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Sun, 18 Feb 2024 17:36:05 -0500 Subject: [PATCH 15/17] feat: Use the lexer for much better completion contexts --- compiler/src/language_server/completion.re | 968 ++++++++++++--------- compiler/src/language_server/document.re | 2 + compiler/src/language_server/document.rei | 2 + compiler/src/language_server/initialize.re | 2 +- compiler/src/parsing/wrapped_lexer.rei | 17 + 5 files changed, 599 insertions(+), 392 deletions(-) create mode 100644 compiler/src/parsing/wrapped_lexer.rei diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 7914862e0..d06d8ee0b 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -1,6 +1,7 @@ open Grain_utils; open Grain_typed; open Grain_diagnostics; +open Grain_parsing; open Sourcetree; // This is the full enumeration of all CompletionItemKind as declared by the language server @@ -42,6 +43,12 @@ type completion_trigger_kind = | CompletionTriggerCharacter | CompletionTriggerForIncompleteCompletions; +[@deriving (enum, yojson)] +type insert_text_format = + // Since these are using ppx_deriving enum, order matters + | [@value 1] InsertTextFormatPlainText + | InsertTextFormatSnippet; + let completion_item_kind_to_yojson = severity => completion_item_kind_to_enum(severity) |> [%to_yojson: int]; let completion_item_kind_of_yojson = json => @@ -62,11 +69,25 @@ let completion_trigger_kind_of_yojson = json => } }); +let insert_text_format_to_yojson = value => + insert_text_format_to_enum(value) |> [%to_yojson: int]; +let insert_text_format_of_yojson = json => + Result.bind(json |> [%of_yojson: int], value => { + switch (insert_text_format_of_enum(value)) { + | Some(value) => Ok(value) + | None => Result.Error("Invalid enum value") + } + }); + [@deriving yojson] type completion_item = { label: string, kind: completion_item_kind, detail: string, + [@key "insertText"] + insert_text: option(string), + [@key "insertTextFormat"] + insert_text_format, documentation: string, }; @@ -107,380 +128,585 @@ let send_completion = ); }; -type completionState = - | CompletableCode(string) - | CompletableExpr(string) - | CompletableType(string) - | CompletableInclude(string); +// completions helpers +let build_completion = + ( + ~detail="", + ~documentation="", + ~insert_text_format=InsertTextFormatPlainText, + ~insert_text=?, + label: string, + kind: completion_item_kind, + ) => { + {label, kind, detail, insert_text, insert_text_format, documentation}; +}; -let convert_position_to_offset = (source, position: Protocol.position) => { - let lines = String.split_on_char('\n', source); - List_utils.fold_lefti( - (offset, line_number, line_str) => - if (position.line == line_number) { - offset + position.character; - } else if (line_number < position.line) { - // Add 1 for the newline - offset + String.length(line_str) + 1; - } else { - offset; - }, - 0, - lines, +// TODO: This is for debugging only +let debug_stringify_tkn = (token: Parser.token) => { + switch (token) { + | Parser.RATIONAL(c) => Printf.sprintf("Token.Rational(%s)", c) + | Parser.NUMBER_INT(c) => Printf.sprintf("Token.NUMBER_INT(%s)", c) + | Parser.NUMBER_FLOAT(c) => Printf.sprintf("Token.NUMBER_FLOAT(%s)", c) + | Parser.INT8(c) => Printf.sprintf("Token.INT8(%s)", c) + | Parser.INT16(c) => Printf.sprintf("Token.INT16(%s)", c) + | Parser.INT32(c) => Printf.sprintf("Token.INT32(%s)", c) + | Parser.INT64(c) => Printf.sprintf("Token.INT64(%s)", c) + | Parser.UINT8(c) => Printf.sprintf("Token.UINT8(%s)", c) + | Parser.UINT16(c) => Printf.sprintf("Token.UINT16(%s)", c) + | Parser.UINT32(c) => Printf.sprintf("Token.UINT32(%s)", c) + | Parser.UINT64(c) => Printf.sprintf("Token.UINT64(%s)", c) + | Parser.FLOAT32(c) => Printf.sprintf("Token.FLOAT32(%s)", c) + | Parser.FLOAT64(c) => Printf.sprintf("Token.FLOAT64(%s)", c) + | Parser.BIGINT(c) => Printf.sprintf("Token.BIGINT(%s)", c) + | Parser.WASMI32(c) => Printf.sprintf("Token.WASMI32(%s)", c) + | Parser.WASMI64(c) => Printf.sprintf("Token.WASMI64(%s)", c) + | Parser.WASMF32(c) => Printf.sprintf("Token.WASMF32(%s)", c) + | Parser.WASMF64(c) => Printf.sprintf("Token.WASMF64(%s)", c) + | Parser.LIDENT(c) => Printf.sprintf("Token.LIDENT(%s)", c) + | Parser.UIDENT(c) => Printf.sprintf("Token.UIDENT(%s)", c) + | Parser.STRING(c) => Printf.sprintf("Token.STRING(%s)", c) + | Parser.BYTES(c) => Printf.sprintf("Token.BYTES(%s)", c) + | Parser.CHAR(c) => Printf.sprintf("Token.CHAR(%s)", c) + | Parser.LBRACK => "Token.LBRACK" + | Parser.LBRACKRCARET => "Token.LBRACKRCARET" + | Parser.RBRACK => "Token.RBRACK" + | Parser.LPAREN => "Token.LPAREN" + | Parser.RPAREN => "Token.RPAREN" + | Parser.LBRACE => "Token.LBRACE" + | Parser.RBRACE => "Token.RBRACE" + | Parser.LCARET => "Token.LCARET" + | Parser.RCARET => "Token.RCARET" + | Parser.COMMA => "Token.COMMA" + | Parser.SEMI => "Token.SEMI" + | Parser.AS => "Token.AS" + | Parser.THICKARROW => "Token.THICKARROW" + | Parser.ARROW => "Token.ARROW" + | Parser.EQUAL => "Token.EQUAL" + | Parser.GETS => "Token.GETS" + | Parser.UNDERSCORE => "Token.UNDERSCORE" + | Parser.COLON => "Token.COLON" + | Parser.QUESTION => "Token.QUESTION" + | Parser.DOT => "Token.DOT" + | Parser.ELLIPSIS => "Token.ELLIPSIS" + | Parser.ASSERT => "Token.ASSERT" + | Parser.FAIL => "Token.FAIL" + | Parser.EXCEPTION => "Token.EXCEPTION" + | Parser.THROW => "Token.THROW" + | Parser.TRUE => "Token.TRUE" + | Parser.FALSE => "Token.FALSE" + | Parser.VOID => "Token.VOID" + | Parser.LET => "Token.LET" + | Parser.MUT => "Token.MUT" + | Parser.REC => "Token.REC" + | Parser.IF => "Token.IF" + | Parser.WHEN => "Token.WHEN" + | Parser.ELSE => "Token.ELSE" + | Parser.MATCH => "Token.MATCH" + | Parser.WHILE => "Token.WHILE" + | Parser.FOR => "Token.FOR" + | Parser.CONTINUE => "Token.CONTINUE" + | Parser.BREAK => "Token.BREAK" + | Parser.RETURN => "Token.RETURN" + | Parser.AT => "Token.AT" + | Parser.INFIX_10(c) => Printf.sprintf("Token.INFIX_10(%s)", c) + | Parser.INFIX_30(c) => Printf.sprintf("Token.INFIX_30(%s)", c) + | Parser.INFIX_40(c) => Printf.sprintf("Token.INFIX_40(%s)", c) + | Parser.INFIX_50(c) => Printf.sprintf("Token.INFIX_50(%s)", c) + | Parser.INFIX_60(c) => Printf.sprintf("Token.INFIX_60(%s)", c) + | Parser.INFIX_70(c) => Printf.sprintf("Token.INFIX_70(%s)", c) + | Parser.INFIX_80(c) => Printf.sprintf("Token.INFIX_80(%s)", c) + | Parser.INFIX_90(c) => Printf.sprintf("Token.INFIX_90(%s)", c) + | Parser.INFIX_100(c) => Printf.sprintf("Token.INFIX_100(%s)", c) + | Parser.INFIX_110(c) => Printf.sprintf("Token.INFIX_110(%s)", c) + | Parser.INFIX_120(c) => Printf.sprintf("Token.INFIX_120(%s)", c) + | Parser.PREFIX_150(c) => Printf.sprintf("Token.PREFIX_150(%s)", c) + | Parser.INFIX_ASSIGNMENT_10(c) => + Printf.sprintf("Token.INFIX_ASSIGNMENT_10(%s)", c) + | Parser.ENUM => "Token.ENUM" + | Parser.RECORD => "Token.RECORD" + | Parser.TYPE => "Token.TYPE" + | Parser.MODULE => "Token.MODULE" + | Parser.INCLUDE => "Token.INCLUDE" + | Parser.USE => "Token.USE" + | Parser.PROVIDE => "Token.PROVIDE" + | Parser.ABSTRACT => "Token.ABSTRACT" + | Parser.FOREIGN => "Token.FOREIGN" + | Parser.WASM => "Token.WASM" + | Parser.PRIMITIVE => "Token.PRIMITIVE" + | Parser.AND => "Token.AND" + | Parser.EXCEPT => "Token.EXCEPT" + | Parser.FROM => "Token.FROM" + | Parser.STAR => "Token.STAR" + | Parser.SLASH => "Token.SLASH" + | Parser.DASH => "Token.DASH" + | Parser.PIPE => "Token.PIPE" + | Parser.EOL => "Token.EOL" + | Parser.EOF => "Token.EOF" + | Parser.TRY => "Token.TRY" + | Parser.CATCH => "Token.CATCH" + | Parser.COLONCOLON => "Token.COLONCOLON" + | Parser.MACRO => "Token.MACRO" + | Parser.YIELD => "Token.YIELD" + | Parser.FUN => "Token.FUN" + }; +}; + +let debug_stringify_tkn_loc = + (token: Parser.token, start_loc: int, end_loc: int) => { + Printf.sprintf( + "Token: %s, Start: %d, End: %d", + debug_stringify_tkn(token), + start_loc, + end_loc, ); }; -let find_completable_state = (documents, uri, position: Protocol.position) => { +// Completion Info + +type completion_value = + | PlainText(string) + | Snippet(string, string); + +let toplevel_keywords = [ + PlainText("exception"), + PlainText("enum"), + PlainText("record"), + PlainText("type"), + PlainText("module"), + PlainText("provide"), + PlainText("abstract"), + Snippet("include", "include \"$1\"$0"), + Snippet("from", "from \"$1\" use { $0 }"), +]; + +let expression_keywods = [ + PlainText("let"), + Snippet("if", "if ($1) $0"), + Snippet("match", "match ($1) {\n $0\n}"), + Snippet("while", "while ($1) {\n $0\n}"), + Snippet("for", "for ($1; $2; $3) {\n $0\n}"), +]; + +// context helpers +type lex_token = { + token: Parser.token, + start_loc: int, + end_loc: int, +}; + +type completable_context = + | CompletableInclude(string) + | CompletableStatement(bool) + | CompletableExpressionWithReturn + | CompletableExpression + | CompletableExpressionPath(Path.t, bool) + | CompletableAs + | CompletableAfterLet + | CompletableUnknown; + +// TODO: This is only for debugging atm +let print_string_of_context = context => { + let ctx = + switch (context) { + | CompletableInclude(_) => "CompleteInclude" + | CompletableAs => "CompleteAs" + | CompletableAfterLet => "CompletableAfterLet" + | CompletableExpressionWithReturn => "CompletableExpressionWithReturn" + | CompletableExpression => "CompleteExpression" + | CompletableExpressionPath(_, _) => "CompleteExpressionPath" + | CompletableStatement(true) => "CompletableStatement(Module)" + | CompletableStatement(false) => "CompletableStatement(Statement | value)" + | CompletableUnknown => "CompleteUnknown" + }; + Trace.log(Printf.sprintf("Context: %s", ctx)); +}; + +let convert_position_to_offset = (source: string, position: Protocol.position) => { + let (_, _, offset) = + List.fold_left( + ((line_num, col_num, offset), c) => + if (line_num == position.line && col_num == position.character) { + (line_num, col_num, offset); + } else { + switch (c) { + | '\r' => (line_num, 0, offset + 1) + | '\n' => (line_num + 1, 0, offset + 1) + | _ => (line_num, col_num + 1, offset + 1) + }; + }, + (0, 0, 0), + List.of_seq(String.to_seq(source)), + ); + offset; +}; + +let in_range = (range_start: int, range_end: int, pos: int) => { + range_start < pos && pos < range_end; +}; +let after_range = (range_end: int, pos: int) => { + range_end < pos; +}; + +let last_token_eq = (token: Parser.token, token_list: list(Parser.token)) => { + switch (token_list) { + | [tkn, ..._] => tkn == token + | [] => false + }; +}; + +let rec token_non_breaking_lst = (token_list: list(Parser.token)) => { + switch (token_list) { + | [Parser.EOF, ...rest] + | [Parser.EOL, ...rest] + | [Parser.COMMA, ...rest] => token_non_breaking_lst(rest) + | [_, ..._] => false + | [] => true + }; +}; + +let rec collect_idents = + ( + acc: option(Path.t), + last_dot: bool, + token_list: list(Parser.token), + ) => { + switch (token_list) { + | _ when !last_dot => (acc, false) + | [Parser.UIDENT(str), ...rest] + | [Parser.LIDENT(str), ...rest] => + let ident = + switch (acc) { + | Some(acc) => Path.PExternal(acc, str) + | None => Path.PIdent(Ident.create(str)) + }; + collect_idents(Some(ident), false, rest); + | [Parser.DOT, ...rest] => collect_idents(acc, true, rest) + | [_, ..._] + | [] => (acc, last_dot) + }; +}; + +let get_completion_context = (documents, uri, position: Protocol.position) => { // try and find the code we are completing in the original source switch (Hashtbl.find_opt(documents, uri)) { - | None => None + | None => CompletableUnknown | Some(source_code) => // Get Document - let source = - Str.global_replace(Str.regexp({|\r\n|}), {|\n|}, source_code); - let offset = convert_position_to_offset(source, position); - let source_chars = - List.rev( - String_utils.explode(String_utils.slice(~last=offset, source)), - ); - // Search file to grab context - let rec searchForContext = + let offset = convert_position_to_offset(source_code, position); + Trace.log(Printf.sprintf("Offset: %d", offset)); + // Collect Tokens until offset + let lexbuf = Sedlexing.Utf8.from_string(source_code); + let lexer = Wrapped_lexer.init(lexbuf); + let token = _ => Wrapped_lexer.token(lexer); + Lexer.reset(); + let rec get_tokens = (tokens: list(lex_token)) => { + let (current_tok, start_loc, end_loc) = token(); + let current_token = { + token: current_tok, + start_loc: start_loc.pos_cnum, + end_loc: end_loc.pos_cnum, + }; + switch (current_tok) { + | _ when current_token.start_loc > offset => tokens + | Parser.EOF => [current_token, ...tokens] + | _ => get_tokens([current_token, ...tokens]) + }; + }; + let tokens = + try(get_tokens([])) { + | _ => [] + }; + List.iter( + current_token => { + Trace.log( + Printf.sprintf( + "Token(%s)", + debug_stringify_tkn_loc( + current_token.token, + current_token.start_loc, + current_token.end_loc, + ), + ), + ) + }, + tokens, + ); + // Determine Context + let rec determine_if_in_block = tokens => { + switch (tokens) { + | [{token: Parser.LBRACE, start_loc}, ..._] when start_loc < offset => + true + | [{token: Parser.RBRACE, start_loc}, ..._] when start_loc < offset => + false + | [_, ...rest] => determine_if_in_block(rest) + | [] => false + }; + }; + let in_block = determine_if_in_block(tokens); + let rec build_context = ( - ~last_char_whitespace=false, - search_code: list(char), - curr_offset, - slice_offset, - has_hit_info, + ~hit_eol: bool, + token_list: list(Parser.token), + tokens: list(lex_token), ) => { - switch (search_code) { - // Context Finders - | ['t', 'e', 'l', ...rest] when last_char_whitespace => - if (has_hit_info) { - Some( - CompletableExpr( - String_utils.slice(~first=slice_offset, ~last=offset, source), - ), - ); + switch (tokens) { + // TODO: Add a state for when we are at from | + // TODO: Add a state for when we are at from XXXX use { | } + // TODO: Add a state for when we are at match (XXXX) { | } <- This could be very useful also could not be + // TODO: Add a state for type XXXX = | + // Tokens that we care about + | [{token: Parser.LET}, ..._] + when !hit_eol && token_non_breaking_lst(token_list) => + CompletableAfterLet + | [{token: Parser.STRING(_), end_loc}, {token: Parser.INCLUDE}, ..._] + when + !hit_eol + && after_range(end_loc, offset) + && !last_token_eq(Parser.AS, token_list) => + CompletableAs + | [ + {token: Parser.STRING(str), start_loc, end_loc}, + {token: Parser.INCLUDE}, + ..._, + ] + when in_range(start_loc, end_loc, offset) => + CompletableInclude(str) + | [{token: Parser.DOT}, {token: Parser.EOL}, ..._] + when !hit_eol && !last_token_eq(Parser.DOT, token_list) => + // TODO: Support test().label on records somehow + // TODO: Implement path collection + let (path, expr_start) = collect_idents(None, true, token_list); + switch (path) { + | Some(path) => CompletableExpressionPath(path, expr_start) + | None => CompletableUnknown + }; + | [{token: Parser.LIDENT(str), start_loc}, {token: Parser.EOL}, ..._] + when !hit_eol && start_loc < offset => + if (!in_block) { + CompletableStatement(false); } else { - None; + CompletableExpression; } - | ['e', 'p', 'y', 't', ...rest] when last_char_whitespace => - if (has_hit_info) { - Some( - CompletableType( - String_utils.slice(~first=slice_offset, ~last=offset, source), - ), - ); - } else { - None; - } - | ['e', 'd', 'u', 'l', 'c', 'n', 'i', ...rest] when last_char_whitespace => - Some( - CompletableInclude( - String_utils.slice(~first=slice_offset, ~last=offset, source), - ), - ) - | ['\n', ...rest] when !has_hit_info => - Some( - CompletableCode( - String_utils.slice(~first=slice_offset, ~last=offset, source), - ), - ) - // Whitespace - | [' ', ...rest] - | ['\n', ...rest] - | ['\t', ...rest] => - searchForContext( - rest, - curr_offset - 1, - slice_offset, - has_hit_info, - ~last_char_whitespace=true, - ) - // Context Seperators - | ['(', ...rest] - | [')', ...rest] - | ['{', ...rest] - | ['}', ...rest] - | ['[', ...rest] - | [']', ...rest] - | [',', ...rest] - | [':', ...rest] - | ['=', ...rest] - | ['<', ...rest] - | ['>', ...rest] => - if (slice_offset == 0) { - searchForContext(rest, curr_offset - 1, curr_offset, true); + | [{token: Parser.UIDENT(_), start_loc}, {token: Parser.EOL}, ..._] + when !hit_eol && start_loc < offset => + if (!in_block) { + CompletableStatement(true); } else { - searchForContext(rest, curr_offset - 1, slice_offset, true); + CompletableExpression; } - // Any old char - | [_, ...rest] => - searchForContext(rest, curr_offset - 1, slice_offset, has_hit_info) - // We Did not find a marker - | [] => None + | [{token: Parser.THICKARROW}, ..._] => + // TODO: Determine if this is a type or expression + CompletableExpression + | [{token: Parser.LPAREN}, ..._] + when token_non_breaking_lst(token_list) => + CompletableExpressionWithReturn + | [{token: Parser.EQUAL}, ..._] + when token_non_breaking_lst(token_list) => + CompletableExpressionWithReturn + | [{token: Parser.EOL, start_loc}, ...rest] when start_loc < offset => + build_context(~hit_eol=true, [Parser.EOL, ...token_list], rest) + | [] => CompletableUnknown + // Most tokens we can skip + | [tok, ...rest] => + build_context(~hit_eol, [tok.token, ...token_list], rest) }; }; - searchForContext(source_chars, offset, 0, false); + build_context(~hit_eol=false, [], tokens); }; }; -let build_keyword_completion = keyword => { - { - label: keyword, - kind: CompletionItemKindKeyword, - detail: "", - documentation: "", - }; -}; -let build_value_completion = - (~env, (value_name: string, value_desc: Types.value_description)) => { - { - label: value_name, - kind: CompletionItemKindValue, - detail: Document.print_type(env, value_desc.val_type), - documentation: "", - }; -}; -let build_module_completion = - (~env, (module_name: string, mod_desc: Types.module_declaration)) => { - { - label: module_name, - kind: CompletionItemKindModule, - detail: Document.print_mod_type(mod_desc), - documentation: "", +let rec resolve_type = (type_desc: Types.type_desc) => { + switch (type_desc) { + | TTySubst({desc}) + | TTyLink({desc}) => resolve_type(desc) + | _ => type_desc }; }; -let build_type_completion = - (~env, (type_name: string, type_desc: Types.type_declaration)) => { - { - label: type_name, - kind: CompletionItemKindTypeParameter, - detail: "", - documentation: "", - }; +let build_keyword_completions = (values: list(completion_value)) => { + List.map( + keyword => + switch (keyword) { + | PlainText(label) => + build_completion(label, CompletionItemKindKeyword) + | Snippet(label, snippet) => + build_completion( + ~insert_text_format=InsertTextFormatSnippet, + ~insert_text=snippet, + label, + CompletionItemKindKeyword, + ) + }, + values, + ); }; -let rec get_completions = - ( - ~include_values, - ~include_types, - env, - root_decl: Types.module_declaration, - path: list(string), - ) => { - // Get The Modules Exports - let provides = - switch (root_decl.md_type) { - | TModSignature(provides) => - List.map( - (s: Types.signature_item) => { - switch (s) { - // Enabled - | TSigValue(ident, decl) when include_values => - Some(build_value_completion(~env, (ident.name, decl))) - | TSigType(ident, decl, _) when include_types => - Some(build_type_completion(~env, (ident.name, decl))) - | TSigModule(ident, decl, _) => - Some(build_module_completion(~env, (ident.name, decl))) - // Dissabled - | TSigValue(_, _) - | TSigType(_, _, _) - | TSigTypeExt(_, _, _) - | TSigModType(_, _) => None - } +let get_expression_completions = + (desire_non_void: bool, program: option(Typedtree.typed_program)) => { + // TODO: Consider using source tree to better infer the env + // builtins + let builtins = [ + build_completion("Ok", CompletionItemKindEnumMember), + build_completion("Err", CompletionItemKindEnumMember), + build_completion("Some", CompletionItemKindEnumMember), + build_completion("None", CompletionItemKindEnumMember), + build_completion("true", CompletionItemKindValue), + build_completion("false", CompletionItemKindValue), + build_completion("void", CompletionItemKindValue), + ]; + // values + let value_completions = + switch (program) { + | Some({env}) => + Env.fold_values( + (tag, _, decl, acc) => { + let (kind, typ) = + switch (resolve_type(decl.val_type.desc)) { + | TTyArrow(_, typ, _) => (CompletionItemKindFunction, typ.desc) + | typ => (CompletionItemKindValue, typ) + }; + switch (List.of_seq(String.to_seq(tag))) { + | [ + '$' | '&' | '*' | '/' | '+' | '-' | '=' | '>' | '<' | '^' | '|' | + '!' | + '?' | + '%' | + ':' | + '.', + ..._, + ] => acc + | _ => + switch (resolve_type(typ)) { + | TTyConstr(id, _, _) + when Path.same(id, Builtin_types.path_void) && desire_non_void => acc + | _ => [ + build_completion( + ~detail=Document.print_type_raw(decl.val_type), + tag, + kind, + ), + ...acc, + ] + } + }; }, - provides, + None, + env, + [], ) - | _ => [] + | None => [] }; - // Filter - switch (path) { - | [_] - | [] => List.filter_map(x => x, provides) - | [pathItem, ...path] => - // Find the desired module - let subMod = - switch (root_decl.md_type) { - | TModSignature(provides) => - List.find_opt( - (s: Types.signature_item) => { - switch (s) { - | TSigModule(ident, decl, _) when ident.name == pathItem => true - | _ => false - } - }, - provides, - ) - | _ => None - }; - switch (subMod) { - | Some(TSigModule(_, decl, _)) => - get_completions(env, decl, path, ~include_values, ~include_types) - | _ => [] + // modules + let module_completions = + switch (program) { + | Some({env}) => + Env.fold_modules( + (tag, _, _, acc) => { + [ + build_completion( + ~detail=Printf.sprintf("module %s", tag), + tag, + CompletionItemKindModule, + ), + ...acc, + ] + }, + None, + env, + [], + ) + | None => [] }; - }; + // merge them all + List.concat([builtins, value_completions, module_completions]); }; -let get_top_level_completions = - ( - ~include_keywords, - ~include_values, - ~include_types, - program: Typedtree.typed_program, - compiled_code: Hashtbl.t(Protocol.uri, Lsp_types.code), - search: list(string), - ) => { - // Keyword Completions - let keyword_completions = - if (include_keywords) { - let keywords = [ - "primitive", - "foreign", - "wasm", - "while", - "for", - "continue", - "break", - "return", - "if", - "when", - "else", - "include", - "use", - "provide", - "abstract", - "except", - "from", - "type", - "enum", - "record", - "module", - "let", - "mut", - "rec", - "match", - "assert", - "fail", - "exception", - "throw", - ]; - List.map(build_keyword_completion, keywords); - } else { - []; - }; - // Value Completions - let value_completions = - if (include_values) { - let values = + +let get_completions_from_context = + (context: completable_context, program: option(Typedtree.typed_program)) => { + // TODO: Consider using the sourcetree to provide some extra env context, thinking type signatures + switch (context) { + | CompletableInclude(str) => + // TODO: Add all paths in Includes + // TODO: Add all relative paths + [build_completion("number", CompletionItemKindFile)] + | CompletableStatement(true) => + switch (program) { + | Some({env}) => + Env.fold_modules( + (tag, _, _, acc) => { + [ + build_completion( + ~detail=Printf.sprintf("module %s", tag), + tag, + CompletionItemKindModule, + ), + ...acc, + ] + }, + None, + env, + [], + ) + | None => [] + } + | CompletableStatement(false) => + let toplevel_completions = build_keyword_completions(toplevel_keywords); + let expression_completions = + build_keyword_completions(expression_keywods); + let value_completions = + switch (program) { + | Some({env}) => Env.fold_values( - (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, - None, - program.env, - [], - ); - let builtins = [ - ("None", CompletionItemKindEnumMember), - ("Some", CompletionItemKindEnumMember), - ("Ok", CompletionItemKindEnumMember), - ("Err", CompletionItemKindEnumMember), - ("true", CompletionItemKindConstant), - ("false", CompletionItemKindConstant), - ("void", CompletionItemKindConstant), - ]; - let builtins = - List.map( - ((label, kind)) => {label, kind, detail: "", documentation: ""}, - builtins, - ); - List.append( - List.map(build_value_completion(~env=program.env), values), - builtins, - ); - } else { - []; - }; - // Type Completions - let type_completions = - if (include_types) { - let types = - Env.fold_types( - (tag, path, (decl, descr), acc) => { - List.append(acc, [(tag, decl)]) + (tag, _, decl, acc) => { + switch (resolve_type(decl.val_type.desc)) { + | TTyArrow(_, _, _) => [ + build_completion( + ~detail=Document.print_type_raw(decl.val_type), + tag, + CompletionItemKindFunction, + ), + ...acc, + ] + | _ => acc + } }, None, - program.env, + env, [], - ); - List.map(build_type_completion(~env=program.env), types); - } else { - []; - }; - // Module Completions - let modules = - Env.fold_modules( - (tag, path, decl, acc) => {List.append(acc, [(tag, decl)])}, - None, - program.env, - [], - ); - let module_completions = - List.map(build_module_completion(~env=program.env), modules); - // Merge Completions - let completions = + ) + | None => [] + }; List.concat([ - keyword_completions, + toplevel_completions, + expression_completions, value_completions, - type_completions, - module_completions, ]); - // Find The Top Level Modules - let completions = - switch (search) { - // User Wrote Nothing - | [] - // User Wrote A Single Word, i.e Top Level Search - | [_] => completions - // User Wrote A Path - | [search, ...path] => - // Find Module Root - switch (List.find_opt(((mod_name, _)) => mod_name == search, modules)) { - | Some((_, root_decl)) => - get_completions( - program.env, - root_decl, - path, - ~include_values, - ~include_types, - ) + | CompletableExpressionWithReturn => + get_expression_completions(true, program) + | CompletableExpression => + let keyword_completions = build_keyword_completions(expression_keywods); + let expression_completions = get_expression_completions(false, program); + List.concat([keyword_completions, expression_completions]); + | CompletableExpressionPath(path, expr_start) => + switch (expr_start) { + | false => + switch (program) { + | Some({env}) => + // TODO: Idk if this is the right function here + [] | None => [] } - }; - // Remove Operators - let operators = [ - '$', - '&', - '*', - '/', - '+', - '-', - '=', - '>', - '<', - '^', - '|', - '!', - '?', - '%', - ':', - '.', - ]; - let completions = - List.filter( - ({label}: completion_item) => - !List.exists(o => String.contains(label, o), operators), - completions, - ); - completions; + // TODO: Handle this + | true => + Trace.log("Unknown Behaviour of expr.XXX"); + []; + } + | CompletableAs => [build_completion("as", CompletionItemKindKeyword)] + | CompletableAfterLet => [ + build_completion("mut", CompletionItemKindKeyword), + build_completion("rec", CompletionItemKindKeyword), + ] + | CompletableUnknown => [] + }; }; let process = @@ -490,58 +716,18 @@ let process = ~documents: Hashtbl.t(Protocol.uri, string), params: RequestParams.t, ) => { - switch (Hashtbl.find_opt(compiled_code, params.text_document.uri)) { - | None => send_completion(~id, []) - | Some({program, sourcetree}) => - // Get Completable state and filter - let completableState = - find_completable_state( - documents, - params.text_document.uri, - params.position, - ); - // Collect Completables - let completions = - switch (completableState) { - | None => [] - | Some(completableState) => - switch (completableState) { - | CompletableCode(str) => - let str = String.trim(str); - let completionPath = String.split_on_char('.', str); - get_top_level_completions( - ~include_keywords=true, - ~include_values=true, - ~include_types=false, - program, - compiled_code, - completionPath, - ); - | CompletableExpr(str) => - let str = String.trim(str); - let completionPath = String.split_on_char('.', str); - get_top_level_completions( - ~include_keywords=false, - ~include_values=true, - ~include_types=false, - program, - compiled_code, - completionPath, - ); - | CompletableType(str) => - let str = String.trim(str); - let completionPath = String.split_on_char('.', str); - get_top_level_completions( - ~include_keywords=false, - ~include_values=false, - ~include_types=true, - program, - compiled_code, - completionPath, - ); - | CompletableInclude(str) => [] - } - }; - send_completion(~id, completions); - }; + let program = + switch (Hashtbl.find_opt(compiled_code, params.text_document.uri)) { + | None => None + | Some({program}) => Some(program) + }; + let context = + get_completion_context( + documents, + params.text_document.uri, + params.position, + ); + print_string_of_context(context); + let completions = get_completions_from_context(context, program); + send_completion(~id, completions); }; diff --git a/compiler/src/language_server/document.re b/compiler/src/language_server/document.re index 72ce07374..51e1ce290 100644 --- a/compiler/src/language_server/document.re +++ b/compiler/src/language_server/document.re @@ -48,6 +48,8 @@ let print_type = (env, ty) => { }; }; +let print_type_raw = ty => Printtyp.string_of_type_scheme(ty); + let print_mod_type = (decl: Types.module_declaration) => { let vals = Modules.get_provides(decl); let signatures = diff --git a/compiler/src/language_server/document.rei b/compiler/src/language_server/document.rei index b5040bf9c..de2930b4a 100644 --- a/compiler/src/language_server/document.rei +++ b/compiler/src/language_server/document.rei @@ -8,4 +8,6 @@ let markdown_join: (string, string) => string; let print_type: (Env.t, Types.type_expr) => string; +let print_type_raw: Types.type_expr => string; + let print_mod_type: Types.module_declaration => string; diff --git a/compiler/src/language_server/initialize.re b/compiler/src/language_server/initialize.re index 54981fe3b..910ff2ba9 100644 --- a/compiler/src/language_server/initialize.re +++ b/compiler/src/language_server/initialize.re @@ -81,7 +81,7 @@ module ResponseResult = { hover_provider: true, completion_provider: { resolve_provider: false, - trigger_characters: [".", ",", "(", ":", "[", "\""], + trigger_characters: [".", ",", "(", ":", "[", "\"", " "], }, definition_provider: { link_support: true, diff --git a/compiler/src/parsing/wrapped_lexer.rei b/compiler/src/parsing/wrapped_lexer.rei new file mode 100644 index 000000000..98402470a --- /dev/null +++ b/compiler/src/parsing/wrapped_lexer.rei @@ -0,0 +1,17 @@ +open Parser; + +type positioned('a) = ('a, Lexing.position, Lexing.position); + +type fn_ctx = + | DiscoverFunctions + | IgnoreFunctions; + +type t = { + lexbuf: Sedlexing.lexbuf, + mutable queued_tokens: list(positioned(token)), + mutable queued_exn: option(exn), + mutable fn_ctx_stack: list(fn_ctx), +}; + +let init: Sedlexing.lexbuf => t; +let token: t => (token, Lexing.position, Lexing.position); From 2223922e37d2737f1428e0987a77bf807e3f24fb Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Tue, 5 Mar 2024 10:45:17 -0500 Subject: [PATCH 16/17] chore: Update for new syntax --- compiler/src/language_server/completion.re | 51 ++++++++++++++++------ compiler/src/language_server/hover.re | 4 +- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index d06d8ee0b..3e5bfc8c3 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -294,7 +294,7 @@ type lex_token = { }; type completable_context = - | CompletableInclude(string) + | CompletableInclude(string, bool) | CompletableStatement(bool) | CompletableExpressionWithReturn | CompletableExpression @@ -307,7 +307,7 @@ type completable_context = let print_string_of_context = context => { let ctx = switch (context) { - | CompletableInclude(_) => "CompleteInclude" + | CompletableInclude(_, _) => "CompleteInclude" | CompletableAs => "CompleteAs" | CompletableAfterLet => "CompletableAfterLet" | CompletableExpressionWithReturn => "CompletableExpressionWithReturn" @@ -449,38 +449,50 @@ let get_completion_context = (documents, uri, position: Protocol.position) => { tokens: list(lex_token), ) => { switch (tokens) { - // TODO: Add a state for when we are at from | - // TODO: Add a state for when we are at from XXXX use { | } // TODO: Add a state for when we are at match (XXXX) { | } <- This could be very useful also could not be // TODO: Add a state for type XXXX = | + // TODO: Add state for use XXXX. // Tokens that we care about | [{token: Parser.LET}, ..._] when !hit_eol && token_non_breaking_lst(token_list) => CompletableAfterLet - | [{token: Parser.STRING(_), end_loc}, {token: Parser.INCLUDE}, ..._] + // TODO: Reimplement the as completion + | [{token: Parser.STRING(str), end_loc}, {token: Parser.FROM}, ..._] when !hit_eol && after_range(end_loc, offset) && !last_token_eq(Parser.AS, token_list) => - CompletableAs + CompletableInclude(str, true) | [ {token: Parser.STRING(str), start_loc, end_loc}, - {token: Parser.INCLUDE}, + {token: Parser.FROM}, ..._, ] when in_range(start_loc, end_loc, offset) => - CompletableInclude(str) + CompletableInclude(str, false) + // TODO: Just capture up to the . | [{token: Parser.DOT}, {token: Parser.EOL}, ..._] when !hit_eol && !last_token_eq(Parser.DOT, token_list) => - // TODO: Support test().label on records somehow + /* + * TODO: Support test().label on records somehow + * This is going to require using sourceTree, to get the return type of the function, (We may also be able to check the env but the problem is we don't have a complete env at this point) + * After we have a type signature it shouldn't be that hard to resolve the completions, until that point it is though. + */ // TODO: Implement path collection let (path, expr_start) = collect_idents(None, true, token_list); switch (path) { | Some(path) => CompletableExpressionPath(path, expr_start) | None => CompletableUnknown }; - | [{token: Parser.LIDENT(str), start_loc}, {token: Parser.EOL}, ..._] + // This is the case of XXXX.X| <- You are actively writing + // TODO: Support test().label on records somehow + | [ + {token: Parser.LIDENT(_) | Parser.UIDENT(_), start_loc}, + {token: Parser.EOL}, + ..._, + ] when !hit_eol && start_loc < offset => + // TODO: Collect the path if (!in_block) { CompletableStatement(false); } else { @@ -624,10 +636,21 @@ let get_completions_from_context = (context: completable_context, program: option(Typedtree.typed_program)) => { // TODO: Consider using the sourcetree to provide some extra env context, thinking type signatures switch (context) { - | CompletableInclude(str) => - // TODO: Add all paths in Includes - // TODO: Add all relative paths - [build_completion("number", CompletionItemKindFile)] + | CompletableInclude(path, afterPath) => + if (afterPath) { + [ + // TODO: Implement completion for include Module name, Note: This is going to take some work as the module is not loaded into the env + // We are at from "path" | <- cursor is represented by | + build_completion("include", CompletionItemKindKeyword), + ]; + } else { + [ + // TODO: Add all paths in Includes + // TODO: Add all relative paths + // We are at from "|" <- cursor is represented by | + build_completion("number", CompletionItemKindFile), + ]; + } | CompletableStatement(true) => switch (program) { | Some({env}) => diff --git a/compiler/src/language_server/hover.re b/compiler/src/language_server/hover.re index 222acbbb3..40f02837b 100644 --- a/compiler/src/language_server/hover.re +++ b/compiler/src/language_server/hover.re @@ -70,7 +70,7 @@ let declaration_lens = (ident: Ident.t, decl: Types.type_declaration) => { }; let include_lens = (env: Env.t, path: Path.t) => { - let header = grain_code_block("module " ++ Path.name(path)); + let header = Document.grain_code_block("module " ++ Path.name(path)); let decl = Env.find_module(path, None, env); let module_decl = switch (Modules.get_provides(decl)) { @@ -78,7 +78,7 @@ let include_lens = (env: Env.t, path: Path.t) => { | [] => None }; switch (module_decl) { - | Some(mod_sig) => markdown_join(header, mod_sig) + | Some(mod_sig) => Document.markdown_join(header, mod_sig) | None => header }; }; From d93ab92ee438cbe14bb7b8b6837bc713105c6142 Mon Sep 17 00:00:00 2001 From: Spotandjake Date: Fri, 2 Aug 2024 17:47:38 -0400 Subject: [PATCH 17/17] chore: Update for 0.6.x --- compiler/src/language_server/completion.re | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/language_server/completion.re b/compiler/src/language_server/completion.re index 3e5bfc8c3..8d19486c8 100644 --- a/compiler/src/language_server/completion.re +++ b/compiler/src/language_server/completion.re @@ -274,8 +274,8 @@ let toplevel_keywords = [ PlainText("module"), PlainText("provide"), PlainText("abstract"), - Snippet("include", "include \"$1\"$0"), - Snippet("from", "from \"$1\" use { $0 }"), + Snippet("from", "from \"$1\" include $0"), + Snippet("use", "use $1.{ $0 }"), ]; let expression_keywods = [ @@ -457,12 +457,12 @@ let get_completion_context = (documents, uri, position: Protocol.position) => { when !hit_eol && token_non_breaking_lst(token_list) => CompletableAfterLet // TODO: Reimplement the as completion - | [{token: Parser.STRING(str), end_loc}, {token: Parser.FROM}, ..._] + | [{token: Parser.MODULE, end_loc}, {token: Parser.INCLUDE}, ..._] when !hit_eol && after_range(end_loc, offset) && !last_token_eq(Parser.AS, token_list) => - CompletableInclude(str, true) + CompletableAs | [ {token: Parser.STRING(str), start_loc, end_loc}, {token: Parser.FROM},