Feedback.java revision 3420:9291bcd53e07
1/*
2 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.internal.jshell.tool;
27
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.Collection;
31import java.util.EnumSet;
32import java.util.HashMap;
33import java.util.Iterator;
34import java.util.List;
35import java.util.Locale;
36import java.util.Map;
37import java.util.Map.Entry;
38import java.util.regex.Matcher;
39import java.util.regex.Pattern;
40import static java.util.stream.Collectors.joining;
41
42/**
43 * Feedback customization support
44 *
45 * @author Robert Field
46 */
47class Feedback {
48
49    // Patern for substituted fields within a customized format string
50    private static final Pattern FIELD_PATTERN = Pattern.compile("\\{(.*?)\\}");
51
52    // Internal field name for truncation length
53    private static final String TRUNCATION_FIELD = "<truncation>";
54
55    // For encoding to Properties String
56    private static final String RECORD_SEPARATOR = "\u241E";
57
58    // Current mode
59    private Mode mode = new Mode("", false); // initial value placeholder during start-up
60
61    // Mapping of mode name to mode
62    private final Map<String, Mode> modeMap = new HashMap<>();
63
64    // Mapping of mode names to encoded retained mode
65    private final Map<String, String> retainedMap = new HashMap<>();
66
67    // Mapping selector enum names to enums
68    private final Map<String, Selector<?>> selectorMap = new HashMap<>();
69
70    private static final long ALWAYS = bits(FormatCase.all, FormatAction.all, FormatWhen.all,
71            FormatResolve.all, FormatUnresolved.all, FormatErrors.all);
72    private static final long ANY = 0L;
73
74    public boolean shouldDisplayCommandFluff() {
75        return mode.commandFluff;
76    }
77
78    public String getPre() {
79        return mode.format("pre", ANY);
80    }
81
82    public String getPost() {
83        return mode.format("post", ANY);
84    }
85
86    public String getErrorPre() {
87        return mode.format("errorpre", ANY);
88    }
89
90    public String getErrorPost() {
91        return mode.format("errorpost", ANY);
92    }
93
94    public String format(FormatCase fc, FormatAction fa, FormatWhen fw,
95                    FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
96                    String name, String type, String value, String unresolved, List<String> errorLines) {
97        return mode.format(fc, fa, fw, fr, fu, fe,
98                name, type, value, unresolved, errorLines);
99    }
100
101    public String getPrompt(String nextId) {
102        return mode.getPrompt(nextId);
103    }
104
105    public String getContinuationPrompt(String nextId) {
106        return mode.getContinuationPrompt(nextId);
107    }
108
109    public boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) {
110        return new Setter(messageHandler, at).setFeedback();
111    }
112
113    public boolean setFormat(MessageHandler messageHandler, ArgTokenizer at) {
114        return new Setter(messageHandler, at).setFormat();
115    }
116
117    public boolean setTruncation(MessageHandler messageHandler, ArgTokenizer at) {
118        return new Setter(messageHandler, at).setTruncation();
119    }
120
121    public boolean setNewMode(MessageHandler messageHandler, ArgTokenizer at) {
122        return new Setter(messageHandler, at).setNewMode();
123    }
124
125    public boolean setPrompt(MessageHandler messageHandler, ArgTokenizer at) {
126        return new Setter(messageHandler, at).setPrompt();
127    }
128
129    public String retainFeedback(MessageHandler messageHandler, ArgTokenizer at) {
130        return new Setter(messageHandler, at).retainFeedback();
131    }
132
133    public String retainMode(MessageHandler messageHandler, ArgTokenizer at) {
134        return new Setter(messageHandler, at).retainMode();
135    }
136
137    public boolean restoreEncodedModes(MessageHandler messageHandler, String encoded) {
138        return new Setter(messageHandler, new ArgTokenizer("")).restoreEncodedModes(encoded);
139    }
140
141    public void markModesReadOnly() {
142        modeMap.values().stream()
143                .forEach(m -> m.readOnly = true);
144    }
145
146    {
147        for (FormatCase e : EnumSet.allOf(FormatCase.class))
148            selectorMap.put(e.name().toLowerCase(Locale.US), e);
149        for (FormatAction e : EnumSet.allOf(FormatAction.class))
150            selectorMap.put(e.name().toLowerCase(Locale.US), e);
151        for (FormatResolve e : EnumSet.allOf(FormatResolve.class))
152            selectorMap.put(e.name().toLowerCase(Locale.US), e);
153        for (FormatUnresolved e : EnumSet.allOf(FormatUnresolved.class))
154            selectorMap.put(e.name().toLowerCase(Locale.US), e);
155        for (FormatErrors e : EnumSet.allOf(FormatErrors.class))
156            selectorMap.put(e.name().toLowerCase(Locale.US), e);
157        for (FormatWhen e : EnumSet.allOf(FormatWhen.class))
158            selectorMap.put(e.name().toLowerCase(Locale.US), e);
159    }
160
161    /**
162     * Holds all the context of a mode mode
163     */
164    private static class Mode {
165
166        // Name of mode
167        final String name;
168
169        // Display command verification/information
170        final boolean commandFluff;
171
172        // Event cases: class, method, expression, ...
173        final Map<String, List<Setting>> cases;
174
175        boolean readOnly = false;
176
177        String prompt = "\n-> ";
178        String continuationPrompt = ">> ";
179
180        static class Setting {
181            final long enumBits;
182            final String format;
183            Setting(long enumBits, String format) {
184                this.enumBits = enumBits;
185                this.format = format;
186            }
187        }
188
189        /**
190         * Set up an empty mode.
191         *
192         * @param name
193         * @param commandFluff True if should display command fluff messages
194         */
195        Mode(String name, boolean commandFluff) {
196            this.name = name;
197            this.commandFluff = commandFluff;
198            cases = new HashMap<>();
199            add("name",       new Setting(ALWAYS, "%1$s"));
200            add("type",       new Setting(ALWAYS, "%2$s"));
201            add("value",      new Setting(ALWAYS, "%3$s"));
202            add("unresolved", new Setting(ALWAYS, "%4$s"));
203            add("errors",     new Setting(ALWAYS, "%5$s"));
204            add("err",        new Setting(ALWAYS, "%6$s"));
205
206            add("errorline",  new Setting(ALWAYS, "    {err}%n"));
207
208            add("pre",        new Setting(ALWAYS, "|  "));
209            add("post",       new Setting(ALWAYS, "%n"));
210            add("errorpre",   new Setting(ALWAYS, "|  "));
211            add("errorpost",  new Setting(ALWAYS, "%n"));
212        }
213
214        /**
215         * Set up a copied mode.
216         *
217         * @param name
218         * @param commandFluff True if should display command fluff messages
219         * @param m Mode to copy, or null for no fresh
220         */
221        Mode(String name, boolean commandFluff, Mode m) {
222            this.name = name;
223            this.commandFluff = commandFluff;
224            cases = new HashMap<>();
225
226            m.cases.entrySet().stream()
227                    .forEach(fes -> fes.getValue()
228                    .forEach(ing -> add(fes.getKey(), ing)));
229
230            this.prompt = m.prompt;
231            this.continuationPrompt = m.continuationPrompt;
232        }
233
234        /**
235         * Set up a mode reconstituted from a preferences string.
236         *
237         * @param it the encoded Mode broken into String chunks, may contain
238         * subsequent encoded modes
239         */
240        Mode(Iterator<String> it) {
241            this.name = it.next();
242            this.commandFluff = Boolean.parseBoolean(it.next());
243            this.prompt = it.next();
244            this.continuationPrompt = it.next();
245            cases = new HashMap<>();
246            String field;
247            while (!(field = it.next()).equals("***")) {
248                String open = it.next();
249                assert open.equals("(");
250                List<Setting> settings = new ArrayList<>();
251                String bits;
252                while (!(bits = it.next()).equals(")")) {
253                    String format = it.next();
254                    Setting ing = new Setting(Long.parseLong(bits), format);
255                    settings.add(ing);
256                }
257                cases.put(field, settings);
258            }
259        }
260
261        /**
262         * Encodes the mode into a String so it can be saved in Preferences.
263         *
264         * @return the string representation
265         */
266        String encode() {
267            List<String> el = new ArrayList<>();
268            el.add(name);
269            el.add(String.valueOf(commandFluff));
270            el.add(prompt);
271            el.add(continuationPrompt);
272            for (Entry<String, List<Setting>> es : cases.entrySet()) {
273                el.add(es.getKey());
274                el.add("(");
275                for (Setting ing : es.getValue()) {
276                    el.add(String.valueOf(ing.enumBits));
277                    el.add(ing.format);
278                }
279                el.add(")");
280            }
281            el.add("***");
282            return String.join(RECORD_SEPARATOR, el);
283        }
284
285        private boolean add(String field, Setting ing) {
286            List<Setting> settings =  cases.computeIfAbsent(field, k -> new ArrayList<>());
287            if (settings == null) {
288                return false;
289            }
290            settings.add(ing);
291            return true;
292        }
293
294        void set(String field,
295                Collection<FormatCase> cc, Collection<FormatAction> ca, Collection<FormatWhen> cw,
296                Collection<FormatResolve> cr, Collection<FormatUnresolved> cu, Collection<FormatErrors> ce,
297                String format) {
298            long bits = bits(cc, ca, cw, cr, cu, ce);
299            set(field, bits, format);
300        }
301
302        void set(String field, long bits, String format) {
303            add(field, new Setting(bits, format));
304        }
305
306        /**
307         * Lookup format Replace fields with context specific formats.
308         *
309         * @return format string
310         */
311        String format(String field, long bits) {
312            List<Setting> settings = cases.get(field);
313            if (settings == null) {
314                return ""; //TODO error?
315            }
316            String format = null;
317            for (int i = settings.size() - 1; i >= 0; --i) {
318                Setting ing = settings.get(i);
319                long mask = ing.enumBits;
320                if ((bits & mask) == bits) {
321                    format = ing.format;
322                    break;
323                }
324            }
325            if (format == null || format.isEmpty()) {
326                return "";
327            }
328            Matcher m = FIELD_PATTERN.matcher(format);
329            StringBuffer sb = new StringBuffer(format.length());
330            while (m.find()) {
331                String fieldName = m.group(1);
332                String sub = format(fieldName, bits);
333                m.appendReplacement(sb, Matcher.quoteReplacement(sub));
334            }
335            m.appendTail(sb);
336            return sb.toString();
337        }
338
339        // Compute the display output given full context and values
340        String format(FormatCase fc, FormatAction fa, FormatWhen fw,
341                    FormatResolve fr, FormatUnresolved fu, FormatErrors fe,
342                    String name, String type, String value, String unresolved, List<String> errorLines) {
343            // Convert the context into a bit representation used as selectors for store field formats
344            long bits = bits(fc, fa, fw, fr, fu, fe);
345            String fname = name==null? "" : name;
346            String ftype = type==null? "" : type;
347            // Compute the representation of value
348            String fvalue;
349            if (value==null) {
350                fvalue = "";
351            } else {
352                // Retrieve the truncation length
353                String truncField = format(TRUNCATION_FIELD, bits);
354                if (truncField.isEmpty()) {
355                    // No truncation set, use whole value
356                    fvalue = value;
357                } else {
358                    // Convert truncation length to int
359                    // this is safe since it has been tested before it is set
360                    int trunc = Integer.parseUnsignedInt(truncField);
361                    if (value.length() > trunc) {
362                        if (trunc <= 5) {
363                            // Very short truncations have no room for "..."
364                            fvalue = value.substring(0, trunc);
365                        } else {
366                            // Normal truncation, make total length equal truncation length
367                            fvalue = value.substring(0, trunc - 4) + " ...";
368                        }
369                    } else {
370                        // Within truncation length, use whole value
371                        fvalue = value;
372                    }
373                }
374            }
375            String funresolved = unresolved==null? "" : unresolved;
376            String errors = errorLines.stream()
377                    .map(el -> String.format(
378                            format("errorline", bits),
379                            fname, ftype, fvalue, funresolved, "*cannot-use-errors-here*", el))
380                    .collect(joining());
381            return String.format(
382                    format("display", bits),
383                    fname, ftype, fvalue, funresolved, errors, "*cannot-use-err-here*");
384        }
385
386        void setPrompts(String prompt, String continuationPrompt) {
387            this.prompt = prompt;
388            this.continuationPrompt = continuationPrompt;
389        }
390
391        String getPrompt(String nextId) {
392            return String.format(prompt, nextId);
393        }
394
395        String getContinuationPrompt(String nextId) {
396            return String.format(continuationPrompt, nextId);
397        }
398    }
399
400    // Representation of one instance of all the enum values as bits in a long
401    private static long bits(FormatCase fc, FormatAction fa, FormatWhen fw,
402            FormatResolve fr, FormatUnresolved fu, FormatErrors fe) {
403        long res = 0L;
404        res |= 1 << fc.ordinal();
405        res <<= FormatAction.count;
406        res |= 1 << fa.ordinal();
407        res <<= FormatWhen.count;
408        res |= 1 << fw.ordinal();
409        res <<= FormatResolve.count;
410        res |= 1 << fr.ordinal();
411        res <<= FormatUnresolved.count;
412        res |= 1 << fu.ordinal();
413        res <<= FormatErrors.count;
414        res |= 1 << fe.ordinal();
415        return res;
416    }
417
418    // Representation of a space of enum values as or'edbits in a long
419    private static long bits(Collection<FormatCase> cc, Collection<FormatAction> ca, Collection<FormatWhen> cw,
420                Collection<FormatResolve> cr, Collection<FormatUnresolved> cu, Collection<FormatErrors> ce) {
421        long res = 0L;
422        for (FormatCase fc : cc)
423            res |= 1 << fc.ordinal();
424        res <<= FormatAction.count;
425        for (FormatAction fa : ca)
426            res |= 1 << fa.ordinal();
427        res <<= FormatWhen.count;
428        for (FormatWhen fw : cw)
429            res |= 1 << fw.ordinal();
430        res <<= FormatResolve.count;
431        for (FormatResolve fr : cr)
432            res |= 1 << fr.ordinal();
433        res <<= FormatUnresolved.count;
434        for (FormatUnresolved fu : cu)
435            res |= 1 << fu.ordinal();
436        res <<= FormatErrors.count;
437        for (FormatErrors fe : ce)
438            res |= 1 << fe.ordinal();
439        return res;
440    }
441
442    interface Selector<E extends Enum<E> & Selector<E>> {
443        SelectorCollector<E> collector(Setter.SelectorList sl);
444        String doc();
445    }
446
447    /**
448     * The event cases
449     */
450    public enum FormatCase implements Selector<FormatCase> {
451        IMPORT("import declaration"),
452        CLASS("class declaration"),
453        INTERFACE("interface declaration"),
454        ENUM("enum declaration"),
455        ANNOTATION("annotation interface declaration"),
456        METHOD("method declaration -- note: {type}==parameter-types"),
457        VARDECL("variable declaration without init"),
458        VARINIT("variable declaration with init"),
459        EXPRESSION("expression -- note: {name}==scratch-variable-name"),
460        VARVALUE("variable value expression"),
461        ASSIGNMENT("assign variable"),
462        STATEMENT("statement");
463        String doc;
464        static final EnumSet<FormatCase> all = EnumSet.allOf(FormatCase.class);
465        static final int count = all.size();
466
467        @Override
468        public SelectorCollector<FormatCase> collector(Setter.SelectorList sl) {
469            return sl.cases;
470        }
471
472        @Override
473        public String doc() {
474            return doc;
475        }
476
477        private FormatCase(String doc) {
478            this.doc = doc;
479        }
480    }
481
482    /**
483     * The event actions
484     */
485    public enum FormatAction implements Selector<FormatAction> {
486        ADDED("snippet has been added"),
487        MODIFIED("an existing snippet has been modified"),
488        REPLACED("an existing snippet has been replaced with a new snippet"),
489        OVERWROTE("an existing snippet has been overwritten"),
490        DROPPED("snippet has been dropped"),
491        USED("snippet was used when it cannot be");
492        String doc;
493        static final EnumSet<FormatAction> all = EnumSet.allOf(FormatAction.class);
494        static final int count = all.size();
495
496        @Override
497        public SelectorCollector<FormatAction> collector(Setter.SelectorList sl) {
498            return sl.actions;
499        }
500
501        @Override
502        public String doc() {
503            return doc;
504        }
505
506        private FormatAction(String doc) {
507            this.doc = doc;
508        }
509    }
510
511    /**
512     * When the event occurs: primary or update
513     */
514    public enum FormatWhen implements Selector<FormatWhen> {
515        PRIMARY("the entered snippet"),
516        UPDATE("an update to a dependent snippet");
517        String doc;
518        static final EnumSet<FormatWhen> all = EnumSet.allOf(FormatWhen.class);
519        static final int count = all.size();
520
521        @Override
522        public SelectorCollector<FormatWhen> collector(Setter.SelectorList sl) {
523            return sl.whens;
524        }
525
526        @Override
527        public String doc() {
528            return doc;
529        }
530
531        private FormatWhen(String doc) {
532            this.doc = doc;
533        }
534    }
535
536    /**
537     * Resolution problems
538     */
539    public enum FormatResolve implements Selector<FormatResolve> {
540        OK("resolved correctly"),
541        DEFINED("defined despite recoverably unresolved references"),
542        NOTDEFINED("not defined because of recoverably unresolved references");
543        String doc;
544        static final EnumSet<FormatResolve> all = EnumSet.allOf(FormatResolve.class);
545        static final int count = all.size();
546
547        @Override
548        public SelectorCollector<FormatResolve> collector(Setter.SelectorList sl) {
549            return sl.resolves;
550        }
551
552        @Override
553        public String doc() {
554            return doc;
555        }
556
557        private FormatResolve(String doc) {
558            this.doc = doc;
559        }
560    }
561
562    /**
563     * Count of unresolved references
564     */
565    public enum FormatUnresolved implements Selector<FormatUnresolved> {
566        UNRESOLVED0("no names are unresolved"),
567        UNRESOLVED1("one name is unresolved"),
568        UNRESOLVED2("two or more names are unresolved");
569        String doc;
570        static final EnumSet<FormatUnresolved> all = EnumSet.allOf(FormatUnresolved.class);
571        static final int count = all.size();
572
573        @Override
574        public SelectorCollector<FormatUnresolved> collector(Setter.SelectorList sl) {
575            return sl.unresolvedCounts;
576        }
577
578        @Override
579        public String doc() {
580            return doc;
581        }
582
583        private FormatUnresolved(String doc) {
584            this.doc = doc;
585        }
586    }
587
588    /**
589     * Count of unresolved references
590     */
591    public enum FormatErrors implements Selector<FormatErrors> {
592        ERROR0("no errors"),
593        ERROR1("one error"),
594        ERROR2("two or more errors");
595        String doc;
596        static final EnumSet<FormatErrors> all = EnumSet.allOf(FormatErrors.class);
597        static final int count = all.size();
598
599        @Override
600        public SelectorCollector<FormatErrors> collector(Setter.SelectorList sl) {
601            return sl.errorCounts;
602        }
603
604        @Override
605        public String doc() {
606            return doc;
607        }
608
609        private FormatErrors(String doc) {
610            this.doc = doc;
611        }
612    }
613
614    class SelectorCollector<E extends Enum<E> & Selector<E>> {
615        final EnumSet<E> all;
616        EnumSet<E> set = null;
617        SelectorCollector(EnumSet<E> all) {
618            this.all = all;
619        }
620        void add(Object o) {
621            @SuppressWarnings("unchecked")
622            E e = (E) o;
623            if (set == null) {
624                set = EnumSet.of(e);
625            } else {
626                set.add(e);
627            }
628        }
629
630        boolean isEmpty() {
631            return set == null;
632        }
633
634        EnumSet<E> getSet() {
635            return set == null
636                    ? all
637                    : set;
638        }
639    }
640
641    // Class used to set custom eval output formats
642    // For both /set format  -- Parse arguments, setting custom format, or printing error
643    private class Setter {
644
645        private final ArgTokenizer at;
646        private final MessageHandler messageHandler;
647        boolean valid = true;
648
649        Setter(MessageHandler messageHandler, ArgTokenizer at) {
650            this.messageHandler = messageHandler;
651            this.at = at;
652        }
653
654        void fluff(String format, Object... args) {
655            messageHandler.fluff(format, args);
656        }
657
658        void fluffmsg(String messageKey, Object... args) {
659            messageHandler.fluffmsg(messageKey, args);
660        }
661
662        void errorat(String messageKey, Object... args) {
663            Object[] a2 = Arrays.copyOf(args, args.length + 2);
664            a2[args.length] = at.whole();
665            messageHandler.errormsg(messageKey, a2);
666        }
667
668        // For /set prompt <mode> "<prompt>" "<continuation-prompt>"
669        boolean setPrompt() {
670            Mode m = nextMode();
671            if (valid && m.readOnly) {
672                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
673                valid = false;
674            }
675            String prompt = valid ? nextFormat() : null;
676            String continuationPrompt = valid ? nextFormat() : null;
677            if (valid) {
678                m.setPrompts(prompt, continuationPrompt);
679            } else {
680                fluffmsg("jshell.msg.see", "/help /set prompt");
681            }
682            return valid;
683        }
684
685        // For /set newmode <new-mode> [-command|-quiet [<old-mode>]]
686        boolean setNewMode() {
687            String umode = at.next();
688            if (umode == null || !at.isIdentifier()) {
689                errorat("jshell.err.feedback.expected.new.feedback.mode");
690                valid = false;
691            }
692            if (modeMap.containsKey(umode)) {
693                errorat("jshell.err.feedback.expected.mode.name", umode);
694                valid = false;
695            }
696            String[] fluffOpt = at.next("-command", "-quiet");
697            boolean fluff = fluffOpt == null || fluffOpt.length != 1 || "-command".equals(fluffOpt[0]);
698            if (fluffOpt != null && fluffOpt.length != 1) {
699                errorat("jshell.err.feedback.command.quiet");
700                valid = false;
701            }
702            Mode om = null;
703            String omode = at.next();
704            if (omode != null) {
705                om = toMode(omode);
706            }
707            if (valid) {
708                Mode nm = (om != null)
709                        ? new Mode(umode, fluff, om)
710                        : new Mode(umode, fluff);
711                modeMap.put(umode, nm);
712                fluffmsg("jshell.msg.feedback.new.mode", nm.name);
713            } else {
714                fluffmsg("jshell.msg.see", "/help /set newmode");
715            }
716            return valid;
717        }
718
719        // For /set feedback <mode>
720        boolean setFeedback() {
721            Mode m = nextMode();
722            if (valid) {
723                mode = m;
724                fluffmsg("jshell.msg.feedback.mode", mode.name);
725            } else {
726                fluffmsg("jshell.msg.see", "/help /set feedback");
727                printFeedbackModes();
728            }
729            return valid;
730        }
731
732        // For /set format <mode> "<format>" <selector>...
733        boolean setFormat() {
734            Mode m = nextMode();
735            if (valid && m.readOnly) {
736                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
737                valid = false;
738            }
739            String field = at.next();
740            if (field == null || !at.isIdentifier()) {
741                errorat("jshell.err.feedback.expected.field");
742                valid = false;
743            }
744            String format = valid ? nextFormat() : null;
745            return installFormat(m, field, format, "/help /set format");
746        }
747
748        // For /set truncation <mode> <length> <selector>...
749        boolean setTruncation() {
750            Mode m = nextMode();
751            if (valid && m.readOnly) {
752                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
753                valid = false;
754            }
755            String length = at.next();
756            if (length == null) {
757                errorat("jshell.err.truncation.expected.length");
758                valid = false;
759            } else {
760                try {
761                    // Assure that integer format is correct
762                    Integer.parseUnsignedInt(length);
763                } catch (NumberFormatException ex) {
764                    errorat("jshell.err.truncation.length.not.integer", length);
765                    valid = false;
766                }
767            }
768            // install length into an internal format field
769            return installFormat(m, TRUNCATION_FIELD, length, "/help /set truncation");
770        }
771
772        String retainFeedback() {
773            String umode = at.next();
774            if (umode != null) {
775                Mode m = toMode(umode);
776                if (valid && !m.readOnly && !retainedMap.containsKey(m.name)) {
777                    errorat("jshell.err.retained.feedback.mode.must.be.retained.or.predefined");
778                    valid = false;
779                }
780                if (valid) {
781                    mode = m;
782                    fluffmsg("jshell.msg.feedback.mode", mode.name);
783                } else {
784                    fluffmsg("jshell.msg.see", "/help /retain feedback");
785                    return null;
786                }
787            }
788            return mode.name;
789        }
790
791        String retainMode() {
792            Mode m = nextMode();
793            if (valid && m.readOnly) {
794                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
795                valid = false;
796            }
797            if (valid) {
798                retainedMap.put(m.name, m.encode());
799                return String.join(RECORD_SEPARATOR, retainedMap.values());
800            } else {
801                fluffmsg("jshell.msg.see", "/help /retain mode");
802                return null;
803            }
804        }
805
806        boolean restoreEncodedModes(String allEncoded) {
807            // Iterate over each record in each encoded mode
808            String[] ms = allEncoded.split(RECORD_SEPARATOR);
809            Iterator<String> itr = Arrays.asList(ms).iterator();
810            while (itr.hasNext()) {
811                // Reconstruct the encoded mode
812                Mode m = new Mode(itr);
813                modeMap.put(m.name, m);
814                // Continue to retain it a new retains occur
815                retainedMap.put(m.name, m.encode());
816            }
817            return true;
818        }
819
820        // install the format of a field under parsed selectors
821        boolean installFormat(Mode m, String field, String format, String help) {
822            String slRaw;
823            List<SelectorList> slList = new ArrayList<>();
824            while (valid && (slRaw = at.next()) != null) {
825                SelectorList sl = new SelectorList();
826                sl.parseSelectorList(slRaw);
827                slList.add(sl);
828            }
829            if (valid) {
830                if (slList.isEmpty()) {
831                    // No selectors specified, then always the format
832                    m.set(field, ALWAYS, format);
833                } else {
834                    // Set the format of the field for specified selector
835                    slList.stream()
836                            .forEach(sl -> m.set(field,
837                                sl.cases.getSet(), sl.actions.getSet(), sl.whens.getSet(),
838                                sl.resolves.getSet(), sl.unresolvedCounts.getSet(), sl.errorCounts.getSet(),
839                                format));
840                }
841            } else {
842                fluffmsg("jshell.msg.see", help);
843            }
844            return valid;
845        }
846
847        Mode nextMode() {
848            String umode = at.next();
849            return toMode(umode);
850        }
851
852        Mode toMode(String umode) {
853            if (umode == null || !at.isIdentifier()) {
854                errorat("jshell.err.feedback.expected.mode");
855                valid = false;
856                return null;
857            }
858            Mode m = modeMap.get(umode);
859            if (m != null) {
860                return m;
861            }
862            // Failing an exact match, go searching
863            Mode[] matches = modeMap.entrySet().stream()
864                    .filter(e -> e.getKey().startsWith(umode))
865                    .map(e -> e.getValue())
866                    .toArray(size -> new Mode[size]);
867            if (matches.length == 1) {
868                return matches[0];
869            } else {
870                valid = false;
871                if (matches.length == 0) {
872                    errorat("jshell.err.feedback.does.not.match.mode", umode);
873                } else {
874                    errorat("jshell.err.feedback.ambiguous.mode", umode);
875                }
876                printFeedbackModes();
877                return null;
878            }
879        }
880
881        void printFeedbackModes() {
882            fluffmsg("jshell.msg.feedback.mode.following");
883            modeMap.keySet().stream()
884                    .forEach(mk -> fluff("   %s", mk));
885        }
886
887        // Test if the format string is correctly
888        final String nextFormat() {
889            String format = at.next();
890            if (format == null) {
891                errorat("jshell.err.feedback.expected.format");
892                valid = false;
893                return null;
894            }
895            if (!at.isQuoted()) {
896                errorat("jshell.err.feedback.must.be.quoted", format);
897                valid = false;
898                return null;
899            }
900            return format;
901        }
902
903        class SelectorList {
904
905            SelectorCollector<FormatCase> cases = new SelectorCollector<>(FormatCase.all);
906            SelectorCollector<FormatAction> actions = new SelectorCollector<>(FormatAction.all);
907            SelectorCollector<FormatWhen> whens = new SelectorCollector<>(FormatWhen.all);
908            SelectorCollector<FormatResolve> resolves = new SelectorCollector<>(FormatResolve.all);
909            SelectorCollector<FormatUnresolved> unresolvedCounts = new SelectorCollector<>(FormatUnresolved.all);
910            SelectorCollector<FormatErrors> errorCounts = new SelectorCollector<>(FormatErrors.all);
911
912            final void parseSelectorList(String sl) {
913                for (String s : sl.split("-")) {
914                    SelectorCollector<?> lastCollector = null;
915                    for (String as : s.split(",")) {
916                        if (!as.isEmpty()) {
917                            Selector<?> sel = selectorMap.get(as);
918                            if (sel == null) {
919                                errorat("jshell.err.feedback.not.a.valid.selector", as, s);
920                                valid = false;
921                                return;
922                            }
923                            SelectorCollector<?> collector = sel.collector(this);
924                            if (lastCollector == null) {
925                                if (!collector.isEmpty()) {
926                                    errorat("jshell.err.feedback.multiple.sections", as, s);
927                                    valid = false;
928                                    return;
929                                }
930                            } else if (collector != lastCollector) {
931                                errorat("jshell.err.feedback.different.selector.kinds", as, s);
932                                valid = false;
933                                return;
934                            }
935                            collector.add(sel);
936                            lastCollector = collector;
937                        }
938                    }
939                }
940            }
941        }
942    }
943}
944