diff --git a/spec/compilers_spec.cr b/spec/compilers_spec.cr index 7c189ba5d..d96e46bd0 100644 --- a/spec/compilers_spec.cr +++ b/spec/compilers_spec.cr @@ -30,7 +30,7 @@ Dir test: nil) json = - Mint::MintJson.new + Mint::MintJson.parse("{}", "mint.json") files = Mint::Bundler.new( diff --git a/spec/installer/repository_spec.cr b/spec/installer/repository_spec.cr index 15ffb151c..0c0c6fa14 100644 --- a/spec/installer/repository_spec.cr +++ b/spec/installer/repository_spec.cr @@ -12,17 +12,23 @@ describe "Repository" do repository = Mint::Installer::Repository.new("name", "success") message = <<-MESSAGE - ░ ERROR (REPOSITORY_INVALID_MINT_JSON) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░ ERROR (INVALID_JSON) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - I could not parse the mint.json for the package: name (success) for the version - or tag: master + I could not parse the following mint.json file: + + ┌ /tmp/mint-packages/success/mint.json:1:1 + ├───────────────────────────────────────── + 1│ hello MESSAGE begin repository.json("master") fail "Should have raised!" rescue error : Mint::Error - error.to_terminal.to_s.uncolorize.should eq(message) + result = + error.to_terminal.to_s.uncolorize + + fail diff(message, result) unless result == message.strip end end diff --git a/spec/language_server/code_actions/module b/spec/language_server/code_actions/module new file mode 100644 index 000000000..85af5365b --- /dev/null +++ b/spec/language_server/code_actions/module @@ -0,0 +1,55 @@ +module Test { + fun test : String { + "" + } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/codeAction", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "range": { + "start": { "line": 1, "character": 8 }, + "end": { "line": 1, "character": 8 } + }, + "context": { + "diagnostics": [] + } + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "title": "Order Entities", + "kind": "source", + "diagnostics": [], + "isPreferred": false, + "edit": { + "changes": { + "file://#{root_path}/test.mint": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 9999, + "character": 999 + } + }, + "newText": "module Test {\n fun test : String {\n \"\"\n }\n}" + } + ] + } + } + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server/code_actions/provider b/spec/language_server/code_actions/provider new file mode 100644 index 000000000..f3eabc6af --- /dev/null +++ b/spec/language_server/code_actions/provider @@ -0,0 +1,59 @@ +type Test { + name : String +} + +provider Test : Test { + fun test : String { + "" + } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/codeAction", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "range": { + "start": { "line": 5, "character": 11 }, + "end": { "line": 5, "character": 11 } + }, + "context": { + "diagnostics": [] + } + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "title": "Order Entities", + "kind": "source", + "diagnostics": [], + "isPreferred": false, + "edit": { + "changes": { + "file://#{root_path}/test.mint": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 9999, + "character": 999 + } + }, + "newText": "type Test {\n name : String\n}\n\nprovider Test : Test {\n fun test : String {\n \"\"\n }\n}" + } + ] + } + } + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server/completion/function b/spec/language_server/completion/function new file mode 100644 index 000000000..989cec478 --- /dev/null +++ b/spec/language_server/completion/function @@ -0,0 +1,37 @@ +component Test { + fun otherFunction (name : String) : String { + name + } + + fun render : String { + "Hello" + } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "position": { + "line": 3, + "character": 4 + } + } +} +-------------------------------------------------------------------------request +{ + "label": "otherFunction", + "kind": 3, + "detail": "Function", + "documentation": "", + "deprecated": false, + "preselect": false, + "sortText": "otherFunction", + "filterText": "otherFunction", + "insertText": "otherFunction()", + "insertTextFormat": 2 +} +--------------------------------------------------------------response 0 contain diff --git a/spec/language_server/completion/html_component b/spec/language_server/completion/html_component new file mode 100644 index 000000000..c6ca6de03 --- /dev/null +++ b/spec/language_server/completion/html_component @@ -0,0 +1,33 @@ +component Test { + fun render : Html { + <> + } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "position": { + "line": 3, + "character": 4 + } + } +} +-------------------------------------------------------------------------request +{ + "label": "Test", + "kind": 15, + "detail": "Component", + "documentation": "", + "deprecated": false, + "preselect": false, + "sortText": "Test", + "filterText": "Test", + "insertText": "\n \n", + "insertTextFormat": 2 +} +--------------------------------------------------------------response 0 contain diff --git a/spec/language_server/completion/html_component_with_snippet_support b/spec/language_server/completion/html_component_with_snippet_support new file mode 100644 index 000000000..fa53a23e5 --- /dev/null +++ b/spec/language_server/completion/html_component_with_snippet_support @@ -0,0 +1,49 @@ +component Test { + fun render : Html { + <> + } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "initialize", + "params": { + "capabilities": { + "textDocument": { + "completion": { + "completionItem": { + "snippetSupport": true + } + } + } + } + } +} +-------------------------------------------------------------------------request +{ + "id": 1, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "position": { + "line": 3, + "character": 4 + } + } +} +-------------------------------------------------------------------------request +{ + "label": "Test", + "kind": 15, + "detail": "Component", + "documentation": "", + "deprecated": false, + "preselect": false, + "sortText": "Test", + "filterText": "Test", + "insertText": "\n $0\n", + "insertTextFormat": 2 +} +--------------------------------------------------------------response 1 contain diff --git a/spec/language_server/completion_spec.cr b/spec/language_server/completion_spec.cr deleted file mode 100644 index cb41b8151..000000000 --- a/spec/language_server/completion_spec.cr +++ /dev/null @@ -1,92 +0,0 @@ -require "../spec_helper" - -struct LSPResult - include JSON::Serializable - - property result : Array(LSP::CompletionItem) -end - -describe "Language Server Completion" do - it "returns snippets for html components while in a Html function" do - with_workspace do |workspace| - workspace.file "test.mint", <<-MINT - component Test { - fun render : Html { - <> - } - } - MINT - - # TODO: Assert - lsp([{ - id: 0, - method: "textDocument/completion", - message: { - textDocument: {uri: workspace.file_path("test.mint")}, - position: {line: 2, character: 4}, - }, - }]) - end - end - - it "returns completions while in a function" do - with_workspace do |workspace| - workspace.file "test.mint", <<-MINT - component Test { - fun otherFunction (name : String) : String { - name - } - - fun render : String { - "Hello" - } - } - MINT - - # TODO: Assert - result = - lsp([ - { - id: 0, - method: "initialize", - message: { - capabilities: { - textDocument: { - completion: { - completionItem: { - snippetSupport: false, - }, - }, - }, - }, - }, - }, - { - id: 1, - method: "textDocument/completion", - message: { - textDocument: {uri: workspace.file_path("test.mint")}, - position: {line: 2, character: 4}, - }, - }, - ]) - - case item = result[1] - when String - completion = - LSPResult - .from_json(item) - .result - .find { |message| message.label == "Test" } - - if completion - completion.insert_text.should_not contain("$0") - else - fail "No completion!" - end - else - fail "Should have succeeded." - end - end - end -end diff --git a/spec/language_server/definition/location/type_destructuring_option b/spec/language_server/definition/location/type_destructuring_option index 62a57023e..d73c2522c 100644 --- a/spec/language_server/definition/location/type_destructuring_option +++ b/spec/language_server/definition/location/type_destructuring_option @@ -3,7 +3,7 @@ enum Status { Error Ok } -------------------------------------------------------------------file status.mint +----------------------------------------------------------------file status.mint module Test { fun toString (status : Status) : String { case status { diff --git a/spec/language_server/definition/location/variable_component_connect b/spec/language_server/definition/location/variable_component_connect index 7d687e9ec..3cdba1aa4 100644 --- a/spec/language_server/definition/location/variable_component_connect +++ b/spec/language_server/definition/location/variable_component_connect @@ -46,14 +46,14 @@ store Theme { "range": { "start": { "line": 1, - "character": 27 + "character": 8 }, "end": { "line": 1, - "character": 34 + "character": 15 } }, - "uri": "file://#{root_path}/test.mint" + "uri": "file://#{root_path}/store.mint" }, "id": 1 } diff --git a/spec/language_server/definition/location/variable_component_connect_as b/spec/language_server/definition/location/variable_component_connect_as index fcbfd2e46..33605076e 100644 --- a/spec/language_server/definition/location/variable_component_connect_as +++ b/spec/language_server/definition/location/variable_component_connect_as @@ -46,14 +46,14 @@ store Theme { "range": { "start": { "line": 1, - "character": 38 + "character": 8 }, "end": { "line": 1, - "character": 50 + "character": 15 } }, - "uri": "file://#{root_path}/test.mint" + "uri": "file://#{root_path}/store.mint" }, "id": 1 } diff --git a/spec/language_server/definition/location_link/type_destructuring_option b/spec/language_server/definition/location_link/type_destructuring_option index 2ced2ee10..4ecf421b3 100644 --- a/spec/language_server/definition/location_link/type_destructuring_option +++ b/spec/language_server/definition/location_link/type_destructuring_option @@ -55,7 +55,7 @@ module Test { "originSelectionRange": { "start": { "line": 3, - "character": 14 + "character": 6 }, "end": { "line": 3, diff --git a/spec/language_server/definition/location_link/variable_component_connect b/spec/language_server/definition/location_link/variable_component_connect index b2c568230..d86bb5a9b 100644 --- a/spec/language_server/definition/location_link/variable_component_connect +++ b/spec/language_server/definition/location_link/variable_component_connect @@ -54,7 +54,7 @@ store Theme { "character": 16 } }, - "targetUri": "file://#{root_path}/test.mint", + "targetUri": "file://#{root_path}/store.mint", "targetRange": { "start": { "line": 1, @@ -68,11 +68,11 @@ store Theme { "targetSelectionRange": { "start": { "line": 1, - "character": 27 + "character": 8 }, "end": { "line": 1, - "character": 34 + "character": 15 } } } diff --git a/spec/language_server/definition/location_link/variable_component_connect_as b/spec/language_server/definition/location_link/variable_component_connect_as index 9b2b40057..366bc5570 100644 --- a/spec/language_server/definition/location_link/variable_component_connect_as +++ b/spec/language_server/definition/location_link/variable_component_connect_as @@ -54,7 +54,7 @@ store Theme { "character": 21 } }, - "targetUri": "file://#{root_path}/test.mint", + "targetUri": "file://#{root_path}/store.mint", "targetRange": { "start": { "line": 1, @@ -62,17 +62,17 @@ store Theme { }, "end": { "line": 1, - "character": 52 + "character": 36 } }, "targetSelectionRange": { "start": { "line": 1, - "character": 38 + "character": 8 }, "end": { "line": 1, - "character": 50 + "character": 15 } } } diff --git a/spec/language_server/did_change b/spec/language_server/did_change new file mode 100644 index 000000000..a5abd92cc --- /dev/null +++ b/spec/language_server/did_change @@ -0,0 +1,19 @@ +component Test { + fun render : Html { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/didChange", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "contentChanges": [{ + "text": "component Test {\n fun render : Html {\n\n\n
\n }\n }" + }] + } +} +-------------------------------------------------------------------------request diff --git a/spec/language_server/did_change_spec.cr b/spec/language_server/did_change_spec.cr deleted file mode 100644 index ab41f0c71..000000000 --- a/spec/language_server/did_change_spec.cr +++ /dev/null @@ -1,35 +0,0 @@ -require "../spec_helper" - -describe "Language Server - DidChange" do - it "there should be no error" do - with_workspace do |workspace| - workspace.file "test.mint", <<-MINT - component Test { - fun render : Html { -
- } - } - MINT - - updated = <<-MINT - component Test { - fun render : Html { - - -
- } - } - MINT - - notify_lsp( - method: "textDocument/didChange", - message: { - textDocument: {uri: workspace.file_path("test.mint"), version: 1}, - contentChanges: [{text: updated, range: nil, rangeLength: nil}], - } - ) - - workspace.workspace.error.should eq(nil) - end - end -end diff --git a/spec/language_server/did_open b/spec/language_server/did_open new file mode 100644 index 000000000..0c40994e7 --- /dev/null +++ b/spec/language_server/did_open @@ -0,0 +1,19 @@ +component Test { + fun render : Html { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint", + "languageId": "mint", + "version": 1, + "text": "component Test {\n fun render : Html {\n\n\n
\n }\n }" + } + } +} +-------------------------------------------------------------------------request diff --git a/spec/language_server/folding_range/component b/spec/language_server/folding_range/component new file mode 100644 index 000000000..93b50326a --- /dev/null +++ b/spec/language_server/folding_range/component @@ -0,0 +1,31 @@ +component Test { + fun render { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/foldingRange", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + } + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "startLine": 1, + "endLine": 3 + }, + { + "startLine": 0, + "endLine": 4 + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server/folding_range/component_with_comments b/spec/language_server/folding_range/component_with_comments new file mode 100644 index 000000000..023983946 --- /dev/null +++ b/spec/language_server/folding_range/component_with_comments @@ -0,0 +1,39 @@ +/* +Comment line 1... +Comment line 2... +*/ +component Test { + fun render { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/foldingRange", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + } + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "startLine": 5, + "endLine": 7 + }, + { + "startLine": 0, + "endLine": 3 + }, + { + "startLine": 3, + "endLine": 8 + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server/folding_range/module b/spec/language_server/folding_range/module new file mode 100644 index 000000000..86107b98e --- /dev/null +++ b/spec/language_server/folding_range/module @@ -0,0 +1,31 @@ +module Test { + fun render { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/foldingRange", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + } + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "startLine": 1, + "endLine": 3 + }, + { + "startLine": 0, + "endLine": 4 + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server/formatting b/spec/language_server/formatting new file mode 100644 index 000000000..31a28a9d7 --- /dev/null +++ b/spec/language_server/formatting @@ -0,0 +1,40 @@ +component Test { + fun render : Html { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/formatting", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "options": { + "tabSize": 2, + "insertSpaces": true + } + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 9999, + "character": 999 + } + }, + "newText": "component Test {\n fun render : Html {\n
\n }\n}" + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server/ls_spec.cr b/spec/language_server/ls_spec.cr deleted file mode 100644 index be40db072..000000000 --- a/spec/language_server/ls_spec.cr +++ /dev/null @@ -1,58 +0,0 @@ -require "../spec_helper" - -def clean_json(workspace : Workspace, path : String) - path.strip.gsub("\#{root_path}", workspace.root_path) -end - -Dir - .glob("./spec/language_server/{hover,semantic_tokens}/**/*") - .select! { |file| File.file?(file) } - .sort! - .each do |file| - it file do - with_workspace do |workspace| - contents = File.read(file) - - position = 0 - - requests = [] of String - responses = [] of String - - contents.scan(/^\-+(\w+)( [\w.]+)?/m) do |match| - text = contents[position, match.begin - position] - - case match[1] - when "file" - workspace.file match[2].strip, text.strip - when "request" - requests << clean_json(workspace, text) - when "response" - responses << clean_json(workspace, text) - else - raise Exception.new("Unknown type #{match[1].inspect}, expected file, request or response") - end - - position = match.end - end - - raise Exception.new("Expected requests") if requests.empty? - raise Exception.new("Expected responses") if responses.empty? - - actual_responses = lsp_json(requests) - - responses.each do |expected_response| - expected_id = JSON.parse(expected_response)["id"].as_i - - actual_response = actual_responses.find! do |response| - JSON.parse(response)["id"].as_i == expected_id - end - - begin - expected_response.should eq(actual_response) - rescue error - fail diff(actual_response, expected_response) - end - end - end - end - end diff --git a/spec/language_server/will_save_wait_until b/spec/language_server/will_save_wait_until new file mode 100644 index 000000000..709ea77c8 --- /dev/null +++ b/spec/language_server/will_save_wait_until @@ -0,0 +1,37 @@ +component Test { + fun render : Html { +
+ } +} +------------------------------------------------------------------file test.mint +{ + "id": 0, + "method": "textDocument/willSaveWaitUntil", + "params": { + "textDocument": { + "uri": "file://#{root_path}/test.mint" + }, + "reason": 1 + } +} +-------------------------------------------------------------------------request +{ + "jsonrpc": "2.0", + "result": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 9999, + "character": 999 + } + }, + "newText": "component Test {\n fun render : Html {\n
\n }\n}" + } + ], + "id": 0 +} +------------------------------------------------------------------------response diff --git a/spec/language_server_spec.cr b/spec/language_server_spec.cr new file mode 100644 index 000000000..0b2cab4ad --- /dev/null +++ b/spec/language_server_spec.cr @@ -0,0 +1,88 @@ +require "./spec_helper" + +def clean_json(workspace : Workspace, path : String) + path.strip.gsub("\#{root_path}", workspace.root_path) +end + +Dir + .glob("./spec/language_server/**/*") + .select! { |file| File.file?(file) } + .sort! + .each do |file| + it file do + with_workspace do |workspace| + contents = File.read(file) + + position = 0 + + requests = [] of String + responses = [] of {String, Int32 | Nil, String} + + contents.scan(/^\-+(\w+)( [\w\-.]+)?( [\w\-.]+)?/m) do |match| + text = + contents[position, match.begin - position].strip + + case match[1] + when "file" + workspace.file match[2].strip, text + when "request" + requests << clean_json(workspace, text) + when "response" + id, param = + if value = match[3]?.try(&.strip) + {match[2]?.to_s.strip, value} + else + {"", match[2]?.to_s.strip} + end + + responses << { + clean_json(workspace, text), + id.to_i32?, + param, + } + else + raise Exception.new("Unknown type #{match[1].inspect}, expected file, request or response") + end + + position = match.end + end + + raise Exception.new("Expected requests") if requests.empty? + + actual_responses = lsp_json(requests) + + if responses.size > 0 + responses.each do |expected_response| + expected_id = + expected_response[1] || + JSON.parse(expected_response[0])["id"].as_i? + + actual_response = + actual_responses.find! do |response| + JSON.parse(response)["id"].as_i? == expected_id + end + + case expected_response[2] + when "contain" + json = + JSON.parse(actual_response) + + expected = + JSON.parse(expected_response[0]) + + json.to_json.should contain(expected.to_json) + else + begin + expected_response[0].should eq(actual_response) + rescue error + fail diff(expected_response[0], actual_response) + end + end + end + elsif actual_responses.size > 0 + puts actual_responses + raise Exception.new("No responses expected") + end + end + end + end diff --git a/spec/mint_json/application_css_prefix_invalid b/spec/mint_json/application_css_prefix_invalid new file mode 100644 index 000000000..d770849f2 --- /dev/null +++ b/spec/mint_json/application_css_prefix_invalid @@ -0,0 +1,18 @@ +{ + "application": { + "css-prefix": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_CSS_PREFIX_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The css-prefix field of the application object should be a string, but it's not: + + ┌ spec/fixtures/mint.json:3:19 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "css-prefix": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_display_invalid b/spec/mint_json/application_display_invalid new file mode 100644 index 000000000..896bbc1c8 --- /dev/null +++ b/spec/mint_json/application_display_invalid @@ -0,0 +1,18 @@ +{ + "application": { + "display": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_DISPLAY_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The display field of the application object should be a string, but it's not: + + ┌ spec/fixtures/mint.json:3:16 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "display": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_display_mismatch b/spec/mint_json/application_display_mismatch new file mode 100644 index 000000000..e473b4e5b --- /dev/null +++ b/spec/mint_json/application_display_mismatch @@ -0,0 +1,25 @@ +{ + "application": { + "display": "xxx" + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_DISPLAY_MISMATCH) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The value of the display field should be one of: + + fullscreen + standalone + minimal-ui + browser + +It is here: + + ┌ spec/fixtures/mint.json:3:16 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "display": "xxx" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_head_invalid b/spec/mint_json/application_head_invalid new file mode 100644 index 000000000..2765a0f0b --- /dev/null +++ b/spec/mint_json/application_head_invalid @@ -0,0 +1,18 @@ +{ + "application": { + "head": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_HEAD_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The head field of the application object should be a string, but it's not: + + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "head": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_head_not_exists b/spec/mint_json/application_head_not_exists new file mode 100644 index 000000000..b9cf78aef --- /dev/null +++ b/spec/mint_json/application_head_not_exists @@ -0,0 +1,22 @@ +{ + "application": { + "head": "head.html" + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_HEAD_NOT_EXISTS) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The head field of the application object points to a file that does not exists. + +It should point to an HTML file, which be injected to the tag of the +generated HTML. It is used to include external dependencies (CSS, JS, analytics, +etc...) + + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "head": "head.html" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_icon_invalid b/spec/mint_json/application_icon_invalid new file mode 100644 index 000000000..561ad1403 --- /dev/null +++ b/spec/mint_json/application_icon_invalid @@ -0,0 +1,18 @@ +{ + "application": { + "icon": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_ICON_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The icon field of the application object should be a string, but it's not: + + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "icon": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_icon_not_exists b/spec/mint_json/application_icon_not_exists new file mode 100644 index 000000000..01f3fc5f0 --- /dev/null +++ b/spec/mint_json/application_icon_not_exists @@ -0,0 +1,21 @@ +{ + "application": { + "icon": "icon.jpg" + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_ICON_NOT_EXISTS) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The icon field of the application object points to a file that does not exists. + +It should point to an image which will be used to generate favicons for the +application. + + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "icon": "icon.jpg" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_invalid b/spec/mint_json/application_invalid new file mode 100644 index 000000000..e2d004922 --- /dev/null +++ b/spec/mint_json/application_invalid @@ -0,0 +1,14 @@ +{ + "application": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The application field should be an object, but it's not: + + ┌ spec/fixtures/mint.json:2:18 + ├───────────────────────────── + 1│ { + 2│ "application": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/application_invalid_key b/spec/mint_json/application_invalid_key new file mode 100644 index 000000000..1653cb8d2 --- /dev/null +++ b/spec/mint_json/application_invalid_key @@ -0,0 +1,22 @@ +{ + "application": { + "xxx": "" + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_INVALID_KEY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The application object has an invalid key: + + xxx + +It is here: + + ┌ spec/fixtures/mint.json:3:12 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "xxx": "" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_meta_invalid b/spec/mint_json/application_meta_invalid new file mode 100644 index 000000000..df5dd802c --- /dev/null +++ b/spec/mint_json/application_meta_invalid @@ -0,0 +1,18 @@ +{ + "application": { + "meta": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_META_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The meta field of the application object should be an object, but it's not: + + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "meta": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_meta_keyword_invalid b/spec/mint_json/application_meta_keyword_invalid new file mode 100644 index 000000000..dcfc192d4 --- /dev/null +++ b/spec/mint_json/application_meta_keyword_invalid @@ -0,0 +1,22 @@ +{ + "application": { + "meta": { + "keywords": [0] + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_META_KEYWORD_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +A keyword should be a string, but it's not: + + ┌ spec/fixtures/mint.json:4:20 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "meta": { + 4│ "keywords": [0] + │ ⌃ + 5│ } + 6│ } + 7│ } diff --git a/spec/mint_json/application_meta_keywords_invalid b/spec/mint_json/application_meta_keywords_invalid new file mode 100644 index 000000000..d2b5092f3 --- /dev/null +++ b/spec/mint_json/application_meta_keywords_invalid @@ -0,0 +1,22 @@ +{ + "application": { + "meta": { + "keywords": 0 + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_META_KEYWORDS_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The keywords field of the meta object should be an array, but it's not: + + ┌ spec/fixtures/mint.json:4:19 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "meta": { + 4│ "keywords": 0 + │ ⌃ + 5│ } + 6│ } + 7│ } diff --git a/spec/mint_json/application_meta_value_invalid b/spec/mint_json/application_meta_value_invalid new file mode 100644 index 000000000..d350f83dd --- /dev/null +++ b/spec/mint_json/application_meta_value_invalid @@ -0,0 +1,22 @@ +{ + "application": { + "meta": { + "viewport": 0 + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_META_VALUE_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The value of a meta field should be a string, but it's not: + + ┌ spec/fixtures/mint.json:4:19 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "meta": { + 4│ "viewport": 0 + │ ⌃ + 5│ } + 6│ } + 7│ } diff --git a/spec/mint_json/application_name b/spec/mint_json/application_name new file mode 100644 index 000000000..887c7e1b7 --- /dev/null +++ b/spec/mint_json/application_name @@ -0,0 +1,18 @@ +{ + "application": { + "name": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_NAME_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The name field of the application object should be a string, but it's not: + + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "name": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_orientation_invalid b/spec/mint_json/application_orientation_invalid new file mode 100644 index 000000000..0d80d5173 --- /dev/null +++ b/spec/mint_json/application_orientation_invalid @@ -0,0 +1,19 @@ +{ + "application": { + "orientation": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_ORIENTATION_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The orientation field of the application object should be a string, but it's +not: + + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "orientation": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_orientation_mismatch b/spec/mint_json/application_orientation_mismatch new file mode 100644 index 000000000..4a728c6fc --- /dev/null +++ b/spec/mint_json/application_orientation_mismatch @@ -0,0 +1,29 @@ +{ + "application": { + "orientation": "xxx" + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_ORIENTATION_MISMATCH) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The value of the orientation field should be one of: + + any + natural + landscape + landscape-primary + landscape-secondary + portrait + portrait-primary + portrait-secondary + +It is here: + + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "orientation": "xxx" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_theme_color_invalid b/spec/mint_json/application_theme_color_invalid new file mode 100644 index 000000000..ae0e4ce14 --- /dev/null +++ b/spec/mint_json/application_theme_color_invalid @@ -0,0 +1,19 @@ +{ + "application": { + "theme-color": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_THEME_COLOR_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The theme-color field of the application object should be a string, but it's +not: + + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "theme-color": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_title_empty b/spec/mint_json/application_title_empty new file mode 100644 index 000000000..fb2c5b71d --- /dev/null +++ b/spec/mint_json/application_title_empty @@ -0,0 +1,18 @@ +{ + "application": { + "title": "" + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_TITLE_EMPTY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The title field of the application object should not be empty, but it is: + + ┌ spec/fixtures/mint.json:3:14 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "title": "" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/application_title_invalid b/spec/mint_json/application_title_invalid new file mode 100644 index 000000000..7ec4afe61 --- /dev/null +++ b/spec/mint_json/application_title_invalid @@ -0,0 +1,18 @@ +{ + "application": { + "title": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (APPLICATION_TITLE_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The title field of the application object should be a string, but it's not: + + ┌ spec/fixtures/mint.json:3:14 + ├───────────────────────────── + 1│ { + 2│ "application": { + 3│ "title": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/dependencies_empty b/spec/mint_json/dependencies_empty new file mode 100644 index 000000000..f2bbfd842 --- /dev/null +++ b/spec/mint_json/dependencies_empty @@ -0,0 +1,16 @@ +{ + "dependencies": {} +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCIES_EMPTY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The dependencies field lists all the dependencies for the application. + +The dependencies object should not be empty, but it is: + + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── + 1│ { + 2│ "dependencies": {} + │ ⌃ + 3│ } diff --git a/spec/mint_json/dependencies_invalid b/spec/mint_json/dependencies_invalid new file mode 100644 index 000000000..0c0f031ed --- /dev/null +++ b/spec/mint_json/dependencies_invalid @@ -0,0 +1,16 @@ +{ + "dependencies": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCIES_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The dependencies field lists all the dependencies for the application. + +It should be an object, but it's not: + + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── + 1│ { + 2│ "dependencies": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/dependency_constraint_bad b/spec/mint_json/dependency_constraint_bad new file mode 100644 index 000000000..634783fcd --- /dev/null +++ b/spec/mint_json/dependency_constraint_bad @@ -0,0 +1,32 @@ +{ + "dependencies": { + "package": { + "repository": "", + "constraint": "xxx" + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_CONSTRAINT_BAD) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The constraint of a dependency is either in this format: + + 0.0.0 <= v < 1.0.0 + +or a git tag / commit / branch followed by the version: + + master:0.1.0 + +I could not find either: + + ┌ spec/fixtures/mint.json:5:21 + ├───────────────────────────── + 1│ { + 2│ "dependencies": { + 3│ "package": { + 4│ "repository": "", + 5│ "constraint": "xxx" + │ ⌃ + 6│ } + 7│ } + 8│ } diff --git a/spec/mint_json/dependency_constraint_invalid b/spec/mint_json/dependency_constraint_invalid new file mode 100644 index 000000000..32d2cee9a --- /dev/null +++ b/spec/mint_json/dependency_constraint_invalid @@ -0,0 +1,24 @@ +{ + "dependencies": { + "package": { + "repository": "", + "constraint": 0 + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_CONSTRAINT_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The constraint field of a depencency must be an string, but it's not: + + ┌ spec/fixtures/mint.json:5:21 + ├───────────────────────────── + 1│ { + 2│ "dependencies": { + 3│ "package": { + 4│ "repository": "", + 5│ "constraint": 0 + │ ⌃ + 6│ } + 7│ } + 8│ } diff --git a/spec/mint_json/dependency_invalid b/spec/mint_json/dependency_invalid new file mode 100644 index 000000000..1ed7e9b51 --- /dev/null +++ b/spec/mint_json/dependency_invalid @@ -0,0 +1,18 @@ +{ + "dependencies": { + "package": 0 + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +A dependency must be an object, but it's not: + + ┌ spec/fixtures/mint.json:3:16 + ├───────────────────────────── + 1│ { + 2│ "dependencies": { + 3│ "package": 0 + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/dependency_invalid_key b/spec/mint_json/dependency_invalid_key new file mode 100644 index 000000000..14ec71ebd --- /dev/null +++ b/spec/mint_json/dependency_invalid_key @@ -0,0 +1,22 @@ +{ + "dependencies": { + "package": { "xxx": "" } + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_INVALID_KEY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +A dependency object has an invalid key: + + xxx + +It is here: + + ┌ spec/fixtures/mint.json:3:25 + ├───────────────────────────── + 1│ { + 2│ "dependencies": { + 3│ "package": { "xxx": "" } + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/dependency_missing_constraint b/spec/mint_json/dependency_missing_constraint new file mode 100644 index 000000000..f700c30f7 --- /dev/null +++ b/spec/mint_json/dependency_missing_constraint @@ -0,0 +1,21 @@ +{ + "dependencies": { + "package": { + "repository": "https://www.github.com/user/repo" + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_MISSING_CONSTRAINT) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +A dependency object is missing the constraint field: + + ┌ spec/fixtures/mint.json:6:3 + ├─────────────────────────────────────────────────────── + 2│ "dependencies": { + 3│ "package": { + 4│ "repository": "https://www.github.com/user/repo" + 5│ } + 6│ } + │ ⌃ + 7│ } diff --git a/spec/mint_json/dependency_missing_repository b/spec/mint_json/dependency_missing_repository new file mode 100644 index 000000000..c4e6c842e --- /dev/null +++ b/spec/mint_json/dependency_missing_repository @@ -0,0 +1,18 @@ +{ + "dependencies": { + "package": { } + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_MISSING_REPOSITORY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +A dependency object is missing the repository field: + + ┌ spec/fixtures/mint.json:4:3 + ├──────────────────────────── + 1│ { + 2│ "dependencies": { + 3│ "package": { } + 4│ } + │ ⌃ + 5│ } diff --git a/spec/mint_json/dependency_repository_invalid b/spec/mint_json/dependency_repository_invalid new file mode 100644 index 000000000..8b3347c30 --- /dev/null +++ b/spec/mint_json/dependency_repository_invalid @@ -0,0 +1,22 @@ +{ + "dependencies": { + "package": { + "repository": 0 + } + } +} +-------------------------------------------------------------------------------- +░ ERROR (DEPENDENCY_REPOSITORY_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The repository field of a depencency must be an string, but it's not: + + ┌ spec/fixtures/mint.json:4:21 + ├───────────────────────────── + 1│ { + 2│ "dependencies": { + 3│ "package": { + 4│ "repository": 0 + │ ⌃ + 5│ } + 6│ } + 7│ } diff --git a/spec/mint_json/formatter_indent_size_invalid b/spec/mint_json/formatter_indent_size_invalid new file mode 100644 index 000000000..cb199d98a --- /dev/null +++ b/spec/mint_json/formatter_indent_size_invalid @@ -0,0 +1,19 @@ +{ + "formatter": { + "indent-size": "" + } +} +-------------------------------------------------------------------------------- +░ ERROR (FORMATTER_INDENT_SIZE_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The indent-size field of the formatter configuration must be a number, but it's +not: + + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── + 1│ { + 2│ "formatter": { + 3│ "indent-size": "" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/formatter_invalid b/spec/mint_json/formatter_invalid new file mode 100644 index 000000000..6c1527507 --- /dev/null +++ b/spec/mint_json/formatter_invalid @@ -0,0 +1,14 @@ +{ + "formatter": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (FORMATTER_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The formatter field should be an object, but it's not: + + ┌ spec/fixtures/mint.json:2:16 + ├───────────────────────────── + 1│ { + 2│ "formatter": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/formatter_invalid_key b/spec/mint_json/formatter_invalid_key new file mode 100644 index 000000000..13add9bdd --- /dev/null +++ b/spec/mint_json/formatter_invalid_key @@ -0,0 +1,22 @@ +{ + "formatter": { + "xxx": "" + } +} +-------------------------------------------------------------------------------- +░ ERROR (FORMATTER_INVALID_KEY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The formatter object has an invalid key: + + xxx + +It is here: + + ┌ spec/fixtures/mint.json:3:12 + ├───────────────────────────── + 1│ { + 2│ "formatter": { + 3│ "xxx": "" + │ ⌃ + 4│ } + 5│ } diff --git a/spec/mint_json/invalid_json b/spec/mint_json/invalid_json new file mode 100644 index 000000000..16147f3d7 --- /dev/null +++ b/spec/mint_json/invalid_json @@ -0,0 +1,14 @@ +{ + , +} +-------------------------------------------------------------------------------- +░ ERROR (INVALID_JSON) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +I could not parse the following mint.json file: + + ┌ spec/fixtures/mint.json:2:3 + ├──────────────────────────── + 1│ { + 2│ , + │ ⌃ + 3│ } diff --git a/spec/mint_json/mint_version_bad b/spec/mint_json/mint_version_bad new file mode 100644 index 000000000..5d6fa8b41 --- /dev/null +++ b/spec/mint_json/mint_version_bad @@ -0,0 +1,18 @@ +{ + "mint-version": "" +} +-------------------------------------------------------------------------------- +░ ERROR (MINT_VERSION_BAD) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The mint-version constraint should be in this format: + + 0.0.0 <= v < 1.0.0 + +It is here: + + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── + 1│ { + 2│ "mint-version": "" + │ ⌃ + 3│ } diff --git a/spec/mint_json/mint_version_invalid b/spec/mint_json/mint_version_invalid new file mode 100644 index 000000000..e3ce649c6 --- /dev/null +++ b/spec/mint_json/mint_version_invalid @@ -0,0 +1,14 @@ +{ + "mint-version": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (MINT_VERSION_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The mint-version field should be a string, but it's not: + + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── + 1│ { + 2│ "mint-version": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/mint_version_mismatch b/spec/mint_json/mint_version_mismatch new file mode 100644 index 000000000..3af200237 --- /dev/null +++ b/spec/mint_json/mint_version_mismatch @@ -0,0 +1,24 @@ +{ + "mint-version": "0.0.0 <= v < 0.0.1" +} +-------------------------------------------------------------------------------- +░ ERROR (MINT_VERSION_MISMATCH) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The mint-version field does not match this version of Mint. + +I was looking for + + 0.0.0 <= v < 0.0.1 + +but found instead: + + 0.20.0-alpha.2 + +It is here: + + ┌ spec/fixtures/mint.json:2:19 + ├─────────────────────────────────────── + 1│ { + 2│ "mint-version": "0.0.0 <= v < 0.0.1" + │ ⌃ + 3│ } diff --git a/spec/mint_json/name_empty b/spec/mint_json/name_empty new file mode 100644 index 000000000..ba3bd102c --- /dev/null +++ b/spec/mint_json/name_empty @@ -0,0 +1,14 @@ +{ + "name": "" +} +-------------------------------------------------------------------------------- +░ ERROR (NAME_EMPTY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The name field should not be empty: + + ┌ spec/fixtures/mint.json:2:11 + ├───────────────────────────── + 1│ { + 2│ "name": "" + │ ⌃ + 3│ } diff --git a/spec/mint_json/name_invalid b/spec/mint_json/name_invalid new file mode 100644 index 000000000..3b1702cc6 --- /dev/null +++ b/spec/mint_json/name_invalid @@ -0,0 +1,14 @@ +{ + "name": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (NAME_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The name field should be a string, but it's not: + + ┌ spec/fixtures/mint.json:2:11 + ├───────────────────────────── + 1│ { + 2│ "name": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/root_invalid b/spec/mint_json/root_invalid new file mode 100644 index 000000000..64b38c201 --- /dev/null +++ b/spec/mint_json/root_invalid @@ -0,0 +1,9 @@ +"" +-------------------------------------------------------------------------------- +░ ERROR (ROOT_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The root item should be an object, but it's not: + + ┌ spec/fixtures/mint.json:1:2 + ├──────────────────────────── + 1│ "" diff --git a/spec/mint_json/root_invalid_key b/spec/mint_json/root_invalid_key new file mode 100644 index 000000000..d96f2d9ad --- /dev/null +++ b/spec/mint_json/root_invalid_key @@ -0,0 +1,18 @@ +{ + "x": "y" +} +-------------------------------------------------------------------------------- +░ ERROR (ROOT_INVALID_KEY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The root object has an invalid key: + + x + +It is here: + + ┌ spec/fixtures/mint.json:2:8 + ├──────────────────────────── + 1│ { + 2│ "x": "y" + │ ⌃ + 3│ } diff --git a/spec/mint_json/source_directories_empty b/spec/mint_json/source_directories_empty new file mode 100644 index 000000000..b0162c4a3 --- /dev/null +++ b/spec/mint_json/source_directories_empty @@ -0,0 +1,17 @@ +{ + "source-directories": [] +} +-------------------------------------------------------------------------------- +░ ERROR (SOURCE_DIRECTORIES_EMPTY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The source-directories field lists all directories (relative to the mint.json +file) which contain the source files of the application. + +The source-directories array should not be empty, but it is: + + ┌ spec/fixtures/mint.json:2:25 + ├───────────────────────────── + 1│ { + 2│ "source-directories": [] + │ ⌃ + 3│ } diff --git a/spec/mint_json/source_directories_invalid b/spec/mint_json/source_directories_invalid new file mode 100644 index 000000000..5daef2712 --- /dev/null +++ b/spec/mint_json/source_directories_invalid @@ -0,0 +1,17 @@ +{ + "source-directories": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (SOURCE_DIRECTORIES_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The source-directories field lists all directories (relative to the mint.json +file) which contain the source files of the application. + +The source-directories field should be an array, but it's not: + + ┌ spec/fixtures/mint.json:2:25 + ├───────────────────────────── + 1│ { + 2│ "source-directories": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/source_directory_invalid b/spec/mint_json/source_directory_invalid new file mode 100644 index 000000000..08a9a51bd --- /dev/null +++ b/spec/mint_json/source_directory_invalid @@ -0,0 +1,14 @@ +{ + "source-directories": [0] +} +-------------------------------------------------------------------------------- +░ ERROR (SOURCE_DIRECTORY_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +All entries in the source-directories array should be string: + + ┌ spec/fixtures/mint.json:2:26 + ├───────────────────────────── + 1│ { + 2│ "source-directories": [0] + │ ⌃ + 3│ } diff --git a/spec/mint_json/source_directory_not_exists b/spec/mint_json/source_directory_not_exists new file mode 100644 index 000000000..041526b2a --- /dev/null +++ b/spec/mint_json/source_directory_not_exists @@ -0,0 +1,14 @@ +{ + "source-directories": ["xxx"] +} +-------------------------------------------------------------------------------- +░ ERROR (SOURCE_DIRECTORY_NOT_EXISTS) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The source directory xxx does not exists: + + ┌ spec/fixtures/mint.json:2:26 + ├──────────────────────────────── + 1│ { + 2│ "source-directories": ["xxx"] + │ ⌃ + 3│ } diff --git a/spec/mint_json/test_directories_empty b/spec/mint_json/test_directories_empty new file mode 100644 index 000000000..a88624722 --- /dev/null +++ b/spec/mint_json/test_directories_empty @@ -0,0 +1,17 @@ +{ + "test-directories": [] +} +-------------------------------------------------------------------------------- +░ ERROR (TEST_DIRECTORIES_EMPTY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The test-directories field lists all directories (relative to the mint.json +file) which contain the test files of the application. + +The test-directories array should not be empty, but it is: + + ┌ spec/fixtures/mint.json:2:23 + ├───────────────────────────── + 1│ { + 2│ "test-directories": [] + │ ⌃ + 3│ } diff --git a/spec/mint_json/test_directories_invalid b/spec/mint_json/test_directories_invalid new file mode 100644 index 000000000..d91e29b76 --- /dev/null +++ b/spec/mint_json/test_directories_invalid @@ -0,0 +1,17 @@ +{ + "test-directories": 0 +} +-------------------------------------------------------------------------------- +░ ERROR (TEST_DIRECTORIES_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The test-directories field lists all directories (relative to the mint.json +file) which contain the test files of the application. + +The test-directories field should be an array, but it's not: + + ┌ spec/fixtures/mint.json:2:23 + ├───────────────────────────── + 1│ { + 2│ "test-directories": 0 + │ ⌃ + 3│ } diff --git a/spec/mint_json/test_directory_invalid b/spec/mint_json/test_directory_invalid new file mode 100644 index 000000000..35b60876a --- /dev/null +++ b/spec/mint_json/test_directory_invalid @@ -0,0 +1,14 @@ +{ + "test-directories": [0] +} +-------------------------------------------------------------------------------- +░ ERROR (TEST_DIRECTORY_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +All entries in the test-directories array should be string: + + ┌ spec/fixtures/mint.json:2:24 + ├───────────────────────────── + 1│ { + 2│ "test-directories": [0] + │ ⌃ + 3│ } diff --git a/spec/mint_json/test_directory_not_exists b/spec/mint_json/test_directory_not_exists new file mode 100644 index 000000000..97c9ef0b8 --- /dev/null +++ b/spec/mint_json/test_directory_not_exists @@ -0,0 +1,14 @@ +{ + "test-directories": ["xxx"] +} +-------------------------------------------------------------------------------- +░ ERROR (TEST_DIRECTORY_NOT_EXISTS) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The test directory xxx does not exists: + + ┌ spec/fixtures/mint.json:2:24 + ├────────────────────────────── + 1│ { + 2│ "test-directories": ["xxx"] + │ ⌃ + 3│ } diff --git a/spec/mint_json_spec.cr b/spec/mint_json_spec.cr new file mode 100644 index 000000000..0c6dd7171 --- /dev/null +++ b/spec/mint_json_spec.cr @@ -0,0 +1,49 @@ +require "./spec_helper" + +Dir + .glob("./spec/mint_json/**/*") + .select! { |file| File.file?(file) } + .sort! + .each do |file| + it file do + source, expected = + File.read(file).split("-" * 80) + + begin + Mint::MintJson.parse(contents: source, path: "spec/fixtures/mint.json") + rescue error : Mint::Error + result = + error.to_terminal.to_s.uncolorize + + fail diff(expected, result) unless result == expected.strip + end + end + end + +it "non existent file" do + begin + Mint::MintJson.parse("test.json") + rescue error : Mint::Error + error.to_terminal.to_s.uncolorize.should eq(<<-TEXT) + ░ ERROR (MINT_JSON_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + + There was a problem trying to open a mint.json file: test.json + + Error opening file with mode 'r': 'test.json': No such file or directory + TEXT + end +end + +it "no mint.json in directory or parents" do + begin + Mint::MintJson.parse("test.json", search: true) + rescue error : Mint::Error + error.to_terminal.to_s.uncolorize.should eq(<<-TEXT) + ░ ERROR (MINT_JSON_NOT_FOUND) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + + I could not find a mint.json file in the path or any of its parent directories: + + test.json + TEXT + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 744592b23..c44c6d5cd 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -80,10 +80,6 @@ class Workspace @files = {} of String => File @id : String - def workspace - Mint::Workspace[File.join(@root, "test.file")] - end - def initialize @id = Random.new.hex(5) @@ -217,6 +213,8 @@ def lsp_json(messages) json = JSON.parse(content) json.to_pretty_json end +rescue IO::EOFError + [] of String end def format_xml(xml) diff --git a/spec/static_documentation_generators/module b/spec/static_documentation_generators/module index 40d815b93..05b0ad967 100644 --- a/spec/static_documentation_generators/module +++ b/spec/static_documentation_generators/module @@ -17,6 +17,7 @@ module Test { + Test diff --git a/spec/watcher_spec.cr b/spec/watcher_spec.cr new file mode 100644 index 000000000..394960e39 --- /dev/null +++ b/spec/watcher_spec.cr @@ -0,0 +1,47 @@ +require "./spec_helper" + +describe Mint::Watcher do + it do + Path[Dir.tempdir, Random::Secure.hex].tap do |directory| + FileUtils.mkdir_p(directory) + + file1 = + Path["#{directory}", "file1.txt"] + .tap(&->FileUtils.touch(Path)) + .to_s + + file2 = + Path["#{directory}", "file2.txt"] + .tap(&->FileUtils.touch(Path)) + .to_s + + modified = + [] of String + + watcher = + Mint::Watcher + .new { |items| modified = items } + .tap(&.patterns = ["#{directory}/**/*"]) + + # Returns all files + modified.should eq([file1, file2]) + + # Returns only modified files + FileUtils.touch(file2) + watcher.scan(:modified) + modified.should eq([file2]) + + # Returns deleted files + FileUtils.rm(file2) + watcher.scan(:modified) + modified.should eq([file2]) + + # Returns all files + watcher.patterns = ["#{directory}/**/*"] + watcher.scan(:modified) + modified.should eq([file1]) + ensure + FileUtils.rm_rf(directory) + end + end +end diff --git a/spec/workspace_spec.cr b/spec/workspace_spec.cr new file mode 100644 index 000000000..8c956d639 --- /dev/null +++ b/spec/workspace_spec.cr @@ -0,0 +1,65 @@ +require "./spec_helper" + +describe Mint::FileWorkspace do + it "notifies immediately (success)" do + with_workspace do |workspace| + workspace.file("Main.mint", "") + + results = + [] of Mint::TypeChecker | Mint::Error + + Mint::FileWorkspace.new( + listener: ->(item : Mint::TypeChecker | Mint::Error) { results << item }, + path: Path[workspace.root_path, "mint.json"].to_s, + check: Mint::Check::Environment, + include_tests: false, + format: false) + + results.size.should eq(1) + results[0].should be_a(Mint::TypeChecker) + end + end + + it "notifies immediately (error - no mint.json found)" do + with_workspace do |workspace| + results = + [] of Mint::TypeChecker | Mint::Error + + Mint::FileWorkspace.new( + listener: ->(item : Mint::TypeChecker | Mint::Error) { results << item }, + check: Mint::Check::Environment, + path: workspace.root_path, + include_tests: false, + format: false) + + results.size.should eq(1) + results[0].should be_a(Mint::Error) + end + end + + it "notifies after change (success)" do + with_workspace do |workspace| + workspace.file("Main.mint", "") + workspace.file("File1.mint", "") + workspace.file("File2.mint", "") + + results = + [] of Mint::TypeChecker | Mint::Error + + Mint::FileWorkspace.new( + listener: ->(item : Mint::TypeChecker | Mint::Error) { results << item }, + path: Path[workspace.root_path, "mint.json"].to_s, + check: Mint::Check::Environment, + include_tests: false, + format: false) + + FileUtils.touch(Path[workspace.root_path, "Main.mint"]) + FileUtils.touch(Path[workspace.root_path, "File1.mint"]) + FileUtils.touch(Path[workspace.root_path, "File2.mint"]) + + sleep 1 + + results.size.should eq(2) + end + end +end diff --git a/spec_cli/build_spec.cr b/spec_cli/build_spec.cr index c2b447216..f726d5520 100644 --- a/spec_cli/build_spec.cr +++ b/spec_cli/build_spec.cr @@ -62,11 +62,11 @@ context "build" do Building application | ×××× Compiling intermediate representation... | ×××× Calculating dependencies for bundles... | ×××× - Bundling and rendering JavaScript... | ×××× + Bundling and generating JavaScript... | ×××× Generating index.html | ×××× Generating icons | ×××× Copying assets | ×××× - Building index.css | ×××× + Generating index.css | ×××× ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ All done in ××××! TEXT diff --git a/spec_cli/spec_helper.cr b/spec_cli/spec_helper.cr index 8b42b7020..6e4c84344 100644 --- a/spec_cli/spec_helper.cr +++ b/spec_cli/spec_helper.cr @@ -7,7 +7,7 @@ require "../src/version" require "../spec/spec_helpers" -def run(args : Array(String), input : String = "") +def run(args : Array(String), input : String = "", clear_env = true) input_io, output, error = {IO::Memory.new(input), IO::Memory.new, IO::Memory.new} @@ -16,7 +16,7 @@ def run(args : Array(String), input : String = "") status = Process.run( - clear_env: true, + clear_env: clear_env, input: input_io, output: output, command: path, @@ -33,9 +33,9 @@ def run(args : Array(String), input : String = "") } end -def expect_output(args : Array(String), template : String, input : String = "") +def expect_output(args : Array(String), template : String, input : String = "", clear_env = true) output, _, status = - run args, input + run args, input, clear_env status.normal_exit?.should eq(true) matches_template(template.rstrip, output.rstrip) diff --git a/spec_cli/test_spec.cr b/spec_cli/test_spec.cr new file mode 100644 index 000000000..737484cb0 --- /dev/null +++ b/spec_cli/test_spec.cr @@ -0,0 +1,56 @@ +require "./spec_helper" + +context "test" do + before_all do + FileUtils.rm_rf "my-project" + run ["init", "my-project"] + FileUtils.cd "my-project" + end + + after_all do + FileUtils.cd ".." + FileUtils.rm_rf "my-project" + end + + it "displays help with '--help' flag" do + expect_output ["test", "--help"], <<-TEXT + Usage: + ×××× test [flags...] [arg...] + + Runs the tests defined for the project. + + Flags: + --browser, -b (default: "chrome") # Which browser to run the tests in (chrome, firefox). + --browser-host, -x (default: ENV["BROWSER_HOST"]? || "127.0.0.1") # Target host, useful when hosted on another machine. + --browser-port, -c (default: (ENV["BROWSER_PORT"]? || "3001").to_i) # Target port, useful when hosted on another machine. + --env, -e # Loads the given .env file. + --help # Displays help for the current command. + --host, -h (default: ENV["HOST"]? || "127.0.0.1") # Host to serve the tests on. + --manual, -m # Start the test server for manual testing. + --port, -p (default: (ENV["PORT"]? || "3001").to_i) # Port to serve the tests on. + --reporter, -r (default: "dot") # Which reporter to use (dot, documentation), + --runtime # If specified, the supplied runtime will be used instead of the default. + --watch, -w # Watch files for changes and rerun tests. + + Arguments: + test # The path to the test file to run. + TEXT + end + + it "runs the tests" do + expect_output ["test"], clear_env: false, template: <<-TEXT + Mint - Running tests + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ⚙ Starting browser... + ⚙ Test server started: http://127.0.0.1:3001/ + ⚙ Running tests: + . + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 1 tests + ➔ 1 passed + ➔ 0 failed + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + All done in ××××! + TEXT + end +end diff --git a/src/all.cr b/src/all.cr index e6388ff06..1399f6220 100644 --- a/src/all.cr +++ b/src/all.cr @@ -68,10 +68,11 @@ require "./test_runner" require "./lsp/**" require "./ls/**" +require "./mint_json/**" require "./mint_json" + require "./scaffold" require "./reactor" -require "./sandbox_server" require "./cli" require "./workspace" require "./debugger" diff --git a/src/ast.cr b/src/ast.cr index 0380c5e5d..c2ffab43c 100644 --- a/src/ast.cr +++ b/src/ast.cr @@ -62,6 +62,19 @@ module Mint self.class.new.merge(self) end + def nodes_at_cursor( + *, + column : Int64, + path : String, + line : Int64 + ) : Array(Ast::Node) + nodes_at_path(path).select!(&.location.contains?(line, column)) + end + + def nodes_at_path(path : String) : Array(Ast::Node) + nodes.select(&.file.path.==(path)) + end + def includes?(node : Ast::Node, other : Ast::Node) node.input == other.input && node.from <= other.from && diff --git a/src/bundler.cr b/src/bundler.cr index 88e6c446d..10be4b7c8 100644 --- a/src/bundler.cr +++ b/src/bundler.cr @@ -6,7 +6,7 @@ module Mint alias Bundle = Compiler::Bundle record Config, - test : NamedTuple(url: String, id: String)?, + test : NamedTuple(url: String, id: String, glob: String)?, generate_manifest : Bool, include_program : Bool, runtime_path : String?, @@ -65,7 +65,7 @@ module Mint # Compile the CSS. files[path_for_asset("index.css")] = ->do - Logger.log "Building index.css" do + Logger.log "Generating index.css" do compiler.style_builder.compile end end @@ -75,7 +75,7 @@ module Mint # Compile tests if there is configration for it. Logger.log "Compiling tests" do [ - compiler.test(test_information[:url], test_information[:id]), + compiler.test(**test_information), ] end end @@ -100,7 +100,7 @@ module Mint artifacts.references.calculate end - Logger.log "Bundling and rendering JavaScript..." do + Logger.log "Bundling and generating JavaScript..." do # Here we separate the compiled items to each bundle. calculated_bundles.each do |node, dependencies| bundles[node] = @@ -301,8 +301,8 @@ module Mint meta name: name, content: content end - unless application.theme.blank? - meta name: "theme-color", content: application.theme + unless application.theme_color.blank? + meta name: "theme-color", content: application.theme_color end if generate_icons? @@ -372,9 +372,9 @@ module Mint end { "orientation" => application.orientation, + "background_color" => application.theme_color, + "theme_color" => application.theme_color, "display" => application.display, - "background_color" => application.theme, - "theme_color" => application.theme, "name" => application.name, "short_name" => application.name, "icons" => icons, diff --git a/src/cli.cr b/src/cli.cr index d2ebd5aba..f8e4dec77 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -1,11 +1,8 @@ require "admiral" -require "./commands/command" +require "./command" require "./commands/**" module Mint - class CliException < Exception - end - class Cli < Admiral::Command include Command diff --git a/src/commands/command.cr b/src/command.cr similarity index 71% rename from src/commands/command.cr rename to src/command.cr index 3a749a9b3..24e87ee1a 100644 --- a/src/commands/command.cr +++ b/src/command.cr @@ -1,7 +1,13 @@ module Mint class Cli < Admiral::Command module Command - def execute(message, *, env : String? = nil, & : -> T) : T? forall T + def execute( + message : String, + *, + check_dependencies : Bool = false, + env : String? = nil, + & : -> T + ) : T? forall T # On Ctrl+C and abort and exit. Signal::INT.trap do terminal.puts @@ -28,11 +34,10 @@ module Mint terminal.puts "#{COG} Loaded environment variables from: #{file}" end + check_dependencies! if check_dependencies + # Measure elapsed time of a command. elapsed = Time.measure { result = yield } - rescue CliException - # In case of a CLI exception just exit. - error nil, position rescue error : Error # In case of an error print it. error error.to_terminal, position @@ -78,6 +83,26 @@ module Mint exit(1) end + def check_dependencies! + MintJson.current.dependencies.each do |dependency| + next if Dir.exists?(".mint/packages/#{dependency.name}") + + terminal.puts "#{COG} Ensuring dependencies..." + terminal.puts " ↳ Not all dependencies in your mint.json file are installed." + terminal.puts " Would you like to install them now? (Y/n)" + + answer = gets.to_s.downcase + terminal.puts AnsiEscapes::Erase.lines(2) + + if answer == "y" + Installer.new + break + else + error "#{WARNING} Missing dependencies...", terminal.position + end + end + end + def terminal Render::Terminal::STDOUT end diff --git a/src/commands/build.cr b/src/commands/build.cr index e3e2bd427..a2522aad6 100644 --- a/src/commands/build.cr +++ b/src/commands/build.cr @@ -43,92 +43,81 @@ module Mint short: "e" def run - execute "Building for production", env: flags.env do - # Initialize the workspace from the current working directory. - # We don't check everything to speed things up so only the hot - # path is checked. - workspace = Workspace.current - workspace.check_everything = false - workspace.check_env = true - - # Check if we have dependencies installed. - workspace.json.check_dependencies! - - # On any change we copy the build to the dist directory. - workspace.on("change") do |result| - terminal.reset if flags.watch - - case result - in Ast - terminal.measure %(#{COG} Clearing the "#{DIST_DIR}" directory...) do - FileUtils.rm_rf DIST_DIR - end + execute "Building for production", + check_dependencies: true, env: flags.env do + FileWorkspace.new( + path: Path[Dir.current, "mint.json"].to_s, + check: Check::Environment, + include_tests: false, + format: false, + listener: ->(result : TypeChecker | Error) do + terminal.reset if flags.watch + + case result + in TypeChecker + terminal.measure %(#{COG} Clearing the "#{DIST_DIR}" directory...) do + FileUtils.rm_rf DIST_DIR + end - files = - terminal.measure "#{COG} Building..." do - Bundler.new( - artifacts: workspace.type_checker.artifacts, - json: workspace.json, - config: Bundler::Config.new( - generate_manifest: flags.generate_manifest, - skip_icons: flags.skip_icons, - optimize: !flags.no_optimize, - runtime_path: flags.runtime, - relative: flags.relative, - include_program: true, - live_reload: false, - hash_assets: true, - test: nil)).bundle - end || {} of String => Proc(String) - - bundle_size = 0 - - files.keys.sort_by!(&.size).reverse!.each do |path| - chopped = - path.lchop('/') - - content = - files[path].call - - size = - content.bytesize - - proc = - ->{ File.write_p(Path[DIST_DIR, chopped], content) } - - bundle_size += - size - - if flags.verbose - terminal.measure "#{COG} Writing #{chopped} (#{size.humanize_bytes(format: :JEDEC)})..." do + files = + terminal.measure "#{COG} Building..." do + Bundler.new( + artifacts: result.artifacts, + json: MintJson.current, + config: Bundler::Config.new( + generate_manifest: flags.generate_manifest, + skip_icons: flags.skip_icons, + optimize: !flags.no_optimize, + runtime_path: flags.runtime, + relative: flags.relative, + include_program: true, + live_reload: false, + hash_assets: true, + test: nil)).bundle + end || {} of String => Proc(String) + + bundle_size = 0 + + files.keys.sort_by!(&.size).reverse!.each do |path| + chopped = + path.lchop('/') + + content = + files[path].call + + size = + content.bytesize + + proc = + ->{ File.write_p(Path[DIST_DIR, chopped], content) } + + bundle_size += + size + + if flags.verbose + terminal.measure "#{COG} Writing #{chopped} (#{size.humanize_bytes(format: :JEDEC)})..." do + proc.call + end + else proc.call end - else - proc.call end - end - - terminal.divider - terminal.puts "Bundle size: #{bundle_size.humanize_bytes(format: :JEDEC)}" - terminal.puts "Files: #{files.size}" - if flags.timings terminal.divider - Logger.print(terminal) - end - in Error - terminal.print result.to_terminal - end - end + terminal.puts "Bundle size: #{bundle_size.humanize_bytes(format: :JEDEC)}" + terminal.puts "Files: #{files.size}" - # Do the initial parsing and type checking. - workspace.update_cache + if flags.timings + terminal.divider + Logger.print(terminal) + end + in Error + terminal.print result.to_terminal + end + end) # Start wathing for changes if the flag is set. - if flags.watch - workspace.watch - sleep - end + sleep if flags.watch end end end diff --git a/src/commands/docs.cr b/src/commands/docs.cr index 3684fa4e7..c7bad32c3 100644 --- a/src/commands/docs.cr +++ b/src/commands/docs.cr @@ -20,18 +20,20 @@ module Mint def run execute "Generating documentation" do - directory = - arguments.directory - - jsons = [MintJson.parse_current] - jsons.concat(SourceFiles.packages) if flags.include_packages + directory = arguments.directory + json = MintJson.current + + jsons = + if flags.include_packages + SourceFiles.packages(json, include_self: true) + else + [json] + end asts = - jsons.map do |json| + Dir.glob(SourceFiles.globs(jsons)).map do |file| Ast.new.tap do |ast| - json.source_files.each do |file| - ast.merge(Parser.parse(File.read(file), file)) - end + ast.merge(Parser.parse(File.read(file), file)) end end @@ -43,9 +45,9 @@ module Mint end terminal.measure "#{COG} Generating documentation..." do - StaticDocumentationGenerator - .generate(asts) - .each { |path, contents| File.write_p(Path[directory, path], contents) } + StaticDocumentationGenerator.generate(asts).each do |path, contents| + File.write_p(Path[directory, path], contents) + end end end end diff --git a/src/commands/format.cr b/src/commands/format.cr index d51f2e7b7..2755dcc18 100644 --- a/src/commands/format.cr +++ b/src/commands/format.cr @@ -86,8 +86,7 @@ module Mint private def format_files pattern = arguments.pattern.presence || json.try do |item| - (item.source_directories | item.test_directories) - .map(&->SourceFiles.glob_pattern(String)) + SourceFiles.globs(item, include_tests: true) end Dir.glob(pattern || "").map do |file| @@ -111,11 +110,11 @@ module Mint # We try to honor the config of the current project but # allow for formatting without one using defaults. private def config - json.try(&.formatter_config) || Formatter::Config.new + json.try(&.formatter) || Formatter::Config.new end private def json - MintJson.parse_current? + MintJson.current? end end end diff --git a/src/commands/lint.cr b/src/commands/lint.cr index ee6afa782..23671fa79 100644 --- a/src/commands/lint.cr +++ b/src/commands/lint.cr @@ -10,22 +10,21 @@ module Mint default: false def run - ast = - Ast.new.merge(Core.ast) - - errors = - [] of Error + ast = Ast.new.merge(Core.ast) + json = MintJson.current + errors = [] of Error + + Dir.glob(SourceFiles.globs(json, include_tests: true)) + .reduce(ast) do |memo, file| + begin + memo.merge(Parser.parse(file)) + rescue error : Error + errors << error + end - Dir.glob(SourceFiles.all).reduce(ast) do |memo, file| - begin - memo.merge(Parser.parse(file)) - rescue error : Error - errors << error + memo end - memo - end - begin TypeChecker.new(ast).tap(&.check) rescue error : Error diff --git a/src/commands/sandbox_server.cr b/src/commands/sandbox_server.cr deleted file mode 100644 index 82b556dff..000000000 --- a/src/commands/sandbox_server.cr +++ /dev/null @@ -1,26 +0,0 @@ -module Mint - class Cli < Admiral::Command - class SandboxServer < Admiral::Command - include Command - - define_help description: "Server for compiling sandbox applications." - - define_flag host : String, - description: "The host the server binds to.", - default: ENV["HOST"]? || "0.0.0.0", - short: "h" - - define_flag port : Int32, - description: "The port the server binds to.", - default: (ENV["PORT"]? || "3003").to_i, - short: "p" - - def run - execute "Running the sandbox server" do - # NOTE: The command and the server itself has the same name. - Mint::SandboxServer.new(flags.host, flags.port) - end - end - end - end -end diff --git a/src/commands/start.cr b/src/commands/start.cr index e8fceec66..34e77e035 100644 --- a/src/commands/start.cr +++ b/src/commands/start.cr @@ -29,7 +29,8 @@ module Mint short: "e" def run - execute "Running the development server", env: flags.env do + execute "Running the development server", + check_dependencies: true, env: flags.env do Reactor.new( format: flags.format, reload: flags.reload, diff --git a/src/commands/test.cr b/src/commands/test.cr index 9148c1e16..47abe17e0 100644 --- a/src/commands/test.cr +++ b/src/commands/test.cr @@ -34,7 +34,8 @@ module Mint short: "m" define_flag watch : Bool, - description: "Watch files for changes and rerun tests." + description: "Watch files for changes and rerun tests.", + short: "w" define_flag host : String, description: "Host to serve the tests on.", diff --git a/src/commands/tool.cr b/src/commands/tool.cr index 08d668a1e..c43656b8f 100644 --- a/src/commands/tool.cr +++ b/src/commands/tool.cr @@ -5,7 +5,7 @@ module Mint define_help description: "Miscellaneous Tools" - register_sub_command "sandbox-server", type: SandboxServer + register_sub_command "ls-websocket", type: LsWebSocket register_sub_command highlight, type: Highlight register_sub_command clean, type: Clean register_sub_command loc, type: Loc diff --git a/src/commands/clean.cr b/src/commands/tool/clean.cr similarity index 100% rename from src/commands/clean.cr rename to src/commands/tool/clean.cr diff --git a/src/commands/highlight.cr b/src/commands/tool/highlight.cr similarity index 100% rename from src/commands/highlight.cr rename to src/commands/tool/highlight.cr diff --git a/src/commands/loc.cr b/src/commands/tool/loc.cr similarity index 61% rename from src/commands/loc.cr rename to src/commands/tool/loc.cr index 5a8c04f93..afca3a398 100644 --- a/src/commands/loc.cr +++ b/src/commands/tool/loc.cr @@ -7,29 +7,24 @@ module Mint def run execute "Counting lines of code" do + files = + Dir.glob(SourceFiles.globs(MintJson.current)).to_a + file_count = files.size.to_s.colorize.mode(:bold) line_count = - count.to_s.colorize.mode(:bold) + files.reduce(0) do |memo, file| + count = + File.read(file).lines.count(&.presence) + + count + memo + end.to_s.colorize.mode(:bold) terminal.puts "#{COG} Files: #{file_count}" terminal.puts "#{COG} Lines of code: #{line_count}" end end - - private def files - Dir.glob(SourceFiles.all).to_a - end - - private def count - files.reduce(0) do |memo, file| - count = - File.read(file).lines.count(&.presence) - - count + memo - end - end end end end diff --git a/src/commands/ls.cr b/src/commands/tool/ls.cr similarity index 79% rename from src/commands/ls.cr rename to src/commands/tool/ls.cr index 67de589ed..0f2d6d8b4 100644 --- a/src/commands/ls.cr +++ b/src/commands/tool/ls.cr @@ -1,7 +1,7 @@ module Mint class Cli < Admiral::Command class Ls < Admiral::Command - define_help description: "Language Server." + define_help description: "Starts the language server process." def run Colorize.enabled = diff --git a/src/commands/tool/ls_web_socket.cr b/src/commands/tool/ls_web_socket.cr new file mode 100644 index 000000000..0564ea954 --- /dev/null +++ b/src/commands/tool/ls_web_socket.cr @@ -0,0 +1,44 @@ +module Mint + class Cli < Admiral::Command + class LsWebSocket < Admiral::Command + include Command + + define_help description: "Starts the language server (websocket server)." + + define_flag sandbox : Bool, + description: "If specified, server will start in sandbox mode.", + default: false + + define_flag host : String, + description: "The host to serve the application on.", + default: ENV["HOST"]? || "0.0.0.0", + short: "h" + + define_flag port : Int32, + description: "The port to serve the application on.", + default: (ENV["PORT"]? || "3004").to_i, + short: "p" + + def run + execute("Running language server over websocket") do + server = + HTTP::Server.new( + [ + CORS.new, + HTTP::WebSocketHandler.new do |socket| + LS::WebSocketServer.new(socket, flags.sandbox) + end, + ]) + + Server.run( + port: flags.port, + host: flags.host, + server: server, + ) do |resolved_host, resolved_port| + terminal.puts "#{COG} Language server started on http://#{resolved_host}:#{resolved_port}/" + end + end + end + end + end +end diff --git a/src/compiler.cr b/src/compiler.cr index 681a4df8a..8712705fa 100644 --- a/src/compiler.cr +++ b/src/compiler.cr @@ -295,9 +295,14 @@ module Mint end # Compile test runner. - def test(url, id) + def test(*, url, id, glob) + subjects = + ast.suites.select do |suite| + File.match?(glob, Path[suite.file.path].relative_to(Dir.current)) + end + suites = - compile(ast.suites) + compile(subjects) ["export default "] + js.arrow_function do js.new(Builtin::TestRunner, [ diff --git a/src/compiler/decoder.cr b/src/compiler/decoder.cr index defbb04e1..67e16b893 100644 --- a/src/compiler/decoder.cr +++ b/src/compiler/decoder.cr @@ -1,6 +1,6 @@ module Mint class Compiler - def decoder(type : TypeChecker::Record) + def decoder(type : TypeChecker::Record) : Compiled @decoders[type] ||= begin node = ast.type_definitions.find!(&.name.value.==(type.name)) diff --git a/src/core.cr b/src/core.cr index f02e3a101..dc2ae652d 100644 --- a/src/core.cr +++ b/src/core.cr @@ -4,11 +4,17 @@ module Mint bake_folder "../core/source" - class_getter json = MintJson.new(%({"name": "core"}), "core", "mint.json") + class_getter cache : Hash(String, Ast) = {} of String => Ast + + class_getter json : MintJson = MintJson.parse( + contents: %({"name": "core"}), + path: "core/mint.json") class_getter ast : Ast do files.reduce(Ast.new) do |memo, file| - memo.merge Parser.parse(file.read, file.path) + (@@cache[file.path] ||= Parser.parse(file.read, file.path)).try do |ast| + memo.merge(ast) + end end end diff --git a/src/errorable.cr b/src/errorable.cr index ea24c16ec..7b31645e9 100644 --- a/src/errorable.cr +++ b/src/errorable.cr @@ -27,7 +27,16 @@ module Mint class Error < Exception alias Element = Text | Bold | Code - record Snippet, value : Ast::Node | String | TypeChecker::Checkable + record SnippetData, + filename : String, + input : String, + from : Int64, + to : Int64 + + alias SnippetTarget = TypeChecker::Checkable | SnippetData | + Ast::Node | Parser | String + + record Snippet, value : TypeChecker::Checkable | String | SnippetData record Code, value : String record Bold, value : String record Text, value : String @@ -45,6 +54,7 @@ module Mint def block(&) with self yield + @blocks << @current @current = [] of Element end @@ -65,23 +75,35 @@ module Mint @current << Bold.new(value) end - def snippet(value : String, node : Ast::Node | TypeChecker::Checkable | String) + def snippet(value : String, node : SnippetTarget) block value snippet node end - def snippet(value : Parser) - from = - value.position - - to = - value.position + value.word.to_s.size - - snippet(Ast::Node.new(file: value.file, from: from, to: to)) - end + def snippet(value : SnippetTarget) + target = + case value + in Parser + SnippetData.new( + to: value.position + value.word.to_s.size, + input: value.file.contents, + filename: value.file.path, + from: value.position) + in Ast::Node + SnippetData.new( + input: value.file.contents, + filename: value.file.path, + from: value.from, + to: value.to) + in SnippetData + value + in TypeChecker::Checkable + value + in String + value + end - def snippet(value : String | Ast::Node | TypeChecker::Checkable) - @blocks << Snippet.new(value) + @blocks << Snippet.new(target) end def expected(subject : TypeChecker::Checkable | String, got : TypeChecker::Checkable) @@ -115,6 +137,10 @@ module Mint end end + def to_s + to_terminal.to_s + end + def to_terminal renderer = Render::Terminal.new renderer.title "ERROR (#{name})" @@ -122,14 +148,7 @@ module Mint blocks.each do |element| case element in Error::Snippet - case node = element.value - in TypeChecker::Checkable - renderer.snippet node - in Ast::Node - renderer.snippet node - in String - renderer.snippet node - end + renderer.snippet element.value in Array(Error::Element) renderer.block do element.each do |item| diff --git a/src/ext/file.cr b/src/ext/file.cr index c3adf5590..517bc76a8 100644 --- a/src/ext/file.cr +++ b/src/ext/file.cr @@ -3,4 +3,18 @@ class File FileUtils.mkdir_p File.dirname(path) File.write path, contents end + + def self.find_in_ancestors(base : String, name : String) : String? + root = File.dirname(base) + + loop do + return nil if root == "." || root == "/" + + if File.exists?(path = Path[root, name]) + return path.to_s + else + root = File.dirname(root) + end + end + end end diff --git a/src/installer.cr b/src/installer.cr index 44b3496fc..632dbca1f 100644 --- a/src/installer.cr +++ b/src/installer.cr @@ -25,7 +25,7 @@ module Mint getter root_dependencies = [] of Dependency def initialize - @root_dependencies = MintJson.parse_current.dependencies + @root_dependencies = MintJson.current.dependencies if @root_dependencies.empty? terminal.puts "There are no dependencies!\nThere is nothing to do!" diff --git a/src/installer/repository.cr b/src/installer/repository.cr index f0df013a2..83c086fbb 100644 --- a/src/installer/repository.cr +++ b/src/installer/repository.cr @@ -90,7 +90,7 @@ module Mint checkout target - MintJson.new(File.read(path), directory, path) + MintJson.parse(contents: File.read(path), path: path) rescue error : Error if error.name.to_s.starts_with?("mint_json") error! :repository_invalid_mint_json do diff --git a/src/logger.cr b/src/logger.cr index 42949a448..aec84a1b8 100644 --- a/src/logger.cr +++ b/src/logger.cr @@ -53,6 +53,8 @@ module Mint io.puts "#{message.ljust(width)} | #{formatted}" end + + @@current = [] of Log end # Logs the message with the block elapsed time. diff --git a/src/ls/code_action.cr b/src/ls/code_action.cr index 645e96bc6..33e1842d8 100644 --- a/src/ls/code_action.cr +++ b/src/ls/code_action.cr @@ -3,34 +3,42 @@ module Mint class CodeAction < LSP::RequestMessage property params : LSP::CodeActionParams - def actions(node : Ast::Module) + def actions(node : Ast::Provider, workspace : FileWorkspace) [ - ModuleActions.order_entities(node, workspace, params.text_document.uri), + ProviderActions.order_entities( + node, workspace, params.text_document.uri), ] end - def actions(node : Ast::Provider) + def actions(node : Ast::Module, workspace : FileWorkspace) [ - ProviderActions.order_entities(node, workspace, params.text_document.uri), + ModuleActions.order_entities( + node, workspace, params.text_document.uri), ] end - def actions(node : Ast::Node) + def actions(node : Ast::Node, workspace : FileWorkspace) [] of LSP::CodeAction end - def execute(server) - return [] of LSP::CodeAction if workspace.error + def execute(server : LSP::Server) + workspace = + server.workspace(params.text_document.path) - server - .nodes_at_cursor(params) - .reduce([] of LSP::CodeAction) do |memo, node| - memo + actions(node) - end - end + nodes = + workspace.nodes_at_cursor( + column: params.range.start.character, + line: params.range.start.line + 1, + path: params.text_document.path) - private def workspace - Workspace[params.text_document.path] + case nodes + in Error + [] of LSP::CodeAction + in Array(Ast::Node) + nodes.reduce([] of LSP::CodeAction) do |memo, node| + memo + actions(node, workspace) + end + end end end end diff --git a/src/ls/code_actions/module_actions.cr b/src/ls/code_actions/module_actions.cr index 7b72ad1d1..8272492fa 100644 --- a/src/ls/code_actions/module_actions.cr +++ b/src/ls/code_actions/module_actions.cr @@ -16,25 +16,21 @@ module Mint node.functions.sort_by(&.name.value)) .each_with_index { |entity, index| entity.from = order[index] } - # This should not fail since we return on errors earlier - formatted = - workspace.format(URI.parse(uri).path.to_s) - - # Create a workspace edit of the formatted document - edit = - LSP::WorkspaceEdit.new({ - uri => [ - LSP::TextEdit.new(new_text: formatted, range: LSP::Range.new( - start: LSP::Position.new(line: 0, character: 0), - end: LSP::Position.new(line: 9999, character: 999) - )), - ], - }) - - LSP::CodeAction.new( - title: "Order Entities", - kind: "source", - edit: edit) + case formatted = workspace.format(URI.parse(uri).path.to_s) + in String + LSP::CodeAction.new( + title: "Order Entities", + kind: "source", + edit: LSP::WorkspaceEdit.new({ + uri => [ + LSP::TextEdit.new(new_text: formatted, range: LSP::Range.new( + start: LSP::Position.new(line: 0, character: 0), + end: LSP::Position.new(line: 9999, character: 999) + )), + ], + })) + in Error, Nil + end end end end diff --git a/src/ls/code_actions/provider_actions.cr b/src/ls/code_actions/provider_actions.cr index 170b3aa73..21111d8e2 100644 --- a/src/ls/code_actions/provider_actions.cr +++ b/src/ls/code_actions/provider_actions.cr @@ -18,25 +18,21 @@ module Mint node.functions.sort_by(&.name.value)) .each_with_index { |entity, index| entity.from = order[index] } - # This should not fail since we return on errors earlier - formatted = - workspace.format(URI.parse(uri).path.to_s) - - # Create a workspace edit of the formatted document - edit = - LSP::WorkspaceEdit.new({ - uri => [ - LSP::TextEdit.new(new_text: formatted, range: LSP::Range.new( - start: LSP::Position.new(line: 0, character: 0), - end: LSP::Position.new(line: 9999, character: 999) - )), - ], - }) - - LSP::CodeAction.new( - title: "Order Entities", - kind: "source", - edit: edit) + case formatted = workspace.format(URI.parse(uri).path.to_s) + in String + LSP::CodeAction.new( + title: "Order Entities", + kind: "source", + edit: LSP::WorkspaceEdit.new({ + uri => [ + LSP::TextEdit.new(new_text: formatted, range: LSP::Range.new( + start: LSP::Position.new(line: 0, character: 0), + end: LSP::Position.new(line: 9999, character: 999) + )), + ], + })) + in Error, Nil + end end end end diff --git a/src/ls/completion.cr b/src/ls/completion.cr index c162dcb84..dc4e00794 100644 --- a/src/ls/completion.cr +++ b/src/ls/completion.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion HTML_TAG_COMPLETIONS = {{ read_file("#{__DIR__}/../assets/html_tags").strip }} .lines @@ -14,48 +14,37 @@ module Mint label: name) end - property params : LSP::CompletionParams - property snippet_support : Bool? - - def completions(node : Ast::Node, global : Bool = false) - [] of LSP::CompletionItem - end - - def workspace - Mint::Workspace[params.path] + def initialize( + *, + @type_checker : TypeChecker, + @workspace : FileWorkspace, + @snippet_support : Bool + ) end - def execute(server) - @snippet_support = - server - .params - .try(&.capabilities.text_document) - .try(&.completion) - .try(&.completion_item) - .try(&.snippet_support) + def process(params : LSP::CompletionParams) + ast = + @type_checker.artifacts.ast global_completions = - (workspace.ast.stores + - workspace.ast.unified_modules + - workspace.ast.components.select(&.global?)) - .flat_map { |node| completions(node, global: true) } + ( + ast.stores + + ast.unified_modules + + ast.components.select(&.global?) + ).flat_map { |node| completions(node, global: true) } scope_completions = - server - .nodes_at_cursor(params) - .flat_map { |node| completions(node) } + ast.nodes_at_cursor( + column: params.position.character, + path: params.text_document.path, + line: params.position.line + 1 + ).flat_map { |node| completions(node) } component_completions = - workspace - .ast - .components - .map { |node| completion_item(node) } + ast.components.map { |node| completion_item(node) } type_completions = - workspace - .ast - .type_definitions - .flat_map { |node| completions(node) } + ast.type_definitions.flat_map { |node| completions(node) } (global_completions + component_completions + @@ -68,10 +57,14 @@ module Mint item.insert_text = item.insert_text .gsub(/\$\d/, "") - .gsub(/\$\{.*\}/, "") unless snippet_support + .gsub(/\$\{.*\}/, "") unless @snippet_support item end end + + def completions(node : Ast::Node, global : Bool = false) + [] of LSP::CompletionItem + end end end end diff --git a/src/ls/completion_item/argument.cr b/src/ls/completion_item/argument.cr index 98570ff35..1c3791fae 100644 --- a/src/ls/completion_item/argument.cr +++ b/src/ls/completion_item/argument.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Argument) : LSP::CompletionItem name = node.name.value diff --git a/src/ls/completion_item/component.cr b/src/ls/completion_item/component.cr index d2a06d5ec..24d97231e 100644 --- a/src/ls/completion_item/component.cr +++ b/src/ls/completion_item/component.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Component) : LSP::CompletionItem index = 0 @@ -10,14 +10,13 @@ module Mint .reject(&.name.value.==("children")) .map do |property| default = - Mint::Formatter - .new(workspace.json.formatter_config) - .format!(property.default) + @workspace + .format(property.default) .to_s .gsub("}", "\\}") type = - workspace.type_checker.cache[property]? + @type_checker.cache[property]? value = case type.try(&.name) diff --git a/src/ls/completion_item/constant.cr b/src/ls/completion_item/constant.cr index d4a43f20e..e9b777cdb 100644 --- a/src/ls/completion_item/constant.cr +++ b/src/ls/completion_item/constant.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Constant, parent_name : Ast::Id? = nil) : LSP::CompletionItem name = if parent_name diff --git a/src/ls/completion_item/function.cr b/src/ls/completion_item/function.cr index a744a11bc..d924ca722 100644 --- a/src/ls/completion_item/function.cr +++ b/src/ls/completion_item/function.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Function, parent_name : Ast::Id? = nil) : LSP::CompletionItem name = if parent_name diff --git a/src/ls/completion_item/get.cr b/src/ls/completion_item/get.cr index c69aeaf7f..248b9cc3c 100644 --- a/src/ls/completion_item/get.cr +++ b/src/ls/completion_item/get.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Get, parent_name : Ast::Id? = nil) : LSP::CompletionItem name = if parent_name diff --git a/src/ls/completion_item/property.cr b/src/ls/completion_item/property.cr index b645bd1c9..7e893c3fe 100644 --- a/src/ls/completion_item/property.cr +++ b/src/ls/completion_item/property.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Property) : LSP::CompletionItem name = node.name.value diff --git a/src/ls/completion_item/style.cr b/src/ls/completion_item/style.cr index 6f30b4c0d..c66c66a45 100644 --- a/src/ls/completion_item/style.cr +++ b/src/ls/completion_item/style.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completion_item(node : Ast::Style) : LSP::CompletionItem name = node.name.value diff --git a/src/ls/completion_request.cr b/src/ls/completion_request.cr new file mode 100644 index 000000000..3602552fb --- /dev/null +++ b/src/ls/completion_request.cr @@ -0,0 +1,30 @@ +module Mint + module LS + class CompletionRequest < LSP::RequestMessage + property params : LSP::CompletionParams + + def execute(server) + snippet_support = + server + .params + .try(&.capabilities.text_document) + .try(&.completion) + .try(&.completion_item) + .try(&.snippet_support) || false + + workspace = + server.workspace(params.path) + + case type_checker = workspace.result + in TypeChecker + Completion.new( + snippet_support: snippet_support, + type_checker: type_checker, + workspace: workspace + ).process(params) + in Error + end + end + end + end +end diff --git a/src/ls/completions/component.cr b/src/ls/completions/component.cr index f1b02a87b..fa4afe724 100644 --- a/src/ls/completions/component.cr +++ b/src/ls/completions/component.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completions(node : Ast::Component, global : Bool = false) : Array(LSP::CompletionItem) name = node.name if global diff --git a/src/ls/completions/functions.cr b/src/ls/completions/functions.cr index b032b62bb..3b371fae6 100644 --- a/src/ls/completions/functions.cr +++ b/src/ls/completions/functions.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completions(node : Ast::Function) : Array(LSP::CompletionItem) node.arguments.map { |item| completion_item(item) } end diff --git a/src/ls/completions/module.cr b/src/ls/completions/module.cr index 50968890d..e54948547 100644 --- a/src/ls/completions/module.cr +++ b/src/ls/completions/module.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completions(node : Ast::Module, global : Bool = false) : Array(LSP::CompletionItem) name = node.name if global diff --git a/src/ls/completions/store.cr b/src/ls/completions/store.cr index f51f51907..87d260c6e 100644 --- a/src/ls/completions/store.cr +++ b/src/ls/completions/store.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completions(node : Ast::Store, global : Bool = false) : Array(LSP::CompletionItem) name = node.name if global diff --git a/src/ls/completions/style.cr b/src/ls/completions/style.cr index 181d0a600..5513a81f8 100644 --- a/src/ls/completions/style.cr +++ b/src/ls/completions/style.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion CSS_PROPERTY_COMPLETIONS = TypeChecker::CSS_PROPERTY_NAMES.map do |name| LSP::CompletionItem.new( diff --git a/src/ls/completions/type_definition.cr b/src/ls/completions/type_definition.cr index 871810461..e5048facd 100644 --- a/src/ls/completions/type_definition.cr +++ b/src/ls/completions/type_definition.cr @@ -1,6 +1,6 @@ module Mint module LS - class Completion < LSP::RequestMessage + class Completion def completions(node : Ast::TypeDefinition) : Array(LSP::CompletionItem) case fields = node.fields when Array(Ast::TypeVariant) diff --git a/src/ls/definition.cr b/src/ls/definition.cr index ce1ba3312..ba94e3e64 100644 --- a/src/ls/definition.cr +++ b/src/ls/definition.cr @@ -5,17 +5,16 @@ module Mint property params : LSP::TextDocumentPositionParams def execute(server) : Array(LSP::LocationLink) | Array(LSP::Location) | LSP::Location | Nil - uri = - URI.parse(params.text_document.uri) - workspace = - Workspace[uri.path.to_s] + server.workspace(params.path) - unless workspace.error + case type_checker = workspace.result + in TypeChecker stack = - server.nodes_at_cursor(params) - - return unless node = stack[0]? + type_checker.artifacts.ast.nodes_at_cursor( + column: params.position.character, + path: params.text_document.path, + line: params.position.line + 1) # stack.each do |item| # print item.class.name.sub("Mint::Ast::", "") @@ -26,6 +25,8 @@ module Mint # puts item.location.start # end + return unless node = stack[0]? + has_link_support = server .params @@ -33,7 +34,12 @@ module Mint .try(&.definition) .try(&.link_support) || false - links = definition(node, workspace, stack) + links = + Definitions.new( + type_checker: type_checker, + params: params, + stack: stack, + ).definition(node) case links when Array(LSP::LocationLink) @@ -49,65 +55,16 @@ module Mint [links] end + in Error end end - def definition(node : Ast::Node, workspace : Workspace, stack : Array(Ast::Node)) - nil - end - - def cursor_intersects?(node : Ast::Node, position : LSP::Position) : Bool - node.location.contains?(position.line + 1, position.character) - end - - def cursor_intersects?(node : Ast::Node, params : LSP::TextDocumentPositionParams) : Bool - cursor_intersects?(node, params.position) - end - - def cursor_intersects?(node : Ast::Node) : Bool - cursor_intersects?(node, params) - end - - def find_component(workspace : Workspace, name : String) : Ast::Component? - # Do not include any core component - return if Core.ast.components.any?(&.name.value.== name) - - workspace.ast.components.find(&.name.value.== name) - end - - def to_lsp_range(location : Ast::Node::Location) : LSP::Range - LSP::Range.new( - start: LSP::Position.new( - line: location.start[0] - 1, - character: location.start[1] - ), - end: LSP::Position.new( - line: location.end[0] - 1, - character: location.end[1] - ) - ) - end - def to_lsp_location(location_link : LSP::LocationLink) : LSP::Location LSP::Location.new( range: location_link.target_selection_range, uri: location_link.target_uri, ) end - - # Returns a `LSP::LocationLink` that links from *source* to the *target* node - # - # The *parent* node is used to provide the full range for the *target* node. - # For example, for a function, *target* would be the function name, and *parent* - # would be the whole node, including function body and any comments - def location_link(source : Ast::Node, target : Ast::Node, parent : Ast::Node) : LSP::LocationLink - LSP::LocationLink.new( - origin_selection_range: to_lsp_range(source.location), - target_uri: "file://#{target.location.filename}", - target_range: to_lsp_range(parent.location), - target_selection_range: to_lsp_range(target.location) - ) - end end end end diff --git a/src/ls/definition/connect.cr b/src/ls/definition/connect.cr deleted file mode 100644 index 23df69be4..000000000 --- a/src/ls/definition/connect.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::Connect, workspace : Workspace, stack : Array(Ast::Node)) - return unless cursor_intersects?(node.store) - - return unless store = - workspace.ast.stores.find(&.name.value.==(node.store.value)) - - location_link node.store, store.name, store - end - end - end -end diff --git a/src/ls/definition/connect_variable.cr b/src/ls/definition/connect_variable.cr deleted file mode 100644 index 7b4a1b2f9..000000000 --- a/src/ls/definition/connect_variable.cr +++ /dev/null @@ -1,24 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::ConnectVariable, workspace : Workspace, stack : Array(Ast::Node)) - return unless cursor_intersects?(node.name) - - return unless connect = stack[1]?.as?(Ast::Connect) - - return unless store = - workspace.ast.stores.find(&.name.value.==(connect.store.value)) - - return unless target = store.functions.find(&.name.value.==(node.name.value)) || - store.constants.find(&.name.value.==(node.name.value)) || - store.states.find(&.name.value.==(node.name.value)) || - store.gets.find(&.name.value.==(node.name.value)) - - case target - when Ast::Function, Ast::State, Ast::Get, Ast::Constant - location_link node.name, target.name, target - end - end - end - end -end diff --git a/src/ls/definition/html_component.cr b/src/ls/definition/html_component.cr deleted file mode 100644 index 63a61d75a..000000000 --- a/src/ls/definition/html_component.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::HtmlComponent, workspace : Workspace, stack : Array(Ast::Node)) - return unless cursor_intersects?(node.component) - - return unless component = - find_component(workspace, node.component.value) - - location_link node.component, component.name, component - end - end - end -end diff --git a/src/ls/definition/html_element.cr b/src/ls/definition/html_element.cr deleted file mode 100644 index aa43a0c5a..000000000 --- a/src/ls/definition/html_element.cr +++ /dev/null @@ -1,13 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::HtmlElement, workspace : Workspace, stack : Array(Ast::Node)) - node.styles.each do |style| - next unless cursor_intersects?(style) - - return definition(style, workspace, stack) - end - end - end - end -end diff --git a/src/ls/definition/id.cr b/src/ls/definition/id.cr deleted file mode 100644 index 3e066472a..000000000 --- a/src/ls/definition/id.cr +++ /dev/null @@ -1,23 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::Id, workspace : Workspace, stack : Array(Ast::Node)) - found = - workspace.ast.type_definitions.find(&.name.value.==(node.value)) || - workspace.ast.stores.find(&.name.value.==(node.value)) || - find_component(workspace, node.value) - - if found.nil? && (next_node = stack[1]) - return definition(next_node, workspace, stack) - end - - return if Core.ast.nodes.includes?(found) - - case found - when Ast::Store, Ast::Component, Ast::TypeDefinition - location_link node, found.name, found - end - end - end - end -end diff --git a/src/ls/definition/type_destructuring.cr b/src/ls/definition/type_destructuring.cr deleted file mode 100644 index a821d496b..000000000 --- a/src/ls/definition/type_destructuring.cr +++ /dev/null @@ -1,8 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::TypeDestructuring, server : Server, workspace : Workspace, stack : Array(Ast::Node)) - end - end - end -end diff --git a/src/ls/definition/variable.cr b/src/ls/definition/variable.cr deleted file mode 100644 index d10916654..000000000 --- a/src/ls/definition/variable.cr +++ /dev/null @@ -1,113 +0,0 @@ -module Mint - module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::Variable, workspace : Workspace, stack : Array(Ast::Node)) - lookup = workspace.type_checker.variables[node]? - - if lookup - entity, parent = lookup - - case {entity, parent} - when {Ast::Component, _}, - {Ast::Store, _} - location_link node, entity.name, entity - when {Ast::Module, _} - links = workspace.ast.modules - .select(&.name.value.==(node.value)) - .reject(&.in?(Core.ast.nodes)) - .sort_by!(&.file.path) - .map do |mod| - location_link node, mod.name, mod - end - - return links.first if links.size == 1 - return links unless links.empty? - when {Ast::Variable, _} - variable_lookup_parent(node, entity, workspace) - when {Ast::ConnectVariable, Ast::Node} - connect = - workspace.ast.nodes - .select(Ast::Connect) - .find!(&.keys.find(&.==(entity))) - - key = - lookup[0].as(Ast::ConnectVariable) - - location_link node, key.target || key.name, connect - else - variable_lookup(node, entity) - end - else - variable_record_key(node, workspace, stack) || - variable_next_key(node, workspace, stack) - end - end - - def variable_lookup_parent(node : Ast::Variable, variable : Ast::Variable, workspace : Workspace) - # For some variables in the .variables` cache, we only have access to the - # target Ast::Variable and not its containing node, so we must search for it - return unless parent = workspace - .ast - .nodes - .select { |other| other.is_a?(Ast::TypeDestructuring) || other.is_a?(Ast::Statement) || other.is_a?(Ast::For) } - .select(&.file.path.==(variable.file.path)) - .find { |other| other.from < variable.from && other.to > variable.to } - - location_link node, variable, parent - end - - def variable_lookup(node : Ast::Variable, target : Ast::Node | TypeChecker::Checkable) - case item = target - when Ast::Node - name = case item - when Ast::Property, - Ast::Constant, - Ast::Function, - Ast::State, - Ast::Get, - Ast::Argument - item.name - else - item - end - - location_link node, name, item - end - end - - def variable_record_key(node : Ast::Variable, workspace : Workspace, stack : Array(Ast::Node)) - case field = stack[1]? - when Ast::Field - return unless record_name = workspace.type_checker.record_field_lookup[field]? - - return unless record_definition_field = workspace - .ast - .type_definitions - .find(&.name.value.==(record_name)) - .try do |item| - case fields = item.fields - when Array(Ast::TypeDefinitionField) - fields.find(&.key.value.==(node.value)) - end - end - - location_link node, record_definition_field.key, record_definition_field - end - end - - def variable_next_key(node : Ast::Variable, workspace : Workspace, stack : Array(Ast::Node)) - case next_call = stack[3]? - when Ast::NextCall - return unless parent = workspace.type_checker.lookups[next_call] - - return unless state = case parent - when Ast::Provider, Ast::Component, Ast::Store - parent.states.find(&.name.value.==(node.value)) - end - - location_link node, state.name, state - end - end - end - end -end diff --git a/src/ls/definitions.cr b/src/ls/definitions.cr new file mode 100644 index 000000000..c4edaba25 --- /dev/null +++ b/src/ls/definitions.cr @@ -0,0 +1,63 @@ +module Mint + module LS + class Definitions + def initialize( + *, + @params : LSP::TextDocumentPositionParams, + @type_checker : TypeChecker, + @stack : Array(Ast::Node) + ) + end + + def definition(node : Ast::Node) + nil + end + + def cursor_intersects?(node : Ast::Node, position : LSP::Position) : Bool + node.location.contains?(position.line + 1, position.character) + end + + def cursor_intersects?(node : Ast::Node, params : LSP::TextDocumentPositionParams) : Bool + cursor_intersects?(node, params.position) + end + + def cursor_intersects?(node : Ast::Node) : Bool + cursor_intersects?(node, @params) + end + + def find_component(name : String) : Ast::Component? + # Do not include any core component + return if Core.ast.components.any?(&.name.value.== name) + + @type_checker.ast.components.find(&.name.value.== name) + end + + def to_lsp_range(location : Ast::Node::Location) : LSP::Range + LSP::Range.new( + start: LSP::Position.new( + line: location.start[0] - 1, + character: location.start[1] + ), + end: LSP::Position.new( + line: location.end[0] - 1, + character: location.end[1] + ) + ) + end + + # Returns a `LSP::LocationLink` that links from *source* to the *target* node + # + # The *parent* node is used to provide the full range for the *target* node. + # For example, for a function, *target* would be the function name, and *parent* + # would be the whole node, including function body and any comments + def location_link(source : Ast::Node, target : Ast::Node, parent : Ast::Node) : LSP::LocationLink + LSP::LocationLink.new( + origin_selection_range: to_lsp_range(source.location), + target_uri: "file://#{target.location.filename}", + target_range: to_lsp_range(parent.location), + target_selection_range: to_lsp_range(target.location) + ) + end + end + end +end diff --git a/src/ls/definition/access.cr b/src/ls/definitions/access.cr similarity index 59% rename from src/ls/definition/access.cr rename to src/ls/definitions/access.cr index 8d94d752d..9251fa76d 100644 --- a/src/ls/definition/access.cr +++ b/src/ls/definitions/access.cr @@ -1,8 +1,9 @@ module Mint module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::Access, server : Server, workspace : Workspace, stack : Array(Ast::Node)) - lookup = workspace.type_checker.variables[node]?.try(&.first) + class Definitions + def definition(node : Ast::Access) + lookup = + @type_checker.variables[node]?.try(&.first) if lookup case lookup diff --git a/src/ls/definitions/connect.cr b/src/ls/definitions/connect.cr new file mode 100644 index 000000000..31bcd1e9c --- /dev/null +++ b/src/ls/definitions/connect.cr @@ -0,0 +1,15 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::Connect) + return unless cursor_intersects?(node.store) + + return unless store = + @type_checker.artifacts.ast.stores + .find(&.name.value.==(node.store.value)) + + location_link node.store, store.name, store + end + end + end +end diff --git a/src/ls/definitions/connect_variable.cr b/src/ls/definitions/connect_variable.cr new file mode 100644 index 000000000..309b768fc --- /dev/null +++ b/src/ls/definitions/connect_variable.cr @@ -0,0 +1,27 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::ConnectVariable) + return unless cursor_intersects?(node.name) + + return unless connect = + @stack[1]?.as?(Ast::Connect) + + return unless store = + @type_checker.artifacts.ast.stores + .find(&.name.value.==(connect.store.value)) + + return unless target = + store.functions.find(&.name.value.==(node.name.value)) || + store.constants.find(&.name.value.==(node.name.value)) || + store.states.find(&.name.value.==(node.name.value)) || + store.gets.find(&.name.value.==(node.name.value)) + + case target + when Ast::Function, Ast::State, Ast::Get, Ast::Constant + location_link node.name, target.name, target + end + end + end + end +end diff --git a/src/ls/definition/html_attribute.cr b/src/ls/definitions/html_attribute.cr similarity index 51% rename from src/ls/definition/html_attribute.cr rename to src/ls/definitions/html_attribute.cr index 7275a1e81..9b25fc38f 100644 --- a/src/ls/definition/html_attribute.cr +++ b/src/ls/definitions/html_attribute.cr @@ -1,13 +1,16 @@ module Mint module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::HtmlAttribute, workspace : Workspace, stack : Array(Ast::Node)) + class Definitions + def definition(node : Ast::HtmlAttribute) return unless cursor_intersects?(node.name) - return unless html_component = stack.find(&.is_a?(Ast::HtmlComponent)).as?(Ast::HtmlComponent) + return unless html_component = + @stack + .find(&.is_a?(Ast::HtmlComponent)) + .as?(Ast::HtmlComponent) return unless component = - find_component(workspace, html_component.component.value) + find_component(html_component.component.value) return unless component_property = component.properties.find(&.name.value.== node.name.value) diff --git a/src/ls/definitions/html_component.cr b/src/ls/definitions/html_component.cr new file mode 100644 index 000000000..b7eb3deef --- /dev/null +++ b/src/ls/definitions/html_component.cr @@ -0,0 +1,14 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::HtmlComponent) + return unless cursor_intersects?(node.component) + + return unless component = + find_component(node.component.value) + + location_link node.component, component.name, component + end + end + end +end diff --git a/src/ls/definitions/html_element.cr b/src/ls/definitions/html_element.cr new file mode 100644 index 000000000..765a5fddb --- /dev/null +++ b/src/ls/definitions/html_element.cr @@ -0,0 +1,13 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::HtmlElement) + node.styles.each do |style| + next unless cursor_intersects?(style) + + return definition(style) + end + end + end + end +end diff --git a/src/ls/definition/html_style.cr b/src/ls/definitions/html_style.cr similarity index 56% rename from src/ls/definition/html_style.cr rename to src/ls/definitions/html_style.cr index 2e62b9667..4dd1e72cd 100644 --- a/src/ls/definition/html_style.cr +++ b/src/ls/definitions/html_style.cr @@ -1,10 +1,11 @@ module Mint module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::HtmlStyle, workspace : Workspace, stack : Array(Ast::Node)) + class Definitions + def definition(node : Ast::HtmlStyle) return unless cursor_intersects?(node.name) - return unless component = stack.find(&.is_a?(Ast::Component)).as?(Ast::Component) + return unless component = + @stack.find(&.is_a?(Ast::Component)).as?(Ast::Component) return unless component_style = component.styles.find(&.name.value.== node.name.value) diff --git a/src/ls/definitions/id.cr b/src/ls/definitions/id.cr new file mode 100644 index 000000000..0df87c98e --- /dev/null +++ b/src/ls/definitions/id.cr @@ -0,0 +1,35 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::Id) + case next_node = @stack[1]? + when Ast::TypeDestructuring + if (type_definition = + @type_checker.artifacts.ast.type_definitions + .find(&.name.value.==(node.value))) && + !Core.ast.nodes.includes?(type_definition) + location_link node, type_definition.name, type_definition + else + definition(next_node) + end + else + found = + @type_checker.artifacts.ast.type_definitions.find(&.name.value.==(node.value)) || + @type_checker.artifacts.ast.stores.find(&.name.value.==(node.value)) || + find_component(node.value) + + if found.nil? && next_node + return definition(next_node) + end + + return if Core.ast.nodes.includes?(found) + + case found + when Ast::Store, Ast::Component, Ast::TypeDefinition + location_link node, found.name, found + end + end + end + end + end +end diff --git a/src/ls/definition/type.cr b/src/ls/definitions/type.cr similarity index 52% rename from src/ls/definition/type.cr rename to src/ls/definitions/type.cr index ee12b2af1..79c1c2035 100644 --- a/src/ls/definition/type.cr +++ b/src/ls/definitions/type.cr @@ -1,11 +1,12 @@ module Mint module LS - class Definition < LSP::RequestMessage - def definition(node : Ast::Type, workspace : Workspace, stack : Array(Ast::Node)) + class Definitions + def definition(node : Ast::Type) return unless cursor_intersects?(node.name) return unless record = - workspace.ast.type_definitions.find(&.name.value.==(node.name.value)) + @type_checker.artifacts.ast.type_definitions + .find(&.name.value.==(node.name.value)) return if Core.ast.type_definitions.includes?(record) diff --git a/src/ls/definitions/type_destructuring.cr b/src/ls/definitions/type_destructuring.cr new file mode 100644 index 000000000..8905a8168 --- /dev/null +++ b/src/ls/definitions/type_destructuring.cr @@ -0,0 +1,16 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::TypeDestructuring) + if item = @type_checker.lookups[node]? + variant, definition = item + + case {variant, definition} + when {Ast::TypeVariant, Ast::TypeDefinition} + location_link node, variant.value, variant + end + end + end + end + end +end diff --git a/src/ls/definitions/variable.cr b/src/ls/definitions/variable.cr new file mode 100644 index 000000000..1a6968b27 --- /dev/null +++ b/src/ls/definitions/variable.cr @@ -0,0 +1,114 @@ +module Mint + module LS + class Definitions + def definition(node : Ast::Variable) + if lookup = @type_checker.variables[node]? + entity, parent = lookup + + case {entity, parent} + when {Ast::TypeDefinition, _} + location_link node, entity.name, entity + when {Ast::TypeVariant, _} + location_link node, entity.value, entity + when {Ast::Component, _}, + {Ast::Store, _} + location_link node, entity.name, entity + when {Ast::Module, _} + links = + @type_checker.artifacts.ast.modules + .select(&.name.value.==(node.value)) + .reject(&.in?(Core.ast.nodes)) + .sort_by!(&.file.path) + .map do |mod| + location_link node, mod.name, mod + end + + return links.first if links.size == 1 + return links unless links.empty? + when {Ast::Variable, _} + variable_lookup_parent(node, entity) + else + variable_lookup(node, entity) + end + else + variable_record_key(node) || + variable_next_key(node) + end + end + + def variable_lookup_parent(node : Ast::Variable, variable : Ast::Variable) + # For some variables in the .variables` cache, we only have access to the + # target Ast::Variable and not its containing node, so we must search for it + return unless parent = + @type_checker.artifacts.ast.nodes + .select { |other| other.is_a?(Ast::TypeDestructuring) || other.is_a?(Ast::Statement) || other.is_a?(Ast::For) } + .select(&.file.path.==(variable.file.path)) + .find { |other| other.from < variable.from && other.to > variable.to } + + location_link node, variable, parent + end + + def variable_lookup(node : Ast::Variable, target : Ast::Node | TypeChecker::Checkable) + case item = target + when Ast::Node + name = case item + when Ast::Property, + Ast::Constant, + Ast::Function, + Ast::State, + Ast::Get, + Ast::Argument + item.name + else + item + end + + location_link node, name, item + end + end + + def variable_record_key(node : Ast::Variable) + target = + @stack[1]?.try do |item| + case item + when Ast::Access + item.field + when Ast::Field + item + end + end + + if target + return unless record_name = + @type_checker.record_field_lookup[target]? + + return unless record_definition_field = + @type_checker.artifacts.ast.type_definitions + .find(&.name.value.==(record_name)) + .try do |item| + case fields = item.fields + when Array(Ast::TypeDefinitionField) + fields.find(&.key.value.==(node.value)) + end + end + + location_link node, record_definition_field.key, record_definition_field + end + end + + def variable_next_key(node : Ast::Variable) + case next_call = @stack[3]? + when Ast::NextCall + return unless parent = next_call.entity + + return unless state = case parent + when Ast::Provider, Ast::Component, Ast::Store + parent.states.find(&.name.value.==(node.value)) + end + + location_link node, state.name, state + end + end + end + end +end diff --git a/src/ls/did_change.cr b/src/ls/did_change.cr index 60d7ca73b..78983a802 100644 --- a/src/ls/did_change.cr +++ b/src/ls/did_change.cr @@ -3,14 +3,13 @@ module Mint class DidChange < LSP::NotificationMessage property params : LSP::DidChangeTextDocumentParams - def execute(server) - uri = - URI.parse(params.text_document.uri) + def execute(server) : Nil + path = + params.text_document.path - workspace = - Workspace[uri.path.to_s] - - workspace.update(params.content_changes.first.text, uri.path) + server + .workspace(path) + .update(params.content_changes.first.text, path) end end end diff --git a/src/ls/did_open.cr b/src/ls/did_open.cr new file mode 100644 index 000000000..8911a7696 --- /dev/null +++ b/src/ls/did_open.cr @@ -0,0 +1,16 @@ +module Mint + module LS + class DidOpen < LSP::NotificationMessage + property params : LSP::DidOpenTextDocumentParams + + def execute(server) : Nil + path = + params.text_document.path + + server + .workspace(path) + .update(params.text_document.text, path) + end + end + end +end diff --git a/src/ls/folding_range.cr b/src/ls/folding_range.cr index 5b314863f..db524d21e 100644 --- a/src/ls/folding_range.cr +++ b/src/ls/folding_range.cr @@ -2,24 +2,20 @@ module Mint module LS class FoldingRange < LSP::RequestMessage property params : LSP::FoldingRangeParams - getter ranges = [] of LSP::FoldingRange - def range(node : Ast::Component) - range(node.comment) - range(node, node.comment) + def range(node : Ast::Component) : Array(LSP::FoldingRange) + range(node.comment.try(&.location)) + range(node, node.comment) end - def range(node : Ast::Module) - range(node.comment) - range(node, node.comment) + def range(node : Ast::Module) : Array(LSP::FoldingRange) + range(node.comment.try(&.location)) + range(node, node.comment) end - def range(node : Ast::Function) - range(node.comment) - range(node, node.comment) + def range(node : Ast::Function) : Array(LSP::FoldingRange) + range(node.comment.try(&.location)) + range(node, node.comment) end - def range(node : Ast::Node, comment : Ast::Comment?) + def range(node : Ast::Node, comment : Ast::Comment?) : Array(LSP::FoldingRange) if comment range(comment.location.end[0], node.location.end[0]) else @@ -27,33 +23,34 @@ module Mint end end - def range(node : Ast::Node?) - nil + def range(node : Ast::Node?) : Array(LSP::FoldingRange) + [] of LSP::FoldingRange end - def range(location : Ast::Node::Location) + def range(location : Ast::Node::Location) : Array(LSP::FoldingRange) range(location.start[0], location.end[0]) end - def range(start_line, end_line) - ranges << LSP::FoldingRange.new( - start_line: start_line - 1, - end_line: end_line - 1) + def range(start_line, end_line) : Array(LSP::FoldingRange) + [ + LSP::FoldingRange.new( + start_line: start_line - 1, + end_line: end_line - 1), + ] end def execute(server) - unless workspace.error - server + workspace = + server.workspace(params.text_document.path) + + case type_checker = workspace.result + in TypeChecker + type_checker.artifacts.ast .nodes_at_path(params.text_document.path) .select { |node| node.location.start[0] != node.location.end[0] } - .compact_map { |node| range(node) } + .flat_map { |node| range(node) } + in Error end - - ranges - end - - private def workspace - Workspace[params.text_document.path] end end end diff --git a/src/ls/formatting.cr b/src/ls/formatting.cr index 40fe7ea43..7e94e4e1c 100644 --- a/src/ls/formatting.cr +++ b/src/ls/formatting.cr @@ -4,29 +4,25 @@ module Mint property params : LSP::DocumentFormattingParams def execute(server) - uri = - URI.parse(params.text_document.uri) - workspace = - Workspace[uri.path.to_s] - - formatted = - workspace.format(uri.path.to_s) - - # If there is an error show that - server.show_message_request("Could not format the file because it contains errors!", 1) if workspace.error + server.workspace(params.text_document.path) - # Respond with the formatted document or an empty response message - # because SublimeText LSP client freezes if an error response is - # returns for this - if !workspace.error && formatted + case formatted = workspace.format(params.text_document.path) + in String [ LSP::TextEdit.new(new_text: formatted, range: LSP::Range.new( start: LSP::Position.new(line: 0, character: 0), end: LSP::Position.new(line: 9999, character: 999) )), ] - else + in Error + # If there is an error show that + server.show_message_request("Could not format the file because it contains errors!", 1) + + # Respond with the formatted document or an empty response message + # because SublimeText LSP client freezes if there is no response. + %w[] + in Nil %w[] end end diff --git a/src/ls/hover.cr b/src/ls/hover.cr index 027fb4b9f..ec58c4362 100644 --- a/src/ls/hover.cr +++ b/src/ls/hover.cr @@ -5,14 +5,22 @@ module Mint property params : LSP::TextDocumentPositionParams # Fallback handler for nil, obviously it should not happen. - def hover(node : Nil, workspace) : Array(String) + def hover( + node : Nil, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) ["This should not happen! Please create an issue about this!"] end # Fallback handler for nodes that does not have a handler yet. - def hover(node : Ast::Node, workspace) : Array(String) + def hover( + node : Ast::Node, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) type = - type_of(node, workspace) + type_of(node, type_checker) [ "Type information for: #{node.class}\n", @@ -21,9 +29,8 @@ module Mint end # Returns the type information of a node from the workspace - def type_of(node : Ast::Node, workspace) - workspace - .type_checker + def type_of(node : Ast::Node, type_checker : TypeChecker) + type_checker .cache[node]? .try(&.to_pretty) .try { |value| "```\n#{value}\n```" } @@ -38,23 +45,29 @@ module Mint # this could take a while because the workspace parses # and type checks all of its source files. workspace = - Workspace[uri.path.to_s] + server.workspace(uri.path.to_s) contents = - if error = workspace.error + case type_checker = workspace.result + in Error # If the workspace has an error we cannot really # provide and hover information, so we just provide # the error instead. [ "Cannot provide hover data because of an error:\n", - "```\n#{error.to_terminal}\n```", + "```\n#{type_checker.to_terminal}\n```", ] - else - # We get the stack of nodes under the cursor - stack = - server.nodes_at_cursor(params) + in TypeChecker + nodes = + type_checker + .artifacts + .ast + .nodes_at_cursor( + column: params.position.character, + path: params.text_document.path, + line: params.position.line + 1) - # stack.each do |item| + # nodes.each do |item| # print item.class.name.sub("Mint::Ast::", "") # case item # when Ast::Id, Ast::Variable @@ -63,32 +76,32 @@ module Mint # puts item.location.start # end - node = stack[0]? - parent = stack[1]? + parent = nodes[1]? + node = nodes[0]? case node when Ast::Variable, Ast::Id # If the first node under the cursor is a `Ast::Variable` - # or `Ast::Id`, then get the associated nodes - # information and hover that otherwise get the hover - # information of the parent. + # or `Ast::Id`, then get the associated node information + # and hover that otherwise get the hover information of + # the parent. lookup = - workspace.type_checker.variables[node]? + type_checker.variables[node]? if lookup case item = lookup[0] when Tuple(Ast::Node, Int32) - hover(item[0], workspace) + hover(item[0], workspace, type_checker) when Ast::Node - hover(item, workspace) + hover(item, workspace, type_checker) else [item.to_s] end else - hover(parent, workspace) + hover(parent, workspace, type_checker) end else - hover(node, workspace) + hover(node, workspace, type_checker) end end diff --git a/src/ls/hover/access.cr b/src/ls/hover/access.cr index 26b06f437..8b3bfccf0 100644 --- a/src/ls/hover/access.cr +++ b/src/ls/hover/access.cr @@ -1,11 +1,15 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Access, workspace) : Array(String) - if item = workspace.type_checker.variables[node]? + def hover( + node : Ast::Access, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) + if item = type_checker.variables[node]? case item[1] when Ast::TypeDefinition - hover(item[1], workspace) + hover(item[1], workspace, type_checker) end end || [] of String end diff --git a/src/ls/hover/argument.cr b/src/ls/hover/argument.cr index fbe9a7107..e95778806 100644 --- a/src/ls/hover/argument.cr +++ b/src/ls/hover/argument.cr @@ -1,9 +1,13 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Argument, workspace) : Array(String) + def hover( + node : Ast::Argument, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) type = - workspace.formatter.format!(node.type) + workspace.format(node.type) ["**#{node.name.value} : #{type}**"] end diff --git a/src/ls/hover/component.cr b/src/ls/hover/component.cr new file mode 100644 index 000000000..b64a07ac4 --- /dev/null +++ b/src/ls/hover/component.cr @@ -0,0 +1,25 @@ +module Mint + module LS + class Hover < LSP::RequestMessage + def hover( + node : Ast::Component, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) + properties = + node + .properties + .flat_map { |property| hover(property, workspace, type_checker) } + + properties_title = + "\n**Properties**\n" unless properties.empty? + + ([ + "**#{node.name.value}**\n", + node.comment.try(&.content.strip), + properties_title, + ] + properties).compact + end + end + end +end diff --git a/src/ls/hover/css_definition.cr b/src/ls/hover/css_definition.cr index 87ff416c3..4ccb1e53f 100644 --- a/src/ls/hover/css_definition.cr +++ b/src/ls/hover/css_definition.cr @@ -1,7 +1,11 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::CssDefinition, workspace) : Array(String) + def hover( + node : Ast::CssDefinition, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) path = URI.encode_path(node.name) diff --git a/src/ls/hover/function.cr b/src/ls/hover/function.cr index df6207539..749e020ad 100644 --- a/src/ls/hover/function.cr +++ b/src/ls/hover/function.cr @@ -1,12 +1,19 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Function, workspace) : Array(String) + def hover( + node : Ast::Function, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) + ast = + type_checker.artifacts.ast + entity = - workspace.ast.unified_modules.find(&.functions.includes?(node)) || - workspace.ast.components.find(&.functions.includes?(node)) || - workspace.ast.providers.find(&.functions.includes?(node)) || - workspace.ast.stores.find(&.functions.includes?(node)) + ast.unified_modules.find(&.functions.includes?(node)) || + ast.components.find(&.functions.includes?(node)) || + ast.providers.find(&.functions.includes?(node)) || + ast.stores.find(&.functions.includes?(node)) name = case entity @@ -19,7 +26,7 @@ module Mint type = node.type.try do |item| - ": #{workspace.formatter.format!(item)}" + ": #{workspace.format(item)}" end [ diff --git a/src/ls/hover/get.cr b/src/ls/hover/get.cr index 25b3db17b..d203fe16f 100644 --- a/src/ls/hover/get.cr +++ b/src/ls/hover/get.cr @@ -1,10 +1,17 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Get, workspace) : Array(String) + def hover( + node : Ast::Get, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) + ast = + type_checker.artifacts.ast + entity = - workspace.ast.components.find(&.gets.includes?(node)) || - workspace.ast.stores.find(&.gets.includes?(node)) + ast.components.find(&.gets.includes?(node)) || + ast.stores.find(&.gets.includes?(node)) name = case entity @@ -14,7 +21,7 @@ module Mint type = node.type.try do |item| - ": #{workspace.formatter.format!(item)}" + ": #{workspace.format(item)}" end [ diff --git a/src/ls/hover/html_attribute.cr b/src/ls/hover/html_attribute.cr index eb1c1a85f..66f8395ab 100644 --- a/src/ls/hover/html_attribute.cr +++ b/src/ls/hover/html_attribute.cr @@ -1,9 +1,13 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::HtmlAttribute, workspace) : Array(String) + def hover( + node : Ast::HtmlAttribute, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) type = - type_of(node, workspace) + type_of(node, type_checker) [ "**#{node.name.value}**", diff --git a/src/ls/hover/html_component.cr b/src/ls/hover/html_component.cr index fba9fcd39..b4ed58b1f 100644 --- a/src/ls/hover/html_component.cr +++ b/src/ls/hover/html_component.cr @@ -1,31 +1,15 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Component, workspace) : Array(String) - properties = - node - .properties - .flat_map { |property| hover(property, workspace) } - - properties_title = - "\n**Properties**\n" unless properties.empty? - - ([ - "**#{node.name.value}**\n", - node.comment.try(&.content.strip), - properties_title, - ] + properties).compact - end - end - - class Hover < LSP::RequestMessage - def hover(node : Ast::HtmlComponent, workspace) : Array(String) + def hover( + node : Ast::HtmlComponent, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) component = - workspace - .type_checker - .lookups[node]?.try(&.first?) + type_checker.lookups[node]?.try(&.first?) - hover(component, workspace) + hover(component, workspace, type_checker) end end end diff --git a/src/ls/hover/html_element.cr b/src/ls/hover/html_element.cr index 45a36bf2e..c524dbc1c 100644 --- a/src/ls/hover/html_element.cr +++ b/src/ls/hover/html_element.cr @@ -1,7 +1,11 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::HtmlElement, workspace) : Array(String) + def hover( + node : Ast::HtmlElement, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) path = URI.encode_path(node.tag.value) diff --git a/src/ls/hover/property.cr b/src/ls/hover/property.cr index f8b9cc04c..f031f81b8 100644 --- a/src/ls/hover/property.cr +++ b/src/ls/hover/property.cr @@ -1,15 +1,19 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Property, workspace) : Array(String) + def hover( + node : Ast::Property, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) default = node.default.try do |item| - " = #{workspace.formatter.format!(item)}" + " = #{workspace.format(item)}" end type = node.type.try do |item| - " : #{workspace.formatter.format!(item)}" + " : #{workspace.format(item)}" end [ diff --git a/src/ls/hover/state.cr b/src/ls/hover/state.cr index 5aaf034d8..2f86f14aa 100644 --- a/src/ls/hover/state.cr +++ b/src/ls/hover/state.cr @@ -1,13 +1,17 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::State, workspace) : Array(String) + def hover( + node : Ast::State, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) default = - " = #{workspace.formatter.format!(node.default)}" + " = #{workspace.format(node.default)}" type = node.type.try do |item| - " : #{workspace.formatter.format!(item)}" + " : #{workspace.format(item)}" end [ diff --git a/src/ls/hover/statement.cr b/src/ls/hover/statement.cr index 20ab5fdd9..36f5d8ae9 100644 --- a/src/ls/hover/statement.cr +++ b/src/ls/hover/statement.cr @@ -1,16 +1,18 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Statement, workspace) : Array(String) + def hover( + node : Ast::Statement, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) type = - type_of(node, workspace) + type_of(node, type_checker) head = node.target.try do |target| formatted = - workspace - .formatter - .format!(target) + workspace.format(target) "**#{formatted} =**" end diff --git a/src/ls/hover/string_literal.cr b/src/ls/hover/string_literal.cr index 1e8296ac6..084bd23f2 100644 --- a/src/ls/hover/string_literal.cr +++ b/src/ls/hover/string_literal.cr @@ -1,7 +1,11 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::StringLiteral, workspace) : Array(String) + def hover( + node : Ast::StringLiteral, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) ["String"] end end diff --git a/src/ls/hover/type.cr b/src/ls/hover/type.cr index 53b6a5727..2304bcac2 100644 --- a/src/ls/hover/type.cr +++ b/src/ls/hover/type.cr @@ -1,18 +1,23 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::Type, workspace) : Array(String) + def hover( + node : Ast::Type, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) definition = - workspace + type_checker + .artifacts .ast .type_definitions .find(&.name.value.==(node.name.value)) if definition - hover(definition, workspace) + hover(definition, workspace, type_checker) else type = - workspace.formatter.format!(node) + workspace.format(node) ["```\n#{type}\n```"] end diff --git a/src/ls/hover/type_definition.cr b/src/ls/hover/type_definition.cr index b3ac4fad9..2692cf7ab 100644 --- a/src/ls/hover/type_definition.cr +++ b/src/ls/hover/type_definition.cr @@ -1,7 +1,11 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::TypeDefinition, workspace) : Array(String) + def hover( + node : Ast::TypeDefinition, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) parameters = "" # workspace.formatter.format_parameters(node.parameters) @@ -23,7 +27,7 @@ module Mint ] + fields).compact else type = - workspace.formatter.format!(node) + workspace.format(node) ["```\n#{type}\n```"] end diff --git a/src/ls/hover/type_destructuring.cr b/src/ls/hover/type_destructuring.cr index 51fd89426..92c7407fe 100644 --- a/src/ls/hover/type_destructuring.cr +++ b/src/ls/hover/type_destructuring.cr @@ -1,11 +1,15 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::TypeDestructuring, workspace) : Array(String) + def hover( + node : Ast::TypeDestructuring, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) item = - workspace.type_checker.lookups[node].try(&.first?) + type_checker.lookups[node].try(&.first?) - hover(item, workspace) + hover(item, workspace, type_checker) end end end diff --git a/src/ls/hover/type_variant.cr b/src/ls/hover/type_variant.cr index 53171af04..e2d916255 100644 --- a/src/ls/hover/type_variant.cr +++ b/src/ls/hover/type_variant.cr @@ -1,9 +1,14 @@ module Mint module LS class Hover < LSP::RequestMessage - def hover(node : Ast::TypeVariant, workspace) : Array(String) + def hover( + node : Ast::TypeVariant, + workspace : FileWorkspace, + type_checker : TypeChecker + ) : Array(String) item = - workspace + type_checker + .artifacts .ast .type_definitions .find do |definition| @@ -13,7 +18,7 @@ module Mint end end - hover(item, workspace) + hover(item, workspace, type_checker) end end end diff --git a/src/ls/semantic_tokens.cr b/src/ls/semantic_tokens.cr index d71ca8bba..dca18ee8f 100644 --- a/src/ls/semantic_tokens.cr +++ b/src/ls/semantic_tokens.cr @@ -1,64 +1,64 @@ module Mint module LS - # This is the class that handles the "textDocument/semanticTokens/full" request. + # This is the class that handles the "textDocument/semanticTokens/full" + # request. class SemanticTokens < LSP::RequestMessage property params : LSP::SemanticTokensParams - def execute(server) - uri = - URI.parse(params.text_document.uri) + def execute(server : LSP::Server) + path = + URI.parse(params.text_document.uri).path.to_s - ast = - Workspace[uri.path.to_s][uri.path.to_s] + data = + case ast = server.workspace(path).ast(path) + when Ast + # This is used later on to convert the line/column of each token + file = + ast.nodes.first.file - # This is used later on to convert the line/column of each token - file = - ast.nodes.first.file + tokenizer = SemanticTokenizer.new + tokenizer.tokenize(ast) - tokenizer = SemanticTokenizer.new - tokenizer.tokenize(ast) + tokens = + tokenizer.tokens.sort_by(&.from).compact_map do |token| + location = + Ast::Node.compute_location(file, token.from, token.to) - data = - tokenizer.tokens.sort_by(&.from).compact_map do |token| - location = - Ast::Node.compute_location(file, token.from, token.to) + type = + token.type.to_s.underscore - type = - token.type.to_s.underscore + if index = SemanticTokenizer::TOKEN_TYPES.index(type) + [ + location.start[0] - 1, + location.start[1], + token.to - token.from, + index.to_i64, + 0_i64, + ] + end + end - if index = SemanticTokenizer::TOKEN_TYPES.index(type) - [ - location.start[0] - 1, - location.start[1], - token.to - token.from, - index.to_i64, - 0_i64, - ] - end - end + tokens.each_with_index.flat_map do |item, index| + current = + item.dup - result = [] of Array(Int64) + unless index.zero? + last = + tokens[index - 1] - data.each_with_index do |item, index| - current = - item.dup + current[0] = + current[0] - last[0] - unless index.zero? - last = - data[index - 1] + current[1] = current[1] - last[1] if current[0] == 0_i64 + end - current[0] = - current[0] - last[0] - - current[1] = current[1] - last[1] if current[0] == 0_i64 + current + end + else + [] of String end - result << current - end - - { - data: result.flatten, - } + {data: data} end end end diff --git a/src/ls/server.cr b/src/ls/server.cr index 4c17c023d..e7c9629cf 100644 --- a/src/ls/server.cr +++ b/src/ls/server.cr @@ -1,56 +1,40 @@ module Mint module LS class Server < LSP::Server - # Lifecycle methods - method "initialize", Initialize - method "shutdown", Shutdown - method "exit", Exit - - # Text document related methods - method "textDocument/willSaveWaitUntil", WillSaveWaitUntil - method "textDocument/semanticTokens/full", SemanticTokens - method "textDocument/foldingRange", FoldingRange - method "textDocument/formatting", Formatting - method "textDocument/completion", Completion - method "textDocument/codeAction", CodeAction - method "textDocument/definition", Definition - method "textDocument/didChange", DidChange - method "textDocument/hover", Hover + @methods = { + # Lifecycle methods + "initialize" => Initialize, + "shutdown" => Shutdown, + "exit" => Exit, + + # Text document related methods + "textDocument/completion" => CompletionRequest, + "textDocument/willSaveWaitUntil" => WillSaveWaitUntil, + "textDocument/semanticTokens/full" => SemanticTokens, + "textDocument/foldingRange" => FoldingRange, + "textDocument/formatting" => Formatting, + "textDocument/codeAction" => CodeAction, + "textDocument/definition" => Definition, + "textDocument/didChange" => DidChange, + "textDocument/didOpen" => DidOpen, + "textDocument/hover" => Hover, + } property params : LSP::InitializeParams? = nil - # Logs the given stack. - def debug_stack(stack : Array(Ast::Node)) - stack.each_with_index do |item, index| - class_name = item.class - - if index.zero? - log(class_name.to_s) - else - log("#{" " * (index - 1)} ↳ #{class_name}") - end - end - end - - # Returns the nodes at the given cursor (position) - def nodes_at_cursor(path : String, position : LSP::Position) : Array(Ast::Node) - nodes_at_path(path) - .select!(&.location.contains?(position.line + 1, position.character)) - end - - def nodes_at_cursor(params : LSP::TextDocumentPositionParams) : Array(Ast::Node) - nodes_at_cursor(params.path, params.position) - end + @@workspaces = {} of String => FileWorkspace - def nodes_at_cursor(params : LSP::CodeActionParams) : Array(Ast::Node) - nodes_at_cursor(params.text_document.path, params.range.start) - end + def workspace(path : String) : FileWorkspace + base = + File.find_in_ancestors(path, "mint.json").to_s - def nodes_at_path(path : String) - Mint::Workspace[path] - .ast - .nodes - .select(&.file.path.==(path)) + @@workspaces[base] ||= + FileWorkspace.new( + check: Check::Unreachable, + include_tests: true, + listener: nil, + format: false, + path: base) end end end diff --git a/src/ls/websocket_server.cr b/src/ls/websocket_server.cr new file mode 100644 index 000000000..440a40763 --- /dev/null +++ b/src/ls/websocket_server.cr @@ -0,0 +1,103 @@ +module Mint + module LS + # A server to use the LSP over websockets. + class WebSocketServer < Server + class Sandbox + getter workspace : FileWorkspace + + @directory : Path + + def initialize(@server : Server) + json = {"source-directories" => ["."]}.to_json + @json = MintJson.parse(json, "mint.json") + @id = Random::Secure.hex + + # We create a temporary directory for the workspace with a "mint.json" + # so the `FileWorkspace` can work. + @directory = + Path[Dir.tempdir, @id].tap do |path| + FileUtils.mkdir_p(path) + File.write(Path[path, "mint.json"], json) + end + + # There is only one workspace. + @workspace = + FileWorkspace.new( + path: Path[@directory, "mint.json"].to_s, + listener: ->build(TypeChecker | Error), + check: Check::Unreachable, + include_tests: true, + format: false) + end + + def build(result : TypeChecker | Error) + bundle = + case result + in TypeChecker + Bundler.new( + artifacts: result.artifacts, + json: @json, + config: Bundler::Config.new( + generate_manifest: false, + include_program: true, + hash_assets: false, + live_reload: false, + runtime_path: nil, + skip_icons: true, + relative: false, + optimize: true, + test: nil), + ).bundle + in Error + {"index.html" => ->{ result.to_html }} + end + + IO::Memory.new.tap do |io| + Compress::Zip::Writer.open(io) do |zip| + bundle.each do |path, contents| + zip.add(path, contents.call) + end + end + + io.rewind + + # TODO: Handle response. + HTTP::Client.post("https://#{@id}.sandbox.mint-lang.com/", body: io) + + # Send notification so the client can refresh. + @server.send_notification("sandbox/compiled", { + url: "https://#{@id}.sandbox.mint-lang.com/", + }) + end + end + + def cleanup + FileUtils.rm_rf(@directory) + end + end + + def initialize(@socket : HTTP::WebSocket, sandbox : Bool = false) + # We need these for compability with the server, they are not used. + @out = IO::Memory.new + @in = IO::Memory.new + + @sandbox = Sandbox.new(self) if sandbox + + @socket.on_message { |message| process(message) } + @socket.on_close { cleanup } + end + + def cleanup + @sandbox.try(&.cleanup) + end + + def workspace(path : String) : FileWorkspace + @sandbox.try(&.workspace) || super(path) + end + + def send(content : String) + @socket.send(content) + end + end + end +end diff --git a/src/ls/will_save_wait_until.cr b/src/ls/will_save_wait_until.cr index 7be5637fe..fc04e1ceb 100644 --- a/src/ls/will_save_wait_until.cr +++ b/src/ls/will_save_wait_until.cr @@ -4,32 +4,26 @@ module Mint property params : LSP::WillSaveTextDocumentParams def execute(server) - uri = - URI.parse(params.text_document.uri) - workspace = - Workspace[uri.path.to_s] - - formatted = - workspace.format(uri.path.to_s) + server.workspace(params.text_document.path) - # If there is an error show that - if workspace.error - server.show_message_request("Could not format the file because it contains errors!", 1) - end - - # Respond with the formatted document or an empty response message - # because SublimeText LSP client freezes if an error response is - # returns for this - if !workspace.error && formatted + case formatted = workspace.format(params.text_document.path) + in String [ LSP::TextEdit.new(new_text: formatted, range: LSP::Range.new( start: LSP::Position.new(line: 0, character: 0), end: LSP::Position.new(line: 9999, character: 999) )), ] - else + in Error + # If there is an error show that + server.show_message_request("Could not format the file because it contains errors!", 1) + + # Respond with the formatted document or an empty response message + # because SublimeText LSP client freezes if there is no response. %w[] + in Nil + %[] end end end diff --git a/src/lsp/protocol/create_file.cr b/src/lsp/protocol/create_file.cr new file mode 100644 index 000000000..6af03b54b --- /dev/null +++ b/src/lsp/protocol/create_file.cr @@ -0,0 +1,22 @@ +module LSP + struct CreateFile + include JSON::Serializable + + # The resource to create. + property uri : String + + # Additional options + property options : CreateFileOptions? + + # An optional annotation identifier describing the operation. + # + # @since 3.16.0 + # + # TODO: + # @[JSON::Field(key: "annotationId")] + # property annotation_id : ChangeAnnotationIdentifier? + + def initialize(@uri) + end + end +end diff --git a/src/lsp/protocol/create_file_options.cr b/src/lsp/protocol/create_file_options.cr new file mode 100644 index 000000000..d571a0645 --- /dev/null +++ b/src/lsp/protocol/create_file_options.cr @@ -0,0 +1,12 @@ +module LSP + struct CreateFileOptions + include JSON::Serializable + + # Overwrite existing file. Overwrite wins over `ignoreIfExists` + property overwrite : Bool? + + # Ignore if exists. + @[JSON::Field(key: "ignoreIfExists")] + property ignore_if_exists : Bool? + end +end diff --git a/src/lsp/protocol/did_open_text_document_params.cr b/src/lsp/protocol/did_open_text_document_params.cr new file mode 100644 index 000000000..d14a98653 --- /dev/null +++ b/src/lsp/protocol/did_open_text_document_params.cr @@ -0,0 +1,9 @@ +module LSP + struct DidOpenTextDocumentParams + include JSON::Serializable + + # The document that was opened. + @[JSON::Field(key: "textDocument")] + property text_document : TextDocumentItem + end +end diff --git a/src/lsp/protocol/optional_versioned_text_document_identifier.cr b/src/lsp/protocol/optional_versioned_text_document_identifier.cr new file mode 100644 index 000000000..ce0cbeff7 --- /dev/null +++ b/src/lsp/protocol/optional_versioned_text_document_identifier.cr @@ -0,0 +1,16 @@ +require "./text_document_identifier" + +module LSP + class OptionalVersionedTextDocumentIdentifier < TextDocumentIdentifier + # The version number of this document. If an optional versioned text document + # identifier is sent from the server to the client and the file is not + # open in the editor (the server has not received an open notification + # before) the server can send `null` to indicate that the version is + # known and the content on disk is the master (as specified with document + # content ownership). + # + # The version number of a document will increase after each change, + # including undo/redo. The number doesn't need to be consecutive. + property version : Int64? + end +end diff --git a/src/lsp/protocol/request_message.cr b/src/lsp/protocol/request_message.cr index fd3b7b6fb..c4dd9e58d 100644 --- a/src/lsp/protocol/request_message.cr +++ b/src/lsp/protocol/request_message.cr @@ -14,6 +14,6 @@ module LSP # The method to be invoked. property method : String - abstract def execute(server : Server) + # abstract def execute(server : Server) end end diff --git a/src/lsp/protocol/text_document_edit.cr b/src/lsp/protocol/text_document_edit.cr new file mode 100644 index 000000000..13a7fd2e4 --- /dev/null +++ b/src/lsp/protocol/text_document_edit.cr @@ -0,0 +1,12 @@ +module LSP + struct TextDocumentEdit + include JSON::Serializable + + # The text document to change. + @[JSON::Field(key: "textDocument")] + property text_document : OptionalVersionedTextDocumentIdentifier + + # The edits to be applied. + property edits : Array(TextEdit) + end +end diff --git a/src/lsp/protocol/text_document_item.cr b/src/lsp/protocol/text_document_item.cr new file mode 100644 index 000000000..2b34381ea --- /dev/null +++ b/src/lsp/protocol/text_document_item.cr @@ -0,0 +1,27 @@ +module LSP + struct TextDocumentItem + include JSON::Serializable + + # The text document's URI. + property uri : String + + # The text document's language identifier. + @[JSON::Field(key: "languageId")] + property language_id : String + + # The version number of this document (it will increase after each + # change, including undo/redo). + property version : Int64 + + # The content of the opened text document. + property text : String + + def initialize(@uri, @version, @language_id, @text) + end + + # Returns the path of the URI + def path + URI.parse(uri).try(&.path).to_s + end + end +end diff --git a/src/lsp/protocol/workspace_edit.cr b/src/lsp/protocol/workspace_edit.cr index 3239abac7..81bdfebf6 100644 --- a/src/lsp/protocol/workspace_edit.cr +++ b/src/lsp/protocol/workspace_edit.cr @@ -2,8 +2,12 @@ module LSP struct WorkspaceEdit include JSON::Serializable + alias Value = Array(TextDocumentEdit | CreateFile) | + Array(TextDocumentEdit) | + Nil + # Holds changes to existing resources. - property changes : Hash(String, Array(TextEdit)) + property changes : Hash(String, Array(TextEdit)) | Nil # Depending on the client capability # `workspace.workspaceEdit.resourceOperations` document changes are either @@ -19,9 +23,8 @@ module LSP # `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s # using the `changes` property are supported. - # TODO: - # @[JSON::Field(key: "documentChanges")] - # property document_changes : Array(TextDocumentEdit) | TextDocumentEdit | CreateFile | RenameFile | DeleteFile? + @[JSON::Field(key: "documentChanges")] + property document_changes : Value # A map of change annotations that can be referenced in # `AnnotatedTextEdit`s or create, rename and delete file / folder diff --git a/src/lsp/server.cr b/src/lsp/server.cr index 52294b2e0..68a0afd79 100644 --- a/src/lsp/server.cr +++ b/src/lsp/server.cr @@ -8,12 +8,6 @@ module LSP # end # class Server - @@methods = {} of String => RequestMessage.class | NotificationMessage.class - - macro method(name, message) - @@methods[{{name}}] = {{message}} - end - def initialize(@in : IO, @out : IO) end @@ -26,6 +20,14 @@ module LSP @out.flush end + def send_notification(method, params) + send({ + "jsonrpc" => "2.0", + "method" => method, + "params" => params, + }.to_json) + end + def send_request(id, method, params) send({ "jsonrpc" => "2.0", @@ -65,48 +67,50 @@ module LSP }) end - # Reads a message from the input IO, and converts to a Message object, - # calls execute on it and send the result if it's a request message. - def read - return exit(1) if @in.closed? - - MessageParser.parse(@in) do |contents| - # Parse the contents as JSON - json = - JSON.parse(contents) + def process(contents) + # Parse the contents as JSON + json = + JSON.parse(contents) - # Get the method name - name = - json["method"]? + # Get the method name + name = + json["method"]? - # If we have a method name get the method class - if name - method = - @@methods[name.as_s]? + # If we have a method name get the method class + if name + method = + @methods[name.as_s]? - # If the given method is implemented - # get an instance using the contents - if method - message = - method.from_json(contents) + # If the given method is implemented + # get an instance using the contents + if method + message = + method.from_json(contents) - result = - message.execute(self) + result = + message.execute(self) - case message - when RequestMessage - send_response(id: message.id, result: result) - end + case message + when RequestMessage + send_response(id: message.id, result: result) end end end + rescue error + show_message_request(error.to_s) + # log() + # error.backtrace?.try(&.each { |item| log(item) }) + end + + # Reads a message from the input IO, and converts to a Message object, + # calls execute on it and send the result if it's a request message. + def read + return exit(1) if @in.closed? + MessageParser.parse(@in, &->process(String)) rescue error : IO::EOFError # Client has exited unexpectedly without # sending an "exit" lifecycle message exit(1) - rescue error - log(error.to_s) - error.backtrace?.try(&.each { |item| log(item) }) end end end diff --git a/src/mint_json.cr b/src/mint_json.cr index ad9d607ac..dd6f6b335 100644 --- a/src/mint_json.cr +++ b/src/mint_json.cr @@ -1,1062 +1,67 @@ module Mint class MintJson - include Errorable - class Application - getter title, meta, icon, head, name, theme, display, orientation, css_prefix - - def initialize(@meta = {} of String => String, - @orientation = "", - @display = "", - @theme = "", - @title = "", - @name = "", - @head = "", - @icon = "", - @css_prefix : String? = nil) - end - end - - @parser = JSON::PullParser.new("{}") - - getter dependencies = [] of Installer::Dependency - getter formatter_config = Formatter::Config.new - getter web_components = {} of String => String - getter source_directories = %w[] - getter test_directories = %w[] - getter application = Application.new - getter root : String - getter name = "" - - def initialize - @json = "" - @root = "" - @file = "" - end - - def initialize(@json : String, @root : String, @file : String) - begin - @parser = JSON::PullParser.new(@json) - rescue exception : JSON::ParseException - error! :mint_json_invalid_json do - block do - text "I could not parse the following" - bold "mint.json" - text "file:" - end - - snippet node(exception) - end - rescue error - error! :mint_json_invalid_file do - block do - text "There was a problem when I was trying to open a" - bold "mint.json" - text "file:" - bold @file - end - - block do - text "The error! I got is this:" - end - - block do - bold error.to_s - end - end - end - - parse_root - end - - def self.from_file(path) - new File.read(path), File.dirname(path), path - end - - def self.parse_current : MintJson - from_file(Path[Dir.current, "mint.json"].to_s) - end - - def self.parse_current? : MintJson? - parse_current + getter meta : Hash(String, String) + getter orientation : String + getter theme_color : String + getter css_prefix : String + getter display : String + getter title : String + getter icon : String + getter head : String + getter name : String + + def initialize( + *, + @orientation, + @theme_color, + @css_prefix, + @display, + @title, + @meta, + @name, + @head, + @icon + ) + end + end + + getter dependencies : Array(Installer::Dependency) + getter source_directories : Array(String) + getter test_directories : Array(String) + getter formatter : Formatter::Config + getter application : Application + getter name : String + getter path : String + + def initialize( + *, + @source_directories, + @test_directories, + @dependencies, + @application, + @formatter, + @name, + @path + ) + end + + def self.parse(path : String, *, search : Bool = false) : MintJson + Parser.parse(path, search: search) + end + + def self.parse(contents : String, path : String) : MintJson + Parser.parse(contents: contents, path: path) + end + + def self.current : MintJson + parse(Path[Dir.current, "mint.json"].to_s) + end + + def self.current? : MintJson? + current rescue nil end - - # Calculating nodes for the snippet in errors. - # -------------------------------------------------------------------------- - - def node(column_number, line_number) - position = - if line_number - 1 == 0 - 0 - else - @json - .lines[0..line_number - 2] - .reduce(0) { |acc, line| acc + line.size + 1 } - end - - to = - position + - @json[position..-1].lines.first.size - - file = - Parser::File.new(@json, @file) - - Ast::Node.new( - to: to, - from: position, - file: file) - end - - def node(exception : JSON::ParseException) - node exception.location - end - - def node(location) - node location[1], location[0] - end - - def current_node - node @parser.location - end - - def source_files - glob = - source_directories.map { |dir| SourceFiles.glob_pattern(@root, dir) } - - Dir.glob(glob) - end - - # Parsing the root object - # -------------------------------------------------------------------------- - - def parse_root - @parser.read_object do |key| - case key - when "name" - parse_name - when "mint-version" - parse_mint_version - when "source-directories" - parse_source_directories - when "test-directories" - parse_test_directories - when "application" - parse_application - when "dependencies" - parse_dependencies - when "formatter" - parse_formatter - when "web-components" - parse_web_components - else - error! :mint_json_root_invalid_key do - block do - text "The root object of a" - bold "mint.json" - text "file has an invalid key:" - bold key - end - - snippet current_node - end - end - end - rescue exception : JSON::ParseException - error! :mint_json_root_not_an_object do - block do - text "There was a problem when parsing" - bold "mint.json" - text "file." - end - - snippet node(exception) - end - end - - # Parsing the name - # -------------------------------------------------------------------------- - - def parse_name - location = - @parser.location - - @name = - @parser.read_string - - error! :mint_json_name_empty do - block do - text "The" - bold "name" - text "field of a" - bold "mint.json" - text "file is empty:" - end - - snippet node(location) - end if @name.empty? - rescue exception : JSON::ParseException - error! :mint_json_name_not_string do - block do - text "The" - bold "name" - text "field of a" - bold "mint.json" - text "file is not a string." - end - - snippet node(exception) - end - end - - # Parsing the mint version - # -------------------------------------------------------------------------- - - def parse_mint_version - location = - @parser.location - - raw = - @parser.read_string - - error! :mint_json_mint_version_empty do - block do - text "The" - bold "mint-version" - text "field in your" - bold "mint.json" - text "file is empty." - end - - snippet node(location) - end if raw.empty? - - match = - raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) - - constraint = - if match - lower = - Installer::Semver.parse?(match[1]) - - upper = - Installer::Semver.parse?(match[2]) - - Installer::SimpleConstraint.new(lower, upper) if upper && lower - end - - error! :mint_json_mint_version_invalid do - block do - text "There was a problem when parsing the" - bold "Mint version constraint." - end - - block "The version constraint should be in this format:" - - block do - bold "0.0.0 <= v < 1.0.0" - end - - snippet node(location) - end unless constraint - - resolved = - Installer::Semver.parse(VERSION.rchop("-devel")) - - error! :mint_json_mint_version_mismatch do - block do - text "The" - bold "mint-version" - text "field in your" - bold "mint.json" - text "file does not match your current version of Mint." - end - - block do - text "I was looking for" - code constraint.to_s - - text "but found" - code VERSION - text "instead." - end - - snippet node(location) - end unless resolved < constraint.upper && resolved >= constraint.lower - rescue exception : JSON::ParseException - error! :mint_json_mint_version_not_string do - block do - text "The" - bold "mint-version" - text "field in your" - bold "mint.json" - text "file is not a string." - end - - snippet node(exception) - end - end - - # Parsing the head - # -------------------------------------------------------------------------- - - def parse_head - location = - @parser.location - - head = - @parser.read_string - - path = - Path[@root, head].to_s - - error! :mint_json_head_not_exists do - block do - text "The" - bold "head" - text "field of" - bold "the application object" - text "points to a file that does not exists." - end - - block do - text "The" - bold "head" - text "field if exists should point to a HTML file." - end - - block do - text "That HTML file will be injected to the HEAD of the generated HTML." - text "It is used to include external dependencies" - text "(CSS, JS, analytics, etc...)" - end - - snippet node(location) - end unless File.exists?(path) - - File.read(path) - rescue exception : JSON::ParseException - error! :mint_json_head_not_string do - block do - text "The" - bold "head" - text "field of" - bold "the application object" - text "is not string:" - end - - snippet node(exception) - end - end - - # Parsing the icon - # -------------------------------------------------------------------------- - - def parse_icon - location = - @parser.location - - icon = - @parser.read_string - - error! :mint_json_icon_not_exists do - block do - text "The" - bold "icon" - text "field of" - bold "the application object" - text "points to a file that does not exists." - end - - block do - text "The" - bold "icon" - text "field if exists should point to an image." - end - - block "That image will used to generate favicons for the application." - - snippet node(location) - end unless File.exists?(icon) - - icon - rescue exception : JSON::ParseException - error! :mint_json_icon_not_string do - block do - text "The" - bold "icon" - text "field of" - bold "the application object" - text "is not string:" - end - - snippet node(exception) - end - end - - # Parsing the source directories - # -------------------------------------------------------------------------- - - def parse_source_directories - location = - @parser.location - - @parser.read_array { parse_source_directory } - - error! :mint_json_source_directories_empty do - block do - text "The" - bold "source-directories" - text "array should not be empty." - end - - block do - text "The" - bold "source-directories" - text "field lists all directories (relative to the mint.json file)" - text "which contain the source files of the application." - end - - snippet node(location) - end if @source_directories.empty? - rescue exception : JSON::ParseException - error! :mint_json_source_directories_invalid do - block do - text "The" - bold "source-directories" - text "field should be an array." - end - - block do - text "The" - bold "source-directories" - text "field lists all directories (relative to the mint.json file)" - text "which contain the source files of the application." - end - - snippet node(exception) - end - end - - def parse_source_directory - location = - @parser.location - - directory = - @parser.read_string - - path = - Path[@root, directory] - - error! :mint_json_source_directory_not_exists do - block do - text "The source directory" - bold directory - text "does not exists." - end - - snippet node(location) - end unless Dir.exists?(path) - - @source_directories << directory - rescue exception : JSON::ParseException - error! :mint_json_source_directory_invalid do - block do - text "All entries in the" - bold "source-directories" - text "array should be string." - end - - snippet "I found one that it is not:", node(exception) - end - end - - # Parsing the test directories - # -------------------------------------------------------------------------- - - def parse_test_directories - @parser.read_array { parse_test_directory } - rescue exception : JSON::ParseException - error! :mint_json_test_directories_invalid do - block do - text "The" - bold "test-directories" - text "field should be an array." - end - - block do - text "The" - bold "test-directories" - text "field lists all directories (relative to the mint.json file)" - text "which contain the test files of the application." - end - - snippet node(exception) - end - end - - def parse_test_directory - location = - @parser.location - - directory = - @parser.read_string - - path = - Path[@root, directory] - - error! :mint_json_test_directory_not_exists do - block do - text "The test directory" - bold directory - text "does not exists." - end - - snippet node(location) - end unless Dir.exists?(path) - - @test_directories << directory - rescue exception : JSON::ParseException - error! :mint_json_test_directory_invalid do - block do - text "All entries in the" - bold "test-directories" - text "array should be string." - end - - snippet "I found one that it is not:", node(exception) - end - end - - # Parsing the formatter config - # -------------------------------------------------------------------------- - - def parse_formatter - indent_size = 2 - - @parser.read_object do |key| - case key - when "indent-size" - indent_size = parse_indent_size - else - error! :mint_json_formatter_config_invalid_key do - block do - text "The" - bold "formatter-config" - text "object of a" - bold "mint.json" - text "file has an invalid key:" - bold key - end - - snippet current_node - end - end - end - - @formatter_config = Formatter::Config.new(indent_size: indent_size) - rescue exception : JSON::ParseException - error! :mint_json_formatter_config_invalid do - block do - text "There was a problem when parsing the" - bold "formatter-config" - text "object of a" - bold "mint.json" - text "file:" - end - - snippet node(exception) - end - end - - # Parsing the ident size - # -------------------------------------------------------------------------- - - def parse_indent_size - @parser.read_int.clamp(0, 100).to_i - rescue exception : JSON::ParseException - error! :mint_json_indent_size_invalid do - block do - text "There was a problem when parsing the" - bold "indent-size field" - text "of an" - bold "formatter-config" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing web components - # -------------------------------------------------------------------------- - def parse_web_components - @parser.read_object do |key| - web_components[key] = @parser.read_string - end - rescue exception : JSON::ParseException - error! :mint_json_web_components_invalid do - block do - text "There was a problem when parsing the" - bold "web-components object:" - end - - snippet node(exception) - end - end - - # Parsing the application - # -------------------------------------------------------------------------- - - def parse_application - meta = - {} of String => String - - orientation = "" - display = "" - title = "" - theme = "" - name = "" - icon = "" - head = "" - css_prefix = nil - - @parser.read_object do |key| - case key - when "head" - head = parse_head - when "title" - title = parse_title - when "meta" - meta = parse_meta - when "name" - name = parse_application_name - when "theme-color" - theme = parse_theme - when "orientation" - orientation = parse_orientation - when "display" - display = parse_display - when "icon" - icon = parse_icon - when "css-prefix" - css_prefix = parse_application_css_prefix - else - error! :mint_json_application_invalid_key do - block do - text "The" - bold "application object" - text "of a" - bold "mint.json" - text "file has an invalid key:" - bold key - end - - snippet current_node - end - end - end - - @application = - Application.new( - title: title, - meta: meta, - icon: icon, - head: head, - name: name, - theme: theme, - orientation: orientation, - display: display, - css_prefix: css_prefix) - rescue exception : JSON::ParseException - error! :mint_json_application_invalid do - block do - text "There was a problem when parsing the" - bold "application object" - text "of a" - bold "mint.json" - text "file:" - end - - snippet node(exception) - end - end - - # Parsing the meta tags - # -------------------------------------------------------------------------- - - def parse_meta - meta = {} of String => String - - @parser.read_object do |key| - value = - case key - when "keywords" - parse_keywords - else - parse_meta_value - end - - meta[key] = value - end - - meta - rescue exception : JSON::ParseException - error! :mint_json_meta_invalid do - block do - text "There was a problem when parsing the" - bold "meta object" - text "of an" - bold "application object" - text "file:" - end - - snippet node(exception) - end - end - - def parse_meta_value - @parser.read_string - rescue exception : JSON::ParseException - error! :mint_json_meta_value_not_string do - block do - text "The" - bold "value" - text "of a" - bold "meta field" - text "is not a string:" - end - - snippet node(exception) - end - end - - def parse_keywords - keywords = %w[] - - @parser.read_array do - keywords << parse_keyword - end - - keywords.join(',') - rescue exception : JSON::ParseException - error! :mint_json_keywords_invalid do - block do - text "There was a problem when parsing the" - bold "keywords array" - text "of a meta object:" - end - - snippet node(exception) - end - end - - def parse_keyword - @parser.read_string - rescue exception : JSON::ParseException - error! :mint_json_keyword_not_string do - block do - text "A provided" - bold "keyword" - text "is not a string:" - end - - snippet node(exception) - end - end - - # Parsing the title - # -------------------------------------------------------------------------- - - def parse_title - location = - @parser.location - - title = - @parser.read_string - - error! :mint_json_title_empty do - block do - text "The" - bold "title" - text "field of an" - bold "application object" - text "is empty:" - end - - snippet node(location) - end if title.empty? - - title - rescue exception : JSON::ParseException - error! :mint_json_title_invalid do - block do - text "There was a problem when parsing the" - bold "title field" - text "of an" - bold "application" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing the name - # -------------------------------------------------------------------------- - - def parse_application_name - @parser.read_string - rescue exception : JSON::ParseException - error! :mint_json_application_name_invalid do - block do - text "There was a problem when parsing the" - bold "name field" - text "of an" - bold "application" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing the theme - # -------------------------------------------------------------------------- - - def parse_theme - @parser.read_string - rescue exception : JSON::ParseException - error! :mint_json_theme_invalid do - block do - text "There was a problem when parsing the" - bold "theme field" - text "of an" - bold "application" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing the orientation - # -------------------------------------------------------------------------- - - def parse_orientation - @parser.read_string - rescue exception : JSON::ParseException - error! :mint_json_orientation_invalid do - block do - text "There was a problem when parsing the" - bold "orientation field" - text "of an" - bold "application" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing the display - # -------------------------------------------------------------------------- - - def parse_display - @parser.read_string - rescue exception : JSON::ParseException - error! :mint_json_display_invalid do - block do - text "There was a problem when parsing the" - bold "display field" - text "of an" - bold "application" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing the css prefix - # -------------------------------------------------------------------------- - - def parse_application_css_prefix - @parser.read_string_or_null - rescue exception : JSON::ParseException - error! :mint_json_css_prefix_invalid do - block do - text "There was a problem when parsing the" - bold "css-prefix field" - text "of an" - bold "application" - text "object:" - end - - snippet node(exception) - end - end - - # Parsing the dependencies - # -------------------------------------------------------------------------- - - def parse_dependencies - @parser.read_object do |key| - @dependencies << parse_dependency key - end - rescue exception : JSON::ParseException - error! :mint_json_dependencies_invalid do - block do - text "There was a problem when parsing the" - bold "dependencies" - text "field of a mint.json file." - end - - block do - text "The" - bold "dependencies" - text "field lists all the dependencies for the application." - end - - snippet node(exception) - end - end - - def parse_dependency(key) - repository = nil - constraint = nil - - @parser.read_object_or_null do |dependency_key| - case dependency_key - when "repository" - repository = @parser.read_string - when "constraint" - constraint = parse_constraint - else - raise Error.new(:mint_json_unkown_dependency_field) - end - end - - raise "Should not happen" unless repository - raise "Should not happen" unless constraint - - Installer::Dependency.new key, repository, constraint - rescue exception : JSON::ParseException - error! :mint_json_dependency_invalid do - block do - text "There was a problem when parsing a" - bold "dependency" - text "of a mint.json file:" - end - - snippet node(exception) - end - end - - def parse_constraint - location = - @parser.location - - raw = - @parser.read_string - - match = - raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) - - constraint = - if match - lower = - Installer::Semver.parse?(match[1]) - - upper = - Installer::Semver.parse?(match[2]) - - Installer::SimpleConstraint.new(lower, upper) if upper && lower - else - match = - raw.match(/(.*?):(\d+\.\d+\.\d+)/) - - if match - version = - Installer::Semver.parse?(match[2]) - - target = - match[1] - - Installer::FixedConstraint.new(version, target) if version - end - end - - error! :mint_json_dependency_invalid_constraint do - block do - text "There was a problem when parsing the" - bold "constraint" - text "of a dependency" - end - - block do - text "The constraint of a dependency is either in this format:" - end - block do - bold "0.0.0 <= v < 1.0.0" - end - - block do - text "or a git tag / commit / branch followed by the version:" - end - - block do - bold "master:0.1.0" - end - - block do - text "I could not find either." - end - - snippet node(location) - end unless constraint - - constraint - rescue exception : JSON::ParseException - error! :mint_json_dependency_constraint_invalid do - block do - text "There was a problem when parsing the" - bold "constraint" - text "of a dependency:" - end - - snippet node(exception) - end - end - - def check_dependencies! - dependencies.each do |dependency| - next if dependency_exists?(dependency.name) - - terminal.puts "#{COG} Ensuring dependencies..." - terminal.puts " ↳ Not all dependencies in your mint.json file are installed." - terminal.puts " Would you like to install them now? (Y/n)" - - answer = gets.to_s.downcase - terminal.puts AnsiEscapes::Erase.lines(2) - - if answer == "y" - Installer.new - break - else - terminal.print "#{WARNING} Missing dependencies..." - raise CliException.new - end - end - end - - def terminal - Render::Terminal::STDOUT - end - - def dependency_exists?(name : String) - Dir.exists?(".mint/packages/#{name}") - end end end diff --git a/src/mint_json/application.cr b/src/mint_json/application.cr new file mode 100644 index 000000000..800b1e4de --- /dev/null +++ b/src/mint_json/application.cr @@ -0,0 +1,72 @@ +module Mint + class MintJson + class Parser + def parse_application : Application + meta = {} of String => String + orientation = "" + theme_color = "" + css_prefix = "" + display = "" + title = "" + name = "" + icon = "" + head = "" + + @parser.read_object do |key| + case key + when "orientation" + orientation = parse_application_orientation + when "theme-color" + theme_color = parse_application_theme_color + when "css-prefix" + css_prefix = parse_application_css_prefix + when "display" + display = parse_application_display + when "title" + title = parse_application_title + when "head" + head = parse_application_head + when "icon" + icon = parse_application_icon + when "meta" + meta = parse_application_meta + when "name" + name = parse_application_name + else + error! :application_invalid_key do + block do + text "The" + bold "application object" + text "has an invalid key:" + end + + snippet key + snippet "It is here:", snippet_data + end + end + end + + Application.new( + theme_color: theme_color, + orientation: orientation, + css_prefix: css_prefix, + display: display, + title: title, + meta: meta, + icon: icon, + head: head, + name: name) + rescue JSON::ParseException + error! :application_invalid do + block do + text "The" + bold "application field" + text "should be an object, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/css_prefix.cr b/src/mint_json/application/css_prefix.cr new file mode 100644 index 000000000..135cfb72a --- /dev/null +++ b/src/mint_json/application/css_prefix.cr @@ -0,0 +1,21 @@ +module Mint + class MintJson + class Parser + def parse_application_css_prefix : String + @parser.read_string + rescue JSON::ParseException + error! :application_css_prefix_invalid do + block do + text "The" + bold "css-prefix field" + text "of the" + bold "application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/display.cr b/src/mint_json/application/display.cr new file mode 100644 index 000000000..a34ca0748 --- /dev/null +++ b/src/mint_json/application/display.cr @@ -0,0 +1,39 @@ +module Mint + class MintJson + class Parser + DISPLAY_VALUES = + %w[fullscreen standalone minimal-ui browser] + + def parse_application_display : String + @parser.location.try do |location| + @parser.read_string.tap do |value| + error! :application_display_mismatch do + block do + text "The" + bold "value" + text "of the" + bold "display field" + text "should be one of:" + end + + snippet DISPLAY_VALUES.join("\n") + snippet "It is here:", snippet_data(location) + end unless DISPLAY_VALUES.includes?(value) + end + end + rescue JSON::ParseException + error! :application_display_invalid do + block do + text "The" + bold "display field" + text "of the" + bold "application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/head.cr b/src/mint_json/application/head.cr new file mode 100644 index 000000000..66232ceb2 --- /dev/null +++ b/src/mint_json/application/head.cr @@ -0,0 +1,48 @@ +module Mint + class MintJson + class Parser + def parse_application_head : String + location = + @parser.location + + head = + @parser.read_string + + path = + Path[root, head].to_s + + error! :application_head_not_exists do + block do + text "The" + bold "head" + text "field of" + bold "the application object" + text "points to a file that does not exists." + end + + block do + text "It should point to an HTML file, which be injected to the" + text " tag of the generated HTML. It is used to include" + text "external dependencies (CSS, JS, analytics, etc...)" + end + + snippet snippet_data(location) + end unless File.exists?(path) + + File.read(path) + rescue JSON::ParseException + error! :application_head_invalid do + block do + text "The" + bold "head" + text "field of" + bold "the application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/icon.cr b/src/mint_json/application/icon.cr new file mode 100644 index 000000000..e06d3f3a3 --- /dev/null +++ b/src/mint_json/application/icon.cr @@ -0,0 +1,47 @@ +module Mint + class MintJson + class Parser + def parse_application_icon : String + location = + @parser.location + + icon = + @parser.read_string + + path = + Path[root, icon].to_s + + error! :application_icon_not_exists do + block do + text "The" + bold "icon" + text "field of" + bold "the application object" + text "points to a file that does not exists." + end + + block do + text "It should point to an image which will be used to generate" + text "favicons for the application." + end + + snippet snippet_data(location) + end unless File.exists?(path) + + icon + rescue JSON::ParseException + error! :application_icon_invalid do + block do + text "The" + bold "icon" + text "field of" + bold "the application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/meta.cr b/src/mint_json/application/meta.cr new file mode 100644 index 000000000..f682bdb20 --- /dev/null +++ b/src/mint_json/application/meta.cr @@ -0,0 +1,87 @@ +module Mint + class MintJson + class Parser + def parse_application_meta : Hash(String, String) + meta = {} of String => String + + @parser.read_object do |key| + value = + case key + when "keywords" + parse_application_meta_keywords + else + parse_application_meta_value + end + + meta[key] = value + end + + meta + rescue JSON::ParseException + error! :application_meta_invalid do + block do + text "The" + bold "meta field" + text "of the" + bold "application object" + text "should be an object, but it's not:" + end + + snippet snippet_data + end + end + + def parse_application_meta_value : String + @parser.read_string + rescue JSON::ParseException + error! :application_meta_value_invalid do + block do + text "The" + bold "value" + text "of a" + bold "meta field" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + + def parse_application_meta_keywords : String + keywords = %w[] + + @parser.read_array do + keywords << parse_application_meta_keyword + end + + keywords.join(',') + rescue JSON::ParseException + error! :application_meta_keywords_invalid do + block do + text "The" + bold "keywords field" + text "of the" + bold "meta object" + text "should be an array, but it's not:" + end + + snippet snippet_data + end + end + + def parse_application_meta_keyword : String + @parser.read_string + rescue JSON::ParseException + error! :application_meta_keyword_invalid do + block do + text "A" + bold "keyword" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/name.cr b/src/mint_json/application/name.cr new file mode 100644 index 000000000..b9d494ce1 --- /dev/null +++ b/src/mint_json/application/name.cr @@ -0,0 +1,21 @@ +module Mint + class MintJson + class Parser + def parse_application_name : String + @parser.read_string + rescue JSON::ParseException + error! :application_name_invalid do + block do + text "The" + bold "name field" + text "of the" + bold "application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/orientation.cr b/src/mint_json/application/orientation.cr new file mode 100644 index 000000000..73fe6c32c --- /dev/null +++ b/src/mint_json/application/orientation.cr @@ -0,0 +1,43 @@ +module Mint + class MintJson + class Parser + ORIENTATION_VALUES = + %w[ + any natural landscape landscape-primary + landscape-secondary portrait portrait-primary + portrait-secondary + ] + + def parse_application_orientation : String + @parser.location.try do |location| + @parser.read_string.tap do |value| + error! :application_orientation_mismatch do + block do + text "The" + bold "value" + text "of the" + bold "orientation field" + text "should be one of:" + end + + snippet ORIENTATION_VALUES.join("\n") + snippet "It is here:", snippet_data(location) + end unless ORIENTATION_VALUES.includes?(value) + end + end + rescue JSON::ParseException + error! :application_orientation_invalid do + block do + text "The" + bold "orientation field" + text "of the" + bold "application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/theme_color.cr b/src/mint_json/application/theme_color.cr new file mode 100644 index 000000000..e9c608c4f --- /dev/null +++ b/src/mint_json/application/theme_color.cr @@ -0,0 +1,21 @@ +module Mint + class MintJson + class Parser + def parse_application_theme_color : String + @parser.read_string + rescue JSON::ParseException + error! :application_theme_color_invalid do + block do + text "The" + bold "theme-color field" + text "of the" + bold "application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/application/title.cr b/src/mint_json/application/title.cr new file mode 100644 index 000000000..b2a0c797a --- /dev/null +++ b/src/mint_json/application/title.cr @@ -0,0 +1,39 @@ +module Mint + class MintJson + class Parser + def parse_application_title : String + location = + @parser.location + + title = + @parser.read_string + + error! :application_title_empty do + block do + text "The" + bold "title" + text "field of the" + bold "application object" + text "should not be empty, but it is:" + end + + snippet snippet_data(location) + end if title.empty? + + title + rescue JSON::ParseException + error! :application_title_invalid do + block do + text "The" + bold "title field" + text "of the" + bold "application object" + text "should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/dependencies.cr b/src/mint_json/dependencies.cr new file mode 100644 index 000000000..d549c0c9f --- /dev/null +++ b/src/mint_json/dependencies.cr @@ -0,0 +1,162 @@ +module Mint + class MintJson + class Parser + def parse_dependencies : Array(Installer::Dependency) + @parser.location.try do |location| + dependencies = [] of Installer::Dependency + + @parser.read_object do |key| + dependencies << parse_dependency(key) + end + + error! :dependencies_empty do + block do + text "The" + bold "dependencies" + text "field lists all the dependencies for the application." + end + + block do + text "The" + bold "dependencies" + text "object should not be empty, but it is:" + end + + snippet snippet_data(location) + end if dependencies.empty? + + dependencies + end + rescue JSON::ParseException + error! :dependencies_invalid do + block do + text "The" + bold "dependencies" + text "field lists all the dependencies for the application." + end + + snippet "It should be an object, but it's not:", snippet_data + end + end + + def parse_dependency(package : String) : Installer::Dependency + repository, constraint = nil, nil + + @parser.read_object do |key| + case key + when "repository" + repository = parse_dependency_repository + when "constraint" + constraint = parse_dependency_constraint + else + error! :dependency_invalid_key do + snippet "A dependency object has an invalid key:", key + snippet "It is here:", snippet_data + end + end + end + + error! :dependency_missing_repository do + block do + text "A" + bold "dependency object" + text "is missing the" + bold "repository" + text "field:" + end + + snippet snippet_data + end unless repository + + error! :dependency_missing_constraint do + block do + text "A" + bold "dependency object" + text "is missing the" + bold "constraint" + text "field:" + end + + snippet snippet_data + end unless constraint + + Installer::Dependency.new package, repository, constraint + rescue JSON::ParseException + error! :dependency_invalid do + snippet "A dependency must be an object, but it's not:", snippet_data + end + end + + def parse_dependency_repository : String + @parser.read_string + rescue JSON::ParseException + error! :dependency_repository_invalid do + block do + text "The" + bold "repository" + text "field of a depencency must be an string, but it's not:" + end + + snippet snippet_data + end + end + + def parse_dependency_constraint : Installer::Constraint + location = + @parser.location + + raw = + @parser.read_string + + match = + raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) + + constraint = + if match + lower = + Installer::Semver.parse?(match[1]) + + upper = + Installer::Semver.parse?(match[2]) + + Installer::SimpleConstraint.new(lower, upper) if upper && lower + else + match = + raw.match(/(.*?):(\d+\.\d+\.\d+)/) + + if match + version = + Installer::Semver.parse?(match[2]) + + target = + match[1] + + Installer::FixedConstraint.new(version, target) if version + end + end + + error! :dependency_constraint_bad do + block "The constraint of a dependency is either in this format:" + snippet "0.0.0 <= v < 1.0.0" + + block "or a git tag / commit / branch followed by the version:" + snippet "master:0.1.0" + + snippet "I could not find either:", snippet_data(location) + end unless constraint + + constraint + rescue JSON::ParseException + error! :dependency_constraint_invalid do + block do + text "The" + bold "constraint" + text "field of a depencency must be an string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/formatter.cr b/src/mint_json/formatter.cr new file mode 100644 index 000000000..a9238c1ab --- /dev/null +++ b/src/mint_json/formatter.cr @@ -0,0 +1,53 @@ +module Mint + class MintJson + class Parser + def parse_formatter : Formatter::Config + indent_size = 2 + + @parser.read_object do |key| + case key + when "indent-size" + indent_size = parse_formatter_indent_size + else + error! :formatter_invalid_key do + block do + text "The" + bold "formatter" + text "object has an invalid key:" + end + + snippet key + snippet "It is here:", snippet_data + end + end + end + + Formatter::Config.new(indent_size: indent_size) + rescue JSON::ParseException + error! :formatter_invalid do + block do + text "The" + bold "formatter" + text "field should be an object, but it's not:" + end + + snippet snippet_data + end + end + + def parse_formatter_indent_size : Int32 + @parser.read_int.clamp(0, 100).to_i + rescue JSON::ParseException + error! :formatter_indent_size_invalid do + block do + text "The" + bold "indent-size" + text "field of the formatter configuration must be a number, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/mint_version.cr b/src/mint_json/mint_version.cr new file mode 100644 index 000000000..6d3eefcf3 --- /dev/null +++ b/src/mint_json/mint_version.cr @@ -0,0 +1,64 @@ +module Mint + class MintJson + class Parser + def parse_mint_version : Nil + location = + @parser.location + + raw = + @parser.read_string + + match = + raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) + + constraint = + if match + lower = + Installer::Semver.parse?(match[1]) + + upper = + Installer::Semver.parse?(match[2]) + + Installer::SimpleConstraint.new(lower, upper) if upper && lower + end + + error! :mint_version_bad do + block do + text "The" + bold "mint-version" + text "constraint should be in this format:" + end + + snippet "0.0.0 <= v < 1.0.0" + snippet "It is here:", snippet_data(location) + end unless constraint + + resolved = + Installer::Semver.parse(VERSION.rchop("-devel")) + + error! :mint_version_mismatch do + block do + text "The" + bold "mint-version" + text "field does not match this version of Mint." + end + + snippet "I was looking for", constraint.to_s + snippet "but found instead:", VERSION + + snippet "It is here:", snippet_data(location) + end unless resolved < constraint.upper && resolved >= constraint.lower + rescue JSON::ParseException + error! :mint_version_invalid do + block do + text "The" + bold "mint-version" + text "field should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/name.cr b/src/mint_json/name.cr new file mode 100644 index 000000000..4a283d071 --- /dev/null +++ b/src/mint_json/name.cr @@ -0,0 +1,35 @@ +module Mint + class MintJson + class Parser + def parse_name : String + location = + @parser.location + + name = + @parser.read_string + + error! :name_empty do + block do + text "The" + bold "name" + text "field should not be empty:" + end + + snippet snippet_data(location) + end if name.empty? + + name + rescue JSON::ParseException + error! :name_invalid do + block do + text "The" + bold "name" + text "field should be a string, but it's not:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/parser.cr b/src/mint_json/parser.cr new file mode 100644 index 000000000..19d6a9061 --- /dev/null +++ b/src/mint_json/parser.cr @@ -0,0 +1,100 @@ +module Mint + class MintJson + class Parser + include Errorable + + def self.parse(*, contents : String, path : String) : MintJson + new(contents: contents, path: path).parse + rescue exception : JSON::ParseException + Errorable.error :invalid_json do + block do + text "I could not parse the following" + bold "mint.json" + text "file:" + end + + snippet snippet_data( + column_number: exception.location[1], + line_number: exception.location[0], + contents: contents, + path: path) + end + end + + def self.parse(path : String, *, search : Bool = false) : MintJson + file = path + + if search + Errorable.error :mint_json_not_found do + block do + text "I could not find a" + bold "mint.json" + text "file in the path or any of its parent directories:" + end + + snippet path + end unless file = File.find_in_ancestors(path, "mint.json") + end + + parse(contents: File.read(file), path: file) + rescue error : Error + raise error # Propagate our own errors. + rescue exception + Errorable.error :mint_json_invalid do + block do + text "There was a problem trying to open a" + bold "mint.json" + text "file:" + bold path + end + + snippet exception.to_s + end + end + + def initialize(*, @contents : String, @path : String) + @parser = JSON::PullParser.new(@contents) + end + + def self.snippet_data( + *, + column_number : Int32, + line_number : Int32, + contents : String, + path : String + ) + position = + if line_number - 1 == 0 + 0 + else + contents + .lines[0..line_number - 2] + .reduce(0) { |acc, line| acc + line.size + 1 } + end + (column_number - 1) + + Error::SnippetData.new( + to: position + 1, + input: contents, + filename: path, + from: position) + end + + def snippet_data(location : Tuple(Int32, Int32)) + self.class.snippet_data( + column_number: location[1], + line_number: location[0], + contents: @contents, + path: @path) + end + + def snippet_data + snippet_data @parser.location + end + + # This is used for checking directories and files. + def root + File.dirname(@path) + end + end + end +end diff --git a/src/mint_json/root.cr b/src/mint_json/root.cr new file mode 100644 index 000000000..aee1b3883 --- /dev/null +++ b/src/mint_json/root.cr @@ -0,0 +1,62 @@ +module Mint + class MintJson + class Parser + def parse : MintJson + dependencies = [] of Installer::Dependency + formatter = Formatter::Config.new + source_directories = %w[] + test_directories = %w[] + name = "" + + application = + Application.new( + meta: {} of String => String, + orientation: "", + theme_color: "", + css_prefix: "", + display: "", + title: "", + name: "", + head: "", + icon: "") + + @parser.read_object do |key| + case key + when "source-directories" + source_directories = parse_source_directories + when "test-directories" + test_directories = parse_test_directories + when "dependencies" + dependencies = parse_dependencies + when "application" + application = parse_application + when "formatter" + formatter = parse_formatter + when "mint-version" + parse_mint_version + when "name" + name = parse_name + else + error! :root_invalid_key do + snippet "The root object has an invalid key:", key + snippet "It is here:", snippet_data + end + end + end + + MintJson.new( + source_directories: source_directories, + test_directories: test_directories, + dependencies: dependencies, + application: application, + formatter: formatter, + path: @path, + name: name) + rescue JSON::ParseException + error! :root_invalid do + snippet "The root item should be an object, but it's not:", snippet_data + end + end + end + end +end diff --git a/src/mint_json/source_directories.cr b/src/mint_json/source_directories.cr new file mode 100644 index 000000000..ac87340db --- /dev/null +++ b/src/mint_json/source_directories.cr @@ -0,0 +1,84 @@ +module Mint + class MintJson + class Parser + def parse_source_directories : Array(String) + @parser.location.try do |location| + directories = %w[] + + @parser.read_array do + directories << parse_source_directory + end + + error! :source_directories_empty do + block do + text "The" + bold "source-directories" + text "field lists all directories (relative to the mint.json file)" + text "which contain the source files of the application." + end + + block do + text "The" + bold "source-directories" + text "array should not be empty, but it is:" + end + + snippet snippet_data(location) + end if directories.empty? + + directories + end + rescue JSON::ParseException + error! :source_directories_invalid do + block do + text "The" + bold "source-directories" + text "field lists all directories (relative to the mint.json file)" + text "which contain the source files of the application." + end + + block do + text "The" + bold "source-directories" + text "field should be an array, but it's not:" + end + + snippet snippet_data + end + end + + def parse_source_directory : String + location = + @parser.location + + directory = + @parser.read_string + + path = + Path[root, directory] + + error! :source_directory_not_exists do + block do + text "The source directory" + bold directory + text "does not exists:" + end + + snippet snippet_data(location) + end unless Dir.exists?(path) + + directory + rescue JSON::ParseException + error! :source_directory_invalid do + block do + text "All entries in the" + bold "source-directories" + text "array should be string:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/mint_json/test_directories.cr b/src/mint_json/test_directories.cr new file mode 100644 index 000000000..7dc51e2dd --- /dev/null +++ b/src/mint_json/test_directories.cr @@ -0,0 +1,84 @@ +module Mint + class MintJson + class Parser + def parse_test_directories : Array(String) + @parser.location.try do |location| + directories = %w[] + + @parser.read_array do + directories << parse_test_directory + end + + error! :test_directories_empty do + block do + text "The" + bold "test-directories" + text "field lists all directories (relative to the mint.json file)" + text "which contain the test files of the application." + end + + block do + text "The" + bold "test-directories" + text "array should not be empty, but it is:" + end + + snippet snippet_data(location) + end if directories.empty? + + directories + end + rescue JSON::ParseException + error! :test_directories_invalid do + block do + text "The" + bold "test-directories" + text "field lists all directories (relative to the mint.json file)" + text "which contain the test files of the application." + end + + block do + text "The" + bold "test-directories" + text "field should be an array, but it's not:" + end + + snippet snippet_data + end + end + + def parse_test_directory : String + location = + @parser.location + + directory = + @parser.read_string + + path = + Path[root, directory] + + error! :test_directory_not_exists do + block do + text "The test directory" + bold directory + text "does not exists:" + end + + snippet snippet_data(location) + end unless Dir.exists?(path) + + directory + rescue JSON::ParseException + error! :test_directory_invalid do + block do + text "All entries in the" + bold "test-directories" + text "array should be string:" + end + + snippet snippet_data + end + end + end + end +end diff --git a/src/parser/top_level.cr b/src/parser/top_level.cr index 280bb9f64..5fd64c210 100644 --- a/src/parser/top_level.cr +++ b/src/parser/top_level.cr @@ -4,12 +4,20 @@ module Mint parse ::File.read(file), file end - def self.parse(contents, file) : Ast + def self.parse?(contents, file) : Ast | Error parser = new(contents, file) parser.parse parser.eof! - parser.errors.first?.try { |error| raise error } - parser.ast + parser.errors.first? || parser.ast + end + + def self.parse(contents, file) : Ast + case result = parse?(contents, file) + in Error + raise result + in Ast + result + end end def parse : Nil diff --git a/src/reactor.cr b/src/reactor.cr index 707346798..7d82fc524 100644 --- a/src/reactor.cr +++ b/src/reactor.cr @@ -19,46 +19,35 @@ module Mint @sockets = [] of HTTP::WebSocket def initialize(*, @host, @port, @format, @reload) - # Initialize the workspace from the current working directory. We don't - # check everything to speed things up so only the hot path is checked. - workspace = Workspace.current - workspace.check_everything = false - workspace.check_env = true - workspace.format = format? - - # Check if we have dependencies installed. - workspace.json.check_dependencies! - - # On any change we update the result and notify all clients to - # reload the application. - workspace.on "change" do |result| - @files = - case result - in Ast - Bundler.new( - artifacts: workspace.type_checker.artifacts, - json: workspace.json, - config: Bundler::Config.new( - generate_manifest: false, - include_program: true, - hash_assets: false, - runtime_path: nil, - live_reload: true, - skip_icons: false, - optimize: false, - relative: false, - test: nil), - ).bundle - in Error - error(result) - end - - @sockets.each(&.send("reload")) - end + FileWorkspace.new( + path: Path[Dir.current, "mint.json"].to_s, + check: Check::Environment, + include_tests: false, + format: format?, + listener: ->(result : TypeChecker | Error) do + @files = + case result + in TypeChecker + Bundler.new( + artifacts: result.artifacts, + json: MintJson.current, + config: Bundler::Config.new( + generate_manifest: false, + include_program: true, + hash_assets: false, + runtime_path: nil, + live_reload: true, + skip_icons: false, + optimize: false, + relative: false, + test: nil), + ).bundle + in Error + error(result) + end - # Do the initial parsing and type checking and start wathing for changes. - workspace.update_cache - workspace.watch + @sockets.each(&.send("reload")) if reload? + end) # The websocket handle saves the sockets when they connect and # removes them when they disconnect. diff --git a/src/render/terminal.cr b/src/render/terminal.cr index b64e9c8ea..f7785cbe4 100644 --- a/src/render/terminal.cr +++ b/src/render/terminal.cr @@ -199,8 +199,13 @@ module Mint print "\n\n" end - def snippet(node : Ast::Node) - print TerminalSnippet.render(node.file.contents, node.file.path, node.from, node.to, width: @width).indent + def snippet(value : Error::SnippetData) + print TerminalSnippet.render( + filename: value.filename, + input: value.input, + from: value.from, + to: value.to, + width: @width).indent print "\n\n" end diff --git a/src/sandbox_server.cr b/src/sandbox_server.cr deleted file mode 100644 index 2f07296df..000000000 --- a/src/sandbox_server.cr +++ /dev/null @@ -1,201 +0,0 @@ -module Mint - class SandboxServer - # Represents a source file. - struct File - include JSON::Serializable - - getter contents : String - getter path : String - - def initialize(@contents, @path) - end - end - - # Represents a project. - struct Project - include JSON::Serializable - - @[JSON::Field(key: "activeFile")] - getter active_file : String - getter files : Array(File) - - def initialize(@files, @active_file) - end - end - - # A handler for allowing cross origin requests. - class CORS - include HTTP::Handler - - def call(context) - context.response.headers["Access-Control-Max-Age"] = 1.day.total_seconds.to_i.to_s - context.response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH" - context.response.headers["Access-Control-Allow-Headers"] = "Content-Type" - context.response.headers["Access-Control-Allow-Credentials"] = "true" - context.response.headers["Access-Control-Allow-Origin"] = "*" - - if context.request.method.upcase == "OPTIONS" - context.response.content_type = "text/html; charset=utf-8" - context.response.status = :ok - else - call_next context - end - end - end - - class Ide - class Message - include JSON::Serializable - - property payload : JSON::Any - property type : String - end - - @socket : HTTP::WebSocket - @directory : Path - @id : String - - def initialize(@socket) - @socket.on_message(&->handle_message(String)) - @socket.on_close { close } - - @id = - Random::Secure.hex - - @directory = - Path[Dir.tempdir, Random::Secure.hex] - .tap(&->FileUtils.mkdir_p(Path)) - - ::File.write(Path[@directory, "mint.json"], { - "source-directories" => ["."], - }.to_json) - - @workspace = Workspace.new(@directory.to_s) - @workspace.check_everything = false - @workspace.on "change" do |result| - bundle = - case result - in Ast - Bundler.new( - artifacts: @workspace.type_checker.artifacts, - json: @workspace.json, - config: Bundler::Config.new( - generate_manifest: false, - include_program: true, - hash_assets: false, - runtime_path: nil, - live_reload: false, - skip_icons: false, - relative: false, - optimize: true, - test: nil), - ).bundle - in Error - {"index.html" => ->{ result.to_html }} - end - - io = - IO::Memory.new - - Compress::Zip::Writer.open(io) do |zip| - bundle.each do |path, contents| - zip.add(path, contents.call) - end - end - - io.rewind - HTTP::Client.post("https://#{@id}.sandbox.mint-lang.com/", body: io) - @socket.send({type: "reload", url: "https://#{@id}.sandbox.mint-lang.com/"}.to_json) - end - end - - def handle_message(raw : String) - message = - Message.from_json(raw) - - case message.type - when "update" - Project.from_json(message.payload.to_json).tap do |project| - project.files.each do |file| - ::File.write(Path[@directory, file.path], file.contents) - end - @workspace.reset_cache - highlight(project.active_file) - end - end - rescue - end - - def highlight(path) - tokens = - begin - ast = - Parser.parse(Path[@directory, path].to_s) - - SemanticTokenizer.new.tap(&.tokenize(ast)).tokens.map do |token| - type = - case token.type - in SemanticTokenizer::TokenType::TypeParameter - :type_parameter - in SemanticTokenizer::TokenType::Type - :type - in SemanticTokenizer::TokenType::Namespace - :namespace - in SemanticTokenizer::TokenType::Property - :property - in SemanticTokenizer::TokenType::Keyword - :keyword - in SemanticTokenizer::TokenType::Comment - :comment - in SemanticTokenizer::TokenType::Variable - :variable - in SemanticTokenizer::TokenType::Operator - :operator - in SemanticTokenizer::TokenType::String - :string - in SemanticTokenizer::TokenType::Number - :number - in SemanticTokenizer::TokenType::Regexp - :regexp - end - - { - from: token.from, - to: token.to, - type: type, - } - end - rescue e - [] of String - end - - @socket.send({type: "highlight", tokens: tokens}.to_json) - end - - def close - FileUtils.rm_rf(@directory) - end - end - - def initialize(host, port) - server = - HTTP::Server.new( - [ - CORS.new, - HTTP::WebSocketHandler.new { |socket| Ide.new(socket) }, - ]) - - Server.run( - server: server, - port: port, - host: host - ) do |resolved_host, resolved_port| - terminal.puts "#{COG} Sandbox server started on http://#{resolved_host}:#{resolved_port}/" - end - end - - def terminal - Render::Terminal::STDOUT - end - end -end diff --git a/src/static_documentation_generator.cr b/src/static_documentation_generator.cr index 7cc27235b..a86f369e6 100644 --- a/src/static_documentation_generator.cr +++ b/src/static_documentation_generator.cr @@ -266,6 +266,7 @@ module Mint HtmlBuilder.build(optimize: true) do |builder| html do head do + meta charset: "utf-8" link rel: "stylesheet", href: "../style.css" title do diff --git a/src/style_builder.cr b/src/style_builder.cr index e8a078aa5..207694a43 100644 --- a/src/style_builder.cr +++ b/src/style_builder.cr @@ -207,7 +207,7 @@ module Mint getter selectors, property_pool, name_pool, style_pool, variables, ifs getter cases - def initialize(@css_prefix : String? = nil, @optimize : Bool = false) + def initialize(@css_prefix : String = "", @optimize : Bool = false) # Three name pools so there would be no clashes, # which also good for optimizations. @style_pool = StylePool.new(optimize: @optimize) diff --git a/src/test_runner.cr b/src/test_runner.cr index 3a255030f..69f677c2a 100644 --- a/src/test_runner.cr +++ b/src/test_runner.cr @@ -21,46 +21,48 @@ module Mint @browser = Browser.new(flags.browser.downcase) @watch = flags.watch || flags.manual - workspace = Workspace.current - workspace.test_path = arguments.test || "*" - workspace.check_env = true - - workspace.on "change" do |result| - case result - in Ast - @files = - Bundler.new( - artifacts: workspace.type_checker.artifacts, - json: MintJson.new, - config: Bundler::Config.new( - runtime_path: flags.runtime, - generate_manifest: false, - include_program: false, - live_reload: false, - hash_assets: true, - skip_icons: true, - optimize: false, - relative: false, - test: { - url: "ws://#{flags.browser_host}:#{flags.browser_port}/", - id: "", - }) - ).bundle - - unless flags.manual - # Stop and cleanup previous browser session - @browser.cleanup - - terminal.puts "#{COG} Starting browser..." unless @watch - - spawn do - @browser.open("http://#{flags.browser_host}:#{flags.browser_port}") + FileWorkspace.new( + path: Path[Dir.current, "mint.json"].to_s, + check: Check::Environment, + include_tests: true, + format: false, + listener: ->(result : TypeChecker | Error) do + case result + in TypeChecker + @files = + Bundler.new( + artifacts: result.artifacts, + json: MintJson.current, + config: Bundler::Config.new( + runtime_path: flags.runtime, + generate_manifest: false, + include_program: false, + live_reload: false, + hash_assets: true, + skip_icons: true, + optimize: false, + relative: false, + test: { + url: "ws://#{flags.browser_host}:#{flags.browser_port}/", + glob: arguments.test || "**/*", + id: "", + }) + ).bundle + + unless flags.manual + # Stop and cleanup previous browser session + @browser.cleanup + + terminal.puts "#{COG} Starting browser..." unless @watch + + spawn do + @browser.open("http://#{flags.browser_host}:#{flags.browser_port}") + end end + in Error + terminal.puts(result.to_terminal) end - in Error - terminal.puts(result.to_terminal) - end - end + end) websocket_handler = HTTP::WebSocketHandler.new do |socket| @@ -119,14 +121,8 @@ module Mint ) do |host, port| terminal.puts "#{COG} Test server started: http://#{host}:#{port}/" - # Trigger first session - workspace.update_cache - if @watch terminal.puts "#{COG} Waiting for a browser to connect..." - - # Watch for changes... - workspace.watch end end end diff --git a/src/type_checker.cr b/src/type_checker.cr index b7ba2f608..5f91a29d5 100644 --- a/src/type_checker.cr +++ b/src/type_checker.cr @@ -59,7 +59,7 @@ module Mint HTML, ] of Checkable - getter records, artifacts, formatter, web_components + getter records, artifacts, formatter getter? check_everything property? checking = true @@ -86,7 +86,7 @@ module Mint @block_stack.last? end - def initialize(ast : Ast, @check_env = true, @web_components = [] of String, @check_everything = true) + def initialize(ast : Ast, @check_env = true, @check_everything = true) ast.normalize @languages = ast.unified_locales.map(&.language) diff --git a/src/type_checkers/access.cr b/src/type_checkers/access.cr index e2ef4fed6..beefd0e70 100644 --- a/src/type_checkers/access.cr +++ b/src/type_checkers/access.cr @@ -61,13 +61,15 @@ module Mint variable.value end - # possibilities.each do |possibility| + # Type variant if possibility if parent = ast.type_definitions.find(&.name.value.==(possibility)) case fields = parent.fields when Array(Ast::TypeVariant) if option = fields.find(&.value.value.==(node.field.value)) variables[node] = {option, parent} + variables[node.field] = {option, parent} + variables[node.expression] = {parent, parent} # puts({Debugger.dbg(parent), Debugger.dbg(node)}) resolve(parent) return to_function_type(option, parent) @@ -75,6 +77,7 @@ module Mint end end + # Constant if entity = scope.resolve(possibility, node).try(&.node) if entity && possibility[0].ascii_uppercase? if target_node = scope.resolve(node.field.value, entity).try(&.node) @@ -142,7 +145,7 @@ module Mint resolve lookups[node.field][0] else - record_field_lookup[node.field] = new_target.name + record_field_lookup[node.field] = target.name end new_target diff --git a/src/type_checkers/top_level.cr b/src/type_checkers/top_level.cr index d579f6199..8f092a799 100644 --- a/src/type_checkers/top_level.cr +++ b/src/type_checkers/top_level.cr @@ -32,12 +32,6 @@ module Mint end end - web_components.each do |component| - node.components.find(&.name.value.==(component)).try do |item| - resolve item - end - end - # Resolve routes resolve node.routes resolve node.suites diff --git a/src/utils/cors.cr b/src/utils/cors.cr new file mode 100644 index 000000000..8353bb631 --- /dev/null +++ b/src/utils/cors.cr @@ -0,0 +1,20 @@ +module Mint + class CORS + include HTTP::Handler + + def call(context) + context.response.headers["Access-Control-Max-Age"] = 1.day.total_seconds.to_i.to_s + context.response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH" + context.response.headers["Access-Control-Allow-Headers"] = "Content-Type" + context.response.headers["Access-Control-Allow-Credentials"] = "true" + context.response.headers["Access-Control-Allow-Origin"] = "*" + + if context.request.method.upcase == "OPTIONS" + context.response.content_type = "text/html; charset=utf-8" + context.response.status = :ok + else + call_next context + end + end + end +end diff --git a/src/utils/source_files.cr b/src/utils/source_files.cr index 4fca210d2..56c74ef6c 100644 --- a/src/utils/source_files.cr +++ b/src/utils/source_files.cr @@ -2,50 +2,46 @@ module Mint module SourceFiles extend self - def glob_pattern(*dirs : Path | String) - Path[*dirs, "**", "*.mint"].to_posix.to_s + def globs(jsons : Array(MintJson), *, include_tests = false) : Array(String) + jsons.flat_map { |json| globs(json, include_tests: include_tests) } end - def tests - MintJson - .parse_current - .test_directories - .map { |dir| glob_pattern(dir) } + def globs(json : MintJson, *, include_tests = false) : Array(String) + if include_tests + json.source_directories | json.test_directories + else + json.source_directories + end.map { |dir| glob_pattern(File.dirname(json.path), dir) } end - def current - MintJson - .parse_current - .source_directories - .map { |dir| glob_pattern(dir) } + def everything(json : MintJson, *, include_tests = false) : Array(String) + packages(json, include_self: true) + .flat_map { |item| globs(item, include_tests: include_tests) + [item.path] } + .push(Path[File.dirname(json.path), ".env"].to_s) end - def each_package(&) - pattern = - Path[".", ".mint", "packages", "**", "mint.json"] - - Dir.glob(pattern).each do |file| - yield MintJson.new(File.read(file), File.dirname(file), file) + def packages(json : MintJson, *, include_self = false) : Array(MintJson) + (include_self ? [json] : [] of MintJson).tap do |jsons| + each_package(json) do |package_json| + jsons << package_json + end end end - def packages - ([] of MintJson).tap do |package_definitions| - each_package { |json| package_definitions << json } + private def each_package(json : MintJson, &) + pattern = + Path[ + File.dirname(json.path), + ".", ".mint", "packages", "**", "mint.json", + ] + + Dir.glob(pattern).each do |file| + yield MintJson.parse(file) end end - def all - current.dup.tap do |package_dirs| - each_package do |json| - dirs = - json.source_directories.map do |dir| - glob_pattern(json.root, dir) - end - - package_dirs.concat dirs - end - end + private def glob_pattern(*dirs : Path | String) + Path[*dirs, "**", "*.mint"].to_posix.to_s end end end diff --git a/src/utils/terminal_snippet.cr b/src/utils/terminal_snippet.cr index 63183fcf2..d90abedf6 100644 --- a/src/utils/terminal_snippet.cr +++ b/src/utils/terminal_snippet.cr @@ -58,6 +58,13 @@ module Mint extend self def render(input : String, filename : String, from : Int64, to : Int64, padding = 4, width = 80) + path = + if (full = Path[filename]).absolute? + if full.expand.to_s.starts_with?(Dir.current.to_s) + full.relative_to?(Dir.current).try(&.to_s) + end + end || filename + # Transform each line into a record for further use. lines = input.lines.reduce({[] of Line, 0}) do |memo, raw| @@ -170,7 +177,7 @@ module Mint input[0..from].lines.last.size title = - "#{filename}:#{line}:#{column}" + "#{path}:#{line}:#{column}" gutter_divider = " " * gutter_width diff --git a/src/utils/watcher.cr b/src/utils/watcher.cr index d2045a49b..f3a9de9f6 100644 --- a/src/utils/watcher.cr +++ b/src/utils/watcher.cr @@ -1,38 +1,48 @@ module Mint + # A class for detecting changes to a set of sepcific files (sepcifically + # `*.mint`, `.env` and `mint.json`). class Watcher - def self.watch(pattern, &) - new(pattern).watch { |files| yield files } - end - - setter pattern + @patterns : Array(String) = [] of String + @state = Set(Tuple(String, Time)).new - def initialize(@pattern : Array(String)) - @state = Set(Tuple(String, Time)).new - detect { } + def initialize(&@listener : Proc(Array(String), Symbol, Nil)) end - def detect(&) - current = Set(Tuple(String, Time)).new + def patterns=(@patterns : Array(String)) + @state.clear + scan :reset + end - Dir.glob(@pattern).each do |file| - path = - Path[file].normalize.to_s + def scan(reason : Symbol) + # We sort so the files can be processed in the same order every time. + current = + Dir.glob(@patterns) + .map(&->info(String)) + .sort_by!(&.last) + .to_set - current.add({path, File.info(path).modification_time}) - end + # Symmetric Difference: returns a new set (self - other) | (other - self). + diff = @state ^ current - yield @state ^ current + # Only notify if there is actual changes. + @listener.call(diff.map(&.first).uniq!, reason) unless diff.empty? @state = current end - def watch(&) - loop do - detect do |diff| - yield diff.map(&.[0]).uniq! unless diff.empty? - end + def info(file : String) + path = + Path[file].normalize.expand.to_s + + {path, File.info(path).modification_time} + end - sleep 0.5 + def watch + spawn do + loop do + sleep 0.5 + scan :modified + end end end end diff --git a/src/workspace.cr b/src/workspace.cr index b9c7ddb30..9a6e153ac 100644 --- a/src/workspace.cr +++ b/src/workspace.cr @@ -1,288 +1,186 @@ module Mint - # A workspace represents a mint project where the root is the directory - # containing the `mint.json` file. + @[Flags] + enum Check + Environment + Unreachable + end + + # A workspace represents a Mint project in the file system. + # - It provides up to date, type checked artifacts which can be used in other + # places (bundler, test runner, development server, etc...). + # - It watches the appropriate files and recompiles when they change. + # - It does a compilation on initialization, so artifacts are ready to be + # used. # - # The workspace provides: - # - an up to date AST of the project - # - provides packages and information - # - emits events for changes - class Workspace - @@workspaces = {} of String => Workspace + class FileWorkspace + # The current artifacts of the program or the current error. + getter result : TypeChecker | Error = Error.new(:unitialized_workspace) - class_getter workspaces + # Stores the AST (or error) of the file at the given path. + @cache : Hash(String, Ast | Error) = {} of String => Ast | Error - def self.from_file(path : String) : Workspace - new(root_from_file(path)) - end + # The listener to call when a new result is ready. + @listener : Proc(TypeChecker | Error, Nil) | Nil - def self.current : Workspace - new(Dir.current) + def initialize( + *, + @listener : Proc(TypeChecker | Error, Nil) | Nil, + @include_tests : Bool, + @format : Bool, + @check : Check, + @path : String + ) + (@watcher = Watcher.new(&->update(Array(String), Symbol))) + .tap { reset } + .watch end - def self.root_from_file(path : String) : String - root = File.dirname(path) - - loop do - raise "Invalid workspace!" if root == "." || root == "/" - - if File.exists?(Path[root, "mint.json"]) - break - else - root = File.dirname(root) - end - end - - root + def update(contents : String, path : String) : Nil + @cache[path] = Parser.parse?(contents, path) end - def self.[](path) - root = root_from_file(path) - - @@workspaces[root] ||= - Workspace.new(root) - .tap(&.update_cache) - .tap(&.watch) + def delete(path : String) : Nil + @cache.delete(path) end - alias ChangeProc = Proc(Ast | Error, Nil) - - @event_handlers = {} of String => Array(ChangeProc) - @cache = {} of String => Ast - @env_watcher : Watcher? - @pattern = %w[] - - getter type_checker : TypeChecker - getter cache : Hash(String, Ast) - getter formatter : Formatter - getter json : MintJson - getter error : Error? - getter root : String - - property? check_everything : Bool = true - property? check_env : Bool = false - property? format : Bool = false - getter test_path : String? - - def test_path=(value) - @test_path = value - update_patterns + def artifacts : TypeChecker::Artifacts | Error + map_error(result, &.artifacts) end - def initialize(@root : String) - json_path = - Path[@root, "mint.json"].to_s - - @json = - FileUtils.cd(@root) do - MintJson.from_file(json_path) - end - - @formatter = - Mint::Formatter.new(json.formatter_config) - - @json_watcher = - Watcher.new([json_path]) - - @source_watcher = - Watcher.new(all_files_pattern) - - @env_watcher = - Env.env.try do |file| - Watcher.new([file]) - end - - @type_checker = - TypeChecker.new(Ast.new) - end - - def on(event, &handler : ChangeProc) - @event_handlers[event] ||= [] of ChangeProc - @event_handlers[event] << handler - end - - def packages : Array(Workspace) - pattern = - Path[root, ".mint", "packages", "**", "mint.json"] - - Dir.glob(pattern).map do |file| - Workspace.from_file(file) - end + def ast(path : String) : Ast | Error | Nil + @cache[path]? end - def ast - result = - @cache.values.reduce(Ast.new) { |memo, item| memo.merge item } - - result.merge(Core.ast) if @json.name != "core" - result.normalize + def ast : Ast | Error + map_error(artifacts, &.ast) end - def []?(file) - @cache[normalize_path(file)]? + def nodes_at_cursor( + *, + column : Int64, + path : String, + line : Int64 + ) : Array(Ast::Node) | Error + map_error(ast, &.nodes_at_cursor( + line: line, column: column, path: path)) end - def [](file) - @cache[normalize_path(file)] + def nodes_at_path(path : String) + map_error(ast, &.nodes_at_path(path)) end - protected def []=(file, value) - @cache[normalize_path(file)] = value + def format(node : Ast::Node | Nil) : String | Nil + Formatter.new.format!(node) end - def initialize_cache(&) - files = self.files - files.each_with_index do |file, index| - self[file] ||= Parser.parse(file) - - yield file, index, files.size + def format(path : String) : String | Error | Nil + case item = ast(path) + in Ast + Formatter.new.format(item) + in Error, Nil + item end end - def watch - spawn do - # Watches all the `*.mint` files - @source_watcher.watch do |files| - # Remove the changed files from the cache - files.each { |file| @cache.delete(file) } - - # Update the cache - update_cache - end - end - - spawn do - # Watches the `mint.json` file - @json_watcher.watch do - # We need to update the patterns because: - # 1. packages could have been added or removed - # 2. source directories could have been added or removed - update_patterns - - # Reset the cache, this will cause a full recompilation, in the - # future this could be changed to only remove files from the cache - # that have been changed. - reset_cache - end - end - - spawn do - @env_watcher.try &.watch do - Env.load do - update_cache - end - end - end - end - - def files - Dir.glob(all_files_pattern) + def reset + @cache.clear + @watcher.patterns = + SourceFiles.everything( + MintJson.parse(@path, search: true), + include_tests: @include_tests) + rescue error : Error + set(error) end - def files_pattern : Array(String) - files = - json - .source_directories - .map { |dir| Path[root, dir, "**", "*.mint"].to_posix.to_s } - - if path = test_path - files + if path == "*" - json - .test_directories - .map { |dir| Path[root, dir, "**", "*.mint"].to_posix.to_s } + def check + Logger.log "Type Checking" do + if error = @cache.values.select(Error).first? + error else - [path] + ast = + @cache + .values + .select(Ast) + .reduce(Ast.new) { |memo, item| memo.merge item } + .tap do |item| + # Only merge the core if it's not the core (if it has `Maybe` + # defined then it's the core). This is so the language server + # works with the core files. + unless item.type_definitions.index(&.name.==("Maybe")) + item.merge(Core.ast) + end + end + .normalize + + TypeChecker.new( + check_everything: @check.unreachable?, + check_env: @check.environment?, + ast: ast + ).tap(&.check) end - else - files end + rescue error : Error + error end - def update_cache + def update(files : Array(String), reason : Symbol) + actions = [] of Symbol + Logger.log "Parsing files" do files.each do |file| - path = - File.realpath(file) - - self[file] ||= process(File.read(path), path) + if File.extname(file) == ".mint" + if File.exists?(file) + contents = File.read(file) + update(contents, file) + + if @format + case ast = ast(file) + when Ast + formatted = + Formatter.new.format(ast) + + if formatted != contents + File.write(file, formatted) + end + end + end + else + delete(file) + end + + actions << :compile + else + # We need to do a reset because: + # 1. packages could have changed + # 2. source directories could have changed + # 3. variables in the .env file cloud have changed + case File.basename(file) + when "mint.json", ".env" + actions << :reset + end + end end end - Logger.log "Type Checking" { check! } - - @error = nil - - call "change", ast - rescue error : Error - @error = error - - call "change", error - end - - def format(file) - Formatter - .new(json.formatter_config) - .format(self[file]) - end - - def update(contents, file) - self[file] = process(contents, file) - @error = nil - - call "change", ast - rescue error : Error - @error = error - - call "change", error - end - - private def normalize_path(file) - Path[file].normalize.to_s - end - - private def process(contents, file) - ast = - Parser.parse(contents, File.realpath(file)) - - if format? - formatted = - Formatter - .new(json.formatter_config) - .format(ast) - - if formatted != File.read(file) - File.write(file, formatted) - end + if actions.includes?(:reset) && reason == :modified + reset + else + set(check) end - - ast end - private def check! - @type_checker = - Mint::TypeChecker.new( - check_everything: check_everything?, - check_env: check_env?, - ast: ast - ).tap(&.check) + private def set(value : TypeChecker | Error) : Nil + @result = value + @listener.try(&.call(value)) end - private def call(event, arg) - @event_handlers[event]?.try(&.each(&.call(arg))) - end - - def reset_cache - @cache = {} of String => Ast - update_cache - end - - private def update_patterns - @source_watcher.pattern = all_files_pattern - end - - private def all_files_pattern : Array(String) - packages - .flat_map(&.files_pattern) - .concat(files_pattern) + private def map_error(item : T | Error, & : T -> R) : R | Error forall T, R + case item + in Error + item + in T + yield item + end end end end diff --git a/test.cr b/test.cr new file mode 100644 index 000000000..e69de29bb