• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2020 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15
16import m from 'mithril';
17
18import {EngineProxy} from '../common/engine';
19import {QueryResponse, runQuery} from '../common/queries';
20
21import {addTab} from './bottom_tab';
22import {globals} from './globals';
23import {createPage} from './pages';
24import {QueryHistoryComponent, queryHistoryStorage} from './query_history';
25import {QueryResultTab} from './query_result_tab';
26import {QueryTable} from './query_table';
27
28const INPUT_PLACEHOLDER = 'Enter query and press Cmd/Ctrl + Enter';
29const INPUT_MIN_LINES = 2;
30const INPUT_MAX_LINES = 10;
31const INPUT_LINE_HEIGHT_EM = 1.2;
32const TAB_SPACES = 2;
33const TAB_SPACES_STRING = ' '.repeat(TAB_SPACES);
34
35interface AnalyzePageState {
36  enteredText: string;
37  executedQuery?: string;
38  queryResult?: QueryResponse;
39}
40
41const state: AnalyzePageState = {
42  enteredText: '',
43};
44
45export function runAnalyzeQuery(query: string) {
46  state.executedQuery = query;
47  state.queryResult = undefined;
48  const engine = getEngine();
49  if (engine) {
50    runQuery(query, engine).then((resp: QueryResponse) => {
51      addTab({
52        kind: QueryResultTab.kind,
53        tag: 'analyze_page_query',
54        config: {
55          query: query,
56          title: 'Standalone Query',
57          prefetchedResponse: resp,
58        },
59      });
60      // We might have started to execute another query. Ignore it in that case.
61      if (state.executedQuery !== query) {
62        return;
63      }
64      state.queryResult = resp;
65      globals.rafScheduler.scheduleFullRedraw();
66    });
67  }
68}
69
70function getEngine(): EngineProxy|undefined {
71  const engineId = globals.getCurrentEngine()?.id;
72  if (engineId === undefined) {
73    return undefined;
74  }
75  const engine = globals.engines.get(engineId)?.getProxy('AnalyzePage');
76  return engine;
77}
78
79class QueryInput implements m.ClassComponent {
80  // How many lines to display if the user hasn't resized the input box.
81  displayLines = INPUT_MIN_LINES;
82
83  static onKeyDown(e: Event) {
84    const event = e as KeyboardEvent;
85    const target = e.target as HTMLTextAreaElement;
86    const {selectionStart, selectionEnd} = target;
87
88    if (event.code === 'Enter' && (event.metaKey || event.ctrlKey)) {
89      event.preventDefault();
90      let query = target.value;
91      if (selectionEnd > selectionStart) {
92        query = query.substring(selectionStart, selectionEnd);
93      }
94      if (!query) return;
95      queryHistoryStorage.saveQuery(query);
96
97      runAnalyzeQuery(query);
98    }
99
100    if (event.code === 'Tab') {
101      // Handle tabs to insert spaces.
102      event.preventDefault();
103      const lastLineBreak = target.value.lastIndexOf('\n', selectionEnd);
104
105      if (selectionStart === selectionEnd || lastLineBreak < selectionStart) {
106        // Selection does not contain line breaks, therefore is on a single
107        // line. In this case, replace the selection with spaces. Replacement is
108        // done via document.execCommand as opposed to direct manipulation of
109        // element's value attribute because modifying latter programmatically
110        // drops the edit history which breaks undo/redo functionality.
111        document.execCommand('insertText', false, TAB_SPACES_STRING);
112      } else {
113        this.handleMultilineTab(target, event);
114      }
115    }
116  }
117
118  // Handle Tab press when the current selection is multiline: find all the
119  // lines intersecting with the selection, and either indent or dedent (if
120  // Shift key is held) them.
121  private static handleMultilineTab(
122      target: HTMLTextAreaElement, event: KeyboardEvent) {
123    const {selectionStart, selectionEnd} = target;
124    const firstLineBreak = target.value.lastIndexOf('\n', selectionStart - 1);
125
126    // If no line break is found (selection begins at the first line),
127    // replacementStart would have the correct value of 0.
128    const replacementStart = firstLineBreak + 1;
129    const replacement = target.value.substring(replacementStart, selectionEnd)
130                            .split('\n')
131                            .map((line) => {
132                              if (event.shiftKey) {
133                                // When Shift is held, remove whitespace at the
134                                // beginning
135                                return this.dedent(line);
136                              } else {
137                                return TAB_SPACES_STRING + line;
138                              }
139                            })
140                            .join('\n');
141    // Select the range to be replaced.
142    target.setSelectionRange(replacementStart, selectionEnd);
143    document.execCommand('insertText', false, replacement);
144    // Restore the selection to match the previous selection, allowing to chain
145    // indent operations by just pressing Tab several times.
146    target.setSelectionRange(
147        replacementStart, replacementStart + replacement.length);
148  }
149
150  // Chop off up to TAB_SPACES leading spaces from a string.
151  private static dedent(line: string): string {
152    let i = 0;
153    while (i < line.length && i < TAB_SPACES && line[i] === ' ') {
154      i++;
155    }
156    return line.substring(i);
157  }
158
159  onInput(textareaValue: string) {
160    const textareaLines = textareaValue.split('\n').length;
161    const clampedNumLines =
162        Math.min(Math.max(textareaLines, INPUT_MIN_LINES), INPUT_MAX_LINES);
163    this.displayLines = clampedNumLines;
164    state.enteredText = textareaValue;
165    globals.rafScheduler.scheduleFullRedraw();
166  }
167
168  // This method exists because unfortunatley setting custom properties on an
169  // element's inline style attribue doesn't seem to work in mithril, even
170  // though the docs claim so.
171  setHeightBeforeResize(node: HTMLElement) {
172    // +2em for some extra breathing space to account for padding.
173    const heightEm = this.displayLines * INPUT_LINE_HEIGHT_EM + 2;
174    // We set a height based on the number of lines that we want to display by
175    // default. If the user resizes the textbox using the resize handle in the
176    // bottom-right corner, this height is overridden.
177    node.style.setProperty('--height-before-resize', `${heightEm}em`);
178    // TODO(dproy): The resized height is lost if user navigates away from the
179    // page and comes back.
180  }
181
182  oncreate(vnode: m.VnodeDOM) {
183    // This makes sure query persists if user navigates to other pages and comes
184    // back to analyze page.
185    const existingQuery = state.enteredText;
186    const textarea = vnode.dom as HTMLTextAreaElement;
187    if (existingQuery) {
188      textarea.value = existingQuery;
189      this.onInput(existingQuery);
190    }
191
192    this.setHeightBeforeResize(textarea);
193  }
194
195  onupdate(vnode: m.VnodeDOM) {
196    this.setHeightBeforeResize(vnode.dom as HTMLElement);
197  }
198
199  view() {
200    return m('textarea.query-input', {
201      placeholder: INPUT_PLACEHOLDER,
202      onkeydown: (e: Event) => QueryInput.onKeyDown(e),
203      oninput: (e: Event) =>
204          this.onInput((e.target as HTMLTextAreaElement).value),
205    });
206  }
207}
208
209
210export const AnalyzePage = createPage({
211  view() {
212    return m(
213        '.analyze-page',
214        m(QueryInput),
215        state.executedQuery === undefined ? null : m(QueryTable, {
216          query: state.executedQuery,
217          resp: state.queryResult,
218          onClose: () => {
219            state.executedQuery = undefined;
220            state.queryResult = undefined;
221            globals.rafScheduler.scheduleFullRedraw();
222          },
223        }),
224        m(QueryHistoryComponent));
225  },
226});
227