Skip to content

Commit d7d555e

Browse files
committed
Improve syntax highlighting in editors covered by foreign extractors
1 parent 4b1c47a commit d7d555e

File tree

11 files changed

+212
-63
lines changed

11 files changed

+212
-63
lines changed

packages/jupyterlab-lsp/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929
},
3030
"scripts": {
3131
"build": "jlpm build:schema && tsc -b",
32-
"build:schema": "jlpm build:schema-backend && jlpm build:schema-completion && jlpm build:schema-hover && jlpm build:schema-diagnostics",
32+
"build:schema": "jlpm build:schema-backend && jlpm build:schema-completion && jlpm build:schema-hover && jlpm build:schema-diagnostics && jlpm build:schema-syntax_highlighting",
3333
"build:schema-backend": "json2ts ../../py_src/jupyter_lsp/schema/schema.json --unreachableDefinitions | prettier --stdin-filepath _schema.d.ts > src/_schema.d.ts",
3434
"build:schema-completion": "json2ts schema/completion.json | prettier --stdin-filepath _completion.d.ts > src/_completion.d.ts",
3535
"build:schema-hover": "json2ts schema/hover.json | prettier --stdin-filepath _hover.d.ts > src/_hover.d.ts",
3636
"build:schema-diagnostics": "json2ts schema/diagnostics.json | prettier --stdin-filepath _diagnostics.d.ts > src/_diagnostics.d.ts",
37+
"build:schema-syntax_highlighting": "json2ts schema/syntax_highlighting.json | prettier --stdin-filepath _syntax_highlighting.d.ts > src/_syntax_highlighting.d.ts",
3738
"bundle": "npm pack .",
3839
"clean": "rimraf lib",
3940
"lab:link": "jupyter labextension link . --no-build",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"jupyter.lab.setting-icon": "lsp:syntax-highlight",
3+
"jupyter.lab.setting-icon-label": "Language integration",
4+
"title": "Code Syntax",
5+
"description": "LSP code syntax highlighting settings.",
6+
"type": "object",
7+
"properties": {
8+
"foreignCodeThreshold": {
9+
"title": "Threshold of foreign code coverage for changing the mode in an editor",
10+
"type": "number",
11+
"default": 0.5,
12+
"description": "If a code editor includes a code fragment in another language (for example a %%markdown magic in IPython) with appropriate foreign code extractor defined , and the extend of this code (coverage of the editor) passes the threshold, the syntax highlighting (i.e. the mode) will change to provide highlighting for the language of the foreign code."
13+
}
14+
},
15+
"jupyter.lab.shortcuts": []
16+
}

packages/jupyterlab-lsp/src/editor_integration/codemirror.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,10 @@ export abstract class CodeMirrorIntegration
363363
let raw_value = doc.getValue('\n');
364364
// extract foreign documents and substitute magics,
365365
// as it was done when the shadow virtual document was being created
366-
let { lines } = document.prepare_code_block(raw_value, editor);
366+
let { lines } = document.prepare_code_block({
367+
value: raw_value,
368+
ce_editor: editor
369+
});
367370
let old_value = lines.join('\n');
368371

369372
if (is_whole_document_edit) {

packages/jupyterlab-lsp/src/extractors/defaults.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ describe('Default extractors', () => {
1111
let document: VirtualDocument;
1212

1313
function extract(code: string) {
14-
return document.extract_foreign_code(code, null, {
15-
line: 0,
16-
column: 0
17-
});
14+
return document.extract_foreign_code(
15+
{ value: code, ce_editor: null },
16+
{
17+
line: 0,
18+
column: 0
19+
}
20+
);
1821
}
1922

2023
function get_the_only_pair(

packages/jupyterlab-lsp/src/extractors/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface IExtractedCode {
2525
*
2626
* The typical examples are:
2727
* - (X=CSS< Y=HTML), or
28-
* - (X=JavaScrip, Y=HTML),
28+
* - (X=JavaScript, Y=HTML),
2929
*
3030
* while in the data analysis realm, examples include:
3131
* - (X=R, Y=IPython),
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { CodeMirrorIntegration } from '../editor_integration/codemirror';
2+
import {
3+
JupyterFrontEnd,
4+
JupyterFrontEndPlugin
5+
} from '@jupyterlab/application';
6+
import { ILSPFeatureManager, PLUGIN_ID } from '../tokens';
7+
import { LabIcon } from '@jupyterlab/ui-components';
8+
import syntaxSvg from '../../style/icons/syntax-highlight.svg';
9+
import {
10+
IEditorIntegrationOptions,
11+
IFeatureLabIntegration,
12+
IFeatureSettings
13+
} from '../feature';
14+
import { VirtualDocument } from '../virtual/document';
15+
import { CodeMirrorEditor } from '@jupyterlab/codemirror';
16+
import CodeMirror from 'codemirror';
17+
import { IEditorMimeTypeService } from '@jupyterlab/codeeditor/lib/mimetype';
18+
import { IEditorServices } from '@jupyterlab/codeeditor/lib/tokens';
19+
import { CodeSyntax as LSPSyntaxHighlightingSettings } from '../_syntax_highlighting';
20+
21+
export const syntaxHighlightingIcon = new LabIcon({
22+
name: 'lsp:syntax-highlighting',
23+
svgstr: syntaxSvg
24+
});
25+
26+
const FEATURE_ID = PLUGIN_ID + ':syntax-highlighting';
27+
28+
export class CMSyntaxHighlighting extends CodeMirrorIntegration {
29+
lab_integration: SyntaxLabIntegration;
30+
settings: IFeatureSettings<LSPSyntaxHighlightingSettings>;
31+
32+
constructor(options: IEditorIntegrationOptions) {
33+
super(options);
34+
this.virtual_document.changed.connect(this.update_mode.bind(this), this);
35+
}
36+
37+
private get_mode(language: string) {
38+
let mimetype = this.lab_integration.mimeTypeService.getMimeTypeByLanguage({
39+
name: language
40+
});
41+
42+
if (!mimetype || mimetype == 'text/plain') {
43+
// if a mimetype cannot be found it will be 'text/plain', therefore do
44+
// not change mode to text/plain, as this could be a step backwards for
45+
// the user experience
46+
return;
47+
}
48+
49+
return CodeMirror.findModeByMIME(mimetype);
50+
}
51+
52+
update_mode(doc: VirtualDocument, changed_document: VirtualDocument) {
53+
let root = this.virtual_document;
54+
for (let map of root.foreign_document_maps) {
55+
for (let [range, block] of map.entries()) {
56+
let ce_editor = block.editor;
57+
// get the ce_editor from block
58+
let editor = (ce_editor as CodeMirrorEditor).editor;
59+
let lines = editor.getValue('\n');
60+
let total_area = lines.concat('').length;
61+
62+
let covered_area =
63+
ce_editor.getOffsetAt(range.end) - ce_editor.getOffsetAt(range.start);
64+
65+
let coverage = covered_area / total_area;
66+
67+
let language = block.virtual_document.language;
68+
69+
let mode = this.get_mode(language);
70+
71+
// if not highlighting mode available, skip this editor
72+
if (typeof mode === 'undefined') {
73+
continue;
74+
}
75+
76+
// change the mode if the majority of the code is the foreign code
77+
if (coverage > this.settings.composite.foreignCodeThreshold) {
78+
editor.setOption('mode', mode.mime);
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
class SyntaxLabIntegration implements IFeatureLabIntegration {
86+
// TODO: we could accept custom mimetype mapping from settings
87+
settings: IFeatureSettings<LSPSyntaxHighlightingSettings>;
88+
89+
constructor(public mimeTypeService: IEditorMimeTypeService) {}
90+
}
91+
92+
export const SYNTAX_HIGHLIGHTING_PLUGIN: JupyterFrontEndPlugin<void> = {
93+
id: FEATURE_ID,
94+
requires: [ILSPFeatureManager, IEditorServices],
95+
autoStart: true,
96+
activate: (
97+
app: JupyterFrontEnd,
98+
featureManager: ILSPFeatureManager,
99+
editorServices: IEditorServices
100+
) => {
101+
featureManager.register({
102+
feature: {
103+
editorIntegrationFactory: new Map([
104+
['CodeMirrorEditor', CMSyntaxHighlighting]
105+
]),
106+
commands: [],
107+
id: FEATURE_ID,
108+
name: 'Syntax highlighting',
109+
labIntegration: new SyntaxLabIntegration(editorServices.mimeTypeService)
110+
}
111+
});
112+
}
113+
};

packages/jupyterlab-lsp/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
3535
import codeCheckSvg from '../style/icons/code-check.svg';
3636
import { DIAGNOSTICS_PLUGIN } from './features/diagnostics';
3737
import { COMPLETION_PLUGIN } from './features/completion';
38+
import { SYNTAX_HIGHLIGHTING_PLUGIN } from './features/syntax_highlighting';
3839

3940
export const codeCheckIcon = new LabIcon({
4041
name: 'lsp:codeCheck',
@@ -214,7 +215,8 @@ const default_features: JupyterFrontEndPlugin<void>[] = [
214215
HOVER_PLUGIN,
215216
RENAME_PLUGIN,
216217
HIGHLIGHTS_PLUGIN,
217-
DIAGNOSTICS_PLUGIN
218+
DIAGNOSTICS_PLUGIN,
219+
SYNTAX_HIGHLIGHTING_PLUGIN
218220
];
219221

220222
const plugins: JupyterFrontEndPlugin<any>[] = [

packages/jupyterlab-lsp/src/virtual/document.spec.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ describe('VirtualDocument', () => {
6060
let {
6161
cell_code_kept,
6262
foreign_document_map
63-
} = document.extract_foreign_code(R_LINE_MAGICS, null, {
64-
line: 0,
65-
column: 0
66-
});
63+
} = document.extract_foreign_code(
64+
{ value: R_LINE_MAGICS, ce_editor: null },
65+
{
66+
line: 0,
67+
column: 0
68+
}
69+
);
6770

6871
// note R cell lines are kept in code (keep_in_host=true)
6972
expect(cell_code_kept).to.equal(R_LINE_MAGICS);
@@ -87,30 +90,31 @@ describe('VirtualDocument', () => {
8790
let ce_editor_for_cell_3 = {} as CodeEditor.IEditor;
8891
let ce_editor_for_cell_4 = {} as CodeEditor.IEditor;
8992
// first block
90-
document.append_code_block(
91-
'test line in Python 1\n%R 1st test line in R line magic 1',
92-
ce_editor_for_cell_1
93-
);
93+
document.append_code_block({
94+
value: 'test line in Python 1\n%R 1st test line in R line magic 1',
95+
ce_editor: ce_editor_for_cell_1
96+
});
9497
// second block
95-
document.append_code_block(
96-
'test line in Python 2\n%R 1st test line in R line magic 2',
97-
ce_editor_for_cell_2
98-
);
98+
document.append_code_block({
99+
value: 'test line in Python 2\n%R 1st test line in R line magic 2',
100+
ce_editor: ce_editor_for_cell_2
101+
});
99102
// third block
100-
document.append_code_block(
101-
'test line in Python 3\n%R -i imported_variable 1st test line in R line magic 3',
102-
ce_editor_for_cell_2
103-
);
103+
document.append_code_block({
104+
value:
105+
'test line in Python 3\n%R -i imported_variable 1st test line in R line magic 3',
106+
ce_editor: ce_editor_for_cell_2
107+
});
104108
// fourth block
105-
document.append_code_block(
106-
'%%R\n1st test line in R cell magic 1',
107-
ce_editor_for_cell_3
108-
);
109+
document.append_code_block({
110+
value: '%%R\n1st test line in R cell magic 1',
111+
ce_editor: ce_editor_for_cell_3
112+
});
109113
// fifth block
110-
document.append_code_block(
111-
'%%R -i imported_variable\n1st test line in R cell magic 2',
112-
ce_editor_for_cell_4
113-
);
114+
document.append_code_block({
115+
value: '%%R -i imported_variable\n1st test line in R cell magic 2',
116+
ce_editor: ce_editor_for_cell_4
117+
});
114118
};
115119

116120
describe('transform_virtual_to_editor', () => {

packages/jupyterlab-lsp/src/virtual/document.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ interface IVirtualLine {
3232
editor: CodeEditor.IEditor;
3333
}
3434

35+
export interface ICodeBlockOptions {
36+
ce_editor: CodeEditor.IEditor;
37+
value: string;
38+
}
39+
3540
export interface IVirtualDocumentBlock {
3641
/**
3742
* Line corresponding to the block in the entire foreign document
3843
*/
3944
virtual_line: number;
4045
virtual_document: VirtualDocument;
46+
editor: CodeEditor.IEditor;
4147
}
4248

4349
export type ForeignDocumentsMap = Map<IRange, IVirtualDocumentBlock>;
@@ -480,15 +486,16 @@ export class VirtualDocument {
480486
}
481487

482488
extract_foreign_code(
483-
cell_code: string,
484-
ce_editor: CodeEditor.IEditor,
489+
block: ICodeBlockOptions,
485490
editor_shift: CodeEditor.IPosition
486491
) {
487492
let foreign_document_map = new Map<
488493
CodeEditor.IRange,
489494
IVirtualDocumentBlock
490495
>();
491496

497+
let cell_code = block.value;
498+
492499
for (let extractor of this.foreign_extractors) {
493500
// first, check if there is any foreign code:
494501

@@ -506,15 +513,18 @@ export class VirtualDocument {
506513

507514
foreign_document_map.set(result.range, {
508515
virtual_line: foreign_document.last_virtual_line,
509-
virtual_document: foreign_document
516+
virtual_document: foreign_document,
517+
editor: block.ce_editor
510518
});
511519
let foreign_shift = {
512520
line: editor_shift.line + result.range.start.line,
513521
column: editor_shift.column + result.range.start.column
514522
};
515523
foreign_document.append_code_block(
516-
result.foreign_code,
517-
ce_editor,
524+
{
525+
value: result.foreign_code,
526+
ce_editor: block.ce_editor
527+
},
518528
foreign_shift,
519529
result.virtual_shift
520530
);
@@ -548,19 +558,17 @@ export class VirtualDocument {
548558
}
549559

550560
prepare_code_block(
551-
cell_code: string,
552-
ce_editor: CodeEditor.IEditor,
561+
block: ICodeBlockOptions,
553562
editor_shift: CodeEditor.IPosition = { line: 0, column: 0 }
554563
) {
555564
let lines: Array<string>;
556565
let skip_inspect: Array<Array<VirtualDocument.id_path>>;
557566

558567
let { cell_code_kept, foreign_document_map } = this.extract_foreign_code(
559-
cell_code,
560-
ce_editor,
568+
block,
561569
editor_shift
562570
);
563-
cell_code = cell_code_kept;
571+
let cell_code = cell_code_kept;
564572

565573
// cell magics are replaced if requested and matched
566574
let cell_override = this.cell_magics_overrides.override_for(cell_code);
@@ -590,16 +598,17 @@ export class VirtualDocument {
590598
}
591599

592600
append_code_block(
593-
cell_code: string,
594-
ce_editor: CodeEditor.IEditor,
601+
block: ICodeBlockOptions,
595602
editor_shift: CodeEditor.IPosition = { line: 0, column: 0 },
596603
virtual_shift?: CodeEditor.IPosition
597604
) {
605+
let cell_code = block.value;
606+
let ce_editor = block.ce_editor;
607+
598608
let source_cell_lines = cell_code.split('\n');
599609

600610
let { lines, foreign_document_map, skip_inspect } = this.prepare_code_block(
601-
cell_code,
602-
ce_editor,
611+
block,
603612
editor_shift
604613
);
605614

@@ -909,10 +918,7 @@ export class UpdateManager {
909918
block: code_block,
910919
virtual_document: this.virtual_document
911920
});
912-
this.virtual_document.append_code_block(
913-
code_block.value,
914-
code_block.ce_editor
915-
);
921+
this.virtual_document.append_code_block(code_block);
916922
}
917923

918924
this.update_finished.emit(blocks);
@@ -935,8 +941,3 @@ export class UpdateManager {
935941
return update;
936942
}
937943
}
938-
939-
export interface ICodeBlockOptions {
940-
ce_editor: CodeEditor.IEditor;
941-
value: string;
942-
}

0 commit comments

Comments
 (0)