diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java index 0099d084a..c52418ec8 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java @@ -9,7 +9,6 @@ import de.peeeq.wurstscript.intermediatelang.interpreter.NativesProvider; import de.peeeq.wurstscript.intermediatelang.interpreter.ProgramState; import net.moonlightflower.wc3libs.bin.ObjMod; -import net.moonlightflower.wc3libs.bin.app.objMod.W3U; import net.moonlightflower.wc3libs.dataTypes.DataType; import net.moonlightflower.wc3libs.dataTypes.app.War3Int; import net.moonlightflower.wc3libs.dataTypes.app.War3Real; @@ -40,20 +39,28 @@ private ILconstTuple makeKey(String key) { public ILconstTuple createObjectDefinition(ILconstString fileType, ILconstInt newUnitId, ILconstInt deriveFrom) { ObjMod dataStore = globalState.getDataStore(fileType.getVal()); String objIdString = ObjectHelper.objectIdIntToString(newUnitId.getVal()); + boolean isMeleeOverride = newUnitId.getVal() == deriveFrom.getVal(); - if (dataStore.getObjs().containsKey(ObjId.valueOf(objIdString))) { + if (!isMeleeOverride && dataStore.getObjs().containsKey(ObjId.valueOf(objIdString))) { globalState.compilationError("Object definition with id " + objIdString + " already exists."); } - ObjMod.Obj objDef = newDefFromFiletype(dataStore, deriveFrom.getVal(), newUnitId.getVal()); - // mark object with special field - ObjMod.Obj.Mod mod = new ObjMod.Obj.Mod(MetaFieldId.valueOf("wurs"), ObjMod.ValType.INT, War3Int.valueOf(ProgramState.GENERATED_BY_WURST)); - objDef.addMod(mod); + ObjMod.Obj objDef = newDefFromFiletype(dataStore, deriveFrom.getVal(), newUnitId.getVal(), isMeleeOverride); + if (!isMeleeOverride) { + // mark object with special field + ObjMod.Obj.Mod mod = new ObjMod.Obj.Mod(MetaFieldId.valueOf("wurs"), ObjMod.ValType.INT, War3Int.valueOf(ProgramState.GENERATED_BY_WURST)); + objDef.addMod(mod); + } String key = globalState.addObjectDefinition(objDef); return makeKey(key); } - private W3U.Obj newDefFromFiletype(ObjMod dataStore, int base, int newId) { + private ObjMod.Obj newDefFromFiletype(ObjMod dataStore, int base, int newId, boolean isMeleeOverride) { + if (isMeleeOverride) { + ObjId id = ObjId.valueOf(ObjectHelper.objectIdIntToString(newId)); + // same id => modify melee/original definition table + return dataStore.addObj(id, null); + } ObjId baseIdS = ObjId.valueOf(ObjectHelper.objectIdIntToString(base)); ObjId newIdS = ObjId.valueOf(ObjectHelper.objectIdIntToString(newId)); return dataStore.addObj(newIdS, baseIdS); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java index 542294e4c..0129f37a3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java @@ -554,17 +554,19 @@ public void exportToWurst(ObjMod dataStore, out.write("// Modified Table (contains all custom objects)\n\n"); exportToWurst(dataStore.getCustomObjs(), fileType, out); - out.write("// Original Table (contains all modified default objects)\n" + - "// Wurst does not support modifying default objects\n" + - "// but you can copy these functions and replace 'xxxx' with a new, custom id.\n\n"); + out.write("// Original Table (contains all modified default/melee objects)\n" + + "// These are emitted when createObjectDefinition uses the same base/new id.\n\n"); exportToWurst(dataStore.getOrigObjs(), fileType, out); } } public void exportToWurst(List customObjs, ObjectFileType fileType, Appendable out) throws IOException { for (ObjMod.Obj obj : customObjs) { - String oldId = obj.getBaseId().getVal(); - String newId = (obj.getNewId() != null ? obj.getNewId().getVal() : "xxxx"); + // Original-table objects (melee overrides) have no base/new id in wc3libs. + // For Wurst export we represent them as same-id overrides. + String objectId = obj.getId().getVal(); + String oldId = (obj.getBaseId() != null ? obj.getBaseId().getVal() : objectId); + String newId = (obj.getNewId() != null ? obj.getNewId().getVal() : objectId); out.append("@compiletime function create_").append(fileType.getExt()).append("_").append(newId) .append("()\n"); out.append("\tlet def = createObjectDefinition(\"").append(fileType.getExt()).append("\", '"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java index 4889770f2..48cabaf77 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java @@ -62,7 +62,34 @@ private DocumentSymbol makeDocumentSymbol(Element p, SymbolKind kind, String nam if (p instanceof AstElementWithParameters) { detail = "(" + HoverInfo.getParameterString((AstElementWithParameters) p) + ")"; } - return new DocumentSymbol(name, kind, Convert.range(p), Convert.errorRange(p), detail, children); + Range fullRange = Convert.range(p); + Range selectionRange = sanitizeSelectionRange(fullRange, Convert.errorRange(p)); + return new DocumentSymbol(name, kind, fullRange, selectionRange, detail, children); + } + + private Range sanitizeSelectionRange(Range fullRange, Range selectionRange) { + if (fullRange == null) { + return selectionRange; + } + if (selectionRange == null) { + return fullRange; + } + if (isRangeContained(fullRange, selectionRange)) { + return selectionRange; + } + return fullRange; + } + + private boolean isRangeContained(Range outer, Range inner) { + return compare(inner.getStart(), outer.getStart()) >= 0 + && compare(inner.getEnd(), outer.getEnd()) <= 0; + } + + private int compare(Position a, Position b) { + if (a.getLine() != b.getLine()) { + return Integer.compare(a.getLine(), b.getLine()); + } + return Integer.compare(a.getCharacter(), b.getCharacter()); } private void addSymbolsForEntity(List result, WEntity e) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java index 5231c4063..653b2fc45 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeNativesTest.java @@ -1,9 +1,11 @@ package tests.wurstscript.tests; import de.peeeq.wurstio.intermediateLang.interpreter.CompiletimeNatives; +import de.peeeq.wurstio.objectreader.ObjectHelper; import de.peeeq.wurstscript.intermediatelang.ILconstString; import net.moonlightflower.wc3libs.bin.ObjMod; import net.moonlightflower.wc3libs.bin.app.objMod.W3A; +import net.moonlightflower.wc3libs.bin.app.objMod.W3U; import net.moonlightflower.wc3libs.dataTypes.DataType; import net.moonlightflower.wc3libs.dataTypes.app.War3Real; import net.moonlightflower.wc3libs.misc.MetaFieldId; @@ -53,4 +55,53 @@ public void modifyObjectKeepsDifferentDataPointers() throws Exception { assertTrue(dataPointers.contains(0)); assertTrue(dataPointers.contains(1)); } + + @Test + public void sameIdObjectDefinitionUsesOriginalTable() throws Exception { + CompiletimeNatives natives = new CompiletimeNatives(null, null, false); + W3U w3u = new W3U(); + + Method newDefFromFiletype = CompiletimeNatives.class.getDeclaredMethod( + "newDefFromFiletype", + ObjMod.class, + int.class, + int.class, + boolean.class + ); + newDefFromFiletype.setAccessible(true); + + int hfoo = ObjectHelper.objectIdStringToInt("hfoo"); + ObjMod.Obj obj = (ObjMod.Obj) newDefFromFiletype.invoke(natives, w3u, hfoo, hfoo, true); + + assertEquals(obj.getId().getVal(), "hfoo"); + assertEquals(obj.getBaseId(), null, "Melee overwrite should be written as original-table mod"); + assertEquals(obj.getNewId(), null, "Melee overwrite should not create a custom/new id"); + assertEquals(w3u.getOrigObjs().size(), 1); + assertEquals(w3u.getCustomObjs().size(), 0); + } + + @Test + public void differentIdsObjectDefinitionUsesCustomTable() throws Exception { + CompiletimeNatives natives = new CompiletimeNatives(null, null, false); + W3U w3u = new W3U(); + + Method newDefFromFiletype = CompiletimeNatives.class.getDeclaredMethod( + "newDefFromFiletype", + ObjMod.class, + int.class, + int.class, + boolean.class + ); + newDefFromFiletype.setAccessible(true); + + int hfoo = ObjectHelper.objectIdStringToInt("hfoo"); + int hf01 = ObjectHelper.objectIdStringToInt("hf01"); + ObjMod.Obj obj = (ObjMod.Obj) newDefFromFiletype.invoke(natives, w3u, hfoo, hf01, false); + + assertEquals(obj.getId().getVal(), "hf01"); + assertEquals(obj.getBaseId().getVal(), "hfoo"); + assertEquals(obj.getNewId().getVal(), "hf01"); + assertEquals(w3u.getOrigObjs().size(), 0); + assertEquals(w3u.getCustomObjs().size(), 1); + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java index 42f011843..a0e924011 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java @@ -5,6 +5,7 @@ import de.peeeq.wurstio.languageserver.ModelManagerImpl; import de.peeeq.wurstio.languageserver.WFile; import de.peeeq.wurstio.languageserver.requests.CodeActionRequest; +import de.peeeq.wurstio.languageserver.requests.DocumentSymbolRequest; import de.peeeq.wurstio.languageserver.requests.GetDefinition; import de.peeeq.wurstio.languageserver.requests.HoverInfo; import de.peeeq.wurstio.languageserver.requests.InlayHintsRequest; @@ -18,6 +19,8 @@ import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintParams; @@ -136,6 +139,32 @@ public void codeActionsReturnQuickFixWithWorkspaceEdit() throws IOException { assertTrue(codeActions.stream().anyMatch(a -> a.getEdit() != null)); } + @Test + public void documentSymbolsHaveSelectionInsideRange() throws IOException { + CompletionTestData data = input( + "package test", + "class C", + " int x = 1", + " function f(int a)", + "init", + " skip", + "endpackage" + ); + TestContext ctx = createContext(data, data.buffer); + + DocumentSymbolParams params = new DocumentSymbolParams(new TextDocumentIdentifier(ctx.uri)); + List> symbols = + new DocumentSymbolRequest(params).execute(ctx.modelManager); + List docs = symbols.stream() + .filter(Either::isRight) + .map(Either::getRight) + .collect(Collectors.toList()); + assertFalse(docs.isEmpty()); + for (DocumentSymbol s : docs) { + assertSelectionContainedRecursive(s); + } + } + @Test public void codeActionCanReplaceVarWithLetForConstantLocal() throws IOException { CompletionTestData data = input( @@ -753,6 +782,29 @@ private List allTextEdits(WorkspaceEdit edit) { .collect(Collectors.toList()); } + private void assertSelectionContainedRecursive(DocumentSymbol s) { + assertTrue(rangeContains(s.getRange(), s.getSelectionRange()), + "selectionRange must be contained in range for symbol " + s.getName() + + " range=" + s.getRange() + " selection=" + s.getSelectionRange()); + if (s.getChildren() != null) { + for (DocumentSymbol child : s.getChildren()) { + assertSelectionContainedRecursive(child); + } + } + } + + private boolean rangeContains(Range outer, Range inner) { + return compare(inner.getStart(), outer.getStart()) >= 0 + && compare(inner.getEnd(), outer.getEnd()) <= 0; + } + + private int compare(Position a, Position b) { + if (a.getLine() != b.getLine()) { + return Integer.compare(a.getLine(), b.getLine()); + } + return Integer.compare(a.getCharacter(), b.getCharacter()); + } + private TestContext createContext(CompletionTestData data, String diskContent) throws IOException { File projectFolder = new File("./temp/lspNative/" + System.nanoTime()); File wurstFolder = new File(projectFolder, "wurst");