Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faulty/Undesired CodeCompletion behaviour #2620

Open
angelickite opened this issue Jun 21, 2024 · 11 comments · May be fixed by #3350
Open

Faulty/Undesired CodeCompletion behaviour #2620

angelickite opened this issue Jun 21, 2024 · 11 comments · May be fixed by #3350
Assignees

Comments

@angelickite
Copy link

Hello. This situation has been driving me (and others) crazy every minute(!) I am coding with eclipse since forever, so I finally took some time out to at least pinpoint the offending plugin and commit. Hope this helps!

This behaviour started back with eclipe 2021-06 while 2021-03 still behaved "well".

Code completion is behaving in an undesired manner in the following situation:

public class Demo {
	public static void main(String[] args) {
		System.out.println("hello");
		if (true) {
			System.out.print|cursor|ln("world");
		}
	}
}

Triggering code completion (ctrl+space) while the cursor is located as indicated gives a list of proposals, so far so good.
Now, after selecting the first proposal (in my case the "print" method), the code gets transformed as follows:

public class Demo {
	public static void main(String[] args) {
		System.out.println("hello");
		if (true) {
			System.out.print(false);
		}
	}
}

The expected behaviour would be that the existing parameter "world" remains in place and only the "ln" part of "println" gets removed, i.e. the method call is updated from "println" to "print", like so:

public class Demo {

	public static void main(String[] args) {
		System.out.println("hello");
		if (true) {
			System.out.print("world");
		}
	}
}

Note that I have enclosed the second println statement in an if block (the true doesnt matter). An empty block does not show the bad behaviour. Here is an (incomplete) overview:

public class Test {

  public static void main (String[] args) {
    // good behaviour
    System.out.println("hello");

    { // good behaviour
      System.out.println("world");
    }

    if (true) { // bad behaviour
      System.out.println("world");
    }

    while (true) { // bad behaviour
      System.out.println("world");
      break;
    }

    for (int i = 0; i < 10; i++) { // bad behaviour
      System.out.println("world");
    }

    Runnable a = () -> { // bad behaviour
      System.out.println("world");
    };

    Nested n = new Nested();
    // good behaviour for deeper1, bad behaviour for deeper2
    n.deeper1(1, 2).deeper2(3, 4);
  }

  public static class Nested {
    public Nested deeper;

    public Nested deeper1 (int a, int b) {
      return deeper;
    }

    public Nested deeper2 (int a, int b) {
      return deeper;
    }
  }

}

My content assist settings:
settings

I was able to pinpoint the issue to the jdt.core, specifically the following commit: e3517dd

To arrive at this commit I used a git bisect:

Due to the complexity of that specific commit, me looking into the eclipse source code for the first time, and my lack of time, I was hoping someone more experienced could pick it up from here.

There are some other completion issues I have been encountering which are possibly related to this, so looking into it is hopefully time well spent.

Additionally possibly related:

#1770
#1016

Thank you!

@srikanth-sankaran
Copy link
Contributor

@stephan-herrmann can you take a look please ?? Thanks

@stephan-herrmann
Copy link
Contributor

@angelickite thanks for narrowing it down so far! What is the most recent version of Eclipse that you tested?

@gayanper this smells a bit like work were we let assistNodeParent point to a control structure, which then affects the expected type (to expect boolean) etc. What's really strange, how could anything like that cause println("world") to be changed to print(false), the false appears in a position that should not be influenced by the assist node parent.

@angelickite
Copy link
Author

angelickite commented Jun 21, 2024

@stephan-herrmann I am usually and currently running on the latest publicly available version of eclipse. Also just grabbed a fresh copy from https://www.eclipse.org/downloads/ with

Eclipse IDE for Enterprise Java and Web Developers (includes Incubating components)
Version: 2024-06 (4.32.0)
Build id: 20240606-1231

The situation with that version is exactly as described so far. As I didn't figure out yet how to fully build eclipse from source I can not test any farther.

@stephan-herrmann
Copy link
Contributor

Version: 2024-06 (4.32.0)

Thanks, @angelickite So my hopes this might have been fixed in the meanwhile are void :)

first bad: e3517dd

Many thanks for this pointer! Unfortunately, it points to a major refactoring, which at that time fixed many bugs at once, but apparently, there are negative effects as well :(

This implies there will be little use in delta-debugging before and after states. There won't be a short cut around debugging the "new strategy" in detail.

@angelickite
Copy link
Author

angelickite commented Jun 22, 2024

I did some more digging.

Here is where the completion gets recognized in the "bad" case:

if (this.assistNode == null && this.lParenPos > this.cursorLocation && nameStart <= this.cursorLocation + 1) {
boolean nextIsCast = this.expressionPtr > -1 && this.expressionStack[this.expressionPtr] instanceof CastExpression;
m = new CompletionOnMessageSendName(null, 0, 0, nextIsCast); // positions will be set in consumeMethodInvocationName(), if that's who called us

Here is why the source gets overwriten:

if (messageSend.statementEnd > messageSend.sourceStart)
setSourceRange(messageSend.sourceStart, messageSend.statementEnd);

  • It seems like the recognition site is already aware of the special case of the cursor being left to the paren
  • The messageSend.statementEnd is not desired for that special case

Please note that I did all the debugging around eclipse 2021-06 so far, though the findings should be applicable for the current codebase.

Hope this helps!

@angelickite
Copy link
Author

angelickite commented Jun 23, 2024

It looks like the new strategy introduced with e3517dd is potentially more "potent" overall but lost some of its previous cursor awareness.

Here is another example that got broken with the new strategy that has been bugging me daily over the years:

public class Demo2 {
	public static void main(String[] args) {
		Test test;
		if (true) {
			test.|cursor|.test;
		}
	}

	public static class Test {
		public Test test;
	}
}

Triggering autocomplete at the given cursor location by typing "tes" and then confirming with enter leads to the following result:

public class Demo2 {
	public static void main(String[] args) {
		Test test;
		if (true) {
			test.tetest;
		}
	}

	public static class Test {
		public Test test;
	}
}

The completion engine correctly triggers a CompletionOnQualifiedNameReference, however sometime (during Parsing?) the sourcePositions that the completion works with gets filled up right to the end of the expression, i.e. in the above example there are 3 source positions with the cursor being placed on the second one, whereas the old strategy only had 2 source positions (ending at the cursor?). The completion handler then fails here:

long completionPosition = sourcePositions[sourcePositions.length - 1];

In this case the cursor is located on the second qualified name at sourcePosition[1], so I would expect that one to be the source boundary.

Not sure what the correct way forward is, be it changing the overall strategy or bringing back cursor awareness to the new strategy, but I would love for the autocomplete to be an enhancer again, not a continuous struggle during development.

@stephan-herrmann
Copy link
Contributor

@angelickite I'm amazed by the depth of your analysis :)

While currently I can't make this issue my main focus, I'd be more than happy to help you getting even closer to the root of the matter.

Some general links you may or may not have seen yet:

@stephan-herrmann
Copy link
Contributor

In this case the cursor is located on the second qualified name at sourcePosition[1], so I would expect that one to be the source boundary.

Here's a concrete thing you could try:

  • have a look at CompletionEngine.actualCompletionPosition
    • quickly check if this is the same as this.parser.scanner.cursorLocation
  • then identify the name segment that contains this position
    • have you seen how the long position is split into start and end locations (int)?
      See, e.g., invocations of setSourceAndTokenRange().
  • after code modification run "all" tests :)

@angelickite
Copy link
Author

angelickite commented Jun 24, 2024

Thank you for your pointers.

I gave the whole contribution thing a quick shot but quickly came up against walls that I am currently not willing to put up with. I know eclipse project is at a high level of complexity, however from my point of view enabling contributing is the number one priority for a project of its kind, so anything beyond a "git clone, checkout, run tests, build" (each a single action) is just very off putting. Please don't take the nagging in a bad taste, just a personal pet peeve I needed to get out.

Here is an attempt of mine in case someone else is able to pick it up from here:

if (this.assistNode == null && this.lParenPos > this.cursorLocation && nameStart <= this.cursorLocation + 1) {
boolean nextIsCast = this.expressionPtr > -1 && this.expressionStack[this.expressionPtr] instanceof CastExpression;
m = new CompletionOnMessageSendName(null, 0, 0, nextIsCast); // positions will be set in consumeMethodInvocationName(), if that's who called us

		CompletionOnMessageSendName completion = new CompletionOnMessageSendName(null, 0, 0, nextIsCast); // positions will be set in consumeMethodInvocationName(), if that's who called us
		completion.cursorIsToTheLeftOfTheLParen = true;
		m = completion;

if (messageSend.statementEnd > messageSend.sourceStart)
setSourceRange(messageSend.sourceStart, messageSend.statementEnd);

			if (messageSend.cursorIsToTheLeftOfTheLParen) {
				// take into consideration the cursor location, i.e. it being to the left of lParen. (GH-2620 @https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2620)
				setSourceAndTokenRange(messageSend.sourceStart, messageSend.sourceEnd);
			} else {
				setTokenRange(messageSend.sourceStart, messageSend.sourceEnd);
				if (messageSend.statementEnd > messageSend.sourceStart)
					setSourceRange(messageSend.sourceStart, messageSend.statementEnd);
			}

long completionPosition = sourcePositions[sourcePositions.length - 1];

		// take into consideration the cursor location. (GH-2620 @https://github.com/eclipse-jdt/eclipse.jdt.core/issues/2620)
		// void x(LinkedListNode n) {
		//   LinkedListNode other = n.ch|cursor|.child.child.parent;
		//   // ...
		// }
		// in the above example, bestPosition will match against the cursor with "ch", i.e. sourcePositions[1].
		// (sourcePositions[0] points to 'n', sourcePositions[4] to 'parent')
		long bestPosition;
		{
			bestPosition = -1L;
			for (int i = 0; i < sourcePositions.length; i++) {
				long p = sourcePositions[i];
				int start = (int) (p >>> 32);
				int end = (int) (p);
				if (start >= this.actualCompletionPosition && end <= this.actualCompletionPosition) {
					bestPosition = p;
					// we take the matching position that is the "farthest to the right".
					// whether that is correct in all future cases remains to be seen.
					// for the time of this writing the last match suffices.
					// (if for example the first match is desired, we could "break;" here)
				}
			}
		}
		if (bestPosition == -1L) {
			// fallback to the previous behaviour, just in case.
			bestPosition = sourcePositions[sourcePositions.length - 1];
		}
		long completionPosition = bestPosition;

@stephan-herrmann
Copy link
Contributor

I know eclipse project is at a high level of complexity, however from my point of view enabling contributing is the number one priority for a project of its kind, so anything beyond a "git clone, checkout, run tests, build" (each a single action) is just very off putting.

Please don't tell this to people like Ed Merks, who have been working very, very hard to make workspace setup as simple as possible (but not simpler than that ;-P ).

Have you tried the eclipse installer? It performs all necessary steps with just a few clicks: select your eclipse version, select the eclipse projects you want to work on, select an installation folder, and swooop, after a coffee break your shiny new workspace should be waiting for you :)

And if something goes wrong during setup, there are friendly folks who will be happy to accept your report (knowing that your reports will show enough details work investigation).

@angelickite
Copy link
Author

I have observed that in some cases the sourcePositions of a completion behave oddly:

public class Demo2 {
	public static void main(String[] args) {
		Test test;
		test.|.test; // (1)
		if (true) {
			test.|.test; // (2)
		}
	}
	public static class Test {
		public Test test;
	}
}

With the cursor at '|' the parser packs the sourcePosition at the cursor location such that start > end, e.g. if start is 89, then end is 88. Seems to happen only specifically in a dot-cursor-dot situation.

Is this an known/accepted oddity of the parser or a new issue by itself?

I have encountered this behaviour during my work on this issue. For now I will proceed by locally swapping start and end in such an edge-case as to not blow up this whole issue into a full rework of the underlying system.

angelickite added a commit to angelickite/eclipse.jdt.core that referenced this issue Nov 25, 2024
angelickite added a commit to angelickite/eclipse.jdt.core that referenced this issue Nov 25, 2024
angelickite added a commit to angelickite/eclipse.jdt.core that referenced this issue Nov 26, 2024
Completions for CompletionOnMessageSendName as well as for
QualifiedNameReference did not take the cursor position into
consideration, leading to various downstream issues like failing to
complete at all, not properly overwriting a source range or overwriting
too eagerly.

fixes eclipse-jdt#2620
@angelickite angelickite linked a pull request Nov 26, 2024 that will close this issue
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants