diff --git a/rascal-lsp/src/main/rascal/library/analysis/diff/edits/AnnotatedTextEdits.rsc b/rascal-lsp/src/main/rascal/library/analysis/diff/edits/AnnotatedTextEdits.rsc new file mode 100644 index 000000000..fa7f0aec3 --- /dev/null +++ b/rascal-lsp/src/main/rascal/library/analysis/diff/edits/AnnotatedTextEdits.rsc @@ -0,0 +1,59 @@ +@license{ +Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +} +module analysis::diff::edits::AnnotatedTextEdits + +extend analysis::diff::edits::TextEdits; + +@synopsis{A ((analysis::diff::edits::TextEdits::TextEdit)) with additional context for LSP.} +@description{ +In LSP, text edits can contain extra information w.r.t. ((analysis::diff::edits::TextEdits::TextEdit)). +* label: Human-readable string that describes the change. +* description: Human-readable string that additionally describes the change, rendered less prominently. +* needsConfirmation: Flags whether the user should confirm this change. By default, this is false, which means that ((analysis::diff::edits::AnnotatedTextEdits::TextEdit))s are applied without user confirmation. + +Typically, clients provide options to group edits by label/description when showing them to the user. +See the [LSP documentation](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#changeAnnotation) for more details. + +Note: to easily annotate all text edits in a ((analysis::diff::edits::TextEdits::FileSystemChange)), use the convenience keywords on ((analysis::diff::edits::AnnotatedTextEdits::FileSystemChange)). +} +@pitfalls{ +When `needsConfirmation = false` for all edits, the client will typically apply them without showing any information from the annotations to the user. +} +data TextEdit(str label = "", str description = label, bool needsConfirmation = false); + +@synopsis{A ((analysis::diff::edits::TextEdits::FileSystemChange)) with additional context for LSP.} +@description{ +Provides extra context for all contained ((analysis::diff::edits::AnnotatedTextEdits::TextEdit))s at once. +} +data FileSystemChange(str label = "", str description = "", bool needsConfirmation = false); + +@synopsis{Shorthand for file changes, with additional context for LSP.} +@description{ +Provides extra context for all contained ((analysis::diff::edits::AnnotatedTextEdits::TextEdit))s at once. +} +FileSystemChange changed(list[TextEdit] edits:[replace(loc l, str _), *_], str label = "", str description = "", bool needsConfirmation = false) + = changed(l.top, edits, label=label, description=description, needsConfirmation=needsConfirmation); diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index c38dcfb37..2bd6233da 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -40,7 +40,7 @@ Language Server Protocol. module util::LanguageServer import util::Reflective; -import analysis::diff::edits::TextEdits; +extend analysis::diff::edits::AnnotatedTextEdits; import IO; import ParseTree; import Message; @@ -727,36 +727,6 @@ the right ((DocumentEdit))s immediately. } data Message(list[CodeAction] fixes = []); -@synopsis{A ((analysis::diff::edits::TextEdits::TextEdit)) with additional context for LSP.} -@description{ -In LSP, text edits can contain extra information w.r.t. ((analysis::diff::edits::TextEdits::TextEdit)). -* label: Human-readable string that describes the change. -* description: Human-readable string that additionally describes the change, rendered less prominently. -* needsConfirmation: Flags whether the user should confirm this change. By default, this is false, which means that ((util::LanguageServer::TextEdit))s are applied without user confirmation. - -Typically, clients provide options to group edits by label/description when showing them to the user. -See the [LSP documentation](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#changeAnnotation) for more details. - -Note: to easily annotate all text edits in a ((analysis::diff::edits::TextEdits::FileSystemChange)), use the convenience keywords on ((util::LanguageServer::FileSystemChange)). -} -@pitfalls{ -When `needsConfirmation = false` for all edits, the client will typically apply them without showing any information from the annotations to the user. -} -data TextEdit(str label = "", str description = label, bool needsConfirmation = false); - -@synopsis{A ((analysis::diff::edits::TextEdits::FileSystemChange)) with additional context for LSP.} -@description{ -Provides extra context for all contained ((util::LanguageServer::TextEdit))s at once. -} -data FileSystemChange(str label = "", str description = "", bool needsConfirmation = false); - -@synopsis{Shorthand for file changes, with additional context for LSP.} -@description{ -Provides extra context for all contained ((util::LanguageServer::TextEdit))s at once. -} -FileSystemChange changed(list[TextEdit] edits:[replace(loc l, str _), *_], str label = "", str description = "", bool needsConfirmation = false) - = changed(l.top, edits, label=label, description=description, needsConfirmation=needsConfirmation); - @synopsis{A Command is a parameter to a CommandExecutor function.} @description{ Commands can be any closed term a() pure value without open variables or function/closure values embedded in it). Add any constructor you need to express the execution parameters @@ -822,7 +792,7 @@ interactive content have to be cleaned or closed in their own respective fashion } @benefits{ * CodeActions provide tight integration with the user experience in the IDE. Including sometimes previews, and always the undo stack. -* CodeActions can be implemented "on the language level", abstracting from UI and scheduling details. See also ((analysis::diff::edits)) for +* CodeActions can be implemented "on the language level", abstracting from UI and scheduling details. See also ((analysis::diff::edits::HiFiTreeDiff)) and ((analysis::diff::edits::HiFiLayoutDiff)) for tools that can produce lists of ((DocumentEdit))s by diffing parse trees or abstract syntax trees. * `edits` are applied on the latest editor content for the current editor; live to the user. * ((util::IDEServices::applyDocumentsEdits)) also works on open editor contents for the current editor. diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Templates.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Templates.rsc index 82363f729..cb8ff5515 100644 --- a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Templates.rsc +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Templates.rsc @@ -30,9 +30,9 @@ module lang::rascal::lsp::Templates import IO; import Location; import String; -import util::LanguageServer; import util::PathConfig; import util::Reflective; +import analysis::diff::edits::AnnotatedTextEdits; import analysis::diff::edits::TextEdits; diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/Rename.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/Rename.rsc index c717008a9..d57536625 100644 --- a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/Rename.rsc +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/Rename.rsc @@ -66,7 +66,6 @@ extend analysis::typepal::refactor::Rename; import util::Util; import util::FileSystem; -import util::LanguageServer; import util::Maybe; import util::Reflective; diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Common.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Common.rsc index 9509278c6..be57c2e14 100644 --- a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Common.rsc +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Common.rsc @@ -45,10 +45,10 @@ import Relation; import Set; import String; import util::FileSystem; -import util::LanguageServer; import util::Maybe; import util::Monitor; import util::Reflective; +import analysis::diff::edits::AnnotatedTextEdits; import util::Util; data RenameConfig( diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Fields.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Fields.rsc index 4f317af9a..b7cd6f0f0 100644 --- a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Fields.rsc +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/refactor/rename/Fields.rsc @@ -46,7 +46,6 @@ import analysis::diff::edits::TextEdits; import Map; import util::Maybe; -import util::LanguageServer; set[IdRole] fieldRoles = {fieldId(), keywordFieldId(), keywordFormalId()}; bool isFieldRole(IdRole role) = role in fieldRoles; diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/tests/rename/TestUtils.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/tests/rename/TestUtils.rsc index 414f490db..5054a5e2d 100644 --- a/rascal-lsp/src/main/rascal/lsp/lang/rascal/tests/rename/TestUtils.rsc +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/tests/rename/TestUtils.rsc @@ -48,7 +48,6 @@ import lang::rascalcore::compile::util::Names; import analysis::diff::edits::ExecuteTextEdits; import util::FileSystem; -import util::LanguageServer; import util::Math; import util::Maybe; import util::Monitor; @@ -120,6 +119,49 @@ bool expectEq(&T expected, &T actual, str epilogue = "") { return true; } +// Rascal port of TreeSearch::computeFocusList +private list[Tree] computeFocusList(amb(set[Tree] alts), int line, int col) { + if (a <- alts) { + return computeFocusList(a, line, col); + } + return []; +} + +private list[Tree] computeFocusList(tr:appl(Production p, _), int line, int col) = [tr] when isLexical(p.def) && isInside(tr, line, col); + +private bool isLexical(\lex(_)) = true; +private bool isLexical(\parameterized-lex(_, _)) = true; +private default bool isLexical(_) = false; + +private list[Tree] computeFocusList(appl(prod, _), int _, int _) = [] when prod.def is \layouts; + +private list[Tree] computeFocusList(tr:appl(_, args), int line, int col) { + list[Tree] focus = isInside(tr, line, col) ? [tr] : []; + for (a <- args, isInside(a, line, col)) { + return computeFocusList(a, line, col) + focus; + } + return focus; +} + +private default list[Tree] computeFocusList(Tree _, int _, int _) = []; + +private bool isInside(Tree tr, int line, int col) = tr.src? && isInside(tr.src, line, col); +private bool isInside(loc l, int line, int col) { + if (!(l.begin? && l.end?)) return false; + if (line < l.begin.line || line > l.end.line) return false; + + if (line == l.begin.line) { + if (line < l.end.line) { + return l.begin.column <= col; + } + return l.begin.column <= col && col <= l.end.column; + } + if (line == l.end.line) { + return col <= l.end.column; + } + return true; +} + bool testProject(set[TestModule] modules, str testName, bool(set[TestModule] mods, loc testDir, PathConfig pcfg) doCheck) { loc testDir = |unknown:///|; bool moduleExistsOnDisk = any(mmm <- modules, mmm is byLoc);