Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/squawk_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ log.workspace = true
simplelog.workspace = true
lsp-server.workspace = true
lsp-types.workspace = true
serde.workspace = true
serde_json.workspace = true
squawk_linter.workspace = true
squawk_syntax.workspace = true
Expand Down
34 changes: 34 additions & 0 deletions crates/squawk_server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ fn main_loop(connection: Connection, params: serde_json::Value) -> Result<()> {
continue;
}

if req.method == "squawk/syntaxTree" {
handle_syntax_tree(&connection, req)?;
continue;
}

info!("Ignoring unhandled request: {}", req.method);
}
Message::Response(resp) => {
Expand Down Expand Up @@ -235,3 +240,32 @@ fn lint(connection: &Connection, uri: lsp_types::Url, content: &str, version: i3

Ok(())
}

#[derive(serde::Deserialize)]
struct SyntaxTreeParams {
#[serde(rename = "textDocument")]
text_document: lsp_types::TextDocumentIdentifier,
// TODO: once we start storing the text doc on the server we won't need to
// send the content across the wire
text: String,
}

fn handle_syntax_tree(connection: &Connection, req: lsp_server::Request) -> Result<()> {
let params: SyntaxTreeParams = serde_json::from_value(req.params)?;
let uri = params.text_document.uri;
let content = params.text;

info!("Generating syntax tree for: {}", uri);

let parse: Parse<SourceFile> = SourceFile::parse(&content);
let syntax_tree = format!("{:#?}", parse.syntax_node());

let resp = Response {
id: req.id,
result: Some(serde_json::to_value(&syntax_tree).unwrap()),
error: None,
};

connection.sender.send(Message::Response(resp))?;
Ok(())
}
16 changes: 15 additions & 1 deletion squawk-vscode/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ export default tseslint.config(
globals: globals.node,
},
plugins: {},
rules: {},
rules: {
"no-console": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
},
)
10 changes: 10 additions & 0 deletions squawk-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@
"command": "squawk.showLogs",
"title": "Show Server Logs",
"category": "Squawk"
},
{
"command": "squawk.showSyntaxTree",
"title": "Show Syntax Tree",
"category": "Squawk"
},
{
"command": "squawk.showClientLogs",
"title": "Show Client Logs",
"category": "Squawk"
}
],
"languages": [
Expand Down
128 changes: 114 additions & 14 deletions squawk-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,28 @@ import {
} from "vscode-languageclient/node"

let client: LanguageClient | undefined
let log: Pick<
vscode.LogOutputChannel,
"trace" | "debug" | "info" | "warn" | "error" | "show"
>

export async function activate(context: vscode.ExtensionContext) {
console.log("Squawk activate")
log = vscode.window.createOutputChannel("Squawk Client", {
log: true,
})

const serverVersionCommand = vscode.commands.registerCommand(
"squawk.serverVersion",
() => {
log.info("Squawk activate")

const syntaxTreeProvider = new SyntaxTreeProvider(context)
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(
"squawk-syntax-tree",
syntaxTreeProvider,
),
)

context.subscriptions.push(
vscode.commands.registerCommand("squawk.serverVersion", () => {
try {
const serverPath = getSquawkPath(context)
const stdout = execFileSync(serverPath.path, ["--version"], {
Expand All @@ -28,17 +43,20 @@ export async function activate(context: vscode.ExtensionContext) {
} catch (error) {
vscode.window.showErrorMessage(`Failed to get server version: ${error}`)
}
},
}),
)
context.subscriptions.push(serverVersionCommand)

const showLogsCommand = vscode.commands.registerCommand(
"squawk.showLogs",
() => {
context.subscriptions.push(
vscode.commands.registerCommand("squawk.showLogs", () => {
client?.outputChannel?.show()
},
}),
)

context.subscriptions.push(
vscode.commands.registerCommand("squawk.showClientLogs", () => {
log.show()
}),
)
context.subscriptions.push(showLogsCommand)

const statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right,
Expand All @@ -57,13 +75,21 @@ export async function deactivate() {
await client?.stop()
}

function isSqlDocument(document: vscode.TextDocument): boolean {
return document.languageId === "sql" || document.languageId === "postgres"
}

function isSqlEditor(editor: vscode.TextEditor): boolean {
return isSqlDocument(editor.document)
}

function getSquawkPath(context: vscode.ExtensionContext): vscode.Uri {
const ext = process.platform === "win32" ? ".exe" : ""
return vscode.Uri.joinPath(context.extensionUri, "server", `squawk${ext}`)
}

async function startServer(context: vscode.ExtensionContext) {
console.log("starting squawk server")
log.info("Starting Squawk Language Server...")

const squawkPath = getSquawkPath(context)
const hasBinary = await vscode.workspace.fs.stat(squawkPath).then(
Expand All @@ -72,11 +98,11 @@ async function startServer(context: vscode.ExtensionContext) {
)
if (!hasBinary) {
const errorMsg = `Squawk binary not found at: ${squawkPath.path}`
console.error(errorMsg)
log.error(`ERROR: ${errorMsg}`)
vscode.window.showErrorMessage(errorMsg)
return
}
console.log(`Found Squawk binary at: ${squawkPath}`)
log.info(`Found Squawk binary at: ${squawkPath.path}`)

const serverExecutable: Executable = {
command: squawkPath.path,
Expand All @@ -94,5 +120,79 @@ async function startServer(context: vscode.ExtensionContext) {
clientOptions,
)

log.info("Language client created, starting...")
client.start()
log.info("Language client started")
}

// Based on rust-analyzer's SyntaxTree support:
// https://github.com/rust-lang/rust-analyzer/blob/c0eaff7dd1fdfffed4e5706780e79967760d1d9b/editors/code/src/commands.ts#L432-L510
class SyntaxTreeProvider implements vscode.TextDocumentContentProvider {
_eventEmitter = new vscode.EventEmitter<vscode.Uri>()
_activeEditor: vscode.TextEditor | undefined
// TODO: for now we only show syntax highlighting when someone has
// rust-analyzer installed which ships with the rast grammar
_uri = vscode.Uri.parse("squawk-syntax-tree://syntaxtree/tree.rast")

constructor(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor((editor) => {
this._onDidChangeActiveTextEditor(editor)
}),
)
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
this._onDidChangeTextDocument(event.document)
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("squawk.showSyntaxTree", async () => {
const doc = await vscode.workspace.openTextDocument(this._uri)
await vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)
}),
)

// initial kick off to make sure we have the editor set
this._onDidChangeActiveTextEditor(vscode.window.activeTextEditor)
}

onDidChange = this._eventEmitter.event

_onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
if (editor && isSqlEditor(editor)) {
this._activeEditor = editor
this._eventEmitter.fire(this._uri)
}
}

_onDidChangeTextDocument(document: vscode.TextDocument) {
if (
isSqlDocument(document) &&
this._activeEditor &&
document === this._activeEditor.document
) {
this._eventEmitter.fire(this._uri)
}
}

async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
try {
const document = this._activeEditor?.document
if (!document) {
return "Error: no active editor found"
}
const text = document.getText()
const uri = document.uri.toString()
log.info(`Requesting syntax tree for: ${uri}`)
const response = await client?.sendRequest("squawk/syntaxTree", {
textDocument: { uri },
text,
})
log.info("Syntax tree received")
return response as string
} catch (error) {
log.error(`Failed to get syntax tree: ${error}`)
return `Error: Failed to get syntax tree: ${error}`
}
}
}
Loading