Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ public void createOrUpdateFromConnection(Connection connection, String schemaFil
}

LOGGER.info("Completer initialized with " + schemas.size() + " schemas, " +
columns.size() + " tables and " + keywords.size() + " keywords");
tables.size() + " tables, " + columns.size() + " columns and " +
keywords.size() + " keywords");
}

} catch (SQLException | IOException e) {
Expand Down
15 changes: 11 additions & 4 deletions zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,12 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
meta: v.meta,
caption: computeCaption(v.name, v.meta),
score: 300,
// Marks this candidate as coming from the interpreter, so that
// completionSupportWithBackend can tell it apart from ace's own local/keyword
// completions. `meta` cannot serve that purpose: interpreters are free to fill it
// in (SqlCompleter sends schema/table/column), and using it as the discriminator
// made those very suggestions disappear.
fromBackend: true,
});
}
}
Expand Down Expand Up @@ -1084,10 +1090,11 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat
let prev = null;

matches = matches.filter(function(item) {
if (!_.isEmpty(item.meta)) {
if (completionListLength !== 0) {
return false;
}
// ZEPPELIN-3001 intent: once the interpreter has answered, hide ace's own local/keyword
// suggestions so the list only shows what the backend knows. Candidates coming from the
// interpreter are always kept - they are the reason it was asked in the first place.
if (!item.fromBackend && completionListLength !== 0) {
return false;
}
let caption = item.snippet || item.caption || item.value;
if (caption === prev) {
Expand Down
147 changes: 147 additions & 0 deletions zeppelin-web/src/app/notebook/paragraph/paragraph.controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ describe('Controller: ParagraphCtrl', function() {
beforeEach(angular.mock.module('zeppelinWebApp'));

let scope;
let rootScope;
let websocketMsgSrvMock = {};
let paragraphMock = {
config: {},
Expand All @@ -19,6 +20,7 @@ describe('Controller: ParagraphCtrl', function() {

beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
rootScope = $rootScope;
$rootScope.notebookScope = $rootScope.$new(true, $rootScope);

$controller('ParagraphCtrl', {
Expand Down Expand Up @@ -50,4 +52,149 @@ describe('Controller: ParagraphCtrl', function() {
it('should set default value of "paragraphFocused" as false', function() {
expect(scope.paragraphFocused).toEqual(false);
});

describe('completion candidate filtering', function() {
let FilteredList;
let originalSetFilter;

let completionParagraph = {
id: 'paragraph_completion',
config: {
editorSetting: {
completionSupport: true,
},
},
settings: {
forms: {},
},
};

let fromInterpreter = function(value, meta) {
return {value: value, caption: value, meta: meta, score: 300, fromBackend: true};
};

let fromAce = function(value, meta) {
return {value: value, caption: value, meta: meta, score: 0};
};

// the filter lives on ace's FilteredList prototype, installed on focus
let applyFilter = function(candidates) {
let list = new FilteredList(candidates);
list.setFilter('');
return list.filtered;
};

beforeEach(function() {
FilteredList = ace.require('ace/autocomplete').FilteredList;
originalSetFilter = FilteredList.prototype.setFilter;
scope.init(completionParagraph);
rootScope.$broadcast('focusParagraph', completionParagraph.id, 0, 0, true);
});

afterEach(function() {
FilteredList.prototype.setFilter = originalSetFilter;
});

it('should keep interpreter candidates that carry a meta label', function() {
let table = fromInterpreter('my_table', 'table');
expect(applyFilter([table])).toContain(table);
});

it('should hide ace candidates while interpreter candidates are available', function() {
let table = fromInterpreter('my_table', 'table');
let local = fromAce('my_local_var', 'local');
let keyword = fromAce('select', 'keyword');

let filtered = applyFilter([table, local, keyword]);

expect(filtered).toContain(table);
expect(filtered).not.toContain(local);
expect(filtered).not.toContain(keyword);
});

describe('when the interpreter answers with no candidates', function() {
let editorElement;

// completionListLength is only settable through a listener aceLoaded registers
beforeEach(function() {
websocketMsgSrvMock.getEditorSetting = function() {};
websocketMsgSrvMock.completion = function() {};

editorElement = document.createElement('div');
editorElement.id = 'completion_test_editor';
document.body.appendChild(editorElement);

scope.aceLoaded(ace.edit(editorElement));
rootScope.$broadcast('completionListLength', 0);
});

afterEach(function() {
document.body.removeChild(editorElement);
});

it('should fall back to ace candidates', function() {
let local = fromAce('my_local_var', 'local');
let keyword = fromAce('select', 'keyword');

let filtered = applyFilter([local, keyword]);

expect(filtered).toContain(local);
expect(filtered).toContain(keyword);
});
});

describe('candidates built from an interpreter answer', function() {
let editorElement;
let editor;

beforeEach(function() {
websocketMsgSrvMock.getEditorSetting = function() {};
websocketMsgSrvMock.completion = function() {};

editorElement = document.createElement('div');
editorElement.id = 'completion_producer_editor';
document.body.appendChild(editorElement);

editor = ace.edit(editorElement);
scope.aceLoaded(editor);
editor.focus();
});

afterEach(function() {
document.body.removeChild(editorElement);
});

// aceLoaded installs remoteCompleter as the first of ace's completers.
let collectCandidates = function(completions) {
let remoteCompleter = editor.completers[0];
let received = null;

remoteCompleter.getCompletions(editor, editor.getSession(), {row: 0, column: 0}, '', function(err, items) {
received = items;
});
rootScope.$broadcast('completionList', {completions: completions});

return received;
};

it('should mark every candidate as coming from the interpreter', function() {
let candidates = collectCandidates([
{name: 'my_table', value: 'my_table', meta: 'table'},
{name: 'my_schema', value: 'my_schema', meta: 'schema'},
]);

expect(candidates).not.toBeNull();
expect(candidates.length).toEqual(2);
candidates.forEach(function(candidate) {
expect(candidate.fromBackend).toBe(true);
});
});

it('should produce candidates that survive the filter', function() {
let candidates = collectCandidates([{name: 'my_table', value: 'my_table', meta: 'table'}]);

expect(applyFilter(candidates)).toEqual(candidates);
});
});
});
});
Loading