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