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

Autocompletion and Syntax Highlight. #2171

Closed
Closed
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
29 changes: 26 additions & 3 deletions packages/uhk-web/package-lock.json

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

1 change: 1 addition & 0 deletions packages/uhk-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.7.0",
"monaco-editor": "0.44.0",
"naive-autocompletion-parser": "^1.1.8",
"ng2-dragula": "5.0.1",
"ng2-nouislider": "2.0.0",
"ngrx-store-freeze": "0.2.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { retrieveUhkGrammar, buildUhkParser, ParserBuilder, Parser, Suggestion } from 'naive-autocompletion-parser';
import { LogService } from 'uhk-common';

export class CustomCompletionProvider implements monaco.languages.CompletionItemProvider {
public readonly triggerCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.\\@( \\$".split("");

private parser: Parser | undefined;
private identifierCharPattern = /[a-zA-Z]/;

constructor( private logService: LogService ) {
this.parser = undefined;

// TODO: here, pass the actual reference-manual.md (from correct repository/tag) body into the buildUhkParser
retrieveUhkGrammar().then ( grammarText => {
this.parser = buildUhkParser(grammarText);
});
}

guessKind(text: string, rule: string): [monaco.languages.CompletionItemKind, string] {
if (rule.slice(rule.length-7) == '_ABBREV') {
// key and scancode abbreviations
return [monaco.languages.CompletionItemKind.Struct, "5"];
} else if (rule == 'MODMASK') {
// modmask
return [monaco.languages.CompletionItemKind.Interface, "4"];
} else if (this.identifierCharPattern.test(text[0])) {
// identifiers, commands
return [monaco.languages.CompletionItemKind.Text, "3"];
} else if (text[0] == '<' && text[text.length-1] == '>') {
// hints
return [monaco.languages.CompletionItemKind.Property, "1"];
} else if (text[1] == '<' && text[text.length-2] == '>') {
// comment hints
return [monaco.languages.CompletionItemKind.Property, "1"];
} else {
// operators
return [monaco.languages.CompletionItemKind.Module, "2"];
}
}

provideCompletionItems(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext,
token: monaco.CancellationToken
): monaco.languages.ProviderResult<monaco.languages.CompletionList> {
const lineNumber = position.lineNumber;
const column = position.column;

const lineText = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 1,
endLineNumber: lineNumber,
endColumn: column
});

if (this.parser) {
let nelaSuggestions: Suggestion[] = [];
try {
nelaSuggestions = this.parser.complete(lineText, "BODY");
} catch (e) {
this.logService.error(e);
}
let monacoSuggestions: monaco.languages.CompletionItem[] = nelaSuggestions.map(it => {
let kind = this.guessKind(it.suggestion, it.originRule);
return {
insertText: it.text(),
kind: kind[0],
label: it.text(),
sortText: kind[1] + it.text(),
range: {
startLineNumber: lineNumber,
startColumn: column - it.overlap,
endLineNumber: lineNumber,
endColumn: column
},
};
});
return {
suggestions: monacoSuggestions
};
} else {
return {
suggestions: []
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
export function highlightProvider(): monaco.languages.IMonarchLanguage {
return {
keywords: [],

typeKeywords: [],

operators: [
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%',
'<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=',
'%=', '<<=', '>>=', '>>>='
],

// we include these common regular expressions
symbols: /[=><!~?:&|+\-*/^%]+/,

// C# style strings
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,

// The main tokenizer for our languages
tokenizer: {
root: [
// identifiers and keywords
[/[a-z_][\w$]*/, {
cases: {
'@typeKeywords': 'keyword',
'@keywords': 'keyword',
'@default': 'identifier'
}
}],
[/\$[A-Za-z_][\w$]*/, 'type.identifier'], // to show class names nicely

// numbers
[/\d*\.\d+([eE][-+]?\d+)?/, 'number.float'],
[/\d+/, 'number'],
[/true|false/, 'number'],

// whitespace
{ include: '@whitespace' },

// delimiters and operators
[/[{}()[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[/@symbols/, {
cases: {
'@operators': 'operator',
'@default': ''
}
}],

// delimiter: after number because of .\d floats
[/[;,.]/, 'delimiter'],

// strings
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string
[/"/, { token: 'string.quote', bracket: '@open', next: '@string' }],

// literalStrings
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string
[/'/, { token: 'string.quote', bracket: '@open', next: '@literalString' }],
],

string: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
],

literalString: [
[/[^\\']+/, 'string'],
[/\\./, 'string.escape.invalid'],
[/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }]
],

whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\/.*$/, 'comment'],
],
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import { LogService } from 'uhk-common';
import { SelectedMacroActionId } from '../../../../../models';
import { SmartMacroDocCommandAction, SmartMacroDocService } from '../../../../../services/smart-macro-doc-service';
import { hasNonAsciiCharacters, NON_ASCII_REGEXP } from '../../../../../util';
import { CustomCompletionProvider } from './completion-provider';
import { highlightProvider } from './highlight-provider';

const MONACO_EDITOR_LINE_HEIGHT_OPTION = 66;
const MONACO_EDITOR_LF_END_OF_LINE_OPTION = 0;
const MACRO_CHANGE_DEBOUNCE_TIME = 250;

function getVsCodeTheme(): string {
return (window as any).getUhkTheme() === 'dark' ? 'uhk-dark' : 'vs';
return (window as any).getUhkTheme() === 'dark' ? 'uhk-dark' : 'uhk-light';
}

@Component({
Expand Down Expand Up @@ -85,6 +87,7 @@ export class MacroCommandEditorComponent implements AfterViewInit, ControlValueA
private insertingMacro = false;
private changeObserver$: Observer<string>;
private subscriptions = new Subscription();
private static completionRegistered: boolean = false;

constructor(private cdRef: ChangeDetectorRef,
@Inject(DOCUMENT) private document: Document,
Expand Down Expand Up @@ -141,6 +144,7 @@ export class MacroCommandEditorComponent implements AfterViewInit, ControlValueA
if (this.editor && this.autoFocus) {
this.editor.focus();
}

this.calculateHeight();
}

Expand All @@ -163,10 +167,42 @@ export class MacroCommandEditorComponent implements AfterViewInit, ControlValueA
this.removeNonAsciiCharachters();
}

private setLanguageProperties(editor: MonacoStandaloneCodeEditor) {
const languageId = 'uhkScript';

if (!MacroCommandEditorComponent.completionRegistered) {
Copy link
Member

Choose a reason for hiding this comment

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

This static variable is not really good for manage new language service registration, because if someone upgrade the firmware we have to apply the new language service.

We have to registerCompletionItemProvider and setMonarchTokensProvider.
We could register these new providers as we register the new themes in the MonacoEditorCustomThemeService. The difference is we have to re-register the providers every time when the configurations changed.

We can create a new MonacoEditorUhkLanguageService this service has to subscribe the monacoLoaderService.isMonacoLoaded$ and when the new getLanguageServiceConfig. To combine 2 observers we could use combineLatest function.

How to create the getLanguageServiceConfig observerable?
We can extend the packages/uhk-web/src/app/store/reducers/smart-macro-doc.reducer.ts or create a new reducer. to be continue tomorrow

Copy link
Member

Choose a reason for hiding this comment

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

The Agent has 2 main "process" the electron thread and the browser thread. The 2 world communicate with each other via electron built in inter-process communication

The packages/uhk-common/src/util/ipcEvents.ts file contains all of the ipc events that Agent uses. I tried to organise them. When the main module reply to an event in async mode I used the Reply suffix.

Every time when the user opens the smart macro document panel.

  • The renderer process dispatches the SmartMacroDoc.downloadDocumentation event.
  • The main process checks the documentation for the firmware is available or not. if not then download it from the server.

This moment is too late to load the auto-complete md file, because maybe the user start using the smart macro editor before opens the smart macro doc panel.
I think everytime when Agents starts or updates the firmware then have to check the smart macro document is available or not. If it not available, then have to download it and after provide the auto complete markdown file to the renderer process.

If we use the web version of the Agent then when Agents start we have to download the auto complete markdown file from the Agent repo.

To achieve this functionality we have to refactor many small things. I don't know should I able to write every step in high level. My assumption is to provide a really step-by-step guide I have to implement the whole code in my mind. I am afraid if I forget something maybe I create a bigger problem for you than guidance.

I think faster if I finish the PR. Unfortunately, my availability will very limited in the next 3 weeks.
I am happy to help you to learn Agent coding, but I think we have to start with a smaller issue if you don't mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think faster if I finish the PR.

Allright, it is yours then.

Unfortunately, my availability will very limited in the next 3 weeks.

No worries ;)

MacroCommandEditorComponent.completionRegistered = true;
monaco.languages.register({ id: languageId });

// Register the custom completion provider
const completionProvider = new CustomCompletionProvider(this.logService);
Copy link
Member

Choose a reason for hiding this comment

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

We can use Angular built-in dependency injection. https://angular.io/guide/architecture-services

const providerRegistration = monaco.languages.registerCompletionItemProvider(
languageId,
completionProvider
);

// Register highlighter
monaco.languages.setMonarchTokensProvider(
languageId,
highlightProvider()
);
}

editor.getModel()?.dispose();
const newModel = monaco.editor.createModel(
editor.getValue(),
languageId
);
editor.setModel(newModel);
}

onEditorInit(editor: MonacoStandaloneCodeEditor) {
this.logService.misc('[MacroCommandEditorComponent] editor initialized.');

this.editor = editor;

this.setLanguageProperties(editor);

this.setLFEndOfLineOption();
if (this.autoFocus) {
this.editor.focus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ export class MonacoEditorCustomThemeService {
'editor.foreground': '#bbbbbb'
}
});
monaco.editor.defineTheme('uhk-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'comment', foreground: '#777777'},
{ token: 'number', foreground: '#aa0000'},
{ token: 'string', foreground: '#55aa55'},
{ token: 'variable', foreground: '#7777ff'},
],
colors: {}
});
});
}
}
Loading