diff --git a/.release-notes/5238.md b/.release-notes/5238.md new file mode 100644 index 0000000000..b2d75648e8 --- /dev/null +++ b/.release-notes/5238.md @@ -0,0 +1,3 @@ +## Pony-lsp: Resolve files external to workspace directories + +Pony-lsp is now able to internally resolve files from outside the current workspace directory and offer LSP functionality for those as well. This includes files in external pony-packages the workspace depends upon, which in turn also includes the pony standard library. diff --git a/tools/pony-lsp/language_server.pony b/tools/pony-lsp/language_server.pony index 269edb41e8..0aa0d82fbf 100644 --- a/tools/pony-lsp/language_server.pony +++ b/tools/pony-lsp/language_server.pony @@ -82,76 +82,126 @@ actor LanguageServer is (Notifier & RequestSender) | Methods.text_document().inlay_hint() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .inlay_hint(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.inlay_hint(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().references() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .references(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.references(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().prepare_rename() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .prepare_rename(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.prepare_rename(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().rename() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .rename(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.rename(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().document_highlight() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .document_highlight(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.document_highlight(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'") ) ) @@ -160,21 +210,27 @@ actor LanguageServer is (Notifier & RequestSender) | Methods.text_document().declaration() | Methods.text_document().definition() => try - let document_uri = - _get_document_uri(r.params)? - // TODO: exptract params into class - // according to spec - (_router.find_workspace(document_uri) - as WorkspaceManager) - .goto_definition(document_uri, r) + let document_uri = _get_document_uri(r.params)? + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.goto_definition(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'") ) ) @@ -182,47 +238,76 @@ actor LanguageServer is (Notifier & RequestSender) | Methods.text_document().type_definition() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .type_definition(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.type_definition(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().hover() => try let document_uri = _get_document_uri(r.params)? - // TODO: exptract params into class according to spec - (_router.find_workspace(document_uri) as WorkspaceManager) - .hover(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.hover(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for request '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().document_symbol() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .document_symbols(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.document_symbols(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for request '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'") ) ) @@ -230,31 +315,51 @@ actor LanguageServer is (Notifier & RequestSender) | Methods.text_document().folding_range() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .folding_range(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.folding_range(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for request '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'"))) end | Methods.text_document().diagnostic() => try let document_uri = _get_document_uri(r.params)? - (_router.find_workspace(document_uri) as WorkspaceManager) - .document_diagnostic(document_uri, r) + _router.handle_request_chained( + document_uri, + r, + {( + mgr: WorkspaceManager, + file_uri: String, + request: RequestMessage val) + => + mgr.document_diagnostic(file_uri, request) + } + ) else this._channel.send( ResponseMessage.create( r.id, None, ResponseError( - ErrorCodes.internal_error(), - "[" + r.method + "] No workspace found for request '" + + ErrorCodes.invalid_request(), + "[" + r.method + "] " + + "'textDocument.uri' missing from request: '" + r.json().string() + "'") ) ) @@ -347,34 +452,60 @@ actor LanguageServer is (Notifier & RequestSender) | Methods.text_document().did_open() => try let document_uri = _get_document_uri(n.params)? - // TODO: extract params into class according to spec - (_router.find_workspace(document_uri) as WorkspaceManager) - .did_open(document_uri, n) + _router.handle_notification_chained( + document_uri, + n, + {( + mgr: WorkspaceManager, + file_uri: String, + notification: Notification) + => + mgr.did_open(file_uri, notification)} + ) else this._channel.log( - "[" + n.method + "] No workspace found for '" + + "[" + n.method + "] " + + "'textDocument.uri' missing from notification: '" + n.json().string() + "'") end | Methods.text_document().did_save() => try let document_uri = _get_document_uri(n.params)? - // TODO: extract params into class according to spec - (_router.find_workspace(document_uri) as WorkspaceManager) - .did_save(document_uri, n) + _router.handle_notification_chained( + document_uri, + n, + {( + mgr: WorkspaceManager, + file_uri: String, + notification: Notification) + => + mgr.did_save(file_uri, notification) + } + ) else this._channel.log( - "[" + n.method + "] No workspace found for '" + + "[" + n.method + "] " + + "'textDocument.uri' missing from notification: '" + n.json().string() + "'") end | Methods.text_document().did_close() => try let document_uri = _get_document_uri(n.params)? - // TODO: extract params into class according to spec - (_router.find_workspace(document_uri) as WorkspaceManager) - .did_close(document_uri, n) + _router.handle_notification_chained( + document_uri, + n, + {( + mgr: WorkspaceManager, + file_uri: String, + notification: Notification) + => + mgr.did_close(file_uri, notification) + } + ) else this._channel.log( - "[" + n.method + "] No workspace found for '" + + "[" + n.method + "] " + + "'textDocument.uri' missing from notification: '" + n.json().string() + "'") end | Methods.workspace().did_change_configuration() diff --git a/tools/pony-lsp/workspace/state.pony b/tools/pony-lsp/workspace/state.pony index bb531b7ad0..5103fc0662 100644 --- a/tools/pony-lsp/workspace/state.pony +++ b/tools/pony-lsp/workspace/state.pony @@ -11,7 +11,7 @@ class PackageState State of a compiled package, containing its modules and document states. """ let path: FilePath - let documents: Map[String, DocumentState] + let open_documents: Map[String, DocumentState] let _channel: Channel var _package: FromCompilerRun[Package] var _compiler_run_id: USize @@ -19,7 +19,7 @@ class PackageState new create(path': FilePath, channel: Channel) => path = path' _channel = channel - documents = documents.create() + open_documents = open_documents.create() _package = _package.empty() _compiler_run_id = 0 @@ -31,7 +31,7 @@ class PackageState "" end ) + "):\n\t" + "\n\t".join( - Iter[(String box, DocumentState box)](documents.pairs()) + Iter[(String box, DocumentState box)](open_documents.pairs()) .map[String]({(kv) => kv._1 + " (" + if kv._2.module() isnt None then @@ -45,17 +45,36 @@ class PackageState fun package(): (Package | None) => this._package.get(this._compiler_run_id) - fun get_document(document_path: String): (this->DocumentState | None) => + fun get_open_document(document_path: String): (this->DocumentState | None) => try - this.documents(document_path)? + this.open_documents(document_path)? end - fun has_document(document_path: String): Bool => - this.documents.contains(document_path) + fun has_open_document(document_path: String): Bool => + """ + Returns true if this package has the document at `document_path` + marked as open in an editor. + """ + this.open_documents.contains(document_path) + + fun has_module(document_path: String): Bool => + """ + Returns true, if this package contains the provided `document_path` as a + pony module. + + This is different to `has_open_document()` in that it also returns true if + `document_path` is a part of the package + but not marked as opened in an editor. + """ + try + (this.package() as Package).find_module(document_path) isnt None + else + false + end fun ref insert_new(document_path: String): DocumentState => """ - Insert a new module by the given `document_path` into this package. + Mark the document as opened in the current workspace. """ let doc_state = DocumentState.create(document_path, this._channel) @@ -65,14 +84,14 @@ class PackageState let module = pkg.find_module(document_path) as Module doc_state.update(this._compiler_run_id, module) end - this.documents.insert(document_path, doc_state) + this.open_documents.insert(document_path, doc_state) - fun ref ensure_document(document_path: String): DocumentState => + fun ref ensure_open_document(document_path: String): DocumentState => """ Returns the document state in a tuple together with a boolean, denoting whether this documents needs compilation. """ - match \exhaustive\ this.get_document(document_path) + match \exhaustive\ this.get_open_document(document_path) | let d: DocumentState => d | None => this.insert_new(document_path) end @@ -89,7 +108,7 @@ class PackageState this._channel.log(this.debug()) // for each open document, update the // document state if we have a module for it - for (doc_path, doc_state) in this.documents.pairs() do + for (doc_path, doc_state) in this.open_documents.pairs() do // TODO: ensure both module and package-state paths are normalized match \exhaustive\ result.find_module(doc_path) | let m: Module val => doc_state.update(run_id, m) @@ -105,7 +124,7 @@ class PackageState end fun dispose() => - for doc_state in this.documents.values() do + for doc_state in this.open_documents.values() do doc_state.dispose() end diff --git a/tools/pony-lsp/workspace/workspace_manager.pony b/tools/pony-lsp/workspace/workspace_manager.pony index 1e5953ccea..0aa8909320 100644 --- a/tools/pony-lsp/workspace/workspace_manager.pony +++ b/tools/pony-lsp/workspace/workspace_manager.pony @@ -27,6 +27,8 @@ actor WorkspaceManager let _compiler: LspCompiler let _packages: Map[String, PackageState] let _global_errors: Array[Diagnostic val] + var _next_workspace: (WorkspaceManager | None) = None + var _prev_workspace: (WorkspaceManager | None) = None var _compile_run: USize = 0 var _current_request: (RequestMessage val | None) = None var _compiling: Bool = false @@ -55,6 +57,67 @@ actor WorkspaceManager _global_errors = Array[Diagnostic val].create(4) + be handle_request_chained( + file_uri: String val, + request: RequestMessage val, + handler: {(WorkspaceManager tag, String val, RequestMessage val)} val + ) => + var file_path = Uris.to_path(file_uri) + // check if we have this file somewhere in package modules + for package in _packages.values() do + if package.has_module(file_path) then + // if so handle it + handler(this, file_uri, request) + return + end + end + match \exhaustive\ _next_workspace + | let next_mgr: WorkspaceManager => + next_mgr.handle_request_chained(file_uri, request, handler) + | None => + // if there is no next workspace, + // return a error response (no workspace found) + this._channel.send( + ResponseMessage.create( + request.id, + None, + ResponseError( + ErrorCodes.internal_error(), + "[" + request.method + "] " + + "No workspace found for request '" + request.json().string() + "'") + ) + ) + end + + be handle_notification_chained( + file_uri: String val, + notification: Notification val, + handler: {(WorkspaceManager tag, String val, Notification val)} val + ) => + var file_path = Uris.to_path(file_uri) + // check if we have this file somewhere in package modules + for package in _packages.values() do + if package.has_module(file_path) then + // if so handle it, if not, pass it to _next_workspace + handler(this, file_uri, notification) + return + end + end + match \exhaustive\ _next_workspace + | let next_mgr: WorkspaceManager => + next_mgr.handle_notification_chained(file_uri, notification, handler) + | None => + this._channel.log( + "[" + notification.method + "] No workspace found for '" + + notification.json().string() + "'") + end + + be set_next_workspace(mgr: WorkspaceManager) => + _next_workspace = mgr + + be set_prev_workspace(mgr: WorkspaceManager) => + _prev_workspace = mgr + fun ref _ensure_package(package_path: FilePath): PackageState => try this._packages(package_path.path)? @@ -147,7 +210,7 @@ actor WorkspaceManager // pre-fill with empty list of errors for // all files for which we have errors now for pkg in this._packages.values() do - for doc in pkg.documents.keys() do + for doc in pkg.open_documents.keys() do errors_by_file(doc) = pc.Vec[JsonValue] end end @@ -176,7 +239,8 @@ actor WorkspaceManager // get the hash of the module file // ponyc considered for compilation let new_mod_hash = - (package_state.get_document(await_comp_file) as DocumentState) + (package_state + .get_open_document(await_comp_file) as DocumentState) .module_hash() this._awaiting_compilation_for.remove(await_comp_file)? requires_another_compilation = @@ -225,7 +289,7 @@ actor WorkspaceManager try let package: FilePath = this._find_workspace_package(err_file)? let package_state = this._ensure_package(package) - let document_state = package_state.ensure_document(err_file) + let document_state = package_state.ensure_open_document(err_file) document_state.add_diagnostic(run, diagnostic) end @@ -383,7 +447,7 @@ actor WorkspaceManager end this._channel.log("did_open in pony package @ " + package.path) let package_state = this._ensure_package(package) - let doc_state = package_state.ensure_document(document_path) + let doc_state = package_state.ensure_open_document(document_path) if doc_state.needs_compilation() then if this._compiling then let current_hash = doc_state.module_hash() @@ -406,7 +470,8 @@ actor WorkspaceManager let package: FilePath = this._find_workspace_package(document_path)? let package_state = this._ensure_package(package) try - let document_state = package_state.documents.remove(document_path)?._2 + let document_state = + package_state.open_documents.remove(document_path)?._2 document_state.dispose() end else @@ -513,7 +578,7 @@ actor WorkspaceManager match \exhaustive\ this._get_package(package) | let pkg_state: PackageState => - match \exhaustive\ pkg_state.get_document(document_path) + match \exhaustive\ pkg_state.get_open_document(document_path) | let doc: DocumentState => match (doc.position_index(), doc.module()) | (let index: PositionIndex, let module: Module val) => @@ -833,7 +898,7 @@ actor WorkspaceManager match \exhaustive\ this._get_package(package) | let pkg_state: PackageState => // this._channel.log(pkg_state.debug()) - match \exhaustive\ pkg_state.get_document(document_path) + match \exhaustive\ pkg_state.get_open_document(document_path) | let doc: DocumentState => let symbols = doc.document_symbols() var json_arr = JsonArray @@ -866,7 +931,7 @@ actor WorkspaceManager let package: FilePath = this._find_workspace_package(document_path)? match \exhaustive\ this._get_package(package) | let pkg_state: PackageState => - match \exhaustive\ pkg_state.get_document(document_path) + match \exhaustive\ pkg_state.get_open_document(document_path) | let doc: DocumentState => try diagnostics = @@ -911,7 +976,7 @@ actor WorkspaceManager let package: FilePath = this._find_workspace_package(document_path)? match \exhaustive\ this._get_package(package) | let pkg_state: PackageState => - match \exhaustive\ pkg_state.get_document(document_path) + match \exhaustive\ pkg_state.get_open_document(document_path) | let doc: DocumentState => match \exhaustive\ doc.module() | let module: Module val => @@ -960,7 +1025,7 @@ actor WorkspaceManager let package: FilePath = this._find_workspace_package(document_path)? match \exhaustive\ this._get_package(package) | let pkg_state: PackageState => - match \exhaustive\ pkg_state.get_document(document_path) + match \exhaustive\ pkg_state.get_open_document(document_path) | let doc: DocumentState => match \exhaustive\ doc.module() | let module: Module val => @@ -989,6 +1054,13 @@ actor WorkspaceManager this._channel.send(ResponseMessage.create(request.id, None)) be dispose() => + // remove from the workspace-chain + match (_prev_workspace, _next_workspace) + | (let prev: WorkspaceManager, let next: WorkspaceManager) => + prev.set_next_workspace(next) + next.set_prev_workspace(prev) + end + // dispose packages etc. for package_state in this._packages.values() do package_state.dispose() end diff --git a/tools/pony-lsp/workspace/workspace_router.pony b/tools/pony-lsp/workspace/workspace_router.pony index 671bcd7f40..0ce15d481e 100644 --- a/tools/pony-lsp/workspace/workspace_router.pony +++ b/tools/pony-lsp/workspace/workspace_router.pony @@ -7,13 +7,72 @@ class WorkspaceRouter Routes document URIs to the appropriate workspace manager. """ let workspaces: Map[String, WorkspaceManager] + var head_workspace: (WorkspaceManager | None) + var tail_workspace: (WorkspaceManager | None) var min_workspace_path_len: USize new ref create() => workspaces = Map[String, WorkspaceManager].create() + head_workspace = None + tail_workspace = None min_workspace_path_len = USize.max_value() + fun handle_request_chained( + file_uri: String, + request: RequestMessage val, + handler: {(WorkspaceManager tag, String val, RequestMessage val)} val) + => + """ + Handle the provided notification for the file denoted by `file_uri` with the + provided `handler`. + + First, the `file_uri` is checked, if it falls within any of the workspace + folders. If so it is handled by this `WorkspaceManager`. If not it is + reached down the chain of available workspaces and each on is checking if + it has a matching module somewhere in the program. + """ + match find_workspace(file_uri) + | let mgr: WorkspaceManager => + handler(mgr, file_uri, request) + else + try + (head_workspace as WorkspaceManager) + .handle_request_chained(file_uri, request, handler) + end + end + + fun handle_notification_chained( + file_uri: String, + notification: Notification val, + handler: {(WorkspaceManager tag, String val, Notification val)} val) + => + """ + Handle the provided notification for the file denoted by `file_uri` with the + provided `handler`. + + First, the `file_uri` is checked, if it falls within any of the workspace + folders. If so it is handled by this `WorkspaceManager`. If not it is + reached down the chain of available workspaces and each on is checking if + it has a matching module somewhere in the program. + """ + match this.find_workspace(file_uri) + | let mgr: WorkspaceManager => + handler(mgr, file_uri, notification) + else + try + (head_workspace as WorkspaceManager) + .handle_notification_chained(file_uri, notification, handler) + end + end + fun find_workspace(file_uri: String): (WorkspaceManager | None) => + """ + Find a workspace that contains this `file_uri`. + + This method only checks if `file_uri` is contained in the main folder + of a workspace. It doesn't check if that workspace has the provided + file_uri as one of its program modules. + """ var file_path = Uris.to_path(file_uri) // check the parent directories upwards if any // of them is part of a workspace @@ -36,10 +95,27 @@ class WorkspaceRouter """ Register a workspace manager for a folder. """ + // link this workspace to the next one + // set the head, if we are the first + if head_workspace is None then + head_workspace = mgr + end + + match tail_workspace + | let last_mgr: WorkspaceManager => + last_mgr.set_next_workspace(mgr) + mgr.set_prev_workspace(last_mgr) + end + // set this as the tail_workspace, to build a chain + tail_workspace = mgr + + // insert this mgr into the known workspaces let abs_folder = folder.canonical()?.path let old_mgr = workspaces(abs_folder) = mgr match old_mgr - | let old: WorkspaceManager => old.dispose() + | let old: WorkspaceManager => + // unlink the old one from the chain + old.dispose() end this.min_workspace_path_len = this.min_workspace_path_len.min(abs_folder.size())