diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f51e51..64cf893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 0.4.4 + +* Fixes error that caused the `Format SQL` bitton to appear even when disabled in the settings + +## 0.4.3 + +* Trigger autocomplete only when the cell begins with `%sql` or `%%sql` + ## 0.4.2 * Removed `Share notebook` button diff --git a/jupysql_plugin/__init__.py b/jupysql_plugin/__init__.py index 6406803..e7d65db 100644 --- a/jupysql_plugin/__init__.py +++ b/jupysql_plugin/__init__.py @@ -5,3 +5,21 @@ def _jupyter_labextension_paths(): return [{"src": "labextension", "dest": "jupysql-plugin"}] + + +def _jupyter_server_extension_points(): + return [{"module": "jupysql_plugin"}] + + +def _load_jupyter_server_extension(serverapp): + """ + This function is called when the extension is loaded. + Parameters + ---------- + server_app: jupyterlab.labapp.LabApp + JupyterLab application instance + """ + serverapp.log.info(f"Registered {_module_name} server extension") + + +load_jupyter_server_extension = _load_jupyter_server_extension diff --git a/package.json b/package.json index d53cf9c..01447df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jupysql-plugin", - "version": "0.4.2", + "version": "0.4.4", "description": "Jupyterlab extension for JupySQL", "private": true, "keywords": [ diff --git a/requirements.txt b/requirements.txt index 4f65029..2285bd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -jupyterlab>=4,<4.0.12 +jupyterlab>=4 build twine hatch diff --git a/src/completer/customconnector.ts b/src/completer/customconnector.ts index 8631525..32953b8 100644 --- a/src/completer/customconnector.ts +++ b/src/completer/customconnector.ts @@ -12,6 +12,9 @@ import { import { keywords } from './keywords.json'; +const CELL_MAGIC = '%%sql'; +const LINE_MAGIC = '%sql'; + /** * A custom connector for completion handlers. */ @@ -31,7 +34,19 @@ export class SQLCompleterProvider implements ICompletionProvider { * @param context - additional information about context of completion request */ async isApplicable(context: ICompletionContext): Promise { - return true; + const editor = context.editor; + if (editor === undefined) + return false; + + // If this is a SQL magic cell, then we can complete + const firstLine = editor.getLine(0); + if (firstLine.slice(0, CELL_MAGIC.length) === CELL_MAGIC) + return true; + + // Otherwise, if we're to the right of a line magic, we can complete + const currPos = editor.getCursorPosition(); + const lineMagicPos = editor.getLine(currPos.line).indexOf(LINE_MAGIC); + return (lineMagicPos > -1 && lineMagicPos + LINE_MAGIC.length < currPos.column); } /** diff --git a/src/formatter/index.ts b/src/formatter/index.ts index 0873ef8..8bb5724 100644 --- a/src/formatter/index.ts +++ b/src/formatter/index.ts @@ -28,6 +28,8 @@ export class FormattingExtension private notebookCodeFormatter: JupyterlabNotebookCodeFormatter; private formatSQLButton: ToolbarButton; private panel: NotebookPanel; + private extensionSettings: boolean; + constructor( tracker: INotebookTracker @@ -39,6 +41,7 @@ export class FormattingExtension } private _onSettingsChanged = (sender: any, settings: JupySQLSettings) => { + this.extensionSettings = settings.showFormatSQL; if (!settings.showFormatSQL) { this.formatSQLButton.parent = null; } else { @@ -65,6 +68,11 @@ export class FormattingExtension this.formatSQLButton.node.setAttribute("data-testid", "format-btn"); panel.toolbar.insertItem(10, 'formatSQL', this.formatSQLButton); + if (!this.extensionSettings) { + this.formatSQLButton.parent = null; + } else { + this.panel.toolbar.insertItem(10, 'formatSQL', this.formatSQLButton); + } return new DisposableDelegate(() => { this.formatSQLButton.dispose(); @@ -97,4 +105,4 @@ const plugin_formatting: JupyterFrontEndPlugin = { }; -export { plugin_formatting } +export { plugin_formatting } \ No newline at end of file diff --git a/ui-tests/tests/completer.test.ts b/ui-tests/tests/completer.test.ts index 7050500..17223ad 100644 --- a/ui-tests/tests/completer.test.ts +++ b/ui-tests/tests/completer.test.ts @@ -14,17 +14,17 @@ async function createNotebook(page: IJupyterLabPageFixture) { const samples = { 'upper case': { - input: 'SEL', + input: '%sql SEL', expected: ['SELECT', 'SELECT DISTINCT', 'SELECT INTO', 'SELECT TOP'], unexpected: ['INSERT'] }, 'lower case': { - input: 'sel', + input: '%sql sel', expected: ['SELECT', 'SELECT DISTINCT', 'SELECT INTO', 'SELECT TOP'], unexpected: ['INSERT'] }, 'in-word': { - input: 'se', + input: '%sql se', expected: ['SELECT', 'SELECT DISTINCT', 'SELECT INTO', 'SELECT TOP', 'INSERT INTO'], unexpected: [] } @@ -56,7 +56,7 @@ test('test complete updates cell', async ({ page }) => { await createNotebook(page); await page.notebook.enterCellEditingMode(0); - await page.keyboard.type('SEL'); + await page.keyboard.type('%sql SEL'); await page.keyboard.press('Tab'); @@ -70,3 +70,58 @@ test('test complete updates cell', async ({ page }) => { }); }); + +const contexts = { + 'valid cell magic': { + input: '%%sql\nSEL', + completion: true + }, + 'invalid cell magic': { + input: ' %%sql\nSEL', + completion: false + }, + 'line magic': { + input: '%sql SEL', + completion: true + }, + 'line magic with python': { + input: 'result = %sql SEL', + completion: true + }, + 'line magic new line': { + input: '%sql SEL\nSEL', + completion: false + }, + 'no magic': { + input: 'SEL', + completion: false + } +} + +for (const [ name, { input, completion} ] of Object.entries(contexts)) + test(`test ${name} ${completion ? 'does' : 'does not'} complete`, async ({ page }) => { + await createNotebook(page); + + await page.notebook.enterCellEditingMode(0); + await page.keyboard.type(input); + + await page.keyboard.press('Tab'); + const suggestions = page.locator('.jp-Completer'); + if (completion) + await expect(suggestions).toBeVisible(); + else + await expect(suggestions).not.toBeVisible(); + }); + +test('test no completion before line magic', async ({ page }) => { + await createNotebook(page); + + await page.notebook.enterCellEditingMode(0); + await page.keyboard.type('SEL = %sql SEL'); + for (let i=0; i<11; i++) + await page.keyboard.press('ArrowLeft'); + + await page.keyboard.press('Tab'); + const suggestions = page.locator('.jp-Completer'); + await expect(suggestions).not.toBeVisible(); +}); diff --git a/ui-tests/tests/format_sql.test.ts b/ui-tests/tests/format_sql.test.ts index deaab7c..a611681 100644 --- a/ui-tests/tests/format_sql.test.ts +++ b/ui-tests/tests/format_sql.test.ts @@ -6,7 +6,7 @@ test('test format SQL', async ({ page }) => { await page.notebook.openByPath("sample.ipynb"); await page.notebook.activate("sample.ipynb"); await page.notebook.addCell("code", "%%sql\nselect * from table") - await page.getByTestId('format-btn').locator('button').click(); + await page.getByTestId('format-btn').locator('button').click({ force: true }); await page.waitForTimeout(2000); await expect(page.locator('body')).toContainText('SELECT');