Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
45dbee9
starting
jakebailey Oct 31, 2025
cf87afb
more
jakebailey Oct 31, 2025
b891674
more
jakebailey Oct 31, 2025
8dd9707
more
jakebailey Oct 31, 2025
8578677
more
jakebailey Oct 31, 2025
fdddc67
more
jakebailey Oct 31, 2025
744e631
more
jakebailey Oct 31, 2025
29dbc8b
more
jakebailey Oct 31, 2025
97d558a
more
jakebailey Oct 31, 2025
914c143
more
jakebailey Oct 31, 2025
4d8bdc1
testing
jakebailey Oct 31, 2025
39a68a3
more testing
jakebailey Oct 31, 2025
8a5d8ec
more testing
jakebailey Oct 31, 2025
dc2c8d3
local
jakebailey Oct 31, 2025
becb5f7
more
jakebailey Oct 31, 2025
8ccf80e
Fix bad commits
jakebailey Oct 31, 2025
eb9a47a
Update
jakebailey Oct 31, 2025
8e7168f
Commit test that the changes fixed
jakebailey Oct 31, 2025
07c09f3
Fix
jakebailey Oct 31, 2025
b1e875b
Fix
jakebailey Oct 31, 2025
02d247d
Fix bad test
jakebailey Oct 31, 2025
083f228
Add other tests containing semantic tokens
jakebailey Oct 31, 2025
c2ca82a
Simplifications
jakebailey Oct 31, 2025
6211ca3
Logic fix
jakebailey Oct 31, 2025
b286156
Doc update
jakebailey Oct 31, 2025
47c2722
Fix JSX issue and add a test for it
jakebailey Oct 31, 2025
a11189f
Address ordering check issue
jakebailey Oct 31, 2025
903dbfe
Panic on multi-line tokens
jakebailey Oct 31, 2025
7eff852
Reuse helper
jakebailey Oct 31, 2025
c4c6995
Merge branch 'main' into jabaile/semantic-tokens
jakebailey Oct 31, 2025
bf5fdec
Merge branch 'main' into jabaile/semantic-tokens
jakebailey Nov 6, 2025
b1c4362
Merge branch 'main' into jabaile/semantic-tokens
jakebailey Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 110 additions & 6 deletions internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,20 @@ function parseFileContent(filename: string, content: string): GoTest | undefined
};
for (const statement of statements) {
const result = parseFourslashStatement(statement);
if (result === SKIP_STATEMENT) {
// Skip this statement but continue parsing
continue;
}
if (!result) {
// Could not parse this statement - mark file as unparsed
unparsedFiles.push(filename);
return undefined;
}
else {
goTest.commands.push(...result);
}
goTest.commands.push(...result);
}
// Skip tests that have no commands (e.g., only syntactic classifications)
if (goTest.commands.length === 0) {
return undefined;
}
return goTest;
}
Expand Down Expand Up @@ -134,11 +141,22 @@ function getTestInput(content: string): string {
return `\`${testInput.join("\n")}\``;
}

// Sentinel value to indicate a statement should be skipped but parsing should continue
const SKIP_STATEMENT: unique symbol = Symbol("SKIP_STATEMENT");
type SkipStatement = typeof SKIP_STATEMENT;

/**
* Parses a Strada fourslash statement and returns the corresponding Corsa commands.
* @returns an array of commands if the statement is a valid fourslash command, or `false` if the statement could not be parsed.
* @returns an array of commands if the statement is a valid fourslash command,
* SKIP_STATEMENT if the statement should be skipped but parsing should continue,
* or `undefined` if the statement could not be parsed and the file should be marked as unparsed.
*/
function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
function parseFourslashStatement(statement: ts.Statement): Cmd[] | SkipStatement | undefined {
// Skip empty statements (bare semicolons)
if (ts.isEmptyStatement(statement)) {
return SKIP_STATEMENT;
}

if (ts.isVariableStatement(statement)) {
// variable declarations (for ranges and markers), e.g. `const range = test.ranges()[0];`
return [];
Expand Down Expand Up @@ -210,6 +228,10 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
case "renameInfoSucceeded":
case "renameInfoFailed":
return parseRenameInfo(func.text, callExpression.arguments);
case "semanticClassificationsAre":
return parseSemanticClassificationsAre(callExpression.arguments);
case "syntacticClassificationsAre":
return SKIP_STATEMENT;
}
}
// `goTo....`
Expand Down Expand Up @@ -1509,6 +1531,71 @@ function parseBaselineSmartSelection(args: ts.NodeArray<ts.Expression>): Cmd {
};
}

function parseSemanticClassificationsAre(args: readonly ts.Expression[]): [VerifySemanticClassificationsCmd] | SkipStatement | undefined {
if (args.length < 1) {
console.error("semanticClassificationsAre requires at least a format argument");
return undefined;
}

const formatArg = args[0];
if (!ts.isStringLiteralLike(formatArg)) {
console.error("semanticClassificationsAre first argument must be a string literal");
return undefined;
}

const format = formatArg.text;

// Only handle "2020" format for semantic tokens
if (format !== "2020") {
// Skip other formats like "original" - return sentinel to continue parsing
return SKIP_STATEMENT;
}

const tokens: Array<{ type: string; text: string; }> = [];

// Parse the classification tokens (c2.semanticToken("type", "text"))
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (!ts.isCallExpression(arg)) {
console.error(`Expected call expression for token at index ${i}`);
return undefined;
}

if (!ts.isPropertyAccessExpression(arg.expression) || arg.expression.name.text !== "semanticToken") {
console.error(`Expected semanticToken call at index ${i}`);
return undefined;
}

if (arg.arguments.length < 2) {
console.error(`semanticToken requires 2 arguments at index ${i}`);
return undefined;
}

const typeArg = arg.arguments[0];
const textArg = arg.arguments[1];

if (!ts.isStringLiteralLike(typeArg) || !ts.isStringLiteralLike(textArg)) {
console.error(`semanticToken arguments must be string literals at index ${i}`);
return undefined;
}

// Map TypeScript's internal "member" type to LSP's "method" type
let tokenType = typeArg.text;
tokenType = tokenType.replace(/\bmember\b/g, "method");

tokens.push({
type: tokenType,
text: textArg.text,
});
}

return [{
kind: "verifySemanticClassifications",
format,
tokens,
}];
}

function parseKind(expr: ts.Expression): string | undefined {
if (!ts.isStringLiteral(expr)) {
console.error(`Expected string literal for kind, got ${expr.getText()}`);
Expand Down Expand Up @@ -1725,6 +1812,12 @@ interface VerifyRenameInfoCmd {
preferences: string;
}

interface VerifySemanticClassificationsCmd {
kind: "verifySemanticClassifications";
format: string;
tokens: Array<{ type: string; text: string; }>;
}

type Cmd =
| VerifyCompletionsCmd
| VerifyApplyCodeActionFromCompletionCmd
Expand All @@ -1739,7 +1832,8 @@ type Cmd =
| VerifyQuickInfoCmd
| VerifyBaselineRenameCmd
| VerifyRenameInfoCmd
| VerifyBaselineInlayHintsCmd;
| VerifyBaselineInlayHintsCmd
| VerifySemanticClassificationsCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string {
let expectedList: string;
Expand Down Expand Up @@ -1840,6 +1934,14 @@ function generateBaselineInlayHints({ span, preferences }: VerifyBaselineInlayHi
return `f.VerifyBaselineInlayHints(t, ${span}, ${preferences})`;
}

function generateSemanticClassifications({ format, tokens }: VerifySemanticClassificationsCmd): string {
const tokensStr = tokens.map(t => `{Type: ${getGoStringLiteral(t.type)}, Text: ${getGoStringLiteral(t.text)}}`).join(",\n\t\t");
const maybeComma = tokens.length > 0 ? "," : "";
return `f.VerifySemanticTokens(t, []fourslash.SemanticToken{
${tokensStr}${maybeComma}
})`;
}

function generateCmd(cmd: Cmd): string {
switch (cmd.kind) {
case "verifyCompletions":
Expand Down Expand Up @@ -1878,6 +1980,8 @@ function generateCmd(cmd: Cmd): string {
return `f.VerifyRenameFailed(t, ${cmd.preferences})`;
case "verifyBaselineInlayHints":
return generateBaselineInlayHints(cmd);
case "verifySemanticClassifications":
return generateSemanticClassifications(cmd);
default:
let neverCommand: never = cmd;
throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`);
Expand Down
1 change: 1 addition & 0 deletions internal/fourslash/_scripts/manualTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ completionsSelfDeclaring1
completionsWithDeprecatedTag4
renameDefaultKeyword
renameForDefaultExport01
semanticModernClassificationFunctions
tsxCompletion12
80 changes: 71 additions & 9 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type FourslashTest struct {
lastKnownMarkerName *string
activeFilename string
selectionEnd *lsproto.Position

// Semantic token configuration
semanticTokenTypes []string
semanticTokenModifiers []string
}

type scriptInfo struct {
Expand Down Expand Up @@ -183,15 +187,17 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten
})

f := &FourslashTest{
server: server,
in: inputWriter,
out: outputReader,
testData: &testData,
userPreferences: lsutil.NewDefaultUserPreferences(), // !!! parse default preferences for fourslash case?
vfs: fs,
scriptInfos: scriptInfos,
converters: converters,
baselines: make(map[string]*strings.Builder),
server: server,
in: inputWriter,
out: outputReader,
testData: &testData,
userPreferences: lsutil.NewDefaultUserPreferences(), // !!! parse default preferences for fourslash case?
vfs: fs,
scriptInfos: scriptInfos,
converters: converters,
baselines: make(map[string]*strings.Builder),
semanticTokenTypes: defaultSemanticTokenTypes(),
semanticTokenModifiers: defaultSemanticTokenModifiers(),
}

// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
Expand Down Expand Up @@ -231,6 +237,50 @@ func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCap
sendNotification(t, f, lsproto.InitializedInfo, &lsproto.InitializedParams{})
}

func defaultSemanticTokenTypes() []string {
return []string{
string(lsproto.SemanticTokenTypesnamespace),
string(lsproto.SemanticTokenTypesclass),
string(lsproto.SemanticTokenTypesenum),
string(lsproto.SemanticTokenTypesinterface),
string(lsproto.SemanticTokenTypesstruct),
string(lsproto.SemanticTokenTypestypeParameter),
string(lsproto.SemanticTokenTypestype),
string(lsproto.SemanticTokenTypesparameter),
string(lsproto.SemanticTokenTypesvariable),
string(lsproto.SemanticTokenTypesproperty),
string(lsproto.SemanticTokenTypesenumMember),
string(lsproto.SemanticTokenTypesdecorator),
string(lsproto.SemanticTokenTypesevent),
string(lsproto.SemanticTokenTypesfunction),
string(lsproto.SemanticTokenTypesmethod),
string(lsproto.SemanticTokenTypesmacro),
string(lsproto.SemanticTokenTypeslabel),
string(lsproto.SemanticTokenTypescomment),
string(lsproto.SemanticTokenTypesstring),
string(lsproto.SemanticTokenTypeskeyword),
string(lsproto.SemanticTokenTypesnumber),
string(lsproto.SemanticTokenTypesregexp),
string(lsproto.SemanticTokenTypesoperator),
}
}

func defaultSemanticTokenModifiers() []string {
return []string{
string(lsproto.SemanticTokenModifiersdeclaration),
string(lsproto.SemanticTokenModifiersdefinition),
string(lsproto.SemanticTokenModifiersreadonly),
string(lsproto.SemanticTokenModifiersstatic),
string(lsproto.SemanticTokenModifiersdeprecated),
string(lsproto.SemanticTokenModifiersabstract),
string(lsproto.SemanticTokenModifiersasync),
string(lsproto.SemanticTokenModifiersmodification),
string(lsproto.SemanticTokenModifiersdocumentation),
string(lsproto.SemanticTokenModifiersdefaultLibrary),
"local",
}
}

var (
ptrTrue = ptrTo(true)
defaultCompletionCapabilities = &lsproto.CompletionClientCapabilities{
Expand Down Expand Up @@ -293,6 +343,18 @@ func getCapabilitiesWithDefaults(capabilities *lsproto.ClientCapabilities) *lspr
},
}
}
if capabilitiesWithDefaults.TextDocument.SemanticTokens == nil {
capabilitiesWithDefaults.TextDocument.SemanticTokens = &lsproto.SemanticTokensClientCapabilities{
Requests: &lsproto.ClientSemanticTokensRequestOptions{
Full: &lsproto.BooleanOrClientSemanticTokensRequestFullDelta{
Boolean: ptrTrue,
},
},
TokenTypes: defaultSemanticTokenTypes(),
TokenModifiers: defaultSemanticTokenModifiers(),
Formats: []lsproto.TokenFormat{lsproto.TokenFormatRelative},
}
}
if capabilitiesWithDefaults.Workspace == nil {
capabilitiesWithDefaults.Workspace = &lsproto.WorkspaceClientCapabilities{}
}
Expand Down
Loading