From 23eeee54e36b23e886bcab8d93ad278758e1d210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Tue, 17 Sep 2024 14:37:23 +0200 Subject: [PATCH 01/10] Save --- spec/static_documentation_generators/module | 1 + src/all.cr | 1 + src/cli.cr | 2 +- src/{commands => }/command.cr | 0 src/commands/{ => tool}/clean.cr | 0 src/commands/{ => tool}/highlight.cr | 0 src/commands/{ => tool}/loc.cr | 0 src/commands/{ => tool}/ls.cr | 0 src/commands/{ => tool}/sandbox_server.cr | 0 src/ls/apply_edit.cr | 12 ++ src/ls/did_change.cr | 2 +- src/ls/did_open.cr | 17 ++ src/ls/sandbox_compile.cr | 48 ++++++ src/ls/semantic_tokens.cr | 2 +- src/ls/server.cr | 47 +++-- src/ls/websocket_server.cr | 46 +++++ src/lsp/protocol/create_file.cr | 22 +++ src/lsp/protocol/create_file_options.cr | 11 ++ .../protocol/did_open_text_document_params.cr | 9 + ...onal_versioned_text_document_identifier.cr | 16 ++ src/lsp/protocol/request_message.cr | 2 +- src/lsp/protocol/text_document_edit.cr | 11 ++ src/lsp/protocol/text_document_item.cr | 22 +++ src/lsp/protocol/workspace_edit.cr | 11 +- src/lsp/server.cr | 75 ++++---- src/parser/top_level.cr | 14 +- src/reactor.cr | 63 ++++--- src/sandbox_server.cr | 161 +----------------- src/static_documentation_generator.cr | 1 + src/utils/watcher.cr | 4 +- src/workspace.cr | 16 +- src/workspace_2.cr | 139 +++++++++++++++ test.cr | 0 33 files changed, 511 insertions(+), 244 deletions(-) rename src/{commands => }/command.cr (100%) rename src/commands/{ => tool}/clean.cr (100%) rename src/commands/{ => tool}/highlight.cr (100%) rename src/commands/{ => tool}/loc.cr (100%) rename src/commands/{ => tool}/ls.cr (100%) rename src/commands/{ => tool}/sandbox_server.cr (100%) create mode 100644 src/ls/apply_edit.cr create mode 100644 src/ls/did_open.cr create mode 100644 src/ls/sandbox_compile.cr create mode 100644 src/ls/websocket_server.cr create mode 100644 src/lsp/protocol/create_file.cr create mode 100644 src/lsp/protocol/create_file_options.cr create mode 100644 src/lsp/protocol/did_open_text_document_params.cr create mode 100644 src/lsp/protocol/optional_versioned_text_document_identifier.cr create mode 100644 src/lsp/protocol/text_document_edit.cr create mode 100644 src/lsp/protocol/text_document_item.cr create mode 100644 src/workspace_2.cr create mode 100644 test.cr 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/src/all.cr b/src/all.cr index e6388ff06..91bf38683 100644 --- a/src/all.cr +++ b/src/all.cr @@ -74,6 +74,7 @@ require "./reactor" require "./sandbox_server" require "./cli" require "./workspace" +require "./workspace_2" require "./debugger" require "./bundler" require "./artifact_cleaner" diff --git a/src/cli.cr b/src/cli.cr index d2ebd5aba..941d55c85 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -1,5 +1,5 @@ require "admiral" -require "./commands/command" +require "./command" require "./commands/**" module Mint diff --git a/src/commands/command.cr b/src/command.cr similarity index 100% rename from src/commands/command.cr rename to src/command.cr 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 100% rename from src/commands/loc.cr rename to src/commands/tool/loc.cr diff --git a/src/commands/ls.cr b/src/commands/tool/ls.cr similarity index 100% rename from src/commands/ls.cr rename to src/commands/tool/ls.cr diff --git a/src/commands/sandbox_server.cr b/src/commands/tool/sandbox_server.cr similarity index 100% rename from src/commands/sandbox_server.cr rename to src/commands/tool/sandbox_server.cr diff --git a/src/ls/apply_edit.cr b/src/ls/apply_edit.cr new file mode 100644 index 000000000..1a88c4e1c --- /dev/null +++ b/src/ls/apply_edit.cr @@ -0,0 +1,12 @@ +module Mint + module LS + class ApplyEdit < LSP::NotificationMessage + property params : LSP::ApplyWorkspaceEditParams + + def execute(server : Server) + puts params.edit.changes + puts params.edit.document_changes + end + end + end +end diff --git a/src/ls/did_change.cr b/src/ls/did_change.cr index 60d7ca73b..91dc32add 100644 --- a/src/ls/did_change.cr +++ b/src/ls/did_change.cr @@ -8,7 +8,7 @@ module Mint URI.parse(params.text_document.uri) workspace = - Workspace[uri.path.to_s] + server.workspace(uri) workspace.update(params.content_changes.first.text, uri.path) end diff --git a/src/ls/did_open.cr b/src/ls/did_open.cr new file mode 100644 index 000000000..d34fc4c51 --- /dev/null +++ b/src/ls/did_open.cr @@ -0,0 +1,17 @@ +module Mint + module LS + class DidOpen < LSP::NotificationMessage + property params : LSP::DidOpenTextDocumentParams + + def execute(server) + uri = + URI.parse(params.text_document.uri) + + workspace = + server.workspace(uri) + + workspace.update(params.text_document.text, uri.path) + end + end + end +end diff --git a/src/ls/sandbox_compile.cr b/src/ls/sandbox_compile.cr new file mode 100644 index 000000000..805843d4d --- /dev/null +++ b/src/ls/sandbox_compile.cr @@ -0,0 +1,48 @@ +module Mint + module LS + # This is a Mint only LSP request to compile the workspace as a sandbox. + class SandboxCompile < LSP::RequestMessage + def execute(server : Server) + workspace = + server.workspace("") + + result = + workspace.update_cache + + 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) + {url: "https://#{@id}.sandbox.mint-lang.com/"} + end + end + end +end diff --git a/src/ls/semantic_tokens.cr b/src/ls/semantic_tokens.cr index d71ca8bba..59e3ea8a2 100644 --- a/src/ls/semantic_tokens.cr +++ b/src/ls/semantic_tokens.cr @@ -9,7 +9,7 @@ module Mint URI.parse(params.text_document.uri) ast = - Workspace[uri.path.to_s][uri.path.to_s] + server.workspace(uri)[uri.path.to_s] # This is used later on to convert the line/column of each token file = diff --git a/src/ls/server.cr b/src/ls/server.cr index 4c17c023d..7e9fb4515 100644 --- a/src/ls/server.cr +++ b/src/ls/server.cr @@ -1,21 +1,30 @@ 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/willSaveWaitUntil" => WillSaveWaitUntil, + "textDocument/semanticTokens/full" => SemanticTokens, + "textDocument/foldingRange" => FoldingRange, + "textDocument/formatting" => Formatting, + "textDocument/completion" => Completion, + "textDocument/codeAction" => CodeAction, + "textDocument/definition" => Definition, + "textDocument/didChange" => DidChange, + "textDocument/didOpen" => DidOpen, + "textDocument/hover" => Hover, + + # Workspace related methods + "workspace/applyEdit" => ApplyEdit, + + # Mint specific methods + "mint/sandboxCompile" => SandboxCompile, + } property params : LSP::InitializeParams? = nil @@ -46,6 +55,14 @@ module Mint nodes_at_cursor(params.text_document.path, params.range.start) end + def workspace(path : String) + Workspace[path] + end + + def workspace(uri : URI) + Workspace[uri.path.to_s] + end + def nodes_at_path(path : String) Mint::Workspace[path] .ast diff --git a/src/ls/websocket_server.cr b/src/ls/websocket_server.cr new file mode 100644 index 000000000..40f993084 --- /dev/null +++ b/src/ls/websocket_server.cr @@ -0,0 +1,46 @@ +module Mint + module LS + # A server to use the LSP over websockets. + class WebSocketServer < Server + getter directory : Path + + def initialize(@socket : HTTP::WebSocket) + @id = Random::Secure.hex + + # We need these for compability with the server. + @out = IO::Memory.new + @in = IO::Memory.new + + # The directory for the workspace. + # TODO: Remove this when we have an in memory only workspace... + @directory = + Path[Dir.tempdir, @id].tap do |path| + FileUtils.mkdir_p(path) + + File.write(Path[path, "mint.json"], { + "source-directories" => ["."], + }.to_json) + end + + # The workspace to use. + @workspace = Workspace.new(@directory.to_s) + @workspace.presist_on_update = true + + @socket.on_message { |message| process(message) } + @socket.on_close { FileUtils.rm_rf(directory) } + end + + def workspace(path : String) + @workspace + end + + def workspace(uri : URI) + @workspace + end + + def send(content : String) + @socket.send(content) + end + 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..40e4c0e75 --- /dev/null +++ b/src/lsp/protocol/create_file_options.cr @@ -0,0 +1,11 @@ +module LSP + struct CreateFileOptions + include JSON::Serializable + + # Overwrite existing file. Overwrite wins over `ignoreIfExists` + property overwrite : Bool? + + # Ignore if exists. + property ignoreIfExists : 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..64abb1fa9 --- /dev/null +++ b/src/lsp/protocol/text_document_edit.cr @@ -0,0 +1,11 @@ +module LSP + struct TextDocumentEdit + include JSON::Serializable + + # The text document to change. + property textDocument : 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..a0cacb518 --- /dev/null +++ b/src/lsp/protocol/text_document_item.cr @@ -0,0 +1,22 @@ +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 + 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..6f357b91f 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,49 @@ 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? + def process(contents) + # Parse the contents as JSON + json = + JSON.parse(contents) - MessageParser.parse(@in) do |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 + log(error.to_s) + 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/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..2db82e37a 100644 --- a/src/reactor.cr +++ b/src/reactor.cr @@ -19,25 +19,13 @@ 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| + FileWorkspace.new(check: Check::Environment, format: format?) do |result| @files = case result - in Ast + in TypeChecker Bundler.new( - artifacts: workspace.type_checker.artifacts, - json: workspace.json, + artifacts: result.artifacts, + json: MintJson.new, config: Bundler::Config.new( generate_manifest: false, include_program: true, @@ -56,9 +44,46 @@ module Mint @sockets.each(&.send("reload")) end - # Do the initial parsing and type checking and start wathing for changes. - workspace.update_cache - workspace.watch + # # 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 + + # # Do the initial parsing and type checking and start wathing for changes. + # workspace.update_cache + # workspace.watch # The websocket handle saves the sockets when they connect and # removes them when they disconnect. diff --git a/src/sandbox_server.cr b/src/sandbox_server.cr index 2f07296df..9b1f7beac 100644 --- a/src/sandbox_server.cr +++ b/src/sandbox_server.cr @@ -1,28 +1,5 @@ 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 @@ -43,146 +20,14 @@ module Mint 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) }, + HTTP::WebSocketHandler.new do |socket| + LS::WebSocketServer.new(socket) + end, ]) Server.run( 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/utils/watcher.cr b/src/utils/watcher.cr index d2045a49b..fb654f351 100644 --- a/src/utils/watcher.cr +++ b/src/utils/watcher.cr @@ -16,7 +16,7 @@ module Mint Dir.glob(@pattern).each do |file| path = - Path[file].normalize.to_s + Path[file].normalize.expand.to_s current.add({path, File.info(path).modification_time}) end @@ -29,7 +29,7 @@ module Mint def watch(&) loop do detect do |diff| - yield diff.map(&.[0]).uniq! unless diff.empty? + yield diff.map(&.first).uniq! unless diff.empty? end sleep 0.5 diff --git a/src/workspace.cr b/src/workspace.cr index b9c7ddb30..ad66e633c 100644 --- a/src/workspace.cr +++ b/src/workspace.cr @@ -54,14 +54,15 @@ module Mint getter type_checker : TypeChecker getter cache : Hash(String, Ast) getter formatter : Formatter + getter test_path : String? getter json : MintJson getter error : Error? getter root : String + property? presist_on_update : Bool = false 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 @@ -212,10 +213,12 @@ module Mint @error = nil call "change", ast + ast rescue error : Error @error = error call "change", error + error end def format(file) @@ -225,7 +228,7 @@ module Mint end def update(contents, file) - self[file] = process(contents, file) + self[file] = process(contents, Path[root, file].to_s) @error = nil call "change", ast @@ -240,8 +243,15 @@ module Mint end private def process(contents, file) + real_path = + if File.exists?(file) + File.realpath(file) + else + file + end + ast = - Parser.parse(contents, File.realpath(file)) + Parser.parse(contents, real_path) if format? formatted = diff --git a/src/workspace_2.cr b/src/workspace_2.cr new file mode 100644 index 000000000..ca922bfc3 --- /dev/null +++ b/src/workspace_2.cr @@ -0,0 +1,139 @@ +module Mint + @[Flags] + enum Check + Environment + Unreachable + end + + class Workspace2 + # Stores the AST (or error) of the file at the given path. + @cache : Hash(String, Ast | Error) = {} of String => Ast | Error + + def initialize(@check : Check) + end + + def update(contents : String, path : String) + @cache[path] = Parser.parse?(contents, path) + end + + def delete(path : String) + @cache.delete(path) + end + + def ast?(path : String) + case ast = @cache[path]? + when Ast + ast + end + end + + def clear + @cache.clear + end + + def process + errors = + @cache.values.select(Error) + + if error = errors.first? + error + else + ast = + @cache + .values + .select(Ast) + .reduce(Ast.new) { |memo, item| memo.merge item } + .tap(&.merge(Core.ast)) + .tap(&.normalize) + + TypeChecker.new( + check_everything: @check.unreachable?, + check_env: @check.environment?, + ast: ast + ).tap(&.check) + end + rescue error : Error + error + end + end + + class FileWorkspace + getter? format : Bool + getter check : Check + + def initialize( + *, + @check : Check, + @format : Bool, + &@listener : TypeChecker | Error -> Nil + ) + @workspace = + Workspace2.new(check) + + @json = + MintJson.current + + @globs = + [ + ".mint/**/*.mint", + ".mint/**/mint.json", + "**/*.mint", + "**/mint.json", + ".env", + ] + + @watcher = + Watcher.new(@globs) + + spawn { @watcher.watch(&->update(Array(String))) } + end + + def reset + @workspace.clear + update(Dir.glob(@globs.select(&.ends_with?(".mint")))) + end + + def update(files : Array(String)) + process = true + + files.each do |file| + if File.extname(file) == ".mint" + if File.exists?(file) + contents = + File.read(file) + + if format? + if ast = @workspace.ast?(file) + formatted = + Formatter.new.format(ast) + + if formatted != contents + File.write(file, formatted) + + # Since formatting a file will trigger another change we skip + # processing this file and don't trigger type checking. + process = false + next + end + end + end + + @workspace.update(contents, file) + else + @workspace.delete(file) + end + else + # We need to do a reset because: + # 1. packages could have been added or removed + # 2. source directories could have been added or removed + case File.basename(file) + when "mint.json" + reset + end + end + end + + @listener.call(@workspace.process) if process + end + end +end diff --git a/test.cr b/test.cr new file mode 100644 index 000000000..e69de29bb From 49626139b2d76f174ef783a270a34fbca89c1c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Mon, 23 Sep 2024 15:09:06 +0200 Subject: [PATCH 02/10] MintJson refactor part I. --- spec/installer/repository_spec.cr | 14 +- spec/mint_json/application_css_prefix_invalid | 18 + spec/mint_json/application_display_invalid | 18 + spec/mint_json/application_display_mismatch | 25 + spec/mint_json/application_head_invalid | 18 + spec/mint_json/application_head_not_exists | 22 + spec/mint_json/application_icon_invalid | 18 + spec/mint_json/application_icon_not_exists | 21 + spec/mint_json/application_invalid | 14 + spec/mint_json/application_invalid_key | 22 + spec/mint_json/application_meta_invalid | 18 + .../application_meta_keyword_invalid | 22 + .../application_meta_keywords_invalid | 22 + spec/mint_json/application_meta_value_invalid | 22 + spec/mint_json/application_name | 18 + .../mint_json/application_orientation_invalid | 19 + .../application_orientation_mismatch | 29 + .../mint_json/application_theme_color_invalid | 19 + spec/mint_json/application_title_empty | 18 + spec/mint_json/application_title_invalid | 18 + spec/mint_json/dependencies_empty | 16 + spec/mint_json/dependencies_invalid | 16 + spec/mint_json/dependency_constraint_bad | 32 + spec/mint_json/dependency_constraint_invalid | 24 + spec/mint_json/dependency_invalid | 18 + spec/mint_json/dependency_invalid_key | 22 + spec/mint_json/dependency_missing_constraint | 21 + spec/mint_json/dependency_missing_repository | 18 + spec/mint_json/dependency_repository_invalid | 22 + spec/mint_json/formatter_indent_size_invalid | 19 + spec/mint_json/formatter_invalid | 14 + spec/mint_json/formatter_invalid_key | 22 + spec/mint_json/invalid_json | 14 + spec/mint_json/mint_version_bad | 18 + spec/mint_json/mint_version_invalid | 14 + spec/mint_json/mint_version_mismatch | 24 + spec/mint_json/name_empty | 14 + spec/mint_json/name_invalid | 14 + spec/mint_json/root_invalid | 9 + spec/mint_json/root_invalid_key | 18 + spec/mint_json/source_directories_empty | 17 + spec/mint_json/source_directories_invalid | 17 + spec/mint_json/source_directory_invalid | 14 + spec/mint_json/source_directory_not_exists | 14 + spec/mint_json/test_directories_empty | 17 + spec/mint_json/test_directories_invalid | 17 + spec/mint_json/test_directory_invalid | 14 + spec/mint_json/test_directory_not_exists | 14 + spec/mint_json_spec.cr | 35 + src/all.cr | 2 + src/bundler.cr | 8 +- src/command.cr | 21 + src/commands/build.cr | 2 +- src/errorable.cr | 57 +- src/lsp/protocol/create_file_options.cr | 3 +- src/lsp/protocol/text_document_edit.cr | 3 +- src/mint_json.cr | 1044 +---------------- src/mint_json/application.cr | 71 ++ src/mint_json/application/css_prefix.cr | 19 + src/mint_json/application/display.cr | 37 + src/mint_json/application/head.cr | 46 + src/mint_json/application/icon.cr | 45 + src/mint_json/application/meta.cr | 85 ++ src/mint_json/application/name.cr | 19 + src/mint_json/application/orientation.cr | 41 + src/mint_json/application/theme_color.cr | 19 + src/mint_json/application/title.cr | 37 + src/mint_json/dependencies.cr | 156 +++ src/mint_json/formatter.cr | 51 + src/mint_json/mint_version.cr | 62 + src/mint_json/name.cr | 31 + src/mint_json/root.cr | 33 + src/mint_json/source_directories.cr | 76 ++ src/mint_json/test_directories.cr | 76 ++ src/render/terminal.cr | 9 +- src/type_checker.cr | 4 +- src/type_checkers/top_level.cr | 6 - src/workspace_2.cr | 2 +- 78 files changed, 1953 insertions(+), 1035 deletions(-) create mode 100644 spec/mint_json/application_css_prefix_invalid create mode 100644 spec/mint_json/application_display_invalid create mode 100644 spec/mint_json/application_display_mismatch create mode 100644 spec/mint_json/application_head_invalid create mode 100644 spec/mint_json/application_head_not_exists create mode 100644 spec/mint_json/application_icon_invalid create mode 100644 spec/mint_json/application_icon_not_exists create mode 100644 spec/mint_json/application_invalid create mode 100644 spec/mint_json/application_invalid_key create mode 100644 spec/mint_json/application_meta_invalid create mode 100644 spec/mint_json/application_meta_keyword_invalid create mode 100644 spec/mint_json/application_meta_keywords_invalid create mode 100644 spec/mint_json/application_meta_value_invalid create mode 100644 spec/mint_json/application_name create mode 100644 spec/mint_json/application_orientation_invalid create mode 100644 spec/mint_json/application_orientation_mismatch create mode 100644 spec/mint_json/application_theme_color_invalid create mode 100644 spec/mint_json/application_title_empty create mode 100644 spec/mint_json/application_title_invalid create mode 100644 spec/mint_json/dependencies_empty create mode 100644 spec/mint_json/dependencies_invalid create mode 100644 spec/mint_json/dependency_constraint_bad create mode 100644 spec/mint_json/dependency_constraint_invalid create mode 100644 spec/mint_json/dependency_invalid create mode 100644 spec/mint_json/dependency_invalid_key create mode 100644 spec/mint_json/dependency_missing_constraint create mode 100644 spec/mint_json/dependency_missing_repository create mode 100644 spec/mint_json/dependency_repository_invalid create mode 100644 spec/mint_json/formatter_indent_size_invalid create mode 100644 spec/mint_json/formatter_invalid create mode 100644 spec/mint_json/formatter_invalid_key create mode 100644 spec/mint_json/invalid_json create mode 100644 spec/mint_json/mint_version_bad create mode 100644 spec/mint_json/mint_version_invalid create mode 100644 spec/mint_json/mint_version_mismatch create mode 100644 spec/mint_json/name_empty create mode 100644 spec/mint_json/name_invalid create mode 100644 spec/mint_json/root_invalid create mode 100644 spec/mint_json/root_invalid_key create mode 100644 spec/mint_json/source_directories_empty create mode 100644 spec/mint_json/source_directories_invalid create mode 100644 spec/mint_json/source_directory_invalid create mode 100644 spec/mint_json/source_directory_not_exists create mode 100644 spec/mint_json/test_directories_empty create mode 100644 spec/mint_json/test_directories_invalid create mode 100644 spec/mint_json/test_directory_invalid create mode 100644 spec/mint_json/test_directory_not_exists create mode 100644 spec/mint_json_spec.cr create mode 100644 src/mint_json/application.cr create mode 100644 src/mint_json/application/css_prefix.cr create mode 100644 src/mint_json/application/display.cr create mode 100644 src/mint_json/application/head.cr create mode 100644 src/mint_json/application/icon.cr create mode 100644 src/mint_json/application/meta.cr create mode 100644 src/mint_json/application/name.cr create mode 100644 src/mint_json/application/orientation.cr create mode 100644 src/mint_json/application/theme_color.cr create mode 100644 src/mint_json/application/title.cr create mode 100644 src/mint_json/dependencies.cr create mode 100644 src/mint_json/formatter.cr create mode 100644 src/mint_json/mint_version.cr create mode 100644 src/mint_json/name.cr create mode 100644 src/mint_json/root.cr create mode 100644 src/mint_json/source_directories.cr create mode 100644 src/mint_json/test_directories.cr 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/mint_json/application_css_prefix_invalid b/spec/mint_json/application_css_prefix_invalid new file mode 100644 index 000000000..6095e6132 --- /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: + + ┌ 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..6f5ff5358 --- /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: + + ┌ 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..2310ec6ce --- /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: + + ┌ 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..671514e61 --- /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: + + ┌ 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..dd1e02fcd --- /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...) + + ┌ 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..e78fb6d8f --- /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: + + ┌ 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..9b8f0b045 --- /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. + + ┌ 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..128e47c93 --- /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: + + ┌ 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..2371bac2d --- /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: + + ┌ 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..39125c151 --- /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: + + ┌ 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..e7c99249a --- /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: + + ┌ 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..730db8cc4 --- /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: + + ┌ 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..09498497a --- /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: + + ┌ 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..891adcbee --- /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: + + ┌ 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..08ff4bd16 --- /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: + + ┌ 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..ee8146895 --- /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: + + ┌ 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..3713a4cc0 --- /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: + + ┌ 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..00baf2a41 --- /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: + + ┌ 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..0b4013b98 --- /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: + + ┌ 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..0367a1c05 --- /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: + + ┌ 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..8801bc38f --- /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: + + ┌ 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..1fe7bd060 --- /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: + + ┌ 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..c3f884639 --- /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: + + ┌ 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..49d5d321f --- /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: + + ┌ 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..199aad856 --- /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: + + ┌ 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..c3a4762fd --- /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: + + ┌ 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..5a3f2f642 --- /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: + + ┌ 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..2e7fbaa4f --- /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: + + ┌ 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..f6efd8c12 --- /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: + + ┌ 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..8463d838f --- /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: + + ┌ 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..2b3fae4ac --- /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: + + ┌ 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..26519a677 --- /dev/null +++ b/spec/mint_json/invalid_json @@ -0,0 +1,14 @@ +{ + , +} +-------------------------------------------------------------------------------- +░ ERROR (INVALID_JSON) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +I could not parse the following mint.json file: + + ┌ 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..95e1411ab --- /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: + + ┌ 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..eda302198 --- /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: + + ┌ 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..479d80898 --- /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: + + ┌ 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..d51a2ac06 --- /dev/null +++ b/spec/mint_json/name_empty @@ -0,0 +1,14 @@ +{ + "name": "" +} +-------------------------------------------------------------------------------- +░ ERROR (NAME_EMPTY) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +The name field should not be empty: + + ┌ 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..78949f512 --- /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: + + ┌ 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..86d33a6b9 --- /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: + + ┌ 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..44cfea38d --- /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: + + ┌ 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..30882c6a8 --- /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: + + ┌ 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..91c50b4c3 --- /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: + + ┌ 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..bb88b3740 --- /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: + + ┌ 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..a29cecd13 --- /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: + + ┌ 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..ad58e27ed --- /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: + + ┌ 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..365b681c4 --- /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: + + ┌ 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..57e4f36c7 --- /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: + + ┌ 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..c9f70dc1b --- /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: + + ┌ 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..87f109ff2 --- /dev/null +++ b/spec/mint_json_spec.cr @@ -0,0 +1,35 @@ +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.new(source, "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.from_file("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 diff --git a/src/all.cr b/src/all.cr index 91bf38683..e825920d9 100644 --- a/src/all.cr +++ b/src/all.cr @@ -68,7 +68,9 @@ require "./test_runner" require "./lsp/**" require "./ls/**" +require "./mint_json/**" require "./mint_json" + require "./scaffold" require "./reactor" require "./sandbox_server" diff --git a/src/bundler.cr b/src/bundler.cr index 2211adddf..cadb5d0af 100644 --- a/src/bundler.cr +++ b/src/bundler.cr @@ -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? @@ -368,9 +368,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/command.cr b/src/command.cr index 3a749a9b3..4c4357ccb 100644 --- a/src/command.cr +++ b/src/command.cr @@ -78,6 +78,27 @@ module Mint exit(1) end + def check_dependencies!(dependencies : Array(Installer::Dependency)) + 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 + terminal.print "#{WARNING} Missing dependencies..." + raise CliException.new + end + end + end + def terminal Render::Terminal::STDOUT end diff --git a/src/commands/build.cr b/src/commands/build.cr index e3e2bd427..4b734311e 100644 --- a/src/commands/build.cr +++ b/src/commands/build.cr @@ -52,7 +52,7 @@ module Mint workspace.check_env = true # Check if we have dependencies installed. - workspace.json.check_dependencies! + check_dependencies!(workspace.json.dependencies) # On any change we copy the build to the dist directory. workspace.on("change") do |result| diff --git a/src/errorable.cr b/src/errorable.cr index ea24c16ec..f42aed760 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) @@ -122,14 +144,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/lsp/protocol/create_file_options.cr b/src/lsp/protocol/create_file_options.cr index 40e4c0e75..d571a0645 100644 --- a/src/lsp/protocol/create_file_options.cr +++ b/src/lsp/protocol/create_file_options.cr @@ -6,6 +6,7 @@ module LSP property overwrite : Bool? # Ignore if exists. - property ignoreIfExists : Bool? + @[JSON::Field(key: "ignoreIfExists")] + property ignore_if_exists : Bool? end end diff --git a/src/lsp/protocol/text_document_edit.cr b/src/lsp/protocol/text_document_edit.cr index 64abb1fa9..13a7fd2e4 100644 --- a/src/lsp/protocol/text_document_edit.cr +++ b/src/lsp/protocol/text_document_edit.cr @@ -3,7 +3,8 @@ module LSP include JSON::Serializable # The text document to change. - property textDocument : OptionalVersionedTextDocumentIdentifier + @[JSON::Field(key: "textDocument")] + property text_document : OptionalVersionedTextDocumentIdentifier # The edits to be applied. property edits : Array(TextEdit) diff --git a/src/mint_json.cr b/src/mint_json.cr index ad9d607ac..e952eb8b0 100644 --- a/src/mint_json.cr +++ b/src/mint_json.cr @@ -3,74 +3,74 @@ module Mint include Errorable class Application - getter title, meta, icon, head, name, theme, display, orientation, css_prefix + getter title, meta, icon, head, name, theme_color, display, orientation + getter css_prefix - def initialize(@meta = {} of String => String, - @orientation = "", - @display = "", - @theme = "", - @title = "", - @name = "", - @head = "", - @icon = "", - @css_prefix : String? = nil) + def initialize( + @meta = {} of String => String, + @css_prefix : String? = nil, + @orientation = "", + @theme_color = "", + @display = "", + @title = "", + @name = "", + @head = "", + @icon = "" + ) end end - @parser = JSON::PullParser.new("{}") - getter dependencies = [] of Installer::Dependency getter formatter_config = Formatter::Config.new - getter web_components = {} of String => String + getter parser = JSON::PullParser.new("{}") + getter application = Application.new getter source_directories = %w[] getter test_directories = %w[] - getter application = Application.new - getter root : String getter name = "" - def initialize - @json = "" - @root = "" - @file = "" - end + getter root : String + getter file : String + getter json : String def initialize(@json : String, @root : String, @file : String) begin @parser = JSON::PullParser.new(@json) rescue exception : JSON::ParseException - error! :mint_json_invalid_json do + error! :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 + snippet snippet_data(exception) end end parse_root end + def initialize + @json = "" + @root = "" + @file = "" + end + def self.from_file(path) new File.read(path), File.dirname(path), path + rescue error : Error + raise error + rescue error + 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 error.to_s + end end def self.parse_current : MintJson @@ -83,10 +83,7 @@ module Mint nil end - # Calculating nodes for the snippet in errors. - # -------------------------------------------------------------------------- - - def node(column_number, line_number) + def snippet_data(line_number : Int32, column_number : Int32) position = if line_number - 1 == 0 0 @@ -94,31 +91,25 @@ module Mint @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) + end + (column_number - 1) - Ast::Node.new( - to: to, - from: position, - file: file) + Error::SnippetData.new( + filename: @file, + input: @json, + to: position + 1, + from: position) end - def node(exception : JSON::ParseException) - node exception.location + def snippet_data(exception : JSON::ParseException) + snippet_data exception.location end - def node(location) - node location[1], location[0] + def snippet_data(location : Tuple(Int32, Int32)) + snippet_data location[0], location[1] end - def current_node - node @parser.location + def snippet_data + snippet_data @parser.location end def source_files @@ -127,936 +118,5 @@ module Mint 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..eb064eac3 --- /dev/null +++ b/src/mint_json/application.cr @@ -0,0 +1,71 @@ +module Mint + class MintJson + def parse_application + meta = {} of String => String + css_prefix = nil + orientation = "" + theme_color = "" + 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 = + 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 diff --git a/src/mint_json/application/css_prefix.cr b/src/mint_json/application/css_prefix.cr new file mode 100644 index 000000000..d1b87de12 --- /dev/null +++ b/src/mint_json/application/css_prefix.cr @@ -0,0 +1,19 @@ +module Mint + class MintJson + def parse_application_css_prefix + @parser.read_string_or_null + 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 diff --git a/src/mint_json/application/display.cr b/src/mint_json/application/display.cr new file mode 100644 index 000000000..b6e049570 --- /dev/null +++ b/src/mint_json/application/display.cr @@ -0,0 +1,37 @@ +module Mint + class MintJson + DISPLAY_VALUES = + %w[fullscreen standalone minimal-ui browser] + + def parse_application_display + @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 diff --git a/src/mint_json/application/head.cr b/src/mint_json/application/head.cr new file mode 100644 index 000000000..50aa5585b --- /dev/null +++ b/src/mint_json/application/head.cr @@ -0,0 +1,46 @@ +module Mint + class MintJson + def parse_application_head + 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 diff --git a/src/mint_json/application/icon.cr b/src/mint_json/application/icon.cr new file mode 100644 index 000000000..2e71ebbc5 --- /dev/null +++ b/src/mint_json/application/icon.cr @@ -0,0 +1,45 @@ +module Mint + class MintJson + def parse_application_icon + 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 diff --git a/src/mint_json/application/meta.cr b/src/mint_json/application/meta.cr new file mode 100644 index 000000000..be4a4dc3c --- /dev/null +++ b/src/mint_json/application/meta.cr @@ -0,0 +1,85 @@ +module Mint + class MintJson + def parse_application_meta + 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 + @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 + 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 + @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 diff --git a/src/mint_json/application/name.cr b/src/mint_json/application/name.cr new file mode 100644 index 000000000..4a5d2537d --- /dev/null +++ b/src/mint_json/application/name.cr @@ -0,0 +1,19 @@ +module Mint + class MintJson + def parse_application_name + @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 diff --git a/src/mint_json/application/orientation.cr b/src/mint_json/application/orientation.cr new file mode 100644 index 000000000..eec952228 --- /dev/null +++ b/src/mint_json/application/orientation.cr @@ -0,0 +1,41 @@ +module Mint + class MintJson + ORIENTATION_VALUES = + %w[ + any natural landscape landscape-primary + landscape-secondary portrait portrait-primary + portrait-secondary + ] + + def parse_application_orientation + @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 diff --git a/src/mint_json/application/theme_color.cr b/src/mint_json/application/theme_color.cr new file mode 100644 index 000000000..b3c0f59da --- /dev/null +++ b/src/mint_json/application/theme_color.cr @@ -0,0 +1,19 @@ +module Mint + class MintJson + def parse_application_theme_color + @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 diff --git a/src/mint_json/application/title.cr b/src/mint_json/application/title.cr new file mode 100644 index 000000000..f5d38257c --- /dev/null +++ b/src/mint_json/application/title.cr @@ -0,0 +1,37 @@ +module Mint + class MintJson + def parse_application_title + 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 diff --git a/src/mint_json/dependencies.cr b/src/mint_json/dependencies.cr new file mode 100644 index 000000000..c60732464 --- /dev/null +++ b/src/mint_json/dependencies.cr @@ -0,0 +1,156 @@ +module Mint + class MintJson + def parse_dependencies + @parser.location.try do |location| + @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 source_directories.empty? + 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 + @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 + 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 diff --git a/src/mint_json/formatter.cr b/src/mint_json/formatter.cr new file mode 100644 index 000000000..27cf0ec23 --- /dev/null +++ b/src/mint_json/formatter.cr @@ -0,0 +1,51 @@ +module Mint + class MintJson + def parse_formatter + 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 = 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 + @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 diff --git a/src/mint_json/mint_version.cr b/src/mint_json/mint_version.cr new file mode 100644 index 000000000..c1e4c37b8 --- /dev/null +++ b/src/mint_json/mint_version.cr @@ -0,0 +1,62 @@ +module Mint + class MintJson + def parse_mint_version + 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 diff --git a/src/mint_json/name.cr b/src/mint_json/name.cr new file mode 100644 index 000000000..e2181db75 --- /dev/null +++ b/src/mint_json/name.cr @@ -0,0 +1,31 @@ +module Mint + class MintJson + def parse_name + 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? + 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 diff --git a/src/mint_json/root.cr b/src/mint_json/root.cr new file mode 100644 index 000000000..b6ce2ec03 --- /dev/null +++ b/src/mint_json/root.cr @@ -0,0 +1,33 @@ +module Mint + class MintJson + def parse_root + @parser.read_object do |key| + case key + when "source-directories" + parse_source_directories + when "test-directories" + parse_test_directories + when "dependencies" + parse_dependencies + when "mint-version" + parse_mint_version + when "application" + parse_application + when "formatter" + parse_formatter + when "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 + 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 diff --git a/src/mint_json/source_directories.cr b/src/mint_json/source_directories.cr new file mode 100644 index 000000000..fa1949e25 --- /dev/null +++ b/src/mint_json/source_directories.cr @@ -0,0 +1,76 @@ +module Mint + class MintJson + def parse_source_directories + @parser.location.try do |location| + @parser.read_array(&->parse_source_directory) + + 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 source_directories.empty? + 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 + 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) + + source_directories << 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 diff --git a/src/mint_json/test_directories.cr b/src/mint_json/test_directories.cr new file mode 100644 index 000000000..eef6c90b3 --- /dev/null +++ b/src/mint_json/test_directories.cr @@ -0,0 +1,76 @@ +module Mint + class MintJson + def parse_test_directories + @parser.location.try do |location| + @parser.read_array(&->parse_test_directory) + + 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 test_directories.empty? + 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 + 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) + + test_directories << 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 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/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/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/workspace_2.cr b/src/workspace_2.cr index ca922bfc3..9b37cf617 100644 --- a/src/workspace_2.cr +++ b/src/workspace_2.cr @@ -71,7 +71,7 @@ module Mint Workspace2.new(check) @json = - MintJson.current + MintJson.parse_current @globs = [ From f450855dc9a39f06ccf21ba1dced3c5588bc8875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Tue, 24 Sep 2024 14:14:43 +0200 Subject: [PATCH 03/10] MintJson refactor part II. --- spec/compilers_spec.cr | 2 +- spec/mint_json/application_css_prefix_invalid | 4 +- spec/mint_json/application_display_invalid | 4 +- spec/mint_json/application_display_mismatch | 4 +- spec/mint_json/application_head_invalid | 4 +- spec/mint_json/application_head_not_exists | 4 +- spec/mint_json/application_icon_invalid | 4 +- spec/mint_json/application_icon_not_exists | 4 +- spec/mint_json/application_invalid | 4 +- spec/mint_json/application_invalid_key | 4 +- spec/mint_json/application_meta_invalid | 4 +- .../application_meta_keyword_invalid | 4 +- .../application_meta_keywords_invalid | 4 +- spec/mint_json/application_meta_value_invalid | 4 +- spec/mint_json/application_name | 4 +- .../mint_json/application_orientation_invalid | 4 +- .../application_orientation_mismatch | 4 +- .../mint_json/application_theme_color_invalid | 4 +- spec/mint_json/application_title_empty | 4 +- spec/mint_json/application_title_invalid | 4 +- spec/mint_json/dependencies_empty | 4 +- spec/mint_json/dependencies_invalid | 4 +- spec/mint_json/dependency_constraint_bad | 4 +- spec/mint_json/dependency_constraint_invalid | 4 +- spec/mint_json/dependency_invalid | 4 +- spec/mint_json/dependency_invalid_key | 2 +- spec/mint_json/dependency_missing_constraint | 2 +- spec/mint_json/dependency_missing_repository | 4 +- spec/mint_json/dependency_repository_invalid | 4 +- spec/mint_json/formatter_indent_size_invalid | 4 +- spec/mint_json/formatter_invalid | 4 +- spec/mint_json/formatter_invalid_key | 4 +- spec/mint_json/invalid_json | 4 +- spec/mint_json/mint_version_bad | 4 +- spec/mint_json/mint_version_invalid | 4 +- spec/mint_json/mint_version_mismatch | 2 +- spec/mint_json/name_empty | 4 +- spec/mint_json/name_invalid | 4 +- spec/mint_json/root_invalid | 4 +- spec/mint_json/root_invalid_key | 4 +- spec/mint_json/source_directories_empty | 4 +- spec/mint_json/source_directories_invalid | 4 +- spec/mint_json/source_directory_invalid | 4 +- spec/mint_json/source_directory_not_exists | 2 +- spec/mint_json/test_directories_empty | 4 +- spec/mint_json/test_directories_invalid | 4 +- spec/mint_json/test_directory_invalid | 4 +- spec/mint_json/test_directory_not_exists | 2 +- spec/mint_json_spec.cr | 4 +- src/commands/docs.cr | 4 +- src/commands/format.cr | 4 +- src/core.cr | 4 +- src/installer.cr | 2 +- src/installer/repository.cr | 2 +- src/ls/completion_item/component.cr | 2 +- src/mint_json.cr | 145 ++++------- src/mint_json/application.cr | 99 ++++---- src/mint_json/application/css_prefix.cr | 26 +- src/mint_json/application/display.cr | 56 +++-- src/mint_json/application/head.cr | 68 ++--- src/mint_json/application/icon.cr | 66 ++--- src/mint_json/application/meta.cr | 128 +++++----- src/mint_json/application/name.cr | 26 +- src/mint_json/application/orientation.cr | 64 ++--- src/mint_json/application/theme_color.cr | 26 +- src/mint_json/application/title.cr | 54 ++-- src/mint_json/dependencies.cr | 232 +++++++++--------- src/mint_json/formatter.cr | 74 +++--- src/mint_json/mint_version.cr | 92 +++---- src/mint_json/name.cr | 46 ++-- src/mint_json/parser.cr | 86 +++++++ src/mint_json/root.cr | 79 ++++-- src/mint_json/source_directories.cr | 104 ++++---- src/mint_json/test_directories.cr | 104 ++++---- src/reactor.cr | 2 +- src/style_builder.cr | 2 +- src/test_runner.cr | 2 +- src/utils/source_files.cr | 15 +- src/workspace.cr | 8 +- src/workspace_2.cr | 2 +- 80 files changed, 963 insertions(+), 845 deletions(-) create mode 100644 src/mint_json/parser.cr 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/mint_json/application_css_prefix_invalid b/spec/mint_json/application_css_prefix_invalid index 6095e6132..d770849f2 100644 --- a/spec/mint_json/application_css_prefix_invalid +++ b/spec/mint_json/application_css_prefix_invalid @@ -8,8 +8,8 @@ The css-prefix field of the application object should be a string, but it's not: - ┌ mint.json:3:19 - ├──────────────────── + ┌ spec/fixtures/mint.json:3:19 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "css-prefix": 0 diff --git a/spec/mint_json/application_display_invalid b/spec/mint_json/application_display_invalid index 6f5ff5358..896bbc1c8 100644 --- a/spec/mint_json/application_display_invalid +++ b/spec/mint_json/application_display_invalid @@ -8,8 +8,8 @@ The display field of the application object should be a string, but it's not: - ┌ mint.json:3:16 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:16 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "display": 0 diff --git a/spec/mint_json/application_display_mismatch b/spec/mint_json/application_display_mismatch index 2310ec6ce..e473b4e5b 100644 --- a/spec/mint_json/application_display_mismatch +++ b/spec/mint_json/application_display_mismatch @@ -15,8 +15,8 @@ The value of the display field should be one of: It is here: - ┌ mint.json:3:16 - ├───────────────────── + ┌ spec/fixtures/mint.json:3:16 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "display": "xxx" diff --git a/spec/mint_json/application_head_invalid b/spec/mint_json/application_head_invalid index 671514e61..2765a0f0b 100644 --- a/spec/mint_json/application_head_invalid +++ b/spec/mint_json/application_head_invalid @@ -8,8 +8,8 @@ The head field of the application object should be a string, but it's not: - ┌ mint.json:3:13 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "head": 0 diff --git a/spec/mint_json/application_head_not_exists b/spec/mint_json/application_head_not_exists index dd1e02fcd..b9cf78aef 100644 --- a/spec/mint_json/application_head_not_exists +++ b/spec/mint_json/application_head_not_exists @@ -12,8 +12,8 @@ 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...) - ┌ mint.json:3:13 - ├──────────────────────── + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "head": "head.html" diff --git a/spec/mint_json/application_icon_invalid b/spec/mint_json/application_icon_invalid index e78fb6d8f..561ad1403 100644 --- a/spec/mint_json/application_icon_invalid +++ b/spec/mint_json/application_icon_invalid @@ -8,8 +8,8 @@ The icon field of the application object should be a string, but it's not: - ┌ mint.json:3:13 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "icon": 0 diff --git a/spec/mint_json/application_icon_not_exists b/spec/mint_json/application_icon_not_exists index 9b8f0b045..01f3fc5f0 100644 --- a/spec/mint_json/application_icon_not_exists +++ b/spec/mint_json/application_icon_not_exists @@ -11,8 +11,8 @@ 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. - ┌ mint.json:3:13 - ├─────────────────────── + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "icon": "icon.jpg" diff --git a/spec/mint_json/application_invalid b/spec/mint_json/application_invalid index 128e47c93..e2d004922 100644 --- a/spec/mint_json/application_invalid +++ b/spec/mint_json/application_invalid @@ -6,8 +6,8 @@ The application field should be an object, but it's not: - ┌ mint.json:2:18 - ├─────────────────── + ┌ spec/fixtures/mint.json:2:18 + ├───────────────────────────── 1│ { 2│ "application": 0 │ ⌃ diff --git a/spec/mint_json/application_invalid_key b/spec/mint_json/application_invalid_key index 2371bac2d..1653cb8d2 100644 --- a/spec/mint_json/application_invalid_key +++ b/spec/mint_json/application_invalid_key @@ -12,8 +12,8 @@ The application object has an invalid key: It is here: - ┌ mint.json:3:12 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:12 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "xxx": "" diff --git a/spec/mint_json/application_meta_invalid b/spec/mint_json/application_meta_invalid index 39125c151..df5dd802c 100644 --- a/spec/mint_json/application_meta_invalid +++ b/spec/mint_json/application_meta_invalid @@ -8,8 +8,8 @@ The meta field of the application object should be an object, but it's not: - ┌ mint.json:3:13 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "meta": 0 diff --git a/spec/mint_json/application_meta_keyword_invalid b/spec/mint_json/application_meta_keyword_invalid index e7c99249a..dcfc192d4 100644 --- a/spec/mint_json/application_meta_keyword_invalid +++ b/spec/mint_json/application_meta_keyword_invalid @@ -10,8 +10,8 @@ A keyword should be a string, but it's not: - ┌ mint.json:4:20 - ├────────────────────── + ┌ spec/fixtures/mint.json:4:20 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "meta": { diff --git a/spec/mint_json/application_meta_keywords_invalid b/spec/mint_json/application_meta_keywords_invalid index 730db8cc4..d2b5092f3 100644 --- a/spec/mint_json/application_meta_keywords_invalid +++ b/spec/mint_json/application_meta_keywords_invalid @@ -10,8 +10,8 @@ The keywords field of the meta object should be an array, but it's not: - ┌ mint.json:4:19 - ├──────────────────── + ┌ spec/fixtures/mint.json:4:19 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "meta": { diff --git a/spec/mint_json/application_meta_value_invalid b/spec/mint_json/application_meta_value_invalid index 09498497a..d350f83dd 100644 --- a/spec/mint_json/application_meta_value_invalid +++ b/spec/mint_json/application_meta_value_invalid @@ -10,8 +10,8 @@ The value of a meta field should be a string, but it's not: - ┌ mint.json:4:19 - ├──────────────────── + ┌ spec/fixtures/mint.json:4:19 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "meta": { diff --git a/spec/mint_json/application_name b/spec/mint_json/application_name index 891adcbee..887c7e1b7 100644 --- a/spec/mint_json/application_name +++ b/spec/mint_json/application_name @@ -8,8 +8,8 @@ The name field of the application object should be a string, but it's not: - ┌ mint.json:3:13 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:13 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "name": 0 diff --git a/spec/mint_json/application_orientation_invalid b/spec/mint_json/application_orientation_invalid index 08ff4bd16..0d80d5173 100644 --- a/spec/mint_json/application_orientation_invalid +++ b/spec/mint_json/application_orientation_invalid @@ -9,8 +9,8 @@ The orientation field of the application object should be a string, but it's not: - ┌ mint.json:3:20 - ├───────────────────── + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "orientation": 0 diff --git a/spec/mint_json/application_orientation_mismatch b/spec/mint_json/application_orientation_mismatch index ee8146895..4a728c6fc 100644 --- a/spec/mint_json/application_orientation_mismatch +++ b/spec/mint_json/application_orientation_mismatch @@ -19,8 +19,8 @@ The value of the orientation field should be one of: It is here: - ┌ mint.json:3:20 - ├───────────────────────── + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "orientation": "xxx" diff --git a/spec/mint_json/application_theme_color_invalid b/spec/mint_json/application_theme_color_invalid index 3713a4cc0..ae0e4ce14 100644 --- a/spec/mint_json/application_theme_color_invalid +++ b/spec/mint_json/application_theme_color_invalid @@ -9,8 +9,8 @@ The theme-color field of the application object should be a string, but it's not: - ┌ mint.json:3:20 - ├───────────────────── + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "theme-color": 0 diff --git a/spec/mint_json/application_title_empty b/spec/mint_json/application_title_empty index 00baf2a41..fb2c5b71d 100644 --- a/spec/mint_json/application_title_empty +++ b/spec/mint_json/application_title_empty @@ -8,8 +8,8 @@ The title field of the application object should not be empty, but it is: - ┌ mint.json:3:14 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:14 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "title": "" diff --git a/spec/mint_json/application_title_invalid b/spec/mint_json/application_title_invalid index 0b4013b98..7ec4afe61 100644 --- a/spec/mint_json/application_title_invalid +++ b/spec/mint_json/application_title_invalid @@ -8,8 +8,8 @@ The title field of the application object should be a string, but it's not: - ┌ mint.json:3:14 - ├─────────────────── + ┌ spec/fixtures/mint.json:3:14 + ├───────────────────────────── 1│ { 2│ "application": { 3│ "title": 0 diff --git a/spec/mint_json/dependencies_empty b/spec/mint_json/dependencies_empty index 0367a1c05..f2bbfd842 100644 --- a/spec/mint_json/dependencies_empty +++ b/spec/mint_json/dependencies_empty @@ -8,8 +8,8 @@ The dependencies field lists all the dependencies for the application. The dependencies object should not be empty, but it is: - ┌ mint.json:2:19 - ├───────────────────── + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── 1│ { 2│ "dependencies": {} │ ⌃ diff --git a/spec/mint_json/dependencies_invalid b/spec/mint_json/dependencies_invalid index 8801bc38f..0c0f031ed 100644 --- a/spec/mint_json/dependencies_invalid +++ b/spec/mint_json/dependencies_invalid @@ -8,8 +8,8 @@ The dependencies field lists all the dependencies for the application. It should be an object, but it's not: - ┌ mint.json:2:19 - ├──────────────────── + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── 1│ { 2│ "dependencies": 0 │ ⌃ diff --git a/spec/mint_json/dependency_constraint_bad b/spec/mint_json/dependency_constraint_bad index 1fe7bd060..634783fcd 100644 --- a/spec/mint_json/dependency_constraint_bad +++ b/spec/mint_json/dependency_constraint_bad @@ -19,8 +19,8 @@ or a git tag / commit / branch followed by the version: I could not find either: - ┌ mint.json:5:21 - ├────────────────────────── + ┌ spec/fixtures/mint.json:5:21 + ├───────────────────────────── 1│ { 2│ "dependencies": { 3│ "package": { diff --git a/spec/mint_json/dependency_constraint_invalid b/spec/mint_json/dependency_constraint_invalid index c3f884639..32d2cee9a 100644 --- a/spec/mint_json/dependency_constraint_invalid +++ b/spec/mint_json/dependency_constraint_invalid @@ -11,8 +11,8 @@ The constraint field of a depencency must be an string, but it's not: - ┌ mint.json:5:21 - ├──────────────────────── + ┌ spec/fixtures/mint.json:5:21 + ├───────────────────────────── 1│ { 2│ "dependencies": { 3│ "package": { diff --git a/spec/mint_json/dependency_invalid b/spec/mint_json/dependency_invalid index 49d5d321f..1ed7e9b51 100644 --- a/spec/mint_json/dependency_invalid +++ b/spec/mint_json/dependency_invalid @@ -8,8 +8,8 @@ A dependency must be an object, but it's not: - ┌ mint.json:3:16 - ├──────────────────── + ┌ spec/fixtures/mint.json:3:16 + ├───────────────────────────── 1│ { 2│ "dependencies": { 3│ "package": 0 diff --git a/spec/mint_json/dependency_invalid_key b/spec/mint_json/dependency_invalid_key index 199aad856..14ec71ebd 100644 --- a/spec/mint_json/dependency_invalid_key +++ b/spec/mint_json/dependency_invalid_key @@ -12,7 +12,7 @@ A dependency object has an invalid key: It is here: - ┌ mint.json:3:25 + ┌ spec/fixtures/mint.json:3:25 ├───────────────────────────── 1│ { 2│ "dependencies": { diff --git a/spec/mint_json/dependency_missing_constraint b/spec/mint_json/dependency_missing_constraint index c3a4762fd..f700c30f7 100644 --- a/spec/mint_json/dependency_missing_constraint +++ b/spec/mint_json/dependency_missing_constraint @@ -10,7 +10,7 @@ A dependency object is missing the constraint field: - ┌ mint.json:6:3 + ┌ spec/fixtures/mint.json:6:3 ├─────────────────────────────────────────────────────── 2│ "dependencies": { 3│ "package": { diff --git a/spec/mint_json/dependency_missing_repository b/spec/mint_json/dependency_missing_repository index 5a3f2f642..c4e6c842e 100644 --- a/spec/mint_json/dependency_missing_repository +++ b/spec/mint_json/dependency_missing_repository @@ -8,8 +8,8 @@ A dependency object is missing the repository field: - ┌ mint.json:4:3 - ├──────────────────── + ┌ spec/fixtures/mint.json:4:3 + ├──────────────────────────── 1│ { 2│ "dependencies": { 3│ "package": { } diff --git a/spec/mint_json/dependency_repository_invalid b/spec/mint_json/dependency_repository_invalid index 2e7fbaa4f..8b3347c30 100644 --- a/spec/mint_json/dependency_repository_invalid +++ b/spec/mint_json/dependency_repository_invalid @@ -10,8 +10,8 @@ The repository field of a depencency must be an string, but it's not: - ┌ mint.json:4:21 - ├────────────────────── + ┌ spec/fixtures/mint.json:4:21 + ├───────────────────────────── 1│ { 2│ "dependencies": { 3│ "package": { diff --git a/spec/mint_json/formatter_indent_size_invalid b/spec/mint_json/formatter_indent_size_invalid index f6efd8c12..cb199d98a 100644 --- a/spec/mint_json/formatter_indent_size_invalid +++ b/spec/mint_json/formatter_indent_size_invalid @@ -9,8 +9,8 @@ The indent-size field of the formatter configuration must be a number, but it's not: - ┌ mint.json:3:20 - ├────────────────────── + ┌ spec/fixtures/mint.json:3:20 + ├───────────────────────────── 1│ { 2│ "formatter": { 3│ "indent-size": "" diff --git a/spec/mint_json/formatter_invalid b/spec/mint_json/formatter_invalid index 8463d838f..6c1527507 100644 --- a/spec/mint_json/formatter_invalid +++ b/spec/mint_json/formatter_invalid @@ -6,8 +6,8 @@ The formatter field should be an object, but it's not: - ┌ mint.json:2:16 - ├───────────────── + ┌ spec/fixtures/mint.json:2:16 + ├───────────────────────────── 1│ { 2│ "formatter": 0 │ ⌃ diff --git a/spec/mint_json/formatter_invalid_key b/spec/mint_json/formatter_invalid_key index 2b3fae4ac..13add9bdd 100644 --- a/spec/mint_json/formatter_invalid_key +++ b/spec/mint_json/formatter_invalid_key @@ -12,8 +12,8 @@ The formatter object has an invalid key: It is here: - ┌ mint.json:3:12 - ├───────────────── + ┌ spec/fixtures/mint.json:3:12 + ├───────────────────────────── 1│ { 2│ "formatter": { 3│ "xxx": "" diff --git a/spec/mint_json/invalid_json b/spec/mint_json/invalid_json index 26519a677..16147f3d7 100644 --- a/spec/mint_json/invalid_json +++ b/spec/mint_json/invalid_json @@ -6,8 +6,8 @@ I could not parse the following mint.json file: - ┌ mint.json:2:3 - ├────────────── + ┌ spec/fixtures/mint.json:2:3 + ├──────────────────────────── 1│ { 2│ , │ ⌃ diff --git a/spec/mint_json/mint_version_bad b/spec/mint_json/mint_version_bad index 95e1411ab..5d6fa8b41 100644 --- a/spec/mint_json/mint_version_bad +++ b/spec/mint_json/mint_version_bad @@ -10,8 +10,8 @@ The mint-version constraint should be in this format: It is here: - ┌ mint.json:2:19 - ├───────────────────── + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── 1│ { 2│ "mint-version": "" │ ⌃ diff --git a/spec/mint_json/mint_version_invalid b/spec/mint_json/mint_version_invalid index eda302198..e3ce649c6 100644 --- a/spec/mint_json/mint_version_invalid +++ b/spec/mint_json/mint_version_invalid @@ -6,8 +6,8 @@ The mint-version field should be a string, but it's not: - ┌ mint.json:2:19 - ├──────────────────── + ┌ spec/fixtures/mint.json:2:19 + ├───────────────────────────── 1│ { 2│ "mint-version": 0 │ ⌃ diff --git a/spec/mint_json/mint_version_mismatch b/spec/mint_json/mint_version_mismatch index 479d80898..3af200237 100644 --- a/spec/mint_json/mint_version_mismatch +++ b/spec/mint_json/mint_version_mismatch @@ -16,7 +16,7 @@ but found instead: It is here: - ┌ mint.json:2:19 + ┌ spec/fixtures/mint.json:2:19 ├─────────────────────────────────────── 1│ { 2│ "mint-version": "0.0.0 <= v < 0.0.1" diff --git a/spec/mint_json/name_empty b/spec/mint_json/name_empty index d51a2ac06..ba3bd102c 100644 --- a/spec/mint_json/name_empty +++ b/spec/mint_json/name_empty @@ -6,8 +6,8 @@ The name field should not be empty: - ┌ mint.json:2:11 - ├─────────────── + ┌ spec/fixtures/mint.json:2:11 + ├───────────────────────────── 1│ { 2│ "name": "" │ ⌃ diff --git a/spec/mint_json/name_invalid b/spec/mint_json/name_invalid index 78949f512..3b1702cc6 100644 --- a/spec/mint_json/name_invalid +++ b/spec/mint_json/name_invalid @@ -6,8 +6,8 @@ The name field should be a string, but it's not: - ┌ mint.json:2:11 - ├─────────────── + ┌ spec/fixtures/mint.json:2:11 + ├───────────────────────────── 1│ { 2│ "name": 0 │ ⌃ diff --git a/spec/mint_json/root_invalid b/spec/mint_json/root_invalid index 86d33a6b9..64b38c201 100644 --- a/spec/mint_json/root_invalid +++ b/spec/mint_json/root_invalid @@ -4,6 +4,6 @@ The root item should be an object, but it's not: - ┌ mint.json:1:2 - ├────────────── + ┌ spec/fixtures/mint.json:1:2 + ├──────────────────────────── 1│ "" diff --git a/spec/mint_json/root_invalid_key b/spec/mint_json/root_invalid_key index 44cfea38d..d96f2d9ad 100644 --- a/spec/mint_json/root_invalid_key +++ b/spec/mint_json/root_invalid_key @@ -10,8 +10,8 @@ The root object has an invalid key: It is here: - ┌ mint.json:2:8 - ├────────────── + ┌ spec/fixtures/mint.json:2:8 + ├──────────────────────────── 1│ { 2│ "x": "y" │ ⌃ diff --git a/spec/mint_json/source_directories_empty b/spec/mint_json/source_directories_empty index 30882c6a8..b0162c4a3 100644 --- a/spec/mint_json/source_directories_empty +++ b/spec/mint_json/source_directories_empty @@ -9,8 +9,8 @@ file) which contain the source files of the application. The source-directories array should not be empty, but it is: - ┌ mint.json:2:25 - ├─────────────────────────── + ┌ spec/fixtures/mint.json:2:25 + ├───────────────────────────── 1│ { 2│ "source-directories": [] │ ⌃ diff --git a/spec/mint_json/source_directories_invalid b/spec/mint_json/source_directories_invalid index 91c50b4c3..5daef2712 100644 --- a/spec/mint_json/source_directories_invalid +++ b/spec/mint_json/source_directories_invalid @@ -9,8 +9,8 @@ file) which contain the source files of the application. The source-directories field should be an array, but it's not: - ┌ mint.json:2:25 - ├────────────────────────── + ┌ spec/fixtures/mint.json:2:25 + ├───────────────────────────── 1│ { 2│ "source-directories": 0 │ ⌃ diff --git a/spec/mint_json/source_directory_invalid b/spec/mint_json/source_directory_invalid index bb88b3740..08a9a51bd 100644 --- a/spec/mint_json/source_directory_invalid +++ b/spec/mint_json/source_directory_invalid @@ -6,8 +6,8 @@ All entries in the source-directories array should be string: - ┌ mint.json:2:26 - ├──────────────────────────── + ┌ spec/fixtures/mint.json:2:26 + ├───────────────────────────── 1│ { 2│ "source-directories": [0] │ ⌃ diff --git a/spec/mint_json/source_directory_not_exists b/spec/mint_json/source_directory_not_exists index a29cecd13..041526b2a 100644 --- a/spec/mint_json/source_directory_not_exists +++ b/spec/mint_json/source_directory_not_exists @@ -6,7 +6,7 @@ The source directory xxx does not exists: - ┌ mint.json:2:26 + ┌ spec/fixtures/mint.json:2:26 ├──────────────────────────────── 1│ { 2│ "source-directories": ["xxx"] diff --git a/spec/mint_json/test_directories_empty b/spec/mint_json/test_directories_empty index ad58e27ed..a88624722 100644 --- a/spec/mint_json/test_directories_empty +++ b/spec/mint_json/test_directories_empty @@ -9,8 +9,8 @@ file) which contain the test files of the application. The test-directories array should not be empty, but it is: - ┌ mint.json:2:23 - ├───────────────────────── + ┌ spec/fixtures/mint.json:2:23 + ├───────────────────────────── 1│ { 2│ "test-directories": [] │ ⌃ diff --git a/spec/mint_json/test_directories_invalid b/spec/mint_json/test_directories_invalid index 365b681c4..d91e29b76 100644 --- a/spec/mint_json/test_directories_invalid +++ b/spec/mint_json/test_directories_invalid @@ -9,8 +9,8 @@ file) which contain the test files of the application. The test-directories field should be an array, but it's not: - ┌ mint.json:2:23 - ├──────────────────────── + ┌ spec/fixtures/mint.json:2:23 + ├───────────────────────────── 1│ { 2│ "test-directories": 0 │ ⌃ diff --git a/spec/mint_json/test_directory_invalid b/spec/mint_json/test_directory_invalid index 57e4f36c7..35b60876a 100644 --- a/spec/mint_json/test_directory_invalid +++ b/spec/mint_json/test_directory_invalid @@ -6,8 +6,8 @@ All entries in the test-directories array should be string: - ┌ mint.json:2:24 - ├────────────────────────── + ┌ spec/fixtures/mint.json:2:24 + ├───────────────────────────── 1│ { 2│ "test-directories": [0] │ ⌃ diff --git a/spec/mint_json/test_directory_not_exists b/spec/mint_json/test_directory_not_exists index c9f70dc1b..97c9ef0b8 100644 --- a/spec/mint_json/test_directory_not_exists +++ b/spec/mint_json/test_directory_not_exists @@ -6,7 +6,7 @@ The test directory xxx does not exists: - ┌ mint.json:2:24 + ┌ spec/fixtures/mint.json:2:24 ├────────────────────────────── 1│ { 2│ "test-directories": ["xxx"] diff --git a/spec/mint_json_spec.cr b/spec/mint_json_spec.cr index 87f109ff2..f2c02c7b4 100644 --- a/spec/mint_json_spec.cr +++ b/spec/mint_json_spec.cr @@ -10,7 +10,7 @@ Dir File.read(file).split("-" * 80) begin - Mint::MintJson.new(source, "spec/fixtures", "mint.json") + Mint::MintJson.parse(contents: source, path: "spec/fixtures/mint.json") rescue error : Mint::Error result = error.to_terminal.to_s.uncolorize @@ -22,7 +22,7 @@ Dir it "non existent file" do begin - Mint::MintJson.from_file("test.json") + Mint::MintJson.parse("test.json") rescue error : Mint::Error error.to_terminal.to_s.uncolorize.should eq(<<-TEXT) ░ ERROR (MINT_JSON_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ diff --git a/src/commands/docs.cr b/src/commands/docs.cr index 3684fa4e7..a89139a55 100644 --- a/src/commands/docs.cr +++ b/src/commands/docs.cr @@ -23,13 +23,13 @@ module Mint directory = arguments.directory - jsons = [MintJson.parse_current] + jsons = [MintJson.current] jsons.concat(SourceFiles.packages) if flags.include_packages asts = jsons.map do |json| Ast.new.tap do |ast| - json.source_files.each do |file| + SourceFiles.source_files(json).each do |file| ast.merge(Parser.parse(File.read(file), file)) end end diff --git a/src/commands/format.cr b/src/commands/format.cr index d51f2e7b7..2ea08bf12 100644 --- a/src/commands/format.cr +++ b/src/commands/format.cr @@ -111,11 +111,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/core.cr b/src/core.cr index f02e3a101..dd90362de 100644 --- a/src/core.cr +++ b/src/core.cr @@ -4,7 +4,9 @@ module Mint bake_folder "../core/source" - class_getter json = MintJson.new(%({"name": "core"}), "core", "mint.json") + 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| 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/ls/completion_item/component.cr b/src/ls/completion_item/component.cr index d2a06d5ec..0aa711c53 100644 --- a/src/ls/completion_item/component.cr +++ b/src/ls/completion_item/component.cr @@ -11,7 +11,7 @@ module Mint .map do |property| default = Mint::Formatter - .new(workspace.json.formatter_config) + .new(workspace.json.formatter) .format!(property.default) .to_s .gsub("}", "\\}") diff --git a/src/mint_json.cr b/src/mint_json.cr index e952eb8b0..100d42a47 100644 --- a/src/mint_json.cr +++ b/src/mint_json.cr @@ -1,122 +1,67 @@ module Mint class MintJson - include Errorable - class Application - getter title, meta, icon, head, name, theme_color, display, orientation - getter css_prefix + 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( - @meta = {} of String => String, - @css_prefix : String? = nil, - @orientation = "", - @theme_color = "", - @display = "", - @title = "", - @name = "", - @head = "", - @icon = "" + *, + @orientation, + @theme_color, + @css_prefix, + @display, + @title, + @meta, + @name, + @head, + @icon ) end end - getter dependencies = [] of Installer::Dependency - getter formatter_config = Formatter::Config.new - getter parser = JSON::PullParser.new("{}") - getter application = Application.new - getter source_directories = %w[] - getter test_directories = %w[] - getter name = "" - - getter root : String - getter file : String - getter json : String - - def initialize(@json : String, @root : String, @file : String) - begin - @parser = JSON::PullParser.new(@json) - rescue exception : JSON::ParseException - error! :invalid_json do - block do - text "I could not parse the following" - bold "mint.json" - text "file:" - end - - snippet snippet_data(exception) - end - end - - parse_root + 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 initialize - @json = "" - @root = "" - @file = "" + def self.parse(contents : String, path : String) + Parser.parse(contents: contents, path: path) end - def self.from_file(path) - new File.read(path), File.dirname(path), path - rescue error : Error - raise error - rescue error - 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 error.to_s - end + def self.parse(path : String) + Parser.parse(path) end - def self.parse_current : MintJson - from_file(Path[Dir.current, "mint.json"].to_s) + def self.current : MintJson + parse(Path[Dir.current, "mint.json"].to_s) end - def self.parse_current? : MintJson? - parse_current + def self.current? : MintJson? + current rescue nil end - - def snippet_data(line_number : Int32, column_number : Int32) - position = - if line_number - 1 == 0 - 0 - else - @json - .lines[0..line_number - 2] - .reduce(0) { |acc, line| acc + line.size + 1 } - end + (column_number - 1) - - Error::SnippetData.new( - filename: @file, - input: @json, - to: position + 1, - from: position) - end - - def snippet_data(exception : JSON::ParseException) - snippet_data exception.location - end - - def snippet_data(location : Tuple(Int32, Int32)) - snippet_data location[0], location[1] - end - - def snippet_data - snippet_data @parser.location - end - - def source_files - glob = - source_directories.map { |dir| SourceFiles.glob_pattern(@root, dir) } - - Dir.glob(glob) - end end end diff --git a/src/mint_json/application.cr b/src/mint_json/application.cr index eb064eac3..800b1e4de 100644 --- a/src/mint_json/application.cr +++ b/src/mint_json/application.cr @@ -1,51 +1,51 @@ module Mint class MintJson - def parse_application - meta = {} of String => String - css_prefix = nil - orientation = "" - theme_color = "" - display = "" - title = "" - name = "" - icon = "" - head = "" + 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 + @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 + snippet key + snippet "It is here:", snippet_data + end end end - end - @application = Application.new( theme_color: theme_color, orientation: orientation, @@ -56,15 +56,16 @@ module Mint 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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/css_prefix.cr b/src/mint_json/application/css_prefix.cr index d1b87de12..135cfb72a 100644 --- a/src/mint_json/application/css_prefix.cr +++ b/src/mint_json/application/css_prefix.cr @@ -1,18 +1,20 @@ module Mint class MintJson - def parse_application_css_prefix - @parser.read_string_or_null - 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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/display.cr b/src/mint_json/application/display.cr index b6e049570..a34ca0748 100644 --- a/src/mint_json/application/display.cr +++ b/src/mint_json/application/display.cr @@ -1,36 +1,38 @@ module Mint class MintJson - DISPLAY_VALUES = - %w[fullscreen standalone minimal-ui browser] + class Parser + DISPLAY_VALUES = + %w[fullscreen standalone minimal-ui browser] - def parse_application_display - @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 + 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:" + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/head.cr b/src/mint_json/application/head.cr index 50aa5585b..66232ceb2 100644 --- a/src/mint_json/application/head.cr +++ b/src/mint_json/application/head.cr @@ -1,45 +1,47 @@ module Mint class MintJson - def parse_application_head - location = - @parser.location + class Parser + def parse_application_head : String + location = + @parser.location - head = - @parser.read_string + head = + @parser.read_string - path = - Path[@root, head].to_s + 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 + 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 + 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) + 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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/icon.cr b/src/mint_json/application/icon.cr index 2e71ebbc5..e06d3f3a3 100644 --- a/src/mint_json/application/icon.cr +++ b/src/mint_json/application/icon.cr @@ -1,44 +1,46 @@ module Mint class MintJson - def parse_application_icon - location = - @parser.location + class Parser + def parse_application_icon : String + location = + @parser.location - icon = - @parser.read_string + icon = + @parser.read_string - path = - Path[@root, icon].to_s + 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 + 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 + 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) + 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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/meta.cr b/src/mint_json/application/meta.cr index be4a4dc3c..f682bdb20 100644 --- a/src/mint_json/application/meta.cr +++ b/src/mint_json/application/meta.cr @@ -1,84 +1,86 @@ module Mint class MintJson - def parse_application_meta - meta = {} of String => String + 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 + @parser.read_object do |key| + value = + case key + when "keywords" + parse_application_meta_keywords + else + parse_application_meta_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:" + meta[key] = value end - snippet snippet_data - end - 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 - def parse_application_meta_value - @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:" + snippet snippet_data end - - snippet snippet_data end - end - def parse_application_meta_keywords - keywords = %w[] + 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 - @parser.read_array do - keywords << parse_application_meta_keyword + snippet snippet_data + end 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:" + def parse_application_meta_keywords : String + keywords = %w[] + + @parser.read_array do + keywords << parse_application_meta_keyword end - snippet snippet_data - end - 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 - def parse_application_meta_keyword - @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:" + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/name.cr b/src/mint_json/application/name.cr index 4a5d2537d..b9d494ce1 100644 --- a/src/mint_json/application/name.cr +++ b/src/mint_json/application/name.cr @@ -1,18 +1,20 @@ module Mint class MintJson - def parse_application_name - @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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/orientation.cr b/src/mint_json/application/orientation.cr index eec952228..73fe6c32c 100644 --- a/src/mint_json/application/orientation.cr +++ b/src/mint_json/application/orientation.cr @@ -1,40 +1,42 @@ module Mint class MintJson - ORIENTATION_VALUES = - %w[ - any natural landscape landscape-primary - landscape-secondary portrait portrait-primary - portrait-secondary - ] + class Parser + ORIENTATION_VALUES = + %w[ + any natural landscape landscape-primary + landscape-secondary portrait portrait-primary + portrait-secondary + ] - def parse_application_orientation - @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 + 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:" + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/theme_color.cr b/src/mint_json/application/theme_color.cr index b3c0f59da..e9c608c4f 100644 --- a/src/mint_json/application/theme_color.cr +++ b/src/mint_json/application/theme_color.cr @@ -1,18 +1,20 @@ module Mint class MintJson - def parse_application_theme_color - @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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/application/title.cr b/src/mint_json/application/title.cr index f5d38257c..b2a0c797a 100644 --- a/src/mint_json/application/title.cr +++ b/src/mint_json/application/title.cr @@ -1,36 +1,38 @@ module Mint class MintJson - def parse_application_title - location = - @parser.location + class Parser + def parse_application_title : String + location = + @parser.location - title = - @parser.read_string + 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 + 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? + 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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/dependencies.cr b/src/mint_json/dependencies.cr index c60732464..d549c0c9f 100644 --- a/src/mint_json/dependencies.cr +++ b/src/mint_json/dependencies.cr @@ -1,155 +1,161 @@ module Mint class MintJson - def parse_dependencies - @parser.location.try do |location| - @parser.read_object do |key| - @dependencies << parse_dependency(key) - end + class Parser + def parse_dependencies : Array(Installer::Dependency) + @parser.location.try do |location| + dependencies = [] of Installer::Dependency - error! :dependencies_empty do - block do - text "The" - bold "dependencies" - text "field lists all the dependencies for the application." + @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 "object should not be empty, but it is:" + text "field lists all the dependencies for the application." end - snippet snippet_data(location) - end if source_directories.empty? - end - rescue JSON::ParseException - error! :dependencies_invalid do - block do - text "The" - bold "dependencies" - text "field lists all the dependencies for the application." + snippet "It should be an object, but it's not:", snippet_data 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 + def parse_dependency(package : String) : Installer::Dependency + repository, constraint = nil, nil - error! :dependency_missing_repository do - block do - text "A" - bold "dependency object" - text "is missing the" - bold "repository" - text "field:" + @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 - snippet snippet_data - end unless repository + error! :dependency_missing_repository do + block do + text "A" + bold "dependency object" + text "is missing the" + bold "repository" + text "field:" + end - 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 repository - snippet snippet_data - end unless constraint + error! :dependency_missing_constraint do + block do + text "A" + bold "dependency object" + text "is missing the" + bold "constraint" + text "field:" + end - 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 + snippet snippet_data + end unless constraint - def parse_dependency_repository - @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:" + 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 - - snippet snippet_data end - end - def parse_dependency_constraint - location = - @parser.location - - raw = + 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 - match = - raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) + snippet snippet_data + end + end - constraint = - if match - lower = - Installer::Semver.parse?(match[1]) + def parse_dependency_constraint : Installer::Constraint + location = + @parser.location - upper = - Installer::Semver.parse?(match[2]) + raw = + @parser.read_string - Installer::SimpleConstraint.new(lower, upper) if upper && lower - else - match = - raw.match(/(.*?):(\d+\.\d+\.\d+)/) + match = + raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) + constraint = if match - version = + lower = + Installer::Semver.parse?(match[1]) + + upper = Installer::Semver.parse?(match[2]) - target = - match[1] + Installer::SimpleConstraint.new(lower, upper) if upper && lower + else + match = + raw.match(/(.*?):(\d+\.\d+\.\d+)/) + + if match + version = + Installer::Semver.parse?(match[2]) - Installer::FixedConstraint.new(version, target) if version + target = + match[1] + + Installer::FixedConstraint.new(version, target) if version + end 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" + 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" + 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 + 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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/formatter.cr b/src/mint_json/formatter.cr index 27cf0ec23..a9238c1ab 100644 --- a/src/mint_json/formatter.cr +++ b/src/mint_json/formatter.cr @@ -1,50 +1,52 @@ module Mint class MintJson - def parse_formatter - indent_size = 2 + 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 + @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 + snippet key + snippet "It is here:", snippet_data + end end end - end - @formatter_config = 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 + 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 + snippet snippet_data + end end - end - def parse_formatter_indent_size - @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 + 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 + snippet snippet_data + end end end end diff --git a/src/mint_json/mint_version.cr b/src/mint_json/mint_version.cr index c1e4c37b8..6d3eefcf3 100644 --- a/src/mint_json/mint_version.cr +++ b/src/mint_json/mint_version.cr @@ -1,61 +1,63 @@ module Mint class MintJson - def parse_mint_version - location = - @parser.location + class Parser + def parse_mint_version : Nil + location = + @parser.location - raw = - @parser.read_string + raw = + @parser.read_string - match = - raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) + match = + raw.match(/(\d+\.\d+\.\d+)\s*<=\s*v\s*<\s*(\d+\.\d+\.\d+)/) - constraint = - if match - lower = - Installer::Semver.parse?(match[1]) + constraint = + if match + lower = + Installer::Semver.parse?(match[1]) - upper = - Installer::Semver.parse?(match[2]) + upper = + Installer::Semver.parse?(match[2]) - Installer::SimpleConstraint.new(lower, upper) if upper && lower - end + 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 + 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 + 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")) + 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 + 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 "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 + snippet snippet_data + end end end end diff --git a/src/mint_json/name.cr b/src/mint_json/name.cr index e2181db75..4a283d071 100644 --- a/src/mint_json/name.cr +++ b/src/mint_json/name.cr @@ -1,30 +1,34 @@ module Mint class MintJson - def parse_name - location = - @parser.location + class Parser + def parse_name : String + location = + @parser.location - @name = - @parser.read_string + name = + @parser.read_string - error! :name_empty do - block do - text "The" - bold "name" - text "field should not be empty:" - end + 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? - 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(location) + end if name.empty? - snippet snippet_data + 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 diff --git a/src/mint_json/parser.cr b/src/mint_json/parser.cr new file mode 100644 index 000000000..98985dd27 --- /dev/null +++ b/src/mint_json/parser.cr @@ -0,0 +1,86 @@ +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) : MintJson + parse(contents: File.read(path), path: path) + 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 index b6ce2ec03..aee1b3883 100644 --- a/src/mint_json/root.cr +++ b/src/mint_json/root.cr @@ -1,32 +1,61 @@ module Mint class MintJson - def parse_root - @parser.read_object do |key| - case key - when "source-directories" - parse_source_directories - when "test-directories" - parse_test_directories - when "dependencies" - parse_dependencies - when "mint-version" - parse_mint_version - when "application" - parse_application - when "formatter" - parse_formatter - when "name" - parse_name - else - error! :root_invalid_key do - snippet "The root object has an invalid key:", key - snippet "It is here:", snippet_data + 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 - end - rescue JSON::ParseException - error! :root_invalid do - snippet "The root item should be an object, but it's not:", snippet_data + + 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 diff --git a/src/mint_json/source_directories.cr b/src/mint_json/source_directories.cr index fa1949e25..ac87340db 100644 --- a/src/mint_json/source_directories.cr +++ b/src/mint_json/source_directories.cr @@ -1,10 +1,35 @@ module Mint class MintJson - def parse_source_directories - @parser.location.try do |location| - @parser.read_array(&->parse_source_directory) + class Parser + def parse_source_directories : Array(String) + @parser.location.try do |location| + directories = %w[] - error! :source_directories_empty do + @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" @@ -15,61 +40,44 @@ module Mint block do text "The" bold "source-directories" - text "array should not be empty, but it is:" + text "field should be an array, but it's not:" end - snippet snippet_data(location) - end if source_directories.empty? - 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." + snippet snippet_data 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 - location = - @parser.location + def parse_source_directory : String + location = + @parser.location - directory = - @parser.read_string + directory = + @parser.read_string - path = - Path[@root, directory] + path = + Path[root, directory] - error! :source_directory_not_exists do - block do - text "The source directory" - bold directory - text "does not exists:" - end + 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) - 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 - source_directories << directory - rescue JSON::ParseException - error! :source_directory_invalid do - block do - text "All entries in the" - bold "source-directories" - text "array should be string:" + snippet snippet_data end - - snippet snippet_data end end end diff --git a/src/mint_json/test_directories.cr b/src/mint_json/test_directories.cr index eef6c90b3..7dc51e2dd 100644 --- a/src/mint_json/test_directories.cr +++ b/src/mint_json/test_directories.cr @@ -1,10 +1,35 @@ module Mint class MintJson - def parse_test_directories - @parser.location.try do |location| - @parser.read_array(&->parse_test_directory) + class Parser + def parse_test_directories : Array(String) + @parser.location.try do |location| + directories = %w[] - error! :test_directories_empty do + @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" @@ -15,61 +40,44 @@ module Mint block do text "The" bold "test-directories" - text "array should not be empty, but it is:" + text "field should be an array, but it's not:" end - snippet snippet_data(location) - end if test_directories.empty? - 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." + snippet snippet_data 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 - location = - @parser.location + def parse_test_directory : String + location = + @parser.location - directory = - @parser.read_string + directory = + @parser.read_string - path = - Path[@root, directory] + path = + Path[root, directory] - error! :test_directory_not_exists do - block do - text "The test directory" - bold directory - text "does not exists:" - end + 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) - 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 - test_directories << directory - rescue JSON::ParseException - error! :test_directory_invalid do - block do - text "All entries in the" - bold "test-directories" - text "array should be string:" + snippet snippet_data end - - snippet snippet_data end end end diff --git a/src/reactor.cr b/src/reactor.cr index 2db82e37a..c1ff8a137 100644 --- a/src/reactor.cr +++ b/src/reactor.cr @@ -25,7 +25,7 @@ module Mint in TypeChecker Bundler.new( artifacts: result.artifacts, - json: MintJson.new, + json: MintJson.current, config: Bundler::Config.new( generate_manifest: false, include_program: true, 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..efcbe029b 100644 --- a/src/test_runner.cr +++ b/src/test_runner.cr @@ -31,7 +31,7 @@ module Mint @files = Bundler.new( artifacts: workspace.type_checker.artifacts, - json: MintJson.new, + json: workspace.json, config: Bundler::Config.new( runtime_path: flags.runtime, generate_manifest: false, diff --git a/src/utils/source_files.cr b/src/utils/source_files.cr index 4fca210d2..fd458da99 100644 --- a/src/utils/source_files.cr +++ b/src/utils/source_files.cr @@ -2,20 +2,27 @@ module Mint module SourceFiles extend self + def source_files(json) + glob = + json.source_directories.map { |dir| glob_pattern(File.dirname(json.path), dir) } + + Dir.glob(glob) + end + def glob_pattern(*dirs : Path | String) Path[*dirs, "**", "*.mint"].to_posix.to_s end def tests MintJson - .parse_current + .current .test_directories .map { |dir| glob_pattern(dir) } end def current MintJson - .parse_current + .current .source_directories .map { |dir| glob_pattern(dir) } end @@ -25,7 +32,7 @@ module Mint Path[".", ".mint", "packages", "**", "mint.json"] Dir.glob(pattern).each do |file| - yield MintJson.new(File.read(file), File.dirname(file), file) + yield MintJson.parse(file) end end @@ -40,7 +47,7 @@ module Mint each_package do |json| dirs = json.source_directories.map do |dir| - glob_pattern(json.root, dir) + glob_pattern(File.dirname(json.path), dir) end package_dirs.concat dirs diff --git a/src/workspace.cr b/src/workspace.cr index ad66e633c..590140a9e 100644 --- a/src/workspace.cr +++ b/src/workspace.cr @@ -75,11 +75,11 @@ module Mint @json = FileUtils.cd(@root) do - MintJson.from_file(json_path) + MintJson.parse(json_path) end @formatter = - Mint::Formatter.new(json.formatter_config) + Mint::Formatter.new(json.formatter) @json_watcher = Watcher.new([json_path]) @@ -223,7 +223,7 @@ module Mint def format(file) Formatter - .new(json.formatter_config) + .new(json.formatter) .format(self[file]) end @@ -256,7 +256,7 @@ module Mint if format? formatted = Formatter - .new(json.formatter_config) + .new(json.formatter) .format(ast) if formatted != File.read(file) diff --git a/src/workspace_2.cr b/src/workspace_2.cr index 9b37cf617..ca922bfc3 100644 --- a/src/workspace_2.cr +++ b/src/workspace_2.cr @@ -71,7 +71,7 @@ module Mint Workspace2.new(check) @json = - MintJson.parse_current + MintJson.current @globs = [ From 4bfddd88e7f773e449218270bcf73b7d7795e2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Tue, 24 Sep 2024 14:52:11 +0200 Subject: [PATCH 04/10] Refactor SourceFiles module. --- src/commands/docs.cr | 20 +++++----- src/commands/format.cr | 5 +-- src/commands/lint.cr | 10 ++--- src/commands/tool/loc.cr | 2 +- src/utils/source_files.cr | 71 ++++++++++++++++++----------------- src/utils/terminal_snippet.cr | 9 ++++- 6 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/commands/docs.cr b/src/commands/docs.cr index a89139a55..3097a80a6 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.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| + SourceFiles.sources(jsons).map do |file| Ast.new.tap do |ast| - SourceFiles.source_files(json).each do |file| - ast.merge(Parser.parse(File.read(file), file)) - end + ast.merge(Parser.parse(File.read(file), file)) end end diff --git a/src/commands/format.cr b/src/commands/format.cr index 2ea08bf12..71eb19b2c 100644 --- a/src/commands/format.cr +++ b/src/commands/format.cr @@ -85,10 +85,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)) - end + arguments.pattern.presence || json.try(&->SourceFiles.all(MintJson)) Dir.glob(pattern || "").map do |file| artifact = diff --git a/src/commands/lint.cr b/src/commands/lint.cr index ee6afa782..baaee6095 100644 --- a/src/commands/lint.cr +++ b/src/commands/lint.cr @@ -10,13 +10,11 @@ module Mint default: false def run - ast = - Ast.new.merge(Core.ast) + ast = Ast.new.merge(Core.ast) + json = MintJson.current + errors = [] of Error - errors = - [] of Error - - Dir.glob(SourceFiles.all).reduce(ast) do |memo, file| + Dir.glob(SourceFiles.all(json)).reduce(ast) do |memo, file| begin memo.merge(Parser.parse(file)) rescue error : Error diff --git a/src/commands/tool/loc.cr b/src/commands/tool/loc.cr index 5a8c04f93..0cf6debd3 100644 --- a/src/commands/tool/loc.cr +++ b/src/commands/tool/loc.cr @@ -19,7 +19,7 @@ module Mint end private def files - Dir.glob(SourceFiles.all).to_a + Dir.glob(SourceFiles.all(MintJson.current)).to_a end private def count diff --git a/src/utils/source_files.cr b/src/utils/source_files.cr index fd458da99..498f37312 100644 --- a/src/utils/source_files.cr +++ b/src/utils/source_files.cr @@ -2,57 +2,58 @@ module Mint module SourceFiles extend self - def source_files(json) - glob = - json.source_directories.map { |dir| glob_pattern(File.dirname(json.path), dir) } - - Dir.glob(glob) + def sources(json : MintJson) : Array(String) + json + .source_directories + .map { |dir| glob_pattern(File.dirname(json.path), dir) } + .try(&->Dir.glob(Array(String))) end - def glob_pattern(*dirs : Path | String) - Path[*dirs, "**", "*.mint"].to_posix.to_s + def tests(json : MintJson) : Array(String) + json + .test_directories + .map { |dir| glob_pattern(File.dirname(json.path), dir) } + .try(&->Dir.glob(Array(String))) end - def tests - MintJson - .current - .test_directories - .map { |dir| glob_pattern(dir) } + def all(json : MintJson) : Array(String) + sources(json) + tests(json) end - def current - MintJson - .current - .source_directories - .map { |dir| glob_pattern(dir) } + def sources(jsons : Array(MintJson)) : Array(String) + jsons.flat_map(&->sources(MintJson)) end - def each_package(&) - pattern = - Path[".", ".mint", "packages", "**", "mint.json"] + def tests(jsons : Array(MintJson)) : Array(String) + jsons.flat_map(&->tests(MintJson)) + end - Dir.glob(pattern).each do |file| - yield MintJson.parse(file) - end + def all(jsons : Array(MintJson)) : Array(String) + sources(jsons) + tests(jsons) end - def packages - ([] of MintJson).tap do |package_definitions| - each_package { |json| package_definitions << json } + def packages(json : MintJson, *, include_self : Bool = false) + (include_self ? [json] : [] of MintJson).tap do |jsons| + each_package(json) do |package_json| + jsons << package_json + end end end - def all - current.dup.tap do |package_dirs| - each_package do |json| - dirs = - json.source_directories.map do |dir| - glob_pattern(File.dirname(json.path), dir) - end + private def each_package(json : MintJson, &) + pattern = + Path[ + File.dirname(json.path), + ".", ".mint", "packages", "**", "mint.json", + ] - package_dirs.concat dirs - end + Dir.glob(pattern).each do |file| + yield MintJson.parse(file) 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 From e4ff5729a938de011dd5a9687576f04abbfe973a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Mon, 30 Sep 2024 15:38:10 +0200 Subject: [PATCH 05/10] Workspace refactor part I. --- spec_cli/build_spec.cr | 4 +- spec_cli/spec_helper.cr | 8 +-- spec_cli/test_spec.cr | 56 +++++++++++++++++++ src/bundler.cr | 8 +-- src/cli.cr | 3 -- src/command.cr | 20 ++++--- src/commands/build.cr | 36 +++++-------- src/commands/docs.cr | 8 +-- src/commands/format.cr | 4 +- src/commands/lint.cr | 17 +++--- src/commands/start.cr | 3 +- src/commands/test.cr | 3 +- src/commands/tool/loc.cr | 23 ++++---- src/commands/tool/ls.cr | 2 +- src/compiler.cr | 9 +++- src/compiler/decoder.cr | 2 +- src/ls/server.cr | 2 +- src/mint_json.cr | 4 +- src/reactor.cr | 51 +++--------------- src/test_runner.cr | 29 +++++----- src/utils/source_files.cr | 38 +++++-------- src/workspace_2.cr | 111 ++++++++++++++++++++------------------ 22 files changed, 224 insertions(+), 217 deletions(-) create mode 100644 spec_cli/test_spec.cr 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/bundler.cr b/src/bundler.cr index cadb5d0af..b0b2b1ccc 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] = diff --git a/src/cli.cr b/src/cli.cr index 941d55c85..f8e4dec77 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -3,9 +3,6 @@ require "./command" require "./commands/**" module Mint - class CliException < Exception - end - class Cli < Admiral::Command include Command diff --git a/src/command.cr b/src/command.cr index 4c4357ccb..24e87ee1a 100644 --- a/src/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,8 +83,8 @@ module Mint exit(1) end - def check_dependencies!(dependencies : Array(Installer::Dependency)) - dependencies.each do |dependency| + def check_dependencies! + MintJson.current.dependencies.each do |dependency| next if Dir.exists?(".mint/packages/#{dependency.name}") terminal.puts "#{COG} Ensuring dependencies..." @@ -93,8 +98,7 @@ module Mint Installer.new break else - terminal.print "#{WARNING} Missing dependencies..." - raise CliException.new + error "#{WARNING} Missing dependencies...", terminal.position end end end diff --git a/src/commands/build.cr b/src/commands/build.cr index 4b734311e..40cdb3bb8 100644 --- a/src/commands/build.cr +++ b/src/commands/build.cr @@ -43,23 +43,19 @@ 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. - check_dependencies!(workspace.json.dependencies) - - # On any change we copy the build to the dist directory. - workspace.on("change") do |result| + 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, + watch: flags.watch, + format: false, + ) do |result| terminal.reset if flags.watch case result - in Ast + in TypeChecker terminal.measure %(#{COG} Clearing the "#{DIST_DIR}" directory...) do FileUtils.rm_rf DIST_DIR end @@ -67,8 +63,8 @@ module Mint files = terminal.measure "#{COG} Building..." do Bundler.new( - artifacts: workspace.type_checker.artifacts, - json: workspace.json, + artifacts: result.artifacts, + json: MintJson.current, config: Bundler::Config.new( generate_manifest: flags.generate_manifest, skip_icons: flags.skip_icons, @@ -121,14 +117,8 @@ module Mint end end - # Do the initial parsing and type checking. - workspace.update_cache - # 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 3097a80a6..c7bad32c3 100644 --- a/src/commands/docs.cr +++ b/src/commands/docs.cr @@ -31,7 +31,7 @@ module Mint end asts = - SourceFiles.sources(jsons).map do |file| + Dir.glob(SourceFiles.globs(jsons)).map do |file| Ast.new.tap do |ast| ast.merge(Parser.parse(File.read(file), file)) end @@ -45,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 71eb19b2c..2755dcc18 100644 --- a/src/commands/format.cr +++ b/src/commands/format.cr @@ -85,7 +85,9 @@ module Mint private def format_files pattern = - arguments.pattern.presence || json.try(&->SourceFiles.all(MintJson)) + arguments.pattern.presence || json.try do |item| + SourceFiles.globs(item, include_tests: true) + end Dir.glob(pattern || "").map do |file| artifact = diff --git a/src/commands/lint.cr b/src/commands/lint.cr index baaee6095..23671fa79 100644 --- a/src/commands/lint.cr +++ b/src/commands/lint.cr @@ -14,15 +14,16 @@ module Mint json = MintJson.current errors = [] of Error - Dir.glob(SourceFiles.all(json)).reduce(ast) do |memo, file| - begin - memo.merge(Parser.parse(file)) - rescue error : Error - errors << error - end + 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 - memo - end + memo + end begin TypeChecker.new(ast).tap(&.check) 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/loc.cr b/src/commands/tool/loc.cr index 0cf6debd3..afca3a398 100644 --- a/src/commands/tool/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(MintJson.current)).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/tool/ls.cr b/src/commands/tool/ls.cr index 67de589ed..0f2d6d8b4 100644 --- a/src/commands/tool/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/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/ls/server.cr b/src/ls/server.cr index 7e9fb4515..30b593f68 100644 --- a/src/ls/server.cr +++ b/src/ls/server.cr @@ -64,7 +64,7 @@ module Mint end def nodes_at_path(path : String) - Mint::Workspace[path] + Workspace[path] .ast .nodes .select(&.file.path.==(path)) diff --git a/src/mint_json.cr b/src/mint_json.cr index 100d42a47..3de5059a2 100644 --- a/src/mint_json.cr +++ b/src/mint_json.cr @@ -46,11 +46,11 @@ module Mint ) end - def self.parse(contents : String, path : String) + def self.parse(contents : String, path : String) : MintJson Parser.parse(contents: contents, path: path) end - def self.parse(path : String) + def self.parse(path : String) : MintJson Parser.parse(path) end diff --git a/src/reactor.cr b/src/reactor.cr index c1ff8a137..e7724a3b4 100644 --- a/src/reactor.cr +++ b/src/reactor.cr @@ -19,7 +19,13 @@ module Mint @sockets = [] of HTTP::WebSocket def initialize(*, @host, @port, @format, @reload) - FileWorkspace.new(check: Check::Environment, format: format?) do |result| + FileWorkspace.new( + path: Path[Dir.current, "mint.json"].to_s, + check: Check::Environment, + include_tests: false, + format: format?, + watch: true + ) do |result| @files = case result in TypeChecker @@ -41,50 +47,9 @@ module Mint error(result) end - @sockets.each(&.send("reload")) + @sockets.each(&.send("reload")) if reload? end - # # 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 - - # # Do the initial parsing and type checking and start wathing for changes. - # workspace.update_cache - # workspace.watch - # The websocket handle saves the sockets when they connect and # removes them when they disconnect. websocket_handler = diff --git a/src/test_runner.cr b/src/test_runner.cr index efcbe029b..6264523a2 100644 --- a/src/test_runner.cr +++ b/src/test_runner.cr @@ -21,17 +21,19 @@ 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| + FileWorkspace.new( + path: Path[Dir.current, "mint.json"].to_s, + check: Check::Environment, + include_tests: true, + format: false, + watch: @watch + ) do |result| case result - in Ast + in TypeChecker @files = Bundler.new( - artifacts: workspace.type_checker.artifacts, - json: workspace.json, + artifacts: result.artifacts, + json: MintJson.current, config: Bundler::Config.new( runtime_path: flags.runtime, generate_manifest: false, @@ -42,8 +44,9 @@ module Mint optimize: false, relative: false, test: { - url: "ws://#{flags.browser_host}:#{flags.browser_port}/", - id: "", + url: "ws://#{flags.browser_host}:#{flags.browser_port}/", + glob: arguments.test || "**/*", + id: "", }) ).bundle @@ -119,14 +122,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/utils/source_files.cr b/src/utils/source_files.cr index 498f37312..56c74ef6c 100644 --- a/src/utils/source_files.cr +++ b/src/utils/source_files.cr @@ -2,37 +2,25 @@ module Mint module SourceFiles extend self - def sources(json : MintJson) : Array(String) - json - .source_directories - .map { |dir| glob_pattern(File.dirname(json.path), dir) } - .try(&->Dir.glob(Array(String))) + def globs(jsons : Array(MintJson), *, include_tests = false) : Array(String) + jsons.flat_map { |json| globs(json, include_tests: include_tests) } end - def tests(json : MintJson) : Array(String) - json - .test_directories - .map { |dir| glob_pattern(File.dirname(json.path), dir) } - .try(&->Dir.glob(Array(String))) + 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 all(json : MintJson) : Array(String) - sources(json) + tests(json) + 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 sources(jsons : Array(MintJson)) : Array(String) - jsons.flat_map(&->sources(MintJson)) - end - - def tests(jsons : Array(MintJson)) : Array(String) - jsons.flat_map(&->tests(MintJson)) - end - - def all(jsons : Array(MintJson)) : Array(String) - sources(jsons) + tests(jsons) - end - - def packages(json : MintJson, *, include_self : Bool = false) + 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 diff --git a/src/workspace_2.cr b/src/workspace_2.cr index ca922bfc3..536c754f8 100644 --- a/src/workspace_2.cr +++ b/src/workspace_2.cr @@ -58,82 +58,87 @@ module Mint end class FileWorkspace + enum Action + Compile + Reset + end + + getter? include_tests : Bool = false getter? format : Bool - getter check : Check + getter path : String def initialize( *, - @check : Check, + @include_tests : Bool, @format : Bool, + @path : String, + check : Check, + watch : Bool, &@listener : TypeChecker | Error -> Nil ) - @workspace = - Workspace2.new(check) - - @json = - MintJson.current + @workspace = Workspace2.new(check) + @watcher = Watcher.new(%w[]) - @globs = - [ - ".mint/**/*.mint", - ".mint/**/mint.json", - "**/*.mint", - "**/mint.json", - ".env", - ] - - @watcher = - Watcher.new(@globs) - - spawn { @watcher.watch(&->update(Array(String))) } + reset(!watch) + spawn { @watcher.watch(&->update(Array(String))) } if watch end - def reset + def reset(process : Bool = true) @workspace.clear - update(Dir.glob(@globs.select(&.ends_with?(".mint")))) + + @watcher.pattern = globs = + SourceFiles.everything( + MintJson.parse(@path), + include_tests: @include_tests) + + update(Dir.glob(globs.select(&.ends_with?(".mint")))) if process end def update(files : Array(String)) - process = true - - files.each do |file| - if File.extname(file) == ".mint" - if File.exists?(file) - contents = - File.read(file) - - if format? - if ast = @workspace.ast?(file) - formatted = - Formatter.new.format(ast) - - if formatted != contents - File.write(file, formatted) - - # Since formatting a file will trigger another change we skip - # processing this file and don't trigger type checking. - process = false - next + actions = [] of Action + + Logger.log "Parsing files" do + files.each do |file| + if File.extname(file) == ".mint" + if File.exists?(file) + contents = + File.read(file) + + if format? + if ast = @workspace.ast?(file) + formatted = + Formatter.new.format(ast) + + if formatted != contents + File.write(file, formatted) + end end end + + @workspace.update(contents, file) + else + @workspace.delete(file) end - @workspace.update(contents, file) + actions << Action::Compile else - @workspace.delete(file) - end - else - # We need to do a reset because: - # 1. packages could have been added or removed - # 2. source directories could have been added or removed - case File.basename(file) - when "mint.json" - reset + # 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 << Action::Reset + end end end end - @listener.call(@workspace.process) if process + if actions.includes?(Action::Reset) + reset + else + @listener.call(Logger.log "Type Checking" { @workspace.process }) + end end end end From 6be4b24ea25c93ff832fd26d06a4e245c5a12551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Wed, 2 Oct 2024 11:39:13 +0200 Subject: [PATCH 06/10] Workspace refactor part II. --- src/commands/build.cr | 114 +++++++++++++------------- src/core.cr | 6 +- src/logger.cr | 2 + src/reactor.cr | 49 +++++------ src/test_runner.cr | 72 ++++++++--------- src/workspace_2.cr | 184 +++++++++++++++++++++++++++++++----------- 6 files changed, 264 insertions(+), 163 deletions(-) diff --git a/src/commands/build.cr b/src/commands/build.cr index 40cdb3bb8..2b0409317 100644 --- a/src/commands/build.cr +++ b/src/commands/build.cr @@ -51,71 +51,71 @@ module Mint include_tests: false, watch: flags.watch, format: false, - ) do |result| - terminal.reset if flags.watch + 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 + 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: 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 + 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) + 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 - in Error - terminal.print result.to_terminal - end - end + end) # Start wathing for changes if the flag is set. sleep if flags.watch diff --git a/src/core.cr b/src/core.cr index dd90362de..dc2ae652d 100644 --- a/src/core.cr +++ b/src/core.cr @@ -4,13 +4,17 @@ module Mint bake_folder "../core/source" + 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/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/reactor.cr b/src/reactor.cr index e7724a3b4..27c355cf5 100644 --- a/src/reactor.cr +++ b/src/reactor.cr @@ -24,31 +24,32 @@ module Mint check: Check::Environment, include_tests: false, format: format?, - watch: true - ) do |result| - @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 + watch: true, + 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 - @sockets.each(&.send("reload")) if reload? - end + terminal.puts "#{COG} Compiled..." + @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/test_runner.cr b/src/test_runner.cr index 6264523a2..21fe04105 100644 --- a/src/test_runner.cr +++ b/src/test_runner.cr @@ -26,44 +26,44 @@ module Mint check: Check::Environment, include_tests: true, format: false, - watch: @watch - ) do |result| - 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}") + watch: @watch, + 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| diff --git a/src/workspace_2.cr b/src/workspace_2.cr index 536c754f8..62e64ea95 100644 --- a/src/workspace_2.cr +++ b/src/workspace_2.cr @@ -5,59 +5,153 @@ module Mint Unreachable end + class LSWorkspace + delegate :artifacts, :ast, :update, :delete, :process, to: @cache + + def initialize(uri : String) + @cache = + if uri.starts_with?("sandbox://") + SandboxWorkspace.new(Check::All) + else + FileWorkspace.new( + include_tests: false, + check: Check::All, + listener: nil, + format: false, + watch: true, + path: uri) + end + end + + def format(uri : String) : String? + Formatter.new.format(ast) if ast = ast(uri) + end + end + class Workspace2 - # Stores the AST (or error) of the file at the given path. - @cache : Hash(String, Ast | Error) = {} of String => Ast | Error + class Cache + # Stores the AST (or error) of the file at the given path. + @cache : Hash(String, Ast | Error) = {} of String => Ast | Error + + def initialize(@check : Check) + end + + def update(contents : String, path : String) + @cache[path] = Parser.parse?(contents, path) + end + + def delete(path : String) + @cache.delete(path) + end + + def ast(path : String) : Ast | Error | Nil + @cache[path]? + end - def initialize(@check : Check) + def clear + @cache.clear + end + + def process + errors = + @cache.values.select(Error) + + if error = errors.first? + error + else + 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). + item.merge(Core.ast) unless item.type_definitions.index(&.name.==("Maybe")) + end + .normalize + + TypeChecker.new( + check_everything: @check.unreachable?, + check_env: @check.environment?, + ast: ast + ).tap(&.check) + end + rescue error : Error + error + end end - def update(contents : String, path : String) - @cache[path] = Parser.parse?(contents, path) + @result : TypeChecker | Error = Error.new(:unitialized_workspace) + @listener : Proc(TypeChecker | Error, Nil) | Nil + @cache : Cache + + @id = 0 + + def initialize + @cache = Workspace.new(Check::All) end - def delete(path : String) - @cache.delete(path) + def artifacts : Artifacts | Error + case result + in TypeChecker + result.artifacts + in Error + result + end end - def ast?(path : String) - case ast = @cache[path]? - when Ast - ast + def ast : Ast | Error + case result + in TypeChecker + result.artifacts.ast + in Error + result end end - def clear - @cache.clear + def ast(path : String) : Ast | Error | Nil + @cache.ast(path) end - def process - errors = - @cache.values.select(Error) + def update(contents : String, path : String) + @cache.update(contents, path) + notify + end - if error = errors.first? - error + def delete(path : String) + @cache.delete(path) + notify + end + + # This is a debounced method so it type checks after + # all changes have processed. + def notify + if @async + id = @id += 1 + + spawn do + sleep 0.5.seconds + next if id != @id + process + @id = 0 + end else - ast = - @cache - .values - .select(Ast) - .reduce(Ast.new) { |memo, item| memo.merge item } - .tap(&.merge(Core.ast)) - .tap(&.normalize) - - TypeChecker.new( - check_everything: @check.unreachable?, - check_env: @check.environment?, - ast: ast - ).tap(&.check) + process end - rescue error : Error - error end + + def process + @result = Logger.log "Type Checking" { @cache.process } + @listener.try(&.call(@result)) + end + end + + class SandboxWorkspace < Workspace2 + @async = true end - class FileWorkspace + class FileWorkspace < Workspace2 enum Action Compile Reset @@ -69,22 +163,23 @@ module Mint def initialize( *, + @listener : Proc(TypeChecker | Error, Nil), @include_tests : Bool, @format : Bool, @path : String, check : Check, - watch : Bool, - &@listener : TypeChecker | Error -> Nil + watch : Bool ) - @workspace = Workspace2.new(check) + @cache = Cache.new(check) @watcher = Watcher.new(%w[]) + @async = watch reset(!watch) spawn { @watcher.watch(&->update(Array(String))) } if watch end def reset(process : Bool = true) - @workspace.clear + @cache.clear @watcher.pattern = globs = SourceFiles.everything( @@ -101,11 +196,12 @@ module Mint files.each do |file| if File.extname(file) == ".mint" if File.exists?(file) - contents = - File.read(file) + contents = File.read(file) + @cache.update(contents, file) if format? - if ast = @workspace.ast?(file) + case ast = @cache.ast(file) + when Ast formatted = Formatter.new.format(ast) @@ -114,10 +210,8 @@ module Mint end end end - - @workspace.update(contents, file) else - @workspace.delete(file) + @cache.delete(file) end actions << Action::Compile @@ -137,7 +231,7 @@ module Mint if actions.includes?(Action::Reset) reset else - @listener.call(Logger.log "Type Checking" { @workspace.process }) + process end end end From 3f1f1598dd4386df34a00148c333c59bdc595994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Fri, 4 Oct 2024 06:07:32 +0200 Subject: [PATCH 07/10] Workspace refactor part III. --- src/ls/server.cr | 2 ++ src/workspace_2.cr | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/ls/server.cr b/src/ls/server.cr index 30b593f68..14e8f6761 100644 --- a/src/ls/server.cr +++ b/src/ls/server.cr @@ -28,6 +28,8 @@ module Mint property params : LSP::InitializeParams? = nil + @@workspaces = {} of String => LSWorkspace + # Logs the given stack. def debug_stack(stack : Array(Ast::Node)) stack.each_with_index do |item, index| diff --git a/src/workspace_2.cr b/src/workspace_2.cr index 62e64ea95..a3b24cedf 100644 --- a/src/workspace_2.cr +++ b/src/workspace_2.cr @@ -6,10 +6,10 @@ module Mint end class LSWorkspace - delegate :artifacts, :ast, :update, :delete, :process, to: @cache + delegate :artifacts, :ast, :update, :delete, :process, to: @workspace def initialize(uri : String) - @cache = + @workspace = if uri.starts_with?("sandbox://") SandboxWorkspace.new(Check::All) else @@ -28,6 +28,10 @@ module Mint end end + # A workspace represents a Mint project either in the file system or in + # memory.A workspace provides up to date, type checked artifacts which + # can be used in other places (bundler, test runner, development server, + # language server, etc...) class Workspace2 class Cache # Stores the AST (or error) of the file at the given path. @@ -82,10 +86,16 @@ module Mint end end + # The current artifacts of the program or the current error. @result : TypeChecker | Error = Error.new(:unitialized_workspace) + + # The listener to call when a new result is ready. @listener : Proc(TypeChecker | Error, Nil) | Nil + + # The AST cache. @cache : Cache + # The ID for debouncing the update. @id = 0 def initialize @@ -147,10 +157,13 @@ module Mint end end + # A sandbox workspace is just an in memory workspace. class SandboxWorkspace < Workspace2 @async = true end + # A file workspace watches the appropriate files of a project and recompiles + # it when they change. class FileWorkspace < Workspace2 enum Action Compile From 2ac951b123f863a03d33849d794df5f269e4b032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Mon, 7 Oct 2024 15:00:24 +0200 Subject: [PATCH 08/10] Workspace refactor part IV. --- spec/language_server/code_actions/module | 55 +++++++++++ spec/language_server/code_actions/provider | 59 ++++++++++++ spec/language_server/completion/function | 37 ++++++++ .../language_server/completion/html_component | 33 +++++++ .../html_component_with_snippet_support | 49 ++++++++++ spec/language_server/completion_spec.cr | 92 ------------------- spec/language_server/ls_spec.cr | 61 +++++++++--- spec/mint_json_spec.cr | 14 +++ src/ast.cr | 13 +++ src/ext/file.cr | 14 +++ src/ls/code_action.cr | 38 +++++--- src/ls/completion.cr | 88 +++++++++++------- src/ls/completion_item/argument.cr | 2 +- src/ls/completion_item/component.cr | 10 +- src/ls/completion_item/constant.cr | 2 +- src/ls/completion_item/function.cr | 2 +- src/ls/completion_item/get.cr | 2 +- src/ls/completion_item/property.cr | 2 +- src/ls/completion_item/style.cr | 2 +- src/ls/completions/component.cr | 2 +- src/ls/completions/functions.cr | 2 +- src/ls/completions/module.cr | 2 +- src/ls/completions/store.cr | 2 +- src/ls/completions/style.cr | 2 +- src/ls/completions/type_definition.cr | 2 +- src/ls/did_change.cr | 13 ++- src/ls/hover.cr | 61 +++++++----- src/ls/hover/access.cr | 10 +- src/ls/hover/argument.cr | 8 +- src/ls/hover/component.cr | 25 +++++ src/ls/hover/css_definition.cr | 6 +- src/ls/hover/function.cr | 19 ++-- src/ls/hover/get.cr | 15 ++- src/ls/hover/html_attribute.cr | 8 +- src/ls/hover/html_component.cr | 30 ++---- src/ls/hover/html_element.cr | 6 +- src/ls/hover/property.cr | 10 +- src/ls/hover/state.cr | 10 +- src/ls/hover/statement.cr | 12 ++- src/ls/hover/string_literal.cr | 6 +- src/ls/hover/type.cr | 13 ++- src/ls/hover/type_definition.cr | 8 +- src/ls/hover/type_destructuring.cr | 10 +- src/ls/hover/type_variant.cr | 11 ++- src/ls/semantic_tokens.cr | 88 +++++++++--------- src/ls/server.cr | 21 ++++- src/lsp/server.cr | 5 +- src/mint_json.cr | 8 +- src/mint_json/parser.cr | 18 +++- src/workspace_2.cr | 77 ++++++++++++---- 50 files changed, 744 insertions(+), 341 deletions(-) create mode 100644 spec/language_server/code_actions/module create mode 100644 spec/language_server/code_actions/provider create mode 100644 spec/language_server/completion/function create mode 100644 spec/language_server/completion/html_component create mode 100644 spec/language_server/completion/html_component_with_snippet_support delete mode 100644 spec/language_server/completion_spec.cr create mode 100644 src/ls/hover/component.cr 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/ls_spec.cr b/spec/language_server/ls_spec.cr index be40db072..64b1b97f6 100644 --- a/spec/language_server/ls_spec.cr +++ b/spec/language_server/ls_spec.cr @@ -1,11 +1,18 @@ require "../spec_helper" +struct CompletionResult + include JSON::Serializable + + getter result : Array(LSP::CompletionItem) + getter id : Int32 +end + def clean_json(workspace : Workspace, path : String) path.strip.gsub("\#{root_path}", workspace.root_path) end Dir - .glob("./spec/language_server/{hover,semantic_tokens}/**/*") + .glob("./spec/language_server/{hover,completion,code_actions,semantic_tokens}/**/*") .select! { |file| File.file?(file) } .sort! .each do |file| @@ -16,18 +23,30 @@ Dir position = 0 requests = [] of String - responses = [] of String + responses = [] of {String, Int32 | Nil, String} - contents.scan(/^\-+(\w+)( [\w.]+)?/m) do |match| - text = contents[position, match.begin - position] + 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.strip + workspace.file match[2].strip, text when "request" requests << clean_json(workspace, text) when "response" - responses << clean_json(workspace, text) + 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 @@ -41,16 +60,30 @@ Dir actual_responses = lsp_json(requests) responses.each do |expected_response| - expected_id = JSON.parse(expected_response)["id"].as_i + 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 + 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) - begin - expected_response.should eq(actual_response) - rescue error - fail diff(actual_response, expected_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 end diff --git a/spec/mint_json_spec.cr b/spec/mint_json_spec.cr index f2c02c7b4..6aeb8042b 100644 --- a/spec/mint_json_spec.cr +++ b/spec/mint_json_spec.cr @@ -33,3 +33,17 @@ it "non existent file" do 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_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 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/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/ls/code_action.cr b/src/ls/code_action.cr index 645e96bc6..847f9c634 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.ws(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/completion.cr b/src/ls/completion.cr index c162dcb84..5105680dc 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 + def initialize( + *, + @type_checker : TypeChecker, + @workspace : FileWorkspace, + @snippet_support : Bool + ) end - def workspace - Mint::Workspace[params.path] - 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,41 @@ 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 + + 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.ws(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/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 0aa711c53..7110ac8c2 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,12 @@ module Mint .reject(&.name.value.==("children")) .map do |property| default = - Mint::Formatter - .new(workspace.json.formatter) - .format!(property.default) - .to_s + @workspace + .format(property.default) .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/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/did_change.cr b/src/ls/did_change.cr index 91dc32add..a297791ae 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 = - server.workspace(uri) - - workspace.update(params.content_changes.first.text, uri.path) + server + .ws(path) + .update(params.content_changes.first.text, path) end end end diff --git a/src/ls/hover.cr b/src/ls/hover.cr index 027fb4b9f..11615cb7a 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.ws(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 59e3ea8a2..c1ec7e98d 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 = - server.workspace(uri)[uri.path.to_s] + data = + case ast = server.ws(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 14e8f6761..66f8ea3fd 100644 --- a/src/ls/server.cr +++ b/src/ls/server.cr @@ -8,11 +8,11 @@ module Mint "exit" => Exit, # Text document related methods + "textDocument/completion" => CompletionRequest, "textDocument/willSaveWaitUntil" => WillSaveWaitUntil, "textDocument/semanticTokens/full" => SemanticTokens, "textDocument/foldingRange" => FoldingRange, "textDocument/formatting" => Formatting, - "textDocument/completion" => Completion, "textDocument/codeAction" => CodeAction, "textDocument/definition" => Definition, "textDocument/didChange" => DidChange, @@ -28,7 +28,7 @@ module Mint property params : LSP::InitializeParams? = nil - @@workspaces = {} of String => LSWorkspace + @@workspaces = {} of String => FileWorkspace # Logs the given stack. def debug_stack(stack : Array(Ast::Node)) @@ -71,6 +71,23 @@ module Mint .nodes .select(&.file.path.==(path)) end + + # TODO: Rename to workspace + def ws(path : String) : FileWorkspace + base = + File.find_in_ancestors(path, "mint.json").to_s + + # TODO: Turn on `watch` when watcher supports direct update on + # initialize. + @@workspaces[base] ||= + FileWorkspace.new( + include_tests: true, + check: Check::Unreachable, + listener: nil, + format: false, + watch: false, + path: base) + end end end end diff --git a/src/lsp/server.cr b/src/lsp/server.cr index 6f357b91f..68a0afd79 100644 --- a/src/lsp/server.cr +++ b/src/lsp/server.cr @@ -97,8 +97,9 @@ module LSP end end rescue error - log(error.to_s) - error.backtrace?.try(&.each { |item| log(item) }) + 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, diff --git a/src/mint_json.cr b/src/mint_json.cr index 3de5059a2..dd6f6b335 100644 --- a/src/mint_json.cr +++ b/src/mint_json.cr @@ -46,12 +46,12 @@ module Mint ) end - def self.parse(contents : String, path : String) : MintJson - Parser.parse(contents: contents, path: path) + def self.parse(path : String, *, search : Bool = false) : MintJson + Parser.parse(path, search: search) end - def self.parse(path : String) : MintJson - Parser.parse(path) + def self.parse(contents : String, path : String) : MintJson + Parser.parse(contents: contents, path: path) end def self.current : MintJson diff --git a/src/mint_json/parser.cr b/src/mint_json/parser.cr index 98985dd27..19d6a9061 100644 --- a/src/mint_json/parser.cr +++ b/src/mint_json/parser.cr @@ -21,8 +21,22 @@ module Mint end end - def self.parse(path : String) : MintJson - parse(contents: File.read(path), path: path) + 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 diff --git a/src/workspace_2.cr b/src/workspace_2.cr index a3b24cedf..9eb526d39 100644 --- a/src/workspace_2.cr +++ b/src/workspace_2.cr @@ -22,10 +22,6 @@ module Mint path: uri) end end - - def format(uri : String) : String? - Formatter.new.format(ast) if ast = ast(uri) - end end # A workspace represents a Mint project either in the file system or in @@ -87,7 +83,7 @@ module Mint end # The current artifacts of the program or the current error. - @result : TypeChecker | Error = Error.new(:unitialized_workspace) + getter result : TypeChecker | Error = Error.new(:unitialized_workspace) # The listener to call when a new result is ready. @listener : Proc(TypeChecker | Error, Nil) | Nil @@ -103,20 +99,20 @@ module Mint end def artifacts : Artifacts | Error - case result + case item = result in TypeChecker - result.artifacts + item.artifacts in Error - result + item end end def ast : Ast | Error - case result + case item = result in TypeChecker - result.artifacts.ast + item.artifacts.ast in Error - result + item end end @@ -126,12 +122,12 @@ module Mint def update(contents : String, path : String) @cache.update(contents, path) - notify + # notify end def delete(path : String) @cache.delete(path) - notify + # notify end # This is a debounced method so it type checks after @@ -176,15 +172,15 @@ module Mint def initialize( *, - @listener : Proc(TypeChecker | Error, Nil), + @listener : Proc(TypeChecker | Error, Nil) | Nil, @include_tests : Bool, @format : Bool, @path : String, check : Check, watch : Bool ) - @cache = Cache.new(check) @watcher = Watcher.new(%w[]) + @cache = Cache.new(check) @async = watch reset(!watch) @@ -196,10 +192,57 @@ module Mint @watcher.pattern = globs = SourceFiles.everything( - MintJson.parse(@path), + MintJson.parse(@path, search: true), include_tests: @include_tests) - update(Dir.glob(globs.select(&.ends_with?(".mint")))) if process + if process + files = + Dir.glob(globs.select(&.ends_with?(".mint"))).map do |item| + Path[item].normalize.to_s + end + + update(files) + end + end + + 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 nodes_at_path(path : String) + map_error(ast, &.nodes_at_path(path)) + end + + def format(node : Nil) : String + "" + end + + def format(node : Ast::Node) : String + Formatter.new.format!(node) + end + + def format(path : String) : String + case ast = ast(path) + when Ast + Formatter.new.format(ast) + else + "" + end + end + + def map_error(item : T | Error, & : T -> R) : R | Error forall T, R + case item + in Error + item + in T + yield item + end end def update(files : Array(String)) From 077f652fe0708e0a48b7bbaeec319eb8248c701b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Wed, 9 Oct 2024 17:07:28 +0200 Subject: [PATCH 09/10] Workspace refactor V. --- .../location/type_destructuring_option | 2 +- .../location/variable_component_connect | 6 +- .../location/variable_component_connect_as | 6 +- .../location_link/type_destructuring_option | 2 +- .../location_link/variable_component_connect | 6 +- .../variable_component_connect_as | 8 +- spec/language_server/did_change | 19 + spec/language_server/did_change_spec.cr | 35 -- spec/language_server/did_open | 19 + spec/language_server/folding_range/component | 31 ++ .../folding_range/component_with_comments | 39 ++ spec/language_server/folding_range/module | 31 ++ spec/language_server/formatting | 40 ++ spec/language_server/will_save_wait_until | 37 ++ .../ls_spec.cr => language_server_spec.cr} | 57 ++- spec/mint_json_spec.cr | 6 +- spec/spec_helper.cr | 6 +- spec/watcher_spec.cr | 47 ++ spec/workspace_spec.cr | 65 +++ src/all.cr | 2 - src/commands/build.cr | 1 - src/commands/tool.cr | 2 +- src/commands/tool/ls_web_socket.cr | 44 ++ src/commands/tool/sandbox_server.cr | 26 -- src/errorable.cr | 4 + src/ls/apply_edit.cr | 12 - src/ls/code_action.cr | 2 +- src/ls/completion.cr | 27 -- src/ls/completion_request.cr | 30 ++ src/ls/definition.cr | 75 +--- src/ls/definition/connect.cr | 14 - src/ls/definition/connect_variable.cr | 24 - src/ls/definition/html_component.cr | 14 - src/ls/definition/html_element.cr | 13 - src/ls/definition/id.cr | 23 - src/ls/definition/type_destructuring.cr | 8 - src/ls/definition/variable.cr | 113 ----- src/ls/definitions.cr | 63 +++ src/ls/{definition => definitions}/access.cr | 7 +- src/ls/definitions/connect.cr | 15 + src/ls/definitions/connect_variable.cr | 27 ++ .../html_attribute.cr | 11 +- src/ls/definitions/html_component.cr | 14 + src/ls/definitions/html_element.cr | 13 + .../{definition => definitions}/html_style.cr | 7 +- src/ls/definitions/id.cr | 35 ++ src/ls/{definition => definitions}/type.cr | 7 +- src/ls/definitions/type_destructuring.cr | 16 + src/ls/definitions/variable.cr | 114 +++++ src/ls/did_change.cr | 2 +- src/ls/did_open.cr | 13 +- src/ls/folding_range.cr | 51 +-- src/ls/formatting.cr | 27 +- src/ls/hover.cr | 2 +- src/ls/sandbox_compile.cr | 48 -- src/ls/semantic_tokens.cr | 2 +- src/ls/server.cr | 56 +-- src/ls/websocket_server.cr | 101 ++++- src/ls/will_save_wait_until.cr | 29 +- src/lsp/protocol/text_document_item.cr | 5 + src/reactor.cr | 2 - src/sandbox_server.cr | 46 -- src/test_runner.cr | 1 - src/type_checkers/access.cr | 7 +- src/utils/cors.cr | 20 + src/utils/watcher.cr | 54 ++- src/workspace.cr | 424 ++++++++---------- src/workspace_2.cr | 294 ------------ 68 files changed, 1173 insertions(+), 1236 deletions(-) create mode 100644 spec/language_server/did_change delete mode 100644 spec/language_server/did_change_spec.cr create mode 100644 spec/language_server/did_open create mode 100644 spec/language_server/folding_range/component create mode 100644 spec/language_server/folding_range/component_with_comments create mode 100644 spec/language_server/folding_range/module create mode 100644 spec/language_server/formatting create mode 100644 spec/language_server/will_save_wait_until rename spec/{language_server/ls_spec.cr => language_server_spec.cr} (57%) create mode 100644 spec/watcher_spec.cr create mode 100644 spec/workspace_spec.cr create mode 100644 src/commands/tool/ls_web_socket.cr delete mode 100644 src/commands/tool/sandbox_server.cr delete mode 100644 src/ls/apply_edit.cr create mode 100644 src/ls/completion_request.cr delete mode 100644 src/ls/definition/connect.cr delete mode 100644 src/ls/definition/connect_variable.cr delete mode 100644 src/ls/definition/html_component.cr delete mode 100644 src/ls/definition/html_element.cr delete mode 100644 src/ls/definition/id.cr delete mode 100644 src/ls/definition/type_destructuring.cr delete mode 100644 src/ls/definition/variable.cr create mode 100644 src/ls/definitions.cr rename src/ls/{definition => definitions}/access.cr (59%) create mode 100644 src/ls/definitions/connect.cr create mode 100644 src/ls/definitions/connect_variable.cr rename src/ls/{definition => definitions}/html_attribute.cr (51%) create mode 100644 src/ls/definitions/html_component.cr create mode 100644 src/ls/definitions/html_element.cr rename src/ls/{definition => definitions}/html_style.cr (56%) create mode 100644 src/ls/definitions/id.cr rename src/ls/{definition => definitions}/type.cr (52%) create mode 100644 src/ls/definitions/type_destructuring.cr create mode 100644 src/ls/definitions/variable.cr delete mode 100644 src/ls/sandbox_compile.cr delete mode 100644 src/sandbox_server.cr create mode 100644 src/utils/cors.cr delete mode 100644 src/workspace_2.cr 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/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/ls_spec.cr b/spec/language_server_spec.cr similarity index 57% rename from spec/language_server/ls_spec.cr rename to spec/language_server_spec.cr index 64b1b97f6..0b2cab4ad 100644 --- a/spec/language_server/ls_spec.cr +++ b/spec/language_server_spec.cr @@ -1,18 +1,11 @@ -require "../spec_helper" - -struct CompletionResult - include JSON::Serializable - - getter result : Array(LSP::CompletionItem) - getter id : Int32 -end +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,completion,code_actions,semantic_tokens}/**/*") + .glob("./spec/language_server/**/*") .select! { |file| File.file?(file) } .sort! .each do |file| @@ -55,36 +48,40 @@ Dir 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 = - expected_response[1] || - JSON.parse(expected_response[0])["id"].as_i? + 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 + 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) + case expected_response[2] + when "contain" + json = + JSON.parse(actual_response) - expected = - JSON.parse(expected_response[0]) + 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) + 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 diff --git a/spec/mint_json_spec.cr b/spec/mint_json_spec.cr index 6aeb8042b..0c6dd7171 100644 --- a/spec/mint_json_spec.cr +++ b/spec/mint_json_spec.cr @@ -39,11 +39,11 @@ it "no mint.json in directory or parents" do Mint::MintJson.parse("test.json", search: true) rescue error : Mint::Error error.to_terminal.to_s.uncolorize.should eq(<<-TEXT) - ░ ERROR (MINT_JSON_INVALID) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░ ERROR (MINT_JSON_NOT_FOUND) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ - There was a problem trying to open a mint.json file: test.json + I could not find a mint.json file in the path or any of its parent directories: - Error opening file with mode 'r': 'test.json': No such file or directory + 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/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/src/all.cr b/src/all.cr index e825920d9..1399f6220 100644 --- a/src/all.cr +++ b/src/all.cr @@ -73,10 +73,8 @@ require "./mint_json" require "./scaffold" require "./reactor" -require "./sandbox_server" require "./cli" require "./workspace" -require "./workspace_2" require "./debugger" require "./bundler" require "./artifact_cleaner" diff --git a/src/commands/build.cr b/src/commands/build.cr index 2b0409317..a2522aad6 100644 --- a/src/commands/build.cr +++ b/src/commands/build.cr @@ -49,7 +49,6 @@ module Mint path: Path[Dir.current, "mint.json"].to_s, check: Check::Environment, include_tests: false, - watch: flags.watch, format: false, listener: ->(result : TypeChecker | Error) do terminal.reset if flags.watch 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/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/commands/tool/sandbox_server.cr b/src/commands/tool/sandbox_server.cr deleted file mode 100644 index 82b556dff..000000000 --- a/src/commands/tool/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/errorable.cr b/src/errorable.cr index f42aed760..7b31645e9 100644 --- a/src/errorable.cr +++ b/src/errorable.cr @@ -137,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})" diff --git a/src/ls/apply_edit.cr b/src/ls/apply_edit.cr deleted file mode 100644 index 1a88c4e1c..000000000 --- a/src/ls/apply_edit.cr +++ /dev/null @@ -1,12 +0,0 @@ -module Mint - module LS - class ApplyEdit < LSP::NotificationMessage - property params : LSP::ApplyWorkspaceEditParams - - def execute(server : Server) - puts params.edit.changes - puts params.edit.document_changes - end - end - end -end diff --git a/src/ls/code_action.cr b/src/ls/code_action.cr index 847f9c634..33e1842d8 100644 --- a/src/ls/code_action.cr +++ b/src/ls/code_action.cr @@ -23,7 +23,7 @@ module Mint def execute(server : LSP::Server) workspace = - server.ws(params.text_document.path) + server.workspace(params.text_document.path) nodes = workspace.nodes_at_cursor( diff --git a/src/ls/completion.cr b/src/ls/completion.cr index 5105680dc..dc4e00794 100644 --- a/src/ls/completion.cr +++ b/src/ls/completion.cr @@ -66,32 +66,5 @@ module Mint [] of LSP::CompletionItem end end - - 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.ws(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/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/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 a297791ae..78983a802 100644 --- a/src/ls/did_change.cr +++ b/src/ls/did_change.cr @@ -8,7 +8,7 @@ module Mint params.text_document.path server - .ws(path) + .workspace(path) .update(params.content_changes.first.text, path) end end diff --git a/src/ls/did_open.cr b/src/ls/did_open.cr index d34fc4c51..8911a7696 100644 --- a/src/ls/did_open.cr +++ b/src/ls/did_open.cr @@ -3,14 +3,13 @@ module Mint class DidOpen < LSP::NotificationMessage property params : LSP::DidOpenTextDocumentParams - def execute(server) - uri = - URI.parse(params.text_document.uri) + def execute(server) : Nil + path = + params.text_document.path - workspace = - server.workspace(uri) - - workspace.update(params.text_document.text, uri.path) + server + .workspace(path) + .update(params.text_document.text, path) 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..7f454fe2f 100644 --- a/src/ls/formatting.cr +++ b/src/ls/formatting.cr @@ -4,30 +4,29 @@ 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) + server.workspace(params.text_document.path) - # If there is an error show that - server.show_message_request("Could not format the file because it contains errors!", 1) if workspace.error + case workspace.ast(params.text_document.path) + in Ast + formatted = + workspace.format(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 [ 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/ls/hover.cr b/src/ls/hover.cr index 11615cb7a..ec58c4362 100644 --- a/src/ls/hover.cr +++ b/src/ls/hover.cr @@ -45,7 +45,7 @@ module Mint # this could take a while because the workspace parses # and type checks all of its source files. workspace = - server.ws(uri.path.to_s) + server.workspace(uri.path.to_s) contents = case type_checker = workspace.result diff --git a/src/ls/sandbox_compile.cr b/src/ls/sandbox_compile.cr deleted file mode 100644 index 805843d4d..000000000 --- a/src/ls/sandbox_compile.cr +++ /dev/null @@ -1,48 +0,0 @@ -module Mint - module LS - # This is a Mint only LSP request to compile the workspace as a sandbox. - class SandboxCompile < LSP::RequestMessage - def execute(server : Server) - workspace = - server.workspace("") - - result = - workspace.update_cache - - 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) - {url: "https://#{@id}.sandbox.mint-lang.com/"} - end - end - end -end diff --git a/src/ls/semantic_tokens.cr b/src/ls/semantic_tokens.cr index c1ec7e98d..dca18ee8f 100644 --- a/src/ls/semantic_tokens.cr +++ b/src/ls/semantic_tokens.cr @@ -10,7 +10,7 @@ module Mint URI.parse(params.text_document.uri).path.to_s data = - case ast = server.ws(path).ast(path) + case ast = server.workspace(path).ast(path) when Ast # This is used later on to convert the line/column of each token file = diff --git a/src/ls/server.cr b/src/ls/server.cr index 66f8ea3fd..e7c9629cf 100644 --- a/src/ls/server.cr +++ b/src/ls/server.cr @@ -18,74 +18,22 @@ module Mint "textDocument/didChange" => DidChange, "textDocument/didOpen" => DidOpen, "textDocument/hover" => Hover, - - # Workspace related methods - "workspace/applyEdit" => ApplyEdit, - - # Mint specific methods - "mint/sandboxCompile" => SandboxCompile, } property params : LSP::InitializeParams? = nil @@workspaces = {} of String => FileWorkspace - # 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 - - 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) - Workspace[path] - end - - def workspace(uri : URI) - Workspace[uri.path.to_s] - end - - def nodes_at_path(path : String) - Workspace[path] - .ast - .nodes - .select(&.file.path.==(path)) - end - - # TODO: Rename to workspace - def ws(path : String) : FileWorkspace + def workspace(path : String) : FileWorkspace base = File.find_in_ancestors(path, "mint.json").to_s - # TODO: Turn on `watch` when watcher supports direct update on - # initialize. @@workspaces[base] ||= FileWorkspace.new( - include_tests: true, check: Check::Unreachable, + include_tests: true, listener: nil, format: false, - watch: false, path: base) end end diff --git a/src/ls/websocket_server.cr b/src/ls/websocket_server.cr index 40f993084..440a40763 100644 --- a/src/ls/websocket_server.cr +++ b/src/ls/websocket_server.cr @@ -2,40 +2,97 @@ module Mint module LS # A server to use the LSP over websockets. class WebSocketServer < Server - getter directory : Path + class Sandbox + getter workspace : FileWorkspace - def initialize(@socket : HTTP::WebSocket) - @id = Random::Secure.hex + @directory : Path - # We need these for compability with the server. - @out = IO::Memory.new - @in = IO::Memory.new + 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 - # The directory for the workspace. - # TODO: Remove this when we have an in memory only workspace... - @directory = - Path[Dir.tempdir, @id].tap do |path| - FileUtils.mkdir_p(path) + io.rewind - File.write(Path[path, "mint.json"], { - "source-directories" => ["."], - }.to_json) + # 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 - # The workspace to use. - @workspace = Workspace.new(@directory.to_s) - @workspace.presist_on_update = true + @sandbox = Sandbox.new(self) if sandbox @socket.on_message { |message| process(message) } - @socket.on_close { FileUtils.rm_rf(directory) } + @socket.on_close { cleanup } end - def workspace(path : String) - @workspace + def cleanup + @sandbox.try(&.cleanup) end - def workspace(uri : URI) - @workspace + def workspace(path : String) : FileWorkspace + @sandbox.try(&.workspace) || super(path) end def send(content : String) diff --git a/src/ls/will_save_wait_until.cr b/src/ls/will_save_wait_until.cr index 7be5637fe..767fc10d9 100644 --- a/src/ls/will_save_wait_until.cr +++ b/src/ls/will_save_wait_until.cr @@ -4,32 +4,29 @@ 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 + case workspace.ast(params.text_document.path) + in Ast + formatted = + workspace.format(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 [ 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/text_document_item.cr b/src/lsp/protocol/text_document_item.cr index a0cacb518..2b34381ea 100644 --- a/src/lsp/protocol/text_document_item.cr +++ b/src/lsp/protocol/text_document_item.cr @@ -18,5 +18,10 @@ module LSP 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/reactor.cr b/src/reactor.cr index 27c355cf5..7d82fc524 100644 --- a/src/reactor.cr +++ b/src/reactor.cr @@ -24,7 +24,6 @@ module Mint check: Check::Environment, include_tests: false, format: format?, - watch: true, listener: ->(result : TypeChecker | Error) do @files = case result @@ -47,7 +46,6 @@ module Mint error(result) end - terminal.puts "#{COG} Compiled..." @sockets.each(&.send("reload")) if reload? end) diff --git a/src/sandbox_server.cr b/src/sandbox_server.cr deleted file mode 100644 index 9b1f7beac..000000000 --- a/src/sandbox_server.cr +++ /dev/null @@ -1,46 +0,0 @@ -module Mint - class SandboxServer - # 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 - - def initialize(host, port) - server = - HTTP::Server.new( - [ - CORS.new, - HTTP::WebSocketHandler.new do |socket| - LS::WebSocketServer.new(socket) - end, - ]) - - 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/test_runner.cr b/src/test_runner.cr index 21fe04105..69f677c2a 100644 --- a/src/test_runner.cr +++ b/src/test_runner.cr @@ -26,7 +26,6 @@ module Mint check: Check::Environment, include_tests: true, format: false, - watch: @watch, listener: ->(result : TypeChecker | Error) do case result in TypeChecker 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/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/watcher.cr b/src/utils/watcher.cr index fb654f351..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.expand.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(&.first).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 590140a9e..80b8402a2 100644 --- a/src/workspace.cr +++ b/src/workspace.cr @@ -1,298 +1,242 @@ module Mint - # A workspace represents a mint project where the root is the directory - # containing the `mint.json` file. - # - # 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_getter workspaces - - def self.from_file(path : String) : Workspace - new(root_from_file(path)) - end - - def self.current : Workspace - new(Dir.current) - end - - def self.root_from_file(path : String) : String - root = File.dirname(path) + @[Flags] + enum Check + Environment + Unreachable + end - loop do - raise "Invalid workspace!" if root == "." || root == "/" + # A workspace represents a Mint project either in the file system or in + # memory.A workspace provides up to date, type checked artifacts which + # can be used in other places (bundler, test runner, development server, + # language server, etc...) + class Workspace2 + class Cache + # Stores the AST (or error) of the file at the given path. + @cache : Hash(String, Ast | Error) = {} of String => Ast | Error - if File.exists?(Path[root, "mint.json"]) - break - else - root = File.dirname(root) - end + def initialize(@check : Check) end - root - end + def update(contents : String, path : String) + @cache[path] = Parser.parse?(contents, path) + end - def self.[](path) - root = root_from_file(path) + def delete(path : String) + @cache.delete(path) + end - @@workspaces[root] ||= - Workspace.new(root) - .tap(&.update_cache) - .tap(&.watch) - end + def ast(path : String) : Ast | Error | Nil + @cache[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 test_path : String? - getter json : MintJson - getter error : Error? - getter root : String - - property? presist_on_update : Bool = false - property? check_everything : Bool = true - property? check_env : Bool = false - property? format : Bool = false - - def test_path=(value) - @test_path = value - update_patterns - end + def clear + @cache.clear + end - def initialize(@root : String) - json_path = - Path[@root, "mint.json"].to_s + def process + errors = + @cache.values.select(Error) - @json = - FileUtils.cd(@root) do - MintJson.parse(json_path) + if error = errors.first? + error + else + 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). + item.merge(Core.ast) unless item.type_definitions.index(&.name.==("Maybe")) + end + .normalize + + TypeChecker.new( + check_everything: @check.unreachable?, + check_env: @check.environment?, + ast: ast + ).tap(&.check) end + rescue error : Error + error + end + end - @formatter = - Mint::Formatter.new(json.formatter) + # The current artifacts of the program or the current error. + getter result : TypeChecker | Error = Error.new(:unitialized_workspace) - @json_watcher = - Watcher.new([json_path]) + # The listener to call when a new result is ready. + @listener : Proc(TypeChecker | Error, Nil) | Nil - @source_watcher = - Watcher.new(all_files_pattern) + # The AST cache. + @cache : Cache - @env_watcher = - Env.env.try do |file| - Watcher.new([file]) - end + # The ID for debouncing the update. + @id = 0 - @type_checker = - TypeChecker.new(Ast.new) + def initialize + @cache = Workspace.new(Check::All) end - def on(event, &handler : ChangeProc) - @event_handlers[event] ||= [] of ChangeProc - @event_handlers[event] << handler + def artifacts : Artifacts | Error + case item = result + in TypeChecker + item.artifacts + in Error + item + end end - def packages : Array(Workspace) - pattern = - Path[root, ".mint", "packages", "**", "mint.json"] - - Dir.glob(pattern).map do |file| - Workspace.from_file(file) + def ast : Ast | Error + case item = result + in TypeChecker + item.artifacts.ast + in Error + item end 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(path : String) : Ast | Error | Nil + @cache.ast(path) end - def []?(file) - @cache[normalize_path(file)]? + def update(contents : String, path : String) + @cache.update(contents, path) + self end - def [](file) - @cache[normalize_path(file)] + def delete(path : String) + @cache.delete(path) end - protected def []=(file, value) - @cache[normalize_path(file)] = value + def delayed_process + spawn { process } end - def initialize_cache(&) - files = self.files - files.each_with_index do |file, index| - self[file] ||= Parser.parse(file) - - yield file, index, files.size - end + def process + @result = Logger.log "Type Checking" { @cache.process } + @listener.try(&.call(@result)) 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) } + # A file workspace watches the appropriate files of a project and recompiles + # it when they change. + class FileWorkspace < Workspace2 + getter? include_tests : Bool = false + getter? format : Bool + getter path : String - # Update the cache - update_cache - end - end + def initialize( + *, + @listener : Proc(TypeChecker | Error, Nil) | Nil, + @include_tests : Bool, + @format : Bool, + @path : String, + check : Check + ) + @watcher = Watcher.new(&->update(Array(String), Symbol)) + @cache = Cache.new(check) - 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 + reset + @watcher.watch + end - spawn do - @env_watcher.try &.watch do - Env.load do - update_cache - end - end - end + 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 files - Dir.glob(all_files_pattern) + def nodes_at_path(path : String) + map_error(ast, &.nodes_at_path(path)) 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 } - else - [path] - end - else - files - end + def format(node : Nil) : String + "" end - def update_cache - Logger.log "Parsing files" do - files.each do |file| - path = - File.realpath(file) + def format(node : Ast::Node) : String + Formatter.new.format!(node) + end - self[file] ||= process(File.read(path), path) - end + def format(path : String) : String + case ast = ast(path) + when Ast + Formatter.new.format(ast) + else + "" end - - Logger.log "Type Checking" { check! } - - @error = nil - - call "change", ast - ast - rescue error : Error - @error = error - - call "change", error - error end - def format(file) - Formatter - .new(json.formatter) - .format(self[file]) + def map_error(item : T | Error, & : T -> R) : R | Error forall T, R + case item + in Error + item + in T + yield item + end end - def update(contents, file) - self[file] = process(contents, Path[root, file].to_s) - @error = nil - - call "change", ast + def reset + @cache.clear + @watcher.patterns = + SourceFiles.everything( + MintJson.parse(@path, search: true), + include_tests: @include_tests) rescue error : Error - @error = error - - call "change", error + @result = error + @listener.try(&.call(@result)) end - private def normalize_path(file) - Path[file].normalize.to_s - end - - private def process(contents, file) - real_path = - if File.exists?(file) - File.realpath(file) - else - file - end + def update(files : Array(String), reason : Symbol) + actions = [] of Symbol - ast = - Parser.parse(contents, real_path) - - if format? - formatted = - Formatter - .new(json.formatter) - .format(ast) - - if formatted != File.read(file) - File.write(file, formatted) + Logger.log "Parsing files" do + files.each do |file| + if File.extname(file) == ".mint" + if File.exists?(file) + contents = File.read(file) + update(contents, file) + + if format? + case ast = @cache.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 - ast - end - - private def check! - @type_checker = - Mint::TypeChecker.new( - check_everything: check_everything?, - check_env: check_env?, - ast: ast - ).tap(&.check) - 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) + if actions.includes?(:reset) && reason == :modified + reset + else + process + end end end end diff --git a/src/workspace_2.cr b/src/workspace_2.cr deleted file mode 100644 index 9eb526d39..000000000 --- a/src/workspace_2.cr +++ /dev/null @@ -1,294 +0,0 @@ -module Mint - @[Flags] - enum Check - Environment - Unreachable - end - - class LSWorkspace - delegate :artifacts, :ast, :update, :delete, :process, to: @workspace - - def initialize(uri : String) - @workspace = - if uri.starts_with?("sandbox://") - SandboxWorkspace.new(Check::All) - else - FileWorkspace.new( - include_tests: false, - check: Check::All, - listener: nil, - format: false, - watch: true, - path: uri) - end - end - end - - # A workspace represents a Mint project either in the file system or in - # memory.A workspace provides up to date, type checked artifacts which - # can be used in other places (bundler, test runner, development server, - # language server, etc...) - class Workspace2 - class Cache - # Stores the AST (or error) of the file at the given path. - @cache : Hash(String, Ast | Error) = {} of String => Ast | Error - - def initialize(@check : Check) - end - - def update(contents : String, path : String) - @cache[path] = Parser.parse?(contents, path) - end - - def delete(path : String) - @cache.delete(path) - end - - def ast(path : String) : Ast | Error | Nil - @cache[path]? - end - - def clear - @cache.clear - end - - def process - errors = - @cache.values.select(Error) - - if error = errors.first? - error - else - 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). - item.merge(Core.ast) unless item.type_definitions.index(&.name.==("Maybe")) - end - .normalize - - TypeChecker.new( - check_everything: @check.unreachable?, - check_env: @check.environment?, - ast: ast - ).tap(&.check) - end - rescue error : Error - error - end - end - - # The current artifacts of the program or the current error. - getter result : TypeChecker | Error = Error.new(:unitialized_workspace) - - # The listener to call when a new result is ready. - @listener : Proc(TypeChecker | Error, Nil) | Nil - - # The AST cache. - @cache : Cache - - # The ID for debouncing the update. - @id = 0 - - def initialize - @cache = Workspace.new(Check::All) - end - - def artifacts : Artifacts | Error - case item = result - in TypeChecker - item.artifacts - in Error - item - end - end - - def ast : Ast | Error - case item = result - in TypeChecker - item.artifacts.ast - in Error - item - end - end - - def ast(path : String) : Ast | Error | Nil - @cache.ast(path) - end - - def update(contents : String, path : String) - @cache.update(contents, path) - # notify - end - - def delete(path : String) - @cache.delete(path) - # notify - end - - # This is a debounced method so it type checks after - # all changes have processed. - def notify - if @async - id = @id += 1 - - spawn do - sleep 0.5.seconds - next if id != @id - process - @id = 0 - end - else - process - end - end - - def process - @result = Logger.log "Type Checking" { @cache.process } - @listener.try(&.call(@result)) - end - end - - # A sandbox workspace is just an in memory workspace. - class SandboxWorkspace < Workspace2 - @async = true - end - - # A file workspace watches the appropriate files of a project and recompiles - # it when they change. - class FileWorkspace < Workspace2 - enum Action - Compile - Reset - end - - getter? include_tests : Bool = false - getter? format : Bool - getter path : String - - def initialize( - *, - @listener : Proc(TypeChecker | Error, Nil) | Nil, - @include_tests : Bool, - @format : Bool, - @path : String, - check : Check, - watch : Bool - ) - @watcher = Watcher.new(%w[]) - @cache = Cache.new(check) - @async = watch - - reset(!watch) - spawn { @watcher.watch(&->update(Array(String))) } if watch - end - - def reset(process : Bool = true) - @cache.clear - - @watcher.pattern = globs = - SourceFiles.everything( - MintJson.parse(@path, search: true), - include_tests: @include_tests) - - if process - files = - Dir.glob(globs.select(&.ends_with?(".mint"))).map do |item| - Path[item].normalize.to_s - end - - update(files) - end - end - - 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 nodes_at_path(path : String) - map_error(ast, &.nodes_at_path(path)) - end - - def format(node : Nil) : String - "" - end - - def format(node : Ast::Node) : String - Formatter.new.format!(node) - end - - def format(path : String) : String - case ast = ast(path) - when Ast - Formatter.new.format(ast) - else - "" - end - end - - def map_error(item : T | Error, & : T -> R) : R | Error forall T, R - case item - in Error - item - in T - yield item - end - end - - def update(files : Array(String)) - actions = [] of Action - - Logger.log "Parsing files" do - files.each do |file| - if File.extname(file) == ".mint" - if File.exists?(file) - contents = File.read(file) - @cache.update(contents, file) - - if format? - case ast = @cache.ast(file) - when Ast - formatted = - Formatter.new.format(ast) - - if formatted != contents - File.write(file, formatted) - end - end - end - else - @cache.delete(file) - end - - actions << Action::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 << Action::Reset - end - end - end - end - - if actions.includes?(Action::Reset) - reset - else - process - end - end - end -end From 6bbfa23714913a82c74f34147ee2c04e7c5f5485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szikszai=20Guszt=C3=A1v?= Date: Thu, 10 Oct 2024 11:35:16 +0200 Subject: [PATCH 10/10] Workspace refactor part VI. --- src/ls/code_actions/module_actions.cr | 34 ++-- src/ls/code_actions/provider_actions.cr | 34 ++-- src/ls/completion_item/component.cr | 1 + src/ls/formatting.cr | 9 +- src/ls/will_save_wait_until.cr | 7 +- src/workspace.cr | 232 +++++++++--------------- 6 files changed, 124 insertions(+), 193 deletions(-) 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_item/component.cr b/src/ls/completion_item/component.cr index 7110ac8c2..24d97231e 100644 --- a/src/ls/completion_item/component.cr +++ b/src/ls/completion_item/component.cr @@ -12,6 +12,7 @@ module Mint default = @workspace .format(property.default) + .to_s .gsub("}", "\\}") type = diff --git a/src/ls/formatting.cr b/src/ls/formatting.cr index 7f454fe2f..7e94e4e1c 100644 --- a/src/ls/formatting.cr +++ b/src/ls/formatting.cr @@ -7,11 +7,8 @@ module Mint workspace = server.workspace(params.text_document.path) - case workspace.ast(params.text_document.path) - in Ast - formatted = - workspace.format(params.text_document.path) - + 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), @@ -26,7 +23,7 @@ module Mint # because SublimeText LSP client freezes if there is no response. %w[] in Nil - %[] + %w[] end end end diff --git a/src/ls/will_save_wait_until.cr b/src/ls/will_save_wait_until.cr index 767fc10d9..fc04e1ceb 100644 --- a/src/ls/will_save_wait_until.cr +++ b/src/ls/will_save_wait_until.cr @@ -7,11 +7,8 @@ module Mint workspace = server.workspace(params.text_document.path) - case workspace.ast(params.text_document.path) - in Ast - formatted = - workspace.format(params.text_document.path) - + 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), diff --git a/src/workspace.cr b/src/workspace.cr index 80b8402a2..9a6e153ac 100644 --- a/src/workspace.cr +++ b/src/workspace.cr @@ -5,141 +5,54 @@ module Mint Unreachable end - # A workspace represents a Mint project either in the file system or in - # memory.A workspace provides up to date, type checked artifacts which - # can be used in other places (bundler, test runner, development server, - # language server, etc...) - class Workspace2 - class Cache - # Stores the AST (or error) of the file at the given path. - @cache : Hash(String, Ast | Error) = {} of String => Ast | Error - - def initialize(@check : Check) - end - - def update(contents : String, path : String) - @cache[path] = Parser.parse?(contents, path) - end - - def delete(path : String) - @cache.delete(path) - end - - def ast(path : String) : Ast | Error | Nil - @cache[path]? - end - - def clear - @cache.clear - end - - def process - errors = - @cache.values.select(Error) - - if error = errors.first? - error - else - 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). - item.merge(Core.ast) unless item.type_definitions.index(&.name.==("Maybe")) - end - .normalize - - TypeChecker.new( - check_everything: @check.unreachable?, - check_env: @check.environment?, - ast: ast - ).tap(&.check) - end - rescue error : Error - error - end - 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. + # + class FileWorkspace # The current artifacts of the program or the current error. getter result : TypeChecker | Error = Error.new(:unitialized_workspace) + # Stores the AST (or error) of the file at the given path. + @cache : Hash(String, Ast | Error) = {} of String => Ast | Error + # The listener to call when a new result is ready. @listener : Proc(TypeChecker | Error, Nil) | Nil - # The AST cache. - @cache : Cache - - # The ID for debouncing the update. - @id = 0 - - def initialize - @cache = Workspace.new(Check::All) - end - - def artifacts : Artifacts | Error - case item = result - in TypeChecker - item.artifacts - in Error - item - end - end - - def ast : Ast | Error - case item = result - in TypeChecker - item.artifacts.ast - in Error - item - end - end - - def ast(path : String) : Ast | Error | Nil - @cache.ast(path) + 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 update(contents : String, path : String) - @cache.update(contents, path) - self + def update(contents : String, path : String) : Nil + @cache[path] = Parser.parse?(contents, path) end - def delete(path : String) + def delete(path : String) : Nil @cache.delete(path) end - def delayed_process - spawn { process } + def artifacts : TypeChecker::Artifacts | Error + map_error(result, &.artifacts) end - def process - @result = Logger.log "Type Checking" { @cache.process } - @listener.try(&.call(@result)) + def ast(path : String) : Ast | Error | Nil + @cache[path]? end - end - - # A file workspace watches the appropriate files of a project and recompiles - # it when they change. - class FileWorkspace < Workspace2 - getter? include_tests : Bool = false - getter? format : Bool - getter path : String - - def initialize( - *, - @listener : Proc(TypeChecker | Error, Nil) | Nil, - @include_tests : Bool, - @format : Bool, - @path : String, - check : Check - ) - @watcher = Watcher.new(&->update(Array(String), Symbol)) - @cache = Cache.new(check) - reset - @watcher.watch + def ast : Ast | Error + map_error(artifacts, &.ast) end def nodes_at_cursor( @@ -148,37 +61,24 @@ module Mint path : String, line : Int64 ) : Array(Ast::Node) | Error - map_error(ast, - &.nodes_at_cursor(line: line, column: column, path: path)) + map_error(ast, &.nodes_at_cursor( + line: line, column: column, path: path)) end def nodes_at_path(path : String) map_error(ast, &.nodes_at_path(path)) end - def format(node : Nil) : String - "" - end - - def format(node : Ast::Node) : String + def format(node : Ast::Node | Nil) : String | Nil Formatter.new.format!(node) end - def format(path : String) : String - case ast = ast(path) - when Ast - Formatter.new.format(ast) - else - "" - end - end - - def map_error(item : T | Error, & : T -> R) : R | Error forall T, R - case item - in Error + def format(path : String) : String | Error | Nil + case item = ast(path) + in Ast + Formatter.new.format(item) + in Error, Nil item - in T - yield item end end @@ -189,8 +89,38 @@ module Mint MintJson.parse(@path, search: true), include_tests: @include_tests) rescue error : Error - @result = error - @listener.try(&.call(@result)) + set(error) + end + + def check + Logger.log "Type Checking" do + if error = @cache.values.select(Error).first? + error + else + 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 + end + rescue error : Error + error end def update(files : Array(String), reason : Symbol) @@ -203,8 +133,8 @@ module Mint contents = File.read(file) update(contents, file) - if format? - case ast = @cache.ast(file) + if @format + case ast = ast(file) when Ast formatted = Formatter.new.format(ast) @@ -235,7 +165,21 @@ module Mint if actions.includes?(:reset) && reason == :modified reset else - process + set(check) + end + end + + private def set(value : TypeChecker | Error) : Nil + @result = value + @listener.try(&.call(value)) + end + + 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