diff --git a/ivette/src/dome/renderer/layout/forms.tsx b/ivette/src/dome/renderer/layout/forms.tsx
index 56b4e428a83ceb70da0e02277bb1505bdc26f31a..86183586e91b5cbd51902f9cbe389a1bc3430244 100644
--- a/ivette/src/dome/renderer/layout/forms.tsx
+++ b/ivette/src/dome/renderer/layout/forms.tsx
@@ -417,7 +417,7 @@ export interface PageProps extends FilterProps, Children {
 export function Page(props: PageProps) {
   const { className, style, children, ...filter } = props;
   const { hidden, disabled } = useContext(filter);
-  const css = Utils.classes('dome-xForm-grid', className);
+  const css = Utils.classes(className ?? 'dome-xForm-grid');
   if (hidden) return null;
   return (
     <div className={css} style={style}>
@@ -598,7 +598,7 @@ export function Field(props: GenericFieldProps) {
       >
         {label}
       </label>
-      <div className={cssField}>
+      <div className={cssField} title={title}>
         {children}
         {WARNING}
       </div>
diff --git a/ivette/src/frama-c/api/generated/kernel/ast/index.ts b/ivette/src/frama-c/api/generated/kernel/ast/index.ts
index e6053cb2610892878eaa809c997d6c6064afa04d..89d14dc6e8599a29643125ac8fdd0c722e8ec1e1 100644
--- a/ivette/src/frama-c/api/generated/kernel/ast/index.ts
+++ b/ivette/src/frama-c/api/generated/kernel/ast/index.ts
@@ -53,14 +53,6 @@ import { jTextSafe } from 'frama-c/api/kernel/data';
 import { tag } from 'frama-c/api/kernel/data';
 //@ts-ignore
 import { text } from 'frama-c/api/kernel/data';
-//@ts-ignore
-import { bySource } from 'frama-c/api/kernel/services';
-//@ts-ignore
-import { jSource } from 'frama-c/api/kernel/services';
-//@ts-ignore
-import { jSourceSafe } from 'frama-c/api/kernel/services';
-//@ts-ignore
-import { source } from 'frama-c/api/kernel/services';
 
 const compute_internal: Server.ExecRequest<null,null> = {
   kind: Server.RqKind.EXEC,
@@ -77,6 +69,33 @@ export const changed: Server.Signal = {
   name: 'kernel.ast.changed',
 };
 
+/** Source file positions. */
+export type source =
+  { dir: string, base: string, file: string, line: number };
+
+/** Loose decoder for `source` */
+export const jSource: Json.Loose<source> =
+  Json.jObject({
+    dir: Json.jFail(Json.jString,'String expected'),
+    base: Json.jFail(Json.jString,'String expected'),
+    file: Json.jFail(Json.jString,'String expected'),
+    line: Json.jFail(Json.jNumber,'Number expected'),
+  });
+
+/** Safe decoder for `source` */
+export const jSourceSafe: Json.Safe<source> =
+  Json.jFail(jSource,'Source expected');
+
+/** Natural order for `source` */
+export const bySource: Compare.Order<source> =
+  Compare.byFields
+    <{ dir: string, base: string, file: string, line: number }>({
+    dir: Compare.string,
+    base: Compare.string,
+    file: Compare.string,
+    line: Compare.number,
+  });
+
 /** Marker kind */
 export enum markerKind {
   /** Expression */
diff --git a/ivette/src/frama-c/api/generated/kernel/properties/index.ts b/ivette/src/frama-c/api/generated/kernel/properties/index.ts
index 6c5a7309f5003aeccd2b0cba0b173e9a7af87853..1379aced2c69b7e54f858187595db95f925a2968 100644
--- a/ivette/src/frama-c/api/generated/kernel/properties/index.ts
+++ b/ivette/src/frama-c/api/generated/kernel/properties/index.ts
@@ -38,21 +38,21 @@ import * as Server from 'frama-c/server';
 import * as State from 'frama-c/states';
 
 //@ts-ignore
-import { byTag } from 'frama-c/api/kernel/data';
+import { bySource } from 'frama-c/api/kernel/ast';
 //@ts-ignore
-import { jTag } from 'frama-c/api/kernel/data';
+import { jSource } from 'frama-c/api/kernel/ast';
 //@ts-ignore
-import { jTagSafe } from 'frama-c/api/kernel/data';
+import { jSourceSafe } from 'frama-c/api/kernel/ast';
 //@ts-ignore
-import { tag } from 'frama-c/api/kernel/data';
+import { source } from 'frama-c/api/kernel/ast';
 //@ts-ignore
-import { bySource } from 'frama-c/api/kernel/services';
+import { byTag } from 'frama-c/api/kernel/data';
 //@ts-ignore
-import { jSource } from 'frama-c/api/kernel/services';
+import { jTag } from 'frama-c/api/kernel/data';
 //@ts-ignore
-import { jSourceSafe } from 'frama-c/api/kernel/services';
+import { jTagSafe } from 'frama-c/api/kernel/data';
 //@ts-ignore
-import { source } from 'frama-c/api/kernel/services';
+import { tag } from 'frama-c/api/kernel/data';
 
 /** Property Kinds */
 export enum propKind {
diff --git a/ivette/src/frama-c/api/generated/kernel/services/index.ts b/ivette/src/frama-c/api/generated/kernel/services/index.ts
index fa99770d45ee200b26aee47c0a16fff8b2bdfc42..d271d8fd722e44a281c5214cc56d93f47746cfc9 100644
--- a/ivette/src/frama-c/api/generated/kernel/services/index.ts
+++ b/ivette/src/frama-c/api/generated/kernel/services/index.ts
@@ -37,6 +37,22 @@ import * as Server from 'frama-c/server';
 //@ts-ignore
 import * as State from 'frama-c/states';
 
+//@ts-ignore
+import { byMarker } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { bySource } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { jMarker } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { jMarkerSafe } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { jSource } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { jSourceSafe } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { marker } from 'frama-c/api/kernel/ast';
+//@ts-ignore
+import { source } from 'frama-c/api/kernel/ast';
 //@ts-ignore
 import { byTag } from 'frama-c/api/kernel/data';
 //@ts-ignore
@@ -87,33 +103,6 @@ const save_internal: Server.SetRequest<string,string | undefined> = {
 /** Save the current session. Returns an error, if not successfull. */
 export const save: Server.SetRequest<string,string | undefined>= save_internal;
 
-/** Source file positions. */
-export type source =
-  { dir: string, base: string, file: string, line: number };
-
-/** Loose decoder for `source` */
-export const jSource: Json.Loose<source> =
-  Json.jObject({
-    dir: Json.jFail(Json.jString,'String expected'),
-    base: Json.jFail(Json.jString,'String expected'),
-    file: Json.jFail(Json.jString,'String expected'),
-    line: Json.jFail(Json.jNumber,'Number expected'),
-  });
-
-/** Safe decoder for `source` */
-export const jSourceSafe: Json.Safe<source> =
-  Json.jFail(jSource,'Source expected');
-
-/** Natural order for `source` */
-export const bySource: Compare.Order<source> =
-  Compare.byFields
-    <{ dir: string, base: string, file: string, line: number }>({
-    dir: Compare.string,
-    base: Compare.string,
-    file: Compare.string,
-    line: Compare.number,
-  });
-
 /** Log messages categories. */
 export enum logkind {
   /** User Error */
@@ -150,6 +139,108 @@ const logkindTags_internal: Server.GetRequest<null,tag[]> = {
 /** Registered tags for the above type. */
 export const logkindTags: Server.GetRequest<null,tag[]>= logkindTags_internal;
 
+/** Data for array rows [`message`](#message)  */
+export interface messageData {
+  /** Entry identifier. */
+  key: Json.key<'#message'>;
+  /** Message kind */
+  kind: logkind;
+  /** Emitter plugin */
+  plugin: string;
+  /** Message text */
+  message: string;
+  /** Message category (only for debug or warning messages) */
+  category?: string;
+  /** Source file position */
+  source?: source;
+  /** Marker at the message position (if any) */
+  marker?: marker;
+  /** Function containing the message position (if any) */
+  fct?: Json.key<'#fct'>;
+}
+
+/** Loose decoder for `messageData` */
+export const jMessageData: Json.Loose<messageData> =
+  Json.jObject({
+    key: Json.jFail(Json.jKey<'#message'>('#message'),'#message expected'),
+    kind: jLogkindSafe,
+    plugin: Json.jFail(Json.jString,'String expected'),
+    message: Json.jFail(Json.jString,'String expected'),
+    category: Json.jString,
+    source: jSource,
+    marker: jMarker,
+    fct: Json.jKey<'#fct'>('#fct'),
+  });
+
+/** Safe decoder for `messageData` */
+export const jMessageDataSafe: Json.Safe<messageData> =
+  Json.jFail(jMessageData,'MessageData expected');
+
+/** Natural order for `messageData` */
+export const byMessageData: Compare.Order<messageData> =
+  Compare.byFields
+    <{ key: Json.key<'#message'>, kind: logkind, plugin: string,
+       message: string, category?: string, source?: source, marker?: marker,
+       fct?: Json.key<'#fct'> }>({
+    key: Compare.string,
+    kind: byLogkind,
+    plugin: Compare.alpha,
+    message: Compare.string,
+    category: Compare.defined(Compare.string),
+    source: Compare.defined(bySource),
+    marker: Compare.defined(byMarker),
+    fct: Compare.defined(Compare.string),
+  });
+
+/** Signal for array [`message`](#message)  */
+export const signalMessage: Server.Signal = {
+  name: 'kernel.services.signalMessage',
+};
+
+const reloadMessage_internal: Server.GetRequest<null,null> = {
+  kind: Server.RqKind.GET,
+  name:   'kernel.services.reloadMessage',
+  input:  Json.jNull,
+  output: Json.jNull,
+  signals: [],
+};
+/** Force full reload for array [`message`](#message)  */
+export const reloadMessage: Server.GetRequest<null,null>= reloadMessage_internal;
+
+const fetchMessage_internal: Server.GetRequest<
+  number,
+  { pending: number, updated: messageData[], removed: Json.key<'#message'>[],
+    reload: boolean }
+  > = {
+  kind: Server.RqKind.GET,
+  name:   'kernel.services.fetchMessage',
+  input:  Json.jNumber,
+  output: Json.jObject({
+            pending: Json.jFail(Json.jNumber,'Number expected'),
+            updated: Json.jList(jMessageData),
+            removed: Json.jList(Json.jKey<'#message'>('#message')),
+            reload: Json.jFail(Json.jBoolean,'Boolean expected'),
+          }),
+  signals: [],
+};
+/** Data fetcher for array [`message`](#message)  */
+export const fetchMessage: Server.GetRequest<
+  number,
+  { pending: number, updated: messageData[], removed: Json.key<'#message'>[],
+    reload: boolean }
+  >= fetchMessage_internal;
+
+const message_internal: State.Array<Json.key<'#message'>,messageData> = {
+  name: 'kernel.services.message',
+  getkey: ((d:messageData) => d.key),
+  signal: signalMessage,
+  fetch: fetchMessage,
+  reload: reloadMessage,
+  order: byMessageData,
+};
+/** Log messages */
+export const message: State.Array<Json.key<'#message'>,messageData> = message_internal;
+
 /** Message event record. */
 export interface log {
   /** Message kind */
diff --git a/ivette/src/frama-c/index.tsx b/ivette/src/frama-c/index.tsx
index 491dd3599af16f3147a99385c529d25ced0e5525..e9433d25ad01fc9ae467b199c7328923b4e3ed2a 100644
--- a/ivette/src/frama-c/index.tsx
+++ b/ivette/src/frama-c/index.tsx
@@ -35,6 +35,7 @@ import ASTinfo from 'frama-c/kernel/ASTinfo';
 import SourceCode from 'frama-c/kernel/SourceCode';
 import Locations from 'frama-c/kernel/Locations';
 import Properties from 'frama-c/kernel/Properties';
+import Messages from 'frama-c/kernel/Messages';
 
 import 'frama-c/kernel/style.css';
 
@@ -83,6 +84,12 @@ Ivette.registerGroup({
     title: 'Status of ASCL Properties',
     children: <Properties />,
   });
+  Ivette.registerComponent({
+    id: 'frama-c.messages',
+    label: 'Messages',
+    title: 'Messages emitted by Frama-C',
+    children: <Messages />,
+  });
 });
 
 /* --------------------------------------------------------------------------*/
diff --git a/ivette/src/frama-c/kernel/Messages.tsx b/ivette/src/frama-c/kernel/Messages.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6a88a35d4d4c8fcc3a40917508cdcfd4c41b5a9a
--- /dev/null
+++ b/ivette/src/frama-c/kernel/Messages.tsx
@@ -0,0 +1,429 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2021                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+import * as React from 'react';
+import * as Dome from 'dome';
+import { TitleBar } from 'ivette';
+
+import { IconButton } from 'dome/controls/buttons';
+import { Label, Cell, Data } from 'dome/controls/labels';
+import { Icon } from 'dome/controls/icons';
+import { Scroll } from 'dome/layout/boxes';
+import { RSplit } from 'dome/layout/splitters';
+import * as Forms from 'dome/layout/forms';
+import * as Arrays from 'dome/table/arrays';
+import { Table, Column, Renderer } from 'dome/table/views';
+import * as Compare from 'dome/data/compare';
+
+import * as States from 'frama-c/states';
+import * as Ast from 'frama-c/api/kernel/ast';
+import * as Kernel from 'frama-c/api/kernel/services';
+
+type Message = Kernel.messageData;
+
+// --------------------------------------------------------------------------
+// --- Filters
+// --------------------------------------------------------------------------
+
+interface Search {
+  category?: string;
+  message?: string;
+}
+
+interface KindFilter {
+  result?: boolean;
+  feedback?: boolean;
+  debug?: boolean;
+  warning?: boolean;
+  error?: boolean;
+  failure?: boolean;
+}
+
+interface PluginFilter {
+  kernel?: boolean;
+  server?: boolean;
+  eva?: boolean;
+  wp?: boolean;
+  nonterm?: boolean;
+  others?: boolean;
+}
+
+interface Filter {
+  currentFct: boolean;
+  search: Search;
+  kind: KindFilter;
+  plugin: PluginFilter;
+}
+
+const defaultFilter: Filter = {
+  currentFct: false,
+  search: {},
+  kind: {
+    result: false,
+    feedback: false,
+    debug: false,
+  },
+  plugin: {},
+};
+
+function filterKind(filter: KindFilter, msg: Message) {
+  const hide =
+    (filter.result === false && msg.kind === 'RESULT')
+    || (filter.feedback === false && msg.kind === 'FEEDBACK')
+    || (filter.debug === false && msg.kind === 'DEBUG')
+    || (filter.warning === false && msg.kind === 'WARNING')
+    || (filter.error === false && msg.kind === 'ERROR')
+    || (filter.failure === false && msg.kind === 'FAILURE');
+  return !hide;
+}
+
+function filterPlugin(filter: PluginFilter, msg: Message) {
+  switch (msg.plugin) {
+    case 'kernel': return !(filter.kernel === false);
+    case 'server': return !(filter.server === false);
+    case 'eva': return !(filter.eva === false);
+    case 'wp': return !(filter.wp === false);
+    case 'nonterm': return !(filter.nonterm === false);
+    default: return !(filter.others === false);
+  }
+}
+
+function searchCategory(search: string | undefined, msg: string | undefined) {
+  if (!search || search.length < 2)
+    return true;
+  if (!msg)
+    return false;
+  const message = msg.toLowerCase();
+  const array = search.toLowerCase().split(' ');
+  let empty = true;
+  let show = false;
+  let hide = false;
+  array.forEach((str: string) => {
+    if (str.length > 1) {
+      if (str.charAt(0) === '-') {
+        if (message.includes(str.substring(1)))
+          hide = true;
+      }
+      else if (message.includes(str))
+        show = true;
+      else
+        empty = false;
+    }
+  });
+  return (empty || show) && !hide;
+}
+
+function searchString(search: string | undefined, msg: string) {
+  if (!search || search.length < 3)
+    return true;
+  if (search.charAt(0) === '"' && search.slice(-1) === '"') {
+    const exact = search.slice(1, -1);
+    return msg.includes(exact);
+  }
+  const message = msg.toLowerCase();
+  const array = search.toLowerCase().split(' ');
+  let show = true;
+  array.forEach((str: string) => {
+    if (str.length > 1 && !message.includes(str))
+      show = false;
+  });
+  return show;
+}
+
+function filterSearched(search: Search, msg: Message) {
+  return (searchString(search.message, msg.message) &&
+          searchCategory(search.category, msg.category));
+}
+
+function filterFunction(filter: Filter, kf: string | undefined, msg: Message) {
+  if (filter.currentFct)
+    return (kf === msg.fct);
+  return true;
+}
+
+function filterMessage(filter: Filter, kf: string | undefined, msg: Message) {
+  return (filterFunction(filter, kf, msg) &&
+          filterSearched(filter.search, msg) &&
+          filterKind(filter.kind, msg) &&
+          filterPlugin(filter.plugin, msg));
+}
+
+// --------------------------------------------------------------------------
+// --- Filters panel and ratio
+// --------------------------------------------------------------------------
+
+function MessageFilter(props: {filter: Forms.FieldState<Filter>}) {
+  const state = props.filter;
+  const search = Forms.useProperty(state, 'search');
+  const categoryState = Forms.useProperty(search, 'category');
+  const messageState = Forms.useProperty(search, 'message');
+
+  const kind = Forms.useProperty(state, 'kind');
+  function kindState(path: keyof KindFilter) {
+    return Forms.useDefault(Forms.useProperty(kind, path), true);
+  }
+
+  const plugin = Forms.useProperty(state, 'plugin');
+  function pluginState(path: keyof PluginFilter) {
+    return Forms.useDefault(Forms.useProperty(plugin, path), true);
+  }
+
+  return (
+    <Scroll>
+      <Forms.Page className="message-search">
+        <Forms.CheckboxField
+          label="Current function"
+          title="Only show messages emitted at the current function"
+          state={Forms.useProperty(state, 'currentFct')}
+        />
+        <Forms.Section
+          label="Search"
+          unfold
+          settings="ivette.messages.search"
+        >
+          <Forms.TextField
+            label="Category"
+            state={categoryState}
+            placeholder="Category"
+            title={'Search in message category.\n'
+                 + 'Use -<name> to hide some categories.'}
+          />
+          <Forms.TextField
+            label="Message"
+            state={messageState}
+            placeholder="Message"
+            title={'Search in message text.\n'
+                 + 'Case-insensitive by default.\n'
+                 + 'Use "text" for an exact case-sensitive search.'}
+          />
+        </Forms.Section>
+        <Forms.Section
+          label="Kind"
+          unfold
+          settings="ivette.messages.filterKind"
+        >
+          <Forms.CheckboxField label="Result" state={kindState('result')} />
+          <Forms.CheckboxField label="Feedback" state={kindState('feedback')} />
+          <Forms.CheckboxField label="Debug" state={kindState('debug')} />
+          <Forms.CheckboxField label="Warning" state={kindState('warning')} />
+          <Forms.CheckboxField label="Error" state={kindState('error')} />
+          <Forms.CheckboxField label="Failure" state={kindState('failure')} />
+        </Forms.Section>
+        <Forms.Section
+          label="Emitter"
+          unfold
+          settings="ivette.messages.filterEmitter"
+        >
+          <Forms.CheckboxField label="Kernel" state={pluginState('kernel')} />
+          <Forms.CheckboxField label="Server" state={pluginState('server')} />
+          <Forms.CheckboxField label="Eva" state={pluginState('eva')} />
+          <Forms.CheckboxField label="WP" state={pluginState('wp')} />
+          <Forms.CheckboxField label="Nonterm" state={pluginState('nonterm')} />
+          <Forms.CheckboxField label="Others" state={pluginState('others')} />
+        </Forms.Section>
+      </Forms.Page>
+    </Scroll>
+  );
+}
+
+function FilterRatio({ model }: { model: Arrays.ArrayModel<any, any> }) {
+  const [filtered, total] = [model.getRowCount(), model.getTotalRowCount()];
+  const title = `${filtered} displayed messages / ${total} total messages`;
+  return (
+    <Label className="component-info" title={title}>
+      {filtered} / {total}
+    </Label>
+  );
+}
+
+// --------------------------------------------------------------------------
+// --- Messages Columns
+// --------------------------------------------------------------------------
+
+const renderKind: Renderer<Kernel.logkind> = (kind: Kernel.logkind) => {
+  const label = kind.toLocaleLowerCase();
+  let icon = '';
+  let color = 'black';
+  switch (kind) {
+    case 'RESULT': icon = 'ANGLE.RIGHT'; break;
+    case 'FEEDBACK': icon = 'CIRC.INFO'; break;
+    case 'DEBUG': icon = 'HELP'; break;
+    case 'WARNING': icon = 'ATTENTION'; color = '#C00000'; break;
+    case 'ERROR': case 'FAILURE': icon = 'WARNING'; color = '#C00000'; break;
+  }
+  return <Icon title={label} id={icon} fill={color} />;
+};
+
+const renderCell: Renderer<string> =
+  (text: string) => (<Cell title={text}>{text}</Cell>);
+
+const renderMessage: Renderer<string> =
+  (text: string) => (<Data title={text}> {text} </Data>);
+
+const renderDir: Renderer<Ast.source> =
+  (loc: Ast.source) => (<Cell label={loc.dir} title={loc.file} />);
+
+const renderFile: Renderer<Ast.source> =
+  (loc: Ast.source) => (
+    <Cell label={`${loc.base}:${loc.line}`} title={loc.file} />
+  );
+
+const MessageColumns = () => (
+  <>
+    <Column
+      id="kind"
+      title="Message kind"
+      label="Kind"
+      width={42}
+      align="center"
+      render={renderKind}
+    />
+    <Column
+      id="plugin"
+      label="Emitter"
+      title="Frama-C kernel or plugin"
+      width={75}
+      render={renderCell}
+    />
+    <Column
+      id="category"
+      label="Category"
+      title="Only for warning and debug messages"
+      width={120}
+      render={renderCell}
+    />
+    <Column
+      id="message"
+      label="Message"
+      fill
+      render={renderMessage}
+    />
+    <Column
+      id="fct"
+      label="Function"
+      width={120}
+      render={renderCell}
+    />
+    <Column
+      id="dir"
+      label="Directory"
+      width={240}
+      visible={false}
+      getter={(msg: Message) => msg?.source}
+      render={renderDir}
+    />
+    <Column
+      id="file"
+      label="File"
+      width={150}
+      getter={(msg: Message) => msg?.source}
+      render={renderFile}
+    />
+    <Column
+      id="key"
+      label="Id"
+      title="Message emission order"
+      width={42}
+      visible={false}
+    />
+  </>
+);
+
+// -------------------------------------------------------------------------
+// --- Mesages Table
+// -------------------------------------------------------------------------
+
+const bySource =
+  Compare.byFields<Ast.source>({ file: Compare.alpha, line: Compare.number });
+
+const byMessage: Compare.ByFields<Message> = {
+  key: Compare.lift(parseInt, Compare.bignum),
+  kind: Compare.structural,
+  plugin: Compare.string,
+  category: Compare.defined(Compare.string),
+  fct: Compare.defined(Compare.alpha),
+  source: Compare.defined(bySource),
+};
+
+export default function RenderMessages() {
+
+  const model = React.useMemo(() => (
+    new Arrays.CompactModel<string, Message>((msg: Message) => msg.key)
+  ), []);
+  const data = States.useSyncArray(Kernel.message).getArray();
+  model.setOrderingByFields(byMessage);
+
+  React.useEffect(() => {
+    model.removeAllData();
+    model.updateData(data);
+    model.reload();
+  }, [model, data]);
+
+  const filterState = Forms.useState<Filter>(defaultFilter);
+  const [selection, updateSelection] = States.useSelection();
+  const selectedFct = selection?.current?.fct;
+
+  React.useEffect(() => {
+    const [filter] = filterState;
+    model.setFilter((msg: Message) => filterMessage(filter, selectedFct, msg));
+  }, [model, filterState, selectedFct]);
+
+  const onMessageSelection = React.useCallback(
+    ({ fct, marker }: Message) => {
+      if (fct && marker) {
+        const location = { fct, marker };
+        updateSelection({ location });
+      }
+    }, [updateSelection],
+  );
+
+  const [showFilter, flipFilter] =
+    Dome.useFlipSettings('ivette.messages.showFilter', true);
+
+  return (
+    <>
+      <TitleBar>
+        <FilterRatio model={model} />
+        <IconButton
+          icon="CLIPBOARD"
+          selected={showFilter}
+          onClick={flipFilter}
+          title="Toggle filters panel"
+        />
+      </TitleBar>
+      <RSplit
+        settings="ivette.messages.filterSplit"
+        unfold={showFilter}
+      >
+        <Table<string, Message>
+          model={model}
+          sorting={model}
+          onSelection={onMessageSelection}
+          settings="ivette.messages.table"
+        >
+          <MessageColumns />
+        </Table>
+        <MessageFilter filter={filterState} />
+      </RSplit>
+    </>
+  );
+}
diff --git a/ivette/src/frama-c/kernel/Properties.tsx b/ivette/src/frama-c/kernel/Properties.tsx
index 57f65e5d2f6ab5190a84b31ed932be0d5aade5c4..ca92fa637777ba381e9b2dd8c94b185ece5934a9 100644
--- a/ivette/src/frama-c/kernel/Properties.tsx
+++ b/ivette/src/frama-c/kernel/Properties.tsx
@@ -42,7 +42,7 @@ import { Scroll, Folder } from 'dome/layout/boxes';
 
 import { RSplit } from 'dome/layout/splitters';
 
-import { source as SourceLoc } from 'frama-c/api/kernel/services';
+import * as Ast from 'frama-c/api/kernel/ast';
 import { statusData } from 'frama-c/api/kernel/properties';
 import * as Properties from 'frama-c/api/kernel/properties';
 import * as Eva from 'frama-c/api/plugins/eva/general';
@@ -245,13 +245,13 @@ const renderNames: Renderer<string[]> =
     return (label ? <Label label={label} /> : null);
   };
 
-const renderDir: Renderer<SourceLoc> =
-  (loc: SourceLoc) => (
+const renderDir: Renderer<Ast.source> =
+  (loc: Ast.source) => (
     <Code className="code-column" label={loc.dir} title={loc.file} />
   );
 
-const renderFile: Renderer<SourceLoc> =
-  (loc: SourceLoc) => (
+const renderFile: Renderer<Ast.source> =
+  (loc: Ast.source) => (
     <Code className="code-column" label={loc.base} title={loc.file} />
   );
 
@@ -286,7 +286,7 @@ function ColumnTag<Row>(props: ColumnProps<Row, States.Tag>) {
 // -------------------------------------------------------------------------
 
 const bySource =
-  Compare.byFields<SourceLoc>({ file: Compare.alpha, line: Compare.number });
+  Compare.byFields<Ast.source>({ file: Compare.alpha, line: Compare.number });
 
 const byStatus =
   Compare.byRank(
@@ -327,8 +327,8 @@ const byProperty: Compare.ByFields<Property> = {
   taint: byTaint,
 };
 
-const byDir = Compare.byFields<SourceLoc>({ dir: Compare.alpha });
-const byFile = Compare.byFields<SourceLoc>({ base: Compare.alpha });
+const byDir = Compare.byFields<Ast.source>({ dir: Compare.alpha });
+const byFile = Compare.byFields<Ast.source>({ base: Compare.alpha });
 
 const byColumn: Arrays.ByColumns<Property> = {
   dir: Compare.byFields<Property>({ source: byDir }),
diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx
index e38461a97fa668b18180a6cc3cef2af45fb0bbe6..b635b6facafb4e03e68f036e3b61d139dbe154bc 100644
--- a/ivette/src/frama-c/kernel/SourceCode.tsx
+++ b/ivette/src/frama-c/kernel/SourceCode.tsx
@@ -33,8 +33,7 @@ import { RichTextBuffer } from 'dome/text/buffers';
 import { Text } from 'dome/text/editors';
 import { TitleBar } from 'ivette';
 import * as Preferences from 'ivette/prefs';
-import { functions, markerInfo } from 'frama-c/api/kernel/ast';
-import { source } from 'frama-c/api/kernel/services';
+import { functions, markerInfo, source } from 'frama-c/api/kernel/ast';
 
 import 'codemirror/addon/selection/active-line';
 import 'codemirror/addon/dialog/dialog.css';
diff --git a/ivette/src/frama-c/kernel/style.css b/ivette/src/frama-c/kernel/style.css
index 04aacdc2e13f2fb6a72f6b6bc2eabbfa7d6decdf..0d546d7f4bee853711c6f7fe3a0869b03681fd84 100644
--- a/ivette/src/frama-c/kernel/style.css
+++ b/ivette/src/frama-c/kernel/style.css
@@ -50,3 +50,35 @@
 }
 
 /* -------------------------------------------------------------------------- */
+/* --- Globals                                                            --- */
+/* -------------------------------------------------------------------------- */
+
+.message-search {
+    padding: 4px 4px 4px 14px;
+}
+
+.message-search .dome-xForm-section {
+    margin-top: 6px ;
+    left: auto; /* Cancel dome left placement */
+}
+
+.message-search .dome-xForm-label {
+    display: none; /* Hide label, only use placeholder */
+}
+
+.message-search input::placeholder {
+    font-style: italic;
+}
+
+.message-search input {
+    vertical-align: middle;
+    margin: 2px 4px 2px 0px;
+}
+
+.message-search .dome-xForm-field {
+    margin: 0px 4px;
+    display: inline-block;
+    min-width: 80px;
+}
+
+/* -------------------------------------------------------------------------- */
diff --git a/ivette/src/renderer/style.css b/ivette/src/renderer/style.css
index 3f56713b0c713f1385e5af163d3fa0b810ff8999..c210426cfccbb0b7b70c7e638c4a6bc3077225a9 100644
--- a/ivette/src/renderer/style.css
+++ b/ivette/src/renderer/style.css
@@ -10,7 +10,7 @@
 }
 
 .component-info {
-    color: #888 ;
+    color: #555 ;
     min-width: 50px;
     padding-top: 4px;
     font-size: smaller;
diff --git a/src/kernel_internals/runtime/messages.ml b/src/kernel_internals/runtime/messages.ml
index 391a0bec9a729f610780b121bbe132040b055064..e5b50b271e57ff5f7a29951169a2942731c7c94c 100644
--- a/src/kernel_internals/runtime/messages.ml
+++ b/src/kernel_internals/runtime/messages.ml
@@ -40,7 +40,7 @@ module DatatypeMessages =
     end)
 
 module Messages =
-  State_builder.List_ref
+  State_builder.Queue
     (DatatypeMessages)
     (struct
       let name = "Messages.message_table"
@@ -51,33 +51,33 @@ let () = Ast.add_monotonic_state Messages.self
 let hooks = ref []
 let add_message m =
   begin
-    Messages.set (m :: Messages.get ()) ;
+    Messages.add m;
     List.iter (fun fn -> fn()) !hooks ;
   end
 
 let nb_errors () =
-  Messages.fold_left
+  Messages.fold
     (fun n e ->
        match e.Log.evt_kind with
        | Log.Error -> succ n
        | _ -> n) 0
 
 let nb_warnings () =
-  Messages.fold_left
+  Messages.fold
     (fun n e ->
        match e.Log.evt_kind with
        | Log.Warning -> succ n
        | _ -> n) 0
 
-let nb_messages() = List.length (Messages.get ())
+let nb_messages = Messages.length
 
 let self = Messages.self
 
-let iter f = List.iter f (List.rev (Messages.get ()))
+let iter = Messages.iter
+let fold = Messages.fold
 let dump_messages () = iter Log.echo
 
-let () =
-  Log.add_listener ~kind:[ Log.Error; Log.Warning ] add_message
+let () = Log.add_listener add_message
 
 module OnceTable =
   State_builder.Hashtbl
diff --git a/src/kernel_internals/runtime/messages.mli b/src/kernel_internals/runtime/messages.mli
index ecac858d28deae0e81b7f4ea69f838de0b7404fd..04f5927556afc7a20555ec6c52fdc1635c228625 100644
--- a/src/kernel_internals/runtime/messages.mli
+++ b/src/kernel_internals/runtime/messages.mli
@@ -20,13 +20,15 @@
 (*                                                                        *)
 (**************************************************************************)
 
-(** Stored messages for persistence between sessions.
-    Currently, only warning and error messages are stored. *)
+(** Stored messages for persistence between sessions. *)
 
 val iter: (Log.event -> unit) -> unit
 (** Iter over all stored messages. The messages are passed in emission order.
     @modify Nitrogen-20111001  Messages are now passed in emission order. *)
 
+val fold: ('a -> Log.event -> 'a) -> 'a -> 'a
+(** Fold over all stored messages. The messages are passed in emission order. *)
+
 val dump_messages: unit -> unit
 (** Dump stored messages to standard channels *)
 
diff --git a/src/kernel_services/ast_printing/printer_tag.ml b/src/kernel_services/ast_printing/printer_tag.ml
index 4f07c37e14609ce2be24fba5135655a1404ab495..c8cc00fef641d915cefd64277a9f297d865f1fce 100644
--- a/src/kernel_services/ast_printing/printer_tag.ml
+++ b/src/kernel_services/ast_printing/printer_tag.ml
@@ -244,6 +244,212 @@ let loc_of_localizable = function
      | None -> Location.unknown
      | Some kf -> Kernel_function.get_location kf)
 
+
+(* -------------------------------------------------------------------------- *)
+(* --- Find localizable at a Filepath.position                            --- *)
+(* -------------------------------------------------------------------------- *)
+
+let dkey = Kernel.register_category "pretty-source"
+
+module LineToLocalizable =
+  Datatype.Hashtbl(Datatype.Int.Hashtbl)(Datatype.Int)
+    (struct let module_name = "Pretty_source.LineToLocalizable" end)
+module FileToLines =
+  Datatype.Hashtbl(Datatype.Filepath.Hashtbl)(Datatype.Filepath)
+    (struct let module_name = "Pretty_source.FilesToLine" end)
+
+module MappingLineLocalizable = struct
+  module LineToLocalizableAux =
+    LineToLocalizable.Make( Datatype.Pair(Location)(Localizable))
+
+  include State_builder.Hashtbl(FileToLines)(LineToLocalizableAux)
+      (struct
+        let size = 5
+        let dependencies = [Ast.self]
+        let name = "Pretty_source.line_to_localizable"
+      end)
+end
+
+class pos_to_localizable =
+  object (self)
+    inherit Visitor.frama_c_inplace
+
+    (* used to keep track of conditional expressions, to add them to the
+       list of relevant localizables *)
+    val mutable insideIf = None
+
+    method add_range loc (localizable : localizable) =
+      if not (Location.equal loc Location.unknown) then (
+        let p1, p2 = loc in
+        if p1.Filepath.pos_path <> p2.Filepath.pos_path then
+          Kernel.debug ~once:true ~dkey
+            "Localizable over two files: %a and %a; %a"
+            Datatype.Filepath.pretty p1.Filepath.pos_path
+            Datatype.Filepath.pretty p2.Filepath.pos_path
+            Localizable.pretty localizable;
+        let file = p1.Filepath.pos_path in
+        let hfile =
+          try MappingLineLocalizable.find file
+          with Not_found ->
+            let h = LineToLocalizable.create 17 in
+            MappingLineLocalizable.add file h;
+            h
+        in
+        for i = p1.Filepath.pos_lnum to p2.Filepath.pos_lnum do
+          LineToLocalizable.add hfile i (loc, localizable);
+        done
+      );
+
+    method! vstmt_aux s =
+      (* we ignore Block statements, since they tend to overlap existing
+         ones which are more precise *)
+      let skip = match s.skind with
+        | Block _ -> true
+        | _ -> false
+      in
+      if not skip then
+        self#add_range (Stmt.loc s) (PStmt (Option.get self#current_kf, s));
+      begin
+        match s.skind with
+        | If (exp, _, _, _) ->
+          (* conditional expressions are treated in a special way *)
+          insideIf <- Some (Kstmt s);
+          ignore (Cil.visitCilExpr (self :> Cil.cilVisitor) exp);
+          insideIf <- None
+        | _ -> ()
+      end;
+      Cil.DoChildren
+
+    method! vexpr exp =
+      begin
+        match insideIf with
+        | Some ki ->
+          (* expressions inside conditionals have a special treatment *)
+          begin
+            match exp.enode with
+            | Lval lv ->
+              (* lvals must be generated differently from other expressions *)
+              self#add_range exp.eloc (PLval(self#current_kf, ki, lv))
+            | _ ->
+              self#add_range exp.eloc (PExp(self#current_kf, ki, exp))
+          end
+        | None -> ()
+      end;
+      Cil.DoChildren
+
+    method! vvdec vi =
+      if not vi.vglob && not vi.vtemp then
+        begin
+          match self#current_kf with
+          | None -> (* should not happen*) ()
+          | Some kf ->
+            self#add_range vi.vdecl (PVDecl (Some kf,self#current_kinstr,vi));
+        end;
+      Cil.DoChildren
+
+    method! vglob_aux g =
+      (match g with
+       | GFun ({ svar = vi }, loc) ->
+         self#add_range loc
+           (PVDecl (Some (Globals.Functions.get vi), Kglobal, vi))
+       | GVar (vi, _, loc) ->
+         self#add_range loc (PVDecl (None, Kglobal, vi))
+       | GFunDecl (_, vi, loc) ->
+         self#add_range loc
+           (PVDecl (Some (Globals.Functions.get vi), Kglobal, vi))
+       | GVarDecl (vi, loc) ->
+         self#add_range loc (PVDecl (None, Kglobal, vi))
+       | _ -> self#add_range (Global.loc g) (PGlobal g)
+      );
+      Cil.DoChildren
+  end
+
+(* Returns [true] if the column [col] is within location [loc]. *)
+let location_contains_col loc col =
+  let (pos_start, pos_end) = loc in
+  let (col_start, col_end) =
+    pos_start.Filepath.pos_cnum - pos_start.Filepath.pos_bol,
+    pos_end.Filepath.pos_cnum - pos_end.Filepath.pos_bol
+  in
+  col_start <= col && col <= col_end
+
+(* Applies several heuristics to try and match the best localizable to a
+   given location [loc]. The list [possible_locs] should contain all
+   localizables in a given line. If [possible_col] is [true], then we try
+   to take column information into account.
+   Some heuristics may return an empty list, in which case a fallback is
+   later used to return a better choice. *)
+let apply_location_heuristics precise_col possible_locs loc =
+  let col = loc.Filepath.pos_cnum - loc.Filepath.pos_bol in
+  Kernel.debug ~dkey
+    "apply_location_heuristics (precise_col:%b): loc: %a, col: %d@\n\
+     possible_locs:@ %a"
+    precise_col Location.pretty (loc, loc) col
+    (Pretty_utils.pp_list ~sep:"@\n"
+       (Pretty_utils.pp_pair ~sep:" :: " Location.pretty Localizable.pretty)) possible_locs;
+  (* Heuristic 1: we try to obtain a subset of localizables related to a given
+     position, or a given column if [precise_col] is true.
+     May result in an empty list. *)
+  let filter_locs l =
+    List.filter (fun (((pos_start, _) as loc'), _) ->
+        if precise_col then location_contains_col loc' col
+        else loc = pos_start
+      ) l
+  in
+  (* Heuristic 2: prioritize expressions if they are present.
+     May result in an empty list. *)
+  let exps l =
+    List.filter (fun (_, lz) -> match lz with | PExp _ -> true | _ -> false) l
+  in
+  (* Heuristic 3: when we have more than one match with the exact same location,
+     we pick the last one in the list. This will be the first statement that has
+     been encountered, and this criterion seems to work well with temporaries
+     introduced by Cil. *)
+  let last l = match List.rev l with [] -> None | (_, lz) :: _ -> Some lz in
+  (* Heuristic 4: when there are no exact locations, we will consider the
+     innermost ones, that is, those at the top of the list. *)
+  let innermost_in loc l =
+    List.filter (fun (loc', _) -> Location.equal loc loc') l
+  in
+  match possible_locs, filter_locs possible_locs with
+  | [], _ -> (* no possible localizables *) None
+  | _, (_ :: _ as exact) ->
+    (* one or more exact localizables; we prioritize expressions *)
+    begin
+      match exps exact with
+      | [] -> (* no expressions, just take the last localizable *) last exact
+      | exps -> (* take the last (usually only) expression *) last exps
+    end
+  | (loc', _) :: __, [] ->
+    (* No exact loc. We consider the innermost statements,
+       ie those at the top of the list *)
+    let filtered = innermost_in loc' possible_locs in
+    last filtered
+
+let loc_to_localizable ?(precise_col=false) loc =
+  if not (MappingLineLocalizable.is_computed ()) then (
+    let vis = new pos_to_localizable in
+    Visitor.visitFramacFile (vis :> Visitor.frama_c_visitor) (Ast.get ());
+    MappingLineLocalizable.mark_as_computed ();
+  );
+  try
+    (* Find the mapping from this file to locs-by-line *)
+    let hfile = MappingLineLocalizable.find loc.Filepath.pos_path in
+    (* Find the localizable for this line *)
+    let all = LineToLocalizable.find_all hfile loc.Filepath.pos_lnum in
+    match apply_location_heuristics precise_col all loc with
+    | Some locz ->
+      Kernel.feedback ~dkey "loc: %a -> locz: %a"
+        Location.pretty (loc,loc) Localizable.pretty locz;
+      Some locz
+    | None ->
+      Kernel.feedback ~dkey "loc: %a -> NO locz" Location.pretty (loc,loc);
+      None
+  with
+  | Not_found ->
+    Kernel.debug ~once:true ~source:loc "no matching localizable found";
+    None
+
 (* -------------------------------------------------------------------------- *)
 (* --- Printer API                                                        --- *)
 (* -------------------------------------------------------------------------- *)
diff --git a/src/kernel_services/ast_printing/printer_tag.mli b/src/kernel_services/ast_printing/printer_tag.mli
index a35466dfc72390bf57d86673f670c26698ec962b..4f67fdebedb480a36d572efb7ad280d6b906aca2 100644
--- a/src/kernel_services/ast_printing/printer_tag.mli
+++ b/src/kernel_services/ast_printing/printer_tag.mli
@@ -62,6 +62,12 @@ val typ_of_localizable: localizable -> typ option
 val loc_of_localizable : localizable -> location
 (** Might return [Location.unknown] *)
 
+val loc_to_localizable: ?precise_col:bool -> Filepath.position -> localizable option
+(** return the (hopefully) most precise localizable that contains the given
+    Filepath.position. If [precise_col] is [true], takes the column number into
+    account (possibly a more precise, but costly, result).
+    @since Frama-C+dev *)
+
 module type Tag =
 sig
   val create : localizable -> string
diff --git a/src/libraries/project/state_builder.ml b/src/libraries/project/state_builder.ml
index de29e89db7baedc1d085c6912ef392d2cd289ba7..1d3ef1c2275b159718d2ddc591276be89920acc0 100644
--- a/src/libraries/project/state_builder.ml
+++ b/src/libraries/project/state_builder.ml
@@ -880,7 +880,9 @@ module type Queue = sig
   val self: State.t
   val add: elt -> unit
   val iter: (elt -> unit) -> unit
+  val fold: ('a -> elt -> 'a) -> 'a -> 'a
   val is_empty: unit -> bool
+  val length: unit -> int
 end
 
 module Queue(Data: Datatype.S)(Info: Info) = struct
@@ -913,7 +915,9 @@ module Queue(Data: Datatype.S)(Info: Info) = struct
 
   let add x = Queue.add x !state
   let iter f = Queue.iter f !state
+  let fold f acc = Queue.fold f acc !state
   let is_empty () = Queue.is_empty !state
+  let length () = Queue.length !state
 
 end
 
diff --git a/src/libraries/project/state_builder.mli b/src/libraries/project/state_builder.mli
index 0815a483f3ebfb42e5a03b50d213519cb78c8e8d..bd94ad5e2bfd7874bb4f732a53e09629c823d2e3 100644
--- a/src/libraries/project/state_builder.mli
+++ b/src/libraries/project/state_builder.mli
@@ -378,7 +378,9 @@ module type Queue = sig
   val self: State.t
   val add: elt -> unit
   val iter: (elt -> unit) -> unit
+  val fold: ('a -> elt -> 'a) -> 'a -> 'a
   val is_empty: unit -> bool
+  val length: unit -> int
 end
 
 module Queue(Data: Datatype.S)(Info: Info) : Queue with type elt = Data.t
diff --git a/src/plugins/gui/design.ml b/src/plugins/gui/design.ml
index 5a9c80fef4174e1a1d6a58cd30f9aacae8e72fd9..1d24782c859ce33e1d9fcfc0cddb12ccf74705e2 100644
--- a/src/plugins/gui/design.ml
+++ b/src/plugins/gui/design.ml
@@ -1689,14 +1689,15 @@ class main_window () : main_window_extension_points =
           ignore
             (lower_notebook#insert_page ~pos:1
                ~tab_label:warnings_tab_label w);
-          let text = Format.sprintf "Messages (%d)" (Messages.nb_messages ()) in
+          let nb_messages = Messages.nb_warnings () + Messages.nb_errors () in
+          let text = Format.sprintf "Messages (%d)" nb_messages in
           let label = GtkMisc.Label.cast warnings_tab_label#as_widget in
           GtkMisc.Label.set_text label text
         in
         let callback e _column =
           Option.iter
             (fun pos ->
-               Option.iter self#scroll (Pretty_source.loc_to_localizable pos);
+               Option.iter self#scroll (Printer_tag.loc_to_localizable pos);
                (* Note: the code below generates double scrolling:
                   the previous call to self#scroll causes the original source
                   viewer to scroll to the beginning of the function, and then
@@ -1710,8 +1711,13 @@ class main_window () : main_window_extension_points =
       let display_warnings () =
         outdated_warnings := false ;
         Warning_manager.clear warning_manager;
-        Messages.iter (fun event -> Warning_manager.append warning_manager event);
-        let text = Format.sprintf "Messages (%d)" (Messages.nb_messages ()) in
+        Messages.iter
+          (fun event ->
+             match event.evt_kind with
+             | Warning | Error -> Warning_manager.append warning_manager event
+             | _ -> ());
+        let nb_messages = Messages.nb_warnings () + Messages.nb_errors () in
+        let text = Format.sprintf "Messages (%d)" nb_messages in
         let label = GtkMisc.Label.cast warnings_tab_label#as_widget in
         GtkMisc.Label.set_text label text
       in
diff --git a/src/plugins/gui/history.ml b/src/plugins/gui/history.ml
index be4771062cbd89762597bd888bcae9a0465157c5..d6c84731ce88000100c3e31dced5e99ecc7baea2 100644
--- a/src/plugins/gui/history.ml
+++ b/src/plugins/gui/history.ml
@@ -276,7 +276,7 @@ let translate_history_elt old_helt =
                        Filepath.pos_cnum = old_loc.Filepath.pos_cnum;
                       }
         in
-        match Pretty_source.loc_to_localizable new_loc with
+        match Printer_tag.loc_to_localizable new_loc with
         | None -> (** the line is unknown *)
           Some (Global g)
         | Some locali ->
diff --git a/src/plugins/gui/pretty_source.ml b/src/plugins/gui/pretty_source.ml
index c2aa878bbe0e137972f273c35595fa538914cab5..a2852f171ab24fd5be7ff777424b1242bec759f4 100644
--- a/src/plugins/gui/pretty_source.ml
+++ b/src/plugins/gui/pretty_source.ml
@@ -22,7 +22,6 @@
 
 open Cil_types
 open Gtk_helper
-open Cil_datatype
 open Printer_tag
 
 type localizable = Printer_tag.localizable =
@@ -41,8 +40,6 @@ type localizable = Printer_tag.localizable =
                           definitions. *)
   | PIP of Property.t
 
-let dkey = Gui_parameters.register_category "pretty-source"
-
 let kf_of_localizable = Printer_tag.kf_of_localizable
 let ki_of_localizable = Printer_tag.ki_of_localizable
 let varinfo_of_localizable = Printer_tag.varinfo_of_localizable
@@ -419,206 +416,6 @@ let display_source globals
          (fun () -> GtkSignal.disconnect event_tag#as_tag id);
     )
 
-
-module LineToLocalizable =
-  Datatype.Hashtbl(Datatype.Int.Hashtbl)(Datatype.Int)
-    (struct let module_name = "Pretty_source.LineToLocalizable" end)
-module FileToLines =
-  Datatype.Hashtbl(Datatype.Filepath.Hashtbl)(Datatype.Filepath)
-    (struct let module_name = "Pretty_source.FilesToLine" end)
-
-module MappingLineLocalizable = struct
-  module LineToLocalizableAux =
-    LineToLocalizable.Make( Datatype.Pair(Location)(Localizable))
-
-  include State_builder.Hashtbl(FileToLines)(LineToLocalizableAux)
-      (struct
-        let size = 5
-        let dependencies = [Ast.self]
-        let name = "Pretty_source.line_to_localizable"
-      end)
-end
-
-class pos_to_localizable =
-  object (self)
-    inherit Visitor.frama_c_inplace
-
-    (* used to keep track of conditional expressions, to add them to the
-       list of relevant localizables *)
-    val mutable insideIf = None
-
-    method add_range loc (localizable : localizable) =
-      if not (Location.equal loc Location.unknown) then (
-        let p1, p2 = loc in
-        if p1.Filepath.pos_path <> p2.Filepath.pos_path then
-          Gui_parameters.debug ~once:true
-            "Localizable over two files: %a and %a; %a"
-            Datatype.Filepath.pretty p1.Filepath.pos_path
-            Datatype.Filepath.pretty p2.Filepath.pos_path
-            Localizable.pretty localizable;
-        let file = p1.Filepath.pos_path in
-        let hfile =
-          try MappingLineLocalizable.find file
-          with Not_found ->
-            let h = LineToLocalizable.create 17 in
-            MappingLineLocalizable.add file h;
-            h
-        in
-        for i = p1.Filepath.pos_lnum to p2.Filepath.pos_lnum do
-          LineToLocalizable.add hfile i (loc, localizable);
-        done
-      );
-
-    method! vstmt_aux s =
-      (* we ignore Block statements, since they tend to overlap existing
-         ones which are more precise *)
-      let skip = match s.skind with
-        | Block _ -> true
-        | _ -> false
-      in
-      if not skip then
-        self#add_range (Stmt.loc s) (PStmt (Option.get self#current_kf, s));
-      begin
-        match s.skind with
-        | If (exp, _, _, _) ->
-          (* conditional expressions are treated in a special way *)
-          insideIf <- Some (Kstmt s);
-          ignore (Cil.visitCilExpr (self :> Cil.cilVisitor) exp);
-          insideIf <- None
-        | _ -> ()
-      end;
-      Cil.DoChildren
-
-    method! vexpr exp =
-      begin
-        match insideIf with
-        | Some ki ->
-          (* expressions inside conditionals have a special treatment *)
-          begin
-            match exp.enode with
-            | Lval lv ->
-              (* lvals must be generated differently from other expressions *)
-              self#add_range exp.eloc (PLval(self#current_kf, ki, lv))
-            | _ ->
-              self#add_range exp.eloc (PExp(self#current_kf, ki, exp))
-          end
-        | None -> ()
-      end;
-      Cil.DoChildren
-
-    method! vvdec vi =
-      if not vi.vglob && not vi.vtemp then
-        begin
-          match self#current_kf with
-          | None -> (* should not happen*) ()
-          | Some kf ->
-            self#add_range vi.vdecl (PVDecl (Some kf,self#current_kinstr,vi));
-        end;
-      Cil.DoChildren
-
-    method! vglob_aux g =
-      (match g with
-       | GFun ({ svar = vi }, loc) ->
-         self#add_range loc
-           (PVDecl (Some (Globals.Functions.get vi), Kglobal, vi))
-       | GVar (vi, _, loc) ->
-         self#add_range loc (PVDecl (None, Kglobal, vi))
-       | GFunDecl (_, vi, loc) ->
-         self#add_range loc
-           (PVDecl (Some (Globals.Functions.get vi), Kglobal, vi))
-       | GVarDecl (vi, loc) ->
-         self#add_range loc (PVDecl (None, Kglobal, vi))
-       | _ -> self#add_range (Global.loc g) (PGlobal g)
-      );
-      Cil.DoChildren
-  end
-
-(* Returns [true] if the column [col] is within location [loc]. *)
-let location_contains_col loc col =
-  let (pos_start, pos_end) = loc in
-  let (col_start, col_end) =
-    pos_start.Filepath.pos_cnum - pos_start.Filepath.pos_bol,
-    pos_end.Filepath.pos_cnum - pos_end.Filepath.pos_bol
-  in
-  col_start <= col && col <= col_end
-
-(* Applies several heuristics to try and match the best localizable to a
-   given location [loc]. The list [possible_locs] should contain all
-   localizables in a given line. If [possible_col] is [true], then we try
-   to take column information into account.
-   Some heuristics may return an empty list, in which case a fallback is
-   later used to return a better choice. *)
-let apply_location_heuristics precise_col possible_locs loc =
-  let col = loc.Filepath.pos_cnum - loc.Filepath.pos_bol in
-  Gui_parameters.debug ~dkey
-    "apply_location_heuristics (precise_col:%b): loc: %a, col: %d@\n\
-     possible_locs:@ %a"
-    precise_col Location.pretty (loc, loc) col
-    (Pretty_utils.pp_list ~sep:"@\n"
-       (Pretty_utils.pp_pair ~sep:" :: " Location.pretty Localizable.pretty)) possible_locs;
-  (* Heuristic 1: we try to obtain a subset of localizables related to a given
-     position, or a given column if [precise_col] is true.
-     May result in an empty list. *)
-  let filter_locs l =
-    List.filter (fun (((pos_start, _) as loc'), _) ->
-        if precise_col then location_contains_col loc' col
-        else loc = pos_start
-      ) l
-  in
-  (* Heuristic 2: prioritize expressions if they are present.
-     May result in an empty list. *)
-  let exps l =
-    List.filter (fun (_, lz) -> match lz with | PExp _ -> true | _ -> false) l
-  in
-  (* Heuristic 3: when we have more than one match with the exact same location,
-     we pick the last one in the list. This will be the first statement that has
-     been encountered, and this criterion seems to work well with temporaries
-     introduced by Cil. *)
-  let last l = match List.rev l with [] -> None | (_, lz) :: _ -> Some lz in
-  (* Heuristic 4: when there are no exact locations, we will consider the
-     innermost ones, that is, those at the top of the list. *)
-  let innermost_in loc l =
-    List.filter (fun (loc', _) -> Location.equal loc loc') l
-  in
-  match possible_locs, filter_locs possible_locs with
-  | [], _ -> (* no possible localizables *) None
-  | _, (_ :: _ as exact) ->
-    (* one or more exact localizables; we prioritize expressions *)
-    begin
-      match exps exact with
-      | [] -> (* no expressions, just take the last localizable *) last exact
-      | exps -> (* take the last (usually only) expression *) last exps
-    end
-  | (loc', _) :: __, [] ->
-    (* No exact loc. We consider the innermost statements,
-       ie those at the top of the list *)
-    let filtered = innermost_in loc' possible_locs in
-    last filtered
-
-let loc_to_localizable ?(precise_col=false) loc =
-  if not (MappingLineLocalizable.is_computed ()) then (
-    let vis = new pos_to_localizable in
-    Visitor.visitFramacFile (vis :> Visitor.frama_c_visitor) (Ast.get ());
-    MappingLineLocalizable.mark_as_computed ();
-  );
-  try
-    (* Find the mapping from this file to locs-by-line *)
-    let hfile = MappingLineLocalizable.find loc.Filepath.pos_path in
-    (* Find the localizable for this line *)
-    let all = LineToLocalizable.find_all hfile loc.Filepath.pos_lnum in
-    match apply_location_heuristics precise_col all loc with
-    | Some locz ->
-      Gui_parameters.feedback ~dkey "loc: %a -> locz: %a"
-        Location.pretty (loc,loc) Localizable.pretty locz;
-      Some locz
-    | None ->
-      Gui_parameters.feedback ~dkey "loc: %a -> NO locz" Location.pretty (loc,loc);
-      None
-  with
-  | Not_found ->
-    Gui_parameters.debug ~once:true ~source:loc "no matching localizable found";
-    None
-
 (*
 Local Variables:
 compile-command: "make -C ../../.."
diff --git a/src/plugins/gui/pretty_source.mli b/src/plugins/gui/pretty_source.mli
index a3544e647fbcc811deb9e0ad65ed6c1d4df2d1a7..96e25243efbfb0c674d2ee01f824d76ad07334c0 100644
--- a/src/plugins/gui/pretty_source.mli
+++ b/src/plugins/gui/pretty_source.mli
@@ -88,13 +88,6 @@ val localizable_from_locs :
     This function is inefficient as it iterates on all the current
     [Locs.state]. *)
 
-val loc_to_localizable: ?precise_col:bool -> Filepath.position -> localizable option
-(** return the (hopefully) most precise localizable that contains the given
-    Filepath.position. If [precise_col] is [true], takes the column number into
-    account (possibly a more precise, but costly, result).
-    @since Nitrogen-20111001 *)
-
-
 (*
 Local Variables:
 compile-command: "make -C ../../.."
diff --git a/src/plugins/gui/source_manager.ml b/src/plugins/gui/source_manager.ml
index d3d250910729b73520a966bd85f8ccd21d627605..7a99892c460b56eb5ee6cbca2cb491a93cac4f6a 100644
--- a/src/plugins/gui/source_manager.ml
+++ b/src/plugins/gui/source_manager.ml
@@ -171,7 +171,7 @@ let load_file w ?title ~(filename : Datatype.Filepath.t) ?(line=(-1)) ~click_cb
                                             Filepath.pos_bol = offset - col;
                                             Filepath.pos_cnum = offset;} in
                                  let localz =
-                                   Pretty_source.loc_to_localizable ~precise_col:true pos
+                                   Printer_tag.loc_to_localizable ~precise_col:true pos
                                  in
                                  click_cb localz
                                with Not_found ->
diff --git a/src/plugins/server/Makefile.in b/src/plugins/server/Makefile.in
index 1c00c79b8609999042fe6023274499198ebf22bb..0736b25b9d0e2ff17b1d17b90d33a96e8a491810 100644
--- a/src/plugins/server/Makefile.in
+++ b/src/plugins/server/Makefile.in
@@ -43,9 +43,9 @@ PLUGIN_CMO:= \
 	data main request states \
 	server_doc \
 	server_batch \
+	kernel_ast \
 	kernel_main \
 	kernel_project \
-	kernel_ast \
 	kernel_properties
 
 PLUGIN_DISTRIBUTED:=$(PLUGIN_ENABLE)
diff --git a/src/plugins/server/kernel_ast.ml b/src/plugins/server/kernel_ast.ml
index dce3b62f90f59c0b486db460125a9c5a9f1b214e..7ce9dd03cc6ed5e4235edfdc1d544b0471ae871b 100644
--- a/src/plugins/server/kernel_ast.ml
+++ b/src/plugins/server/kernel_ast.ml
@@ -46,6 +46,52 @@ let ast_update_hook f =
 
 let () = ast_update_hook (fun _ -> Request.emit changed_signal)
 
+(* -------------------------------------------------------------------------- *)
+(* --- File Positions                                                     --- *)
+(* -------------------------------------------------------------------------- *)
+
+module Position =
+struct
+  type t = Filepath.position
+
+  let jtype = Data.declare ~package ~name:"source"
+      ~descr:(Md.plain "Source file positions.")
+      (Jrecord [
+          "dir", Jstring;
+          "base", Jstring;
+          "file", Jstring;
+          "line", Jnumber;
+        ])
+
+  let to_json p =
+    let path = Filepath.(Normalized.to_pretty_string p.pos_path) in
+    let file =
+      if Server_parameters.has_relative_filepath ()
+      then path
+      else (p.Filepath.pos_path :> string)
+    in
+    `Assoc [
+      "dir"  , `String (Filename.dirname path) ;
+      "base" , `String (Filename.basename path) ;
+      "file" , `String file ;
+      "line" , `Int p.Filepath.pos_lnum ;
+    ]
+
+  let of_json js =
+    let fail () = failure_from_type_error "Invalid source format" js in
+    match js with
+    | `Assoc assoc ->
+      begin
+        match List.assoc "file" assoc, List.assoc "line" assoc with
+        | `String path, `Int line ->
+          Log.source ~file:(Filepath.Normalized.of_string path) ~line
+        | _, _ -> fail ()
+        | exception Not_found -> fail ()
+      end
+    | _ -> fail ()
+
+end
+
 (* -------------------------------------------------------------------------- *)
 (* ---  Printers                                                          --- *)
 (* -------------------------------------------------------------------------- *)
@@ -207,7 +253,7 @@ struct
       States.column
         ~name:"sloc"
         ~descr:(Md.plain "Source location")
-        ~data:(module Kernel_main.LogSource)
+        ~data:(module Position)
         ~get:(fun (tag, _) -> fst (Printer_tag.loc_of_localizable tag))
         model
     in
@@ -471,7 +517,7 @@ struct
       States.column model
         ~name:"sloc"
         ~descr:(Md.plain "Source location")
-        ~data:(module Kernel_main.LogSource)
+        ~data:(module Position)
         ~get:(fun kf -> fst (Kernel_function.get_location kf));
       States.register_array model
         ~package ~key
diff --git a/src/plugins/server/kernel_ast.mli b/src/plugins/server/kernel_ast.mli
index aa917218f410d85d2ade7922da578687cffe8355..d85697dec6786bf566dabe44780959aa27ab7572 100644
--- a/src/plugins/server/kernel_ast.mli
+++ b/src/plugins/server/kernel_ast.mli
@@ -27,6 +27,8 @@
 open Package
 open Cil_types
 
+module Position : Data.S with type t = Filepath.position
+
 module Kf : Data.S with type t = kernel_function
 module Ki : Data.S with type t = kinstr
 module Stmt : Data.S with type t = stmt
diff --git a/src/plugins/server/kernel_main.ml b/src/plugins/server/kernel_main.ml
index 3c5fb34a4619c6ea96e51827dfdfbb1c63420ae0..dd00d78a8f55782fb857f39cfab1eb2129cb0204 100644
--- a/src/plugins/server/kernel_main.ml
+++ b/src/plugins/server/kernel_main.ml
@@ -84,51 +84,6 @@ let () =
        try Project.save_all (Filepath.Normalized.of_string file); None
        with Project.IOError err -> Some err)
 
-(* -------------------------------------------------------------------------- *)
-(* --- File Positions                                                     --- *)
-(* -------------------------------------------------------------------------- *)
-
-module LogSource =
-struct
-  type t = Filepath.position
-
-  let jtype = Data.declare ~package ~name:"source"
-      ~descr:(Md.plain "Source file positions.")
-      (Jrecord [
-          "dir", Jstring;
-          "base", Jstring;
-          "file", Jstring;
-          "line", Jnumber;
-        ])
-
-  let to_json p =
-    let path = Filepath.(Normalized.to_pretty_string p.pos_path) in
-    let file =
-      if Server_parameters.has_relative_filepath ()
-      then path
-      else (p.Filepath.pos_path :> string)
-    in
-    `Assoc [
-      "dir"  , `String (Filename.dirname path) ;
-      "base" , `String (Filename.basename path) ;
-      "file" , `String file ;
-      "line" , `Int p.Filepath.pos_lnum ;
-    ]
-
-  let of_json js =
-    let fail () = failure_from_type_error "Invalid source format" js in
-    match js with
-    | `Assoc assoc ->
-      begin
-        match List.assoc "file" assoc, List.assoc "line" assoc with
-        | `String path, `Int line ->
-          Log.source ~file:(Filepath.Normalized.of_string path) ~line
-        | _, _ -> fail ()
-        | exception Not_found -> fail ()
-      end
-    | _ -> fail ()
-
-end
 
 (* -------------------------------------------------------------------------- *)
 (* --- Log Lind                                                           --- *)
@@ -166,6 +121,65 @@ struct
   include (val data : S with type t = Log.kind)
 end
 
+(* -------------------------------------------------------------------------- *)
+(* --- Synchronized array of log events                                   --- *)
+(* -------------------------------------------------------------------------- *)
+
+let model = States.model ()
+
+let () = States.column model ~name:"kind"
+    ~descr:(Md.plain "Message kind")
+    ~data:(module LogKind)
+    ~get:(fun (evt, _) -> evt.Log.evt_kind)
+
+let () = States.column model ~name:"plugin"
+    ~descr:(Md.plain "Emitter plugin")
+    ~data:(module Jalpha)
+    ~get:(fun (evt, _) -> evt.Log.evt_plugin)
+
+let () = States.column model ~name:"message"
+    ~descr:(Md.plain "Message text")
+    ~data:(module Jstring)
+    ~get:(fun (evt, _) -> evt.Log.evt_message)
+
+let () = States.option model ~name:"category"
+    ~descr:(Md.plain "Message category (only for debug or warning messages)")
+    ~data:(module Jstring)
+    ~get:(fun (evt, _) -> evt.Log.evt_category)
+
+let () = States.option model ~name:"source"
+    ~descr:(Md.plain "Source file position")
+    ~data:(module Kernel_ast.Position)
+    ~get:(fun (evt, _) -> evt.Log.evt_source)
+
+let getMarker (evt, _id) =
+  Option.bind evt.Log.evt_source Printer_tag.loc_to_localizable
+
+let getFunction t =
+  Option.bind (getMarker t) Printer_tag.kf_of_localizable
+
+let () = States.option model ~name:"marker"
+    ~descr:(Md.plain "Marker at the message position (if any)")
+    ~data:(module Kernel_ast.Marker)
+    ~get:getMarker
+
+let () = States.option model ~name:"fct"
+    ~descr:(Md.plain "Function containing the message position (if any)")
+    ~data:(module Kernel_ast.Kf)
+    ~get:getFunction
+
+let iter f = ignore (Messages.fold (fun i evt -> f (evt, i); succ i) 0)
+
+let _array =
+  States.register_array
+    ~package
+    ~name:"message"
+    ~descr:(Md.plain "Log messages")
+    ~key:(fun (_evt, i) -> string_of_int i)
+    ~iter:iter
+    ~add_reload_hook:Messages.add_global_hook
+    model
+
 (* -------------------------------------------------------------------------- *)
 (* --- Log Events                                                         --- *)
 (* -------------------------------------------------------------------------- *)
@@ -186,7 +200,7 @@ struct
   let category = Record.option jlog ~name:"category"
       ~descr:(Md.plain "Message category (DEBUG or WARNING)") (module Jstring)
   let source = Record.option jlog ~name:"source"
-      ~descr:(Md.plain "Source file position") (module LogSource)
+      ~descr:(Md.plain "Source file position") (module Kernel_ast.Position)
 
   let data = Record.publish ~package ~name:"log"
       ~descr:(Md.plain "Message event record.") jlog
diff --git a/src/plugins/server/kernel_main.mli b/src/plugins/server/kernel_main.mli
index ead76ffab68c5141d8c8c01a9db4bceb9d09a883..b8dd233dda0114f5408609e620b4c97c80429cc5 100644
--- a/src/plugins/server/kernel_main.mli
+++ b/src/plugins/server/kernel_main.mli
@@ -24,7 +24,6 @@
 (** Kernel Services *)
 (* -------------------------------------------------------------------------- *)
 
-module LogSource : Data.S with type t = Filepath.position
 module LogEvent : Data.S with type t = Log.event
 
 (* -------------------------------------------------------------------------- *)
diff --git a/src/plugins/server/kernel_properties.ml b/src/plugins/server/kernel_properties.ml
index 534bee463c92d8665d56f5b438d6ca7a5851f08f..4e91e13653b2d5171d6fa710f32f87523dccea77 100644
--- a/src/plugins/server/kernel_properties.ml
+++ b/src/plugins/server/kernel_properties.ml
@@ -24,7 +24,6 @@ module Md = Markdown
 module Pkg = Package
 
 open Data
-open Kernel_main
 open Kernel_ast
 
 let package = Pkg.package ~title:"Property Services" ~name:"properties" ()
@@ -297,7 +296,7 @@ let () = States.column model ~name:"kinstr"
 
 let () = States.column model ~name:"source"
     ~descr:(Md.plain "Position")
-    ~data:(module LogSource)
+    ~data:(module Kernel_ast.Position)
     ~get:(fun ip -> Property.location ip |> fst)
 
 let () = States.column model ~name:"alarm"