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

Adds formatting capabilities to vs-code extension #402

Merged
merged 13 commits into from
Mar 12, 2025

Conversation

simonthuresson
Copy link
Contributor

@simonthuresson simonthuresson commented Mar 11, 2025

Description

This PR adds a document formatting endpoint to the language server package, allowing clients to request document formatting. It also sets the flag that the server is capable of formatting a document.

Updates the vs-code extension README to include the added formatting feature.

Initially, we investigated implementing cursor position tracking using our internal cursor tracking, but discovered that VS Code already handles cursor position updates automatically. Therefore, no additional logic was needed for cursor position management.

Copy link

changeset-bot bot commented Mar 11, 2025

⚠️ No Changeset found

Latest commit: 456f1bb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@@ -12,6 +12,7 @@ Our extension preview provides a rich set of features for working with Cypher, t
- Syntax checking - both simple and semantic errors (e.g. type errors, unknown labels, etc)
- Autocompletion for Cypher keywords, functions, labels, properties, database names and more
- Signature help for functions - shows the signature of the function while typing
- Formatting - format the document according to the Cypher styleguide
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@OskarDamkjaer
Copy link
Collaborator

OskarDamkjaer commented Mar 11, 2025

Really cool to see the queries get automatically formatted on save, however I couldn't get this one to work and I don't understand why (since i get no syntax errors):

CALL {
  CALL {
      UNWIND range(1,4) AS direction
      MATCH p = (start:Cell)(()-[r WHERE r.direction = direction]->()){4}(end)
      OPTIONAL MATCH (before_start)-[r0]->(start)
          WHERE r0.direction = direction
      OPTIONAL MATCH (end)-[r5]->(after_end)
          WHERE r5.direction = direction
      RETURN p, before_start, start, end, after_end, direction
  } // -- all valid paths

  WITH *
      WHERE all(node IN nodes(p) WHERE node.state IS NULL OR node.state = $symbol) OR 
            all(node IN nodes(p) WHERE node.state IS NULL OR node.state <> $symbol) // -- exclude all paths that include both symbols since they cannot lead to victory

  WITH *,
      CASE
      WHEN before_start IS NOT NULL AND before_start.state IS NULL AND end.state IS NULL THEN 1
      WHEN start.state IS NULL AND after_end IS NOT NULL AND after_end.state IS NULL THEN 2
      ELSE 0
      END AS openEnded

  WITH *, 
      size([node IN nodes(p) WHERE node.state = $symbol]) AS myScore, 
      size([node IN nodes(p) WHERE node.state <> $symbol]) AS otherScore
      // -- the two size filters above are enough, since any path including both symbols have already been excluded above

  UNWIND nodes(p) AS candidate 
  WITH * WHERE candidate.state IS NULL

  WITH *,
  CASE
      WHEN myScore = 4 THEN 2
      WHEN otherScore = 4 THEN 1
      ELSE 0
  END AS isWinningMove, // -- 2 means it is my win, 1 means that opponent could win in next move
  CASE
      WHEN myScore = 3 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 2     
      WHEN otherScore = 3 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 1
      ELSE 0
  END AS isThreeWinningMove,  
  CASE
      WHEN myScore = 3 THEN 1
      ELSE 0
  END AS isMyThreeMove,
  CASE
      WHEN otherScore = 3 THEN 1
      ELSE 0
  END AS isOtherThreeMove, 
  CASE
      WHEN myScore = 2 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 1
      ELSE 0
  END AS isMyTwoMove,
  CASE
      WHEN otherScore = 2 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 1
      ELSE 0
  END AS isOtherTwoMove

  WITH candidate, 
      direction,
      max(isWinningMove) AS maxIsWinningMove,
      max(isThreeWinningMove) AS maxIsThreeWinningMove,
      max(isMyThreeMove) AS maxIsMyThreeMove,
      max(isOtherThreeMove) AS maxIsOtherThreeMove,
      max(isMyTwoMove) AS maxIsMyTwoMove,
      max(isOtherTwoMove) AS maxIsOtherTwoMove,
      sum((myScore + otherScore + toInteger(openEnded > 0))) AS candidateScore // -- give an additional point for open-ended paths, as a heuristic

  WITH candidate, 
      sum(maxIsWinningMove) AS isWinningMove,
      sum(maxIsThreeWinningMove) AS isThreeWinningMove,
      sum(maxIsMyThreeMove) AS isMyThreeMoveSum,
      sum(maxIsOtherThreeMove) AS isOtherThreeMoveSum,
      sum(maxIsMyTwoMove) AS isMyTwoMoveSum,
      sum(maxIsOtherTwoMove) AS isOtherTwoMoveSum,
      sum(candidateScore) + count(*) AS score // -- give one point extra for each direction that a candidate advances play in, as a heuristic

  WITH *,
  CASE
      WHEN isMyThreeMoveSum >= 2 THEN 2
      WHEN isOtherThreeMoveSum >= 2 THEN 1
      WHEN isMyThreeMoveSum > 0 AND isMyTwoMoveSum > 0 THEN 2
      WHEN isOtherThreeMoveSum > 0 AND isOtherTwoMoveSum > 0 THEN 1
      WHEN isMyTwoMoveSum  >= 2 THEN 2
      WHEN isOtherTwoMoveSum  >= 2 THEN 1
      ELSE 0
  END AS isFork

  WITH * ORDER BY isWinningMove DESC, isThreeWinningMove DESC, isFork DESC, score DESC 
  LIMIT 1
  RETURN candidate, isWinningMove
  
  UNION 

  // -- When the game will for sure end in a tie we still need to update the game

  MATCH (candidate) WHERE candidate.state IS NULL
  RETURN candidate, -1 AS isWinningMove LIMIT 1
}

WITH * ORDER BY isWinningMove DESC
LIMIT 1

SET candidate.state = $symbol

Logs say:

[Error - 10:49:51] Request textDocument/formatting failed.
  Message: Request textDocument/formatting failed with message: Cannot read properties of null (reading 'getText')
  Code: -32603 

@simonthuresson
Copy link
Contributor Author

Really cool to see the queries get automatically formatted on save, however I couldn't get this one to work and I don't understand why (since i get no syntax errors):

CALL {
  CALL {
      UNWIND range(1,4) AS direction
      MATCH p = (start:Cell)(()-[r WHERE r.direction = direction]->()){4}(end)
      OPTIONAL MATCH (before_start)-[r0]->(start)
          WHERE r0.direction = direction
      OPTIONAL MATCH (end)-[r5]->(after_end)
          WHERE r5.direction = direction
      RETURN p, before_start, start, end, after_end, direction
  } // -- all valid paths

  WITH *
      WHERE all(node IN nodes(p) WHERE node.state IS NULL OR node.state = $symbol) OR 
            all(node IN nodes(p) WHERE node.state IS NULL OR node.state <> $symbol) // -- exclude all paths that include both symbols since they cannot lead to victory

  WITH *,
      CASE
      WHEN before_start IS NOT NULL AND before_start.state IS NULL AND end.state IS NULL THEN 1
      WHEN start.state IS NULL AND after_end IS NOT NULL AND after_end.state IS NULL THEN 2
      ELSE 0
      END AS openEnded

  WITH *, 
      size([node IN nodes(p) WHERE node.state = $symbol]) AS myScore, 
      size([node IN nodes(p) WHERE node.state <> $symbol]) AS otherScore
      // -- the two size filters above are enough, since any path including both symbols have already been excluded above

  UNWIND nodes(p) AS candidate 
  WITH * WHERE candidate.state IS NULL

  WITH *,
  CASE
      WHEN myScore = 4 THEN 2
      WHEN otherScore = 4 THEN 1
      ELSE 0
  END AS isWinningMove, // -- 2 means it is my win, 1 means that opponent could win in next move
  CASE
      WHEN myScore = 3 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 2     
      WHEN otherScore = 3 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 1
      ELSE 0
  END AS isThreeWinningMove,  
  CASE
      WHEN myScore = 3 THEN 1
      ELSE 0
  END AS isMyThreeMove,
  CASE
      WHEN otherScore = 3 THEN 1
      ELSE 0
  END AS isOtherThreeMove, 
  CASE
      WHEN myScore = 2 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 1
      ELSE 0
  END AS isMyTwoMove,
  CASE
      WHEN otherScore = 2 AND ((openEnded = 1 AND candidate <> end) OR (openEnded = 2 AND candidate <> start)) THEN 1
      ELSE 0
  END AS isOtherTwoMove

  WITH candidate, 
      direction,
      max(isWinningMove) AS maxIsWinningMove,
      max(isThreeWinningMove) AS maxIsThreeWinningMove,
      max(isMyThreeMove) AS maxIsMyThreeMove,
      max(isOtherThreeMove) AS maxIsOtherThreeMove,
      max(isMyTwoMove) AS maxIsMyTwoMove,
      max(isOtherTwoMove) AS maxIsOtherTwoMove,
      sum((myScore + otherScore + toInteger(openEnded > 0))) AS candidateScore // -- give an additional point for open-ended paths, as a heuristic

  WITH candidate, 
      sum(maxIsWinningMove) AS isWinningMove,
      sum(maxIsThreeWinningMove) AS isThreeWinningMove,
      sum(maxIsMyThreeMove) AS isMyThreeMoveSum,
      sum(maxIsOtherThreeMove) AS isOtherThreeMoveSum,
      sum(maxIsMyTwoMove) AS isMyTwoMoveSum,
      sum(maxIsOtherTwoMove) AS isOtherTwoMoveSum,
      sum(candidateScore) + count(*) AS score // -- give one point extra for each direction that a candidate advances play in, as a heuristic

  WITH *,
  CASE
      WHEN isMyThreeMoveSum >= 2 THEN 2
      WHEN isOtherThreeMoveSum >= 2 THEN 1
      WHEN isMyThreeMoveSum > 0 AND isMyTwoMoveSum > 0 THEN 2
      WHEN isOtherThreeMoveSum > 0 AND isOtherTwoMoveSum > 0 THEN 1
      WHEN isMyTwoMoveSum  >= 2 THEN 2
      WHEN isOtherTwoMoveSum  >= 2 THEN 1
      ELSE 0
  END AS isFork

  WITH * ORDER BY isWinningMove DESC, isThreeWinningMove DESC, isFork DESC, score DESC 
  LIMIT 1
  RETURN candidate, isWinningMove
  
  UNION 

  // -- When the game will for sure end in a tie we still need to update the game

  MATCH (candidate) WHERE candidate.state IS NULL
  RETURN candidate, -1 AS isWinningMove LIMIT 1
}

WITH * ORDER BY isWinningMove DESC
LIMIT 1

SET candidate.state = $symbol

Logs say:

[Error - 10:49:51] Request textDocument/formatting failed.
  Message: Request textDocument/formatting failed with message: Cannot read properties of null (reading 'getText')
  Code: -32603 

Hmm I can not get it to work either, will look into it before merge!

@simonthuresson
Copy link
Contributor Author

Really cool to see the queries get automatically formatted on save, however I couldn't get this one to work and I don't understand why (since i get no syntax errors):

I traced this to a issue with the formatter itself, I am getting the same issue running this in codemirror. I have added a Trello card for this. So I dont think this is related to this PR.

@tomasnyberg
Copy link
Contributor

tomasnyberg commented Mar 11, 2025

The query above errors because of an issue in visitQuantifier, fix here #403

@ncordon ncordon self-assigned this Mar 11, 2025
Comment on lines 1169 to 1179
visitMergeClause = (ctx: MergeClauseContext) => {
handleMergeClause(
ctx,
(node) => this.visit(node),
() => this.startGroupAlsoOnComment(),
(id) => this.endGroup(id),
() => this.avoidBreakBetween(),
);
this.visit(ctx.MERGE());
this.avoidBreakBetween();
const patternGrp = this.startGroupAlsoOnComment();
this.visit(ctx.pattern());
this.endGroup(patternGrp);
const n = ctx.mergeAction_list().length;
for (let i = 0; i < n; i++) {
this.visit(ctx.mergeAction(i));
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is not part of this pr right? Not sure why it's still showing in the diff if it was merged to main

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is definitely not. Not sure why this is here at all. I merged main into this PR and after that it dissapeared. So i guess I was just not up to date with main.

@@ -69,7 +69,7 @@ export async function testSyntaxValidation({
);
}

suite.only('Syntax validation spec', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Collaborator

@ncordon ncordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd make sense to clean up the history if you could before merge so the diff shows only the relevant changes

@simonthuresson
Copy link
Contributor Author

It'd make sense to clean up the history if you could before merge so the diff shows only the relevant changes

Will definitely do that. Merge main into this branch sorted things out.

@simonthuresson simonthuresson merged commit e2c47b7 into neo4j:main Mar 12, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants